From 6d795b887400fe25e87b209dd2add92696e2c1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 6 Nov 2025 15:42:05 +0100 Subject: [PATCH 01/92] Update GHC from 9.0.2 to 9.6.7 (#3305) --- .github/actions/setup-haskell/action.yaml | 2 +- .github/workflows/ci-waspc-build.yaml | 4 - waspc/cabal.project | 11 ++- waspc/cli/src/Wasp/Cli/Command.hs | 6 +- waspc/cli/src/Wasp/Cli/Command/Build.hs | 18 ++-- .../src/Wasp/Cli/Command/BuildStart/Config.hs | 3 +- waspc/cli/src/Wasp/Cli/Command/Common.hs | 3 +- waspc/cli/src/Wasp/Cli/Command/Compile.hs | 3 +- .../Wasp/Cli/Command/CreateNewProject/AI.hs | 93 ++++++++++--------- .../CreateNewProject/StarterTemplates.hs | 3 +- waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Info.hs | 2 +- waspc/cli/src/Wasp/Cli/Command/Studio.hs | 3 +- waspc/cli/src/Wasp/Cli/FileSystem.hs | 2 +- waspc/cli/src/Wasp/Cli/GithubRepo.hs | 2 +- waspc/cli/src/Wasp/Cli/Interactive.hs | 4 +- waspc/cli/tests/DbMigrateTest.hs | 2 +- waspc/cli/tests/TerminalTest.hs | 2 +- .../Wasp/Cli/Command/Telemetry/ProjectTest.hs | 2 +- .../tests/Wasp/Cli/Util/EnvVarArgumentTest.hs | 2 +- waspc/dev-tool.project | 8 +- waspc/e2e-tests/ShellCommands.hs | 15 ++- waspc/e2e-tests/SnapshotTest/ShellCommands.hs | 13 ++- waspc/e2e-tests/WaspProject/ShellCommands.hs | 20 ++-- .../.wasp/out/installedNpmDepsLog.json | 2 +- .../.wasp/build/installedNpmDepsLog.json | 2 +- .../.wasp/out/installedNpmDepsLog.json | 2 +- .../.wasp/out/installedNpmDepsLog.json | 2 +- waspc/run | 2 +- waspc/src/FilePath/Extra.hs | 3 +- waspc/src/Wasp/AI/CodeAgent.hs | 7 +- .../src/Wasp/AI/GenerateNewProject/Common.hs | 2 +- waspc/src/Wasp/AI/GenerateNewProject/Plan.hs | 29 ++++-- .../Evaluation/TypedExpr/Combinators.hs | 4 +- .../Analyzer/Evaluator/EvaluationError.hs | 3 +- .../Wasp/Analyzer/Parser/AST/PrettyPrinter.hs | 9 +- .../Wasp/Analyzer/Parser/ConcreteParser.hs | 11 ++- .../Parser/ConcreteParser/ParserLib.hs | 2 +- .../Wasp/Analyzer/Parser/Lexer/Internal.hs | 6 +- .../TypeDefinitions/Class/IsEnumType.hs | 2 +- waspc/src/Wasp/AppSpec.hs | 6 +- waspc/src/Wasp/AppSpec/App/Auth.hs | 11 ++- waspc/src/Wasp/AppSpec/Core/Ref.hs | 7 +- waspc/src/Wasp/AppSpec/Job.hs | 5 +- waspc/src/Wasp/AppSpec/Valid.hs | 7 +- waspc/src/Wasp/Generator/Crud.hs | 4 +- .../src/Wasp/Generator/DbGenerator/Common.hs | 4 +- .../Wasp/Generator/DbGenerator/Operations.hs | 11 ++- .../Generator/FileDraft/TemplateFileDraft.hs | 2 +- waspc/src/Wasp/Generator/JsImport.hs | 2 +- waspc/src/Wasp/Generator/NpmInstall.hs | 3 +- .../Client/OperationsGenerator.hs | 3 +- .../SdkGenerator/Server/JobGenerator.hs | 6 +- .../Generator/ServerGenerator/JobGenerator.hs | 3 +- waspc/src/Wasp/Project/Common.hs | 2 +- waspc/src/Wasp/Project/Waspignore.hs | 2 +- waspc/src/Wasp/Psl/Generator/Attribute.hs | 3 +- waspc/src/Wasp/Psl/Generator/Model.hs | 3 +- waspc/src/Wasp/Psl/Parser/Argument.hs | 3 +- waspc/src/Wasp/TypeScript/Inspect/Exports.hs | 3 +- waspc/src/Wasp/Util.hs | 27 +++--- waspc/src/Wasp/Util/Aeson.hs | 6 +- waspc/src/Wasp/Util/Control/Monad.hs | 2 +- waspc/src/Wasp/Util/Json.hs | 2 +- waspc/src/Wasp/Util/Network/HTTP.hs | 5 +- waspc/src/Wasp/Util/StrongPath.hs | 8 +- waspc/src/Wasp/Version.hs | 6 +- waspc/testEnv.hs | 2 +- .../tests/AI/GenerateNewProject/LogMsgTest.hs | 2 +- .../Evaluation/EvaluationErrorTest.hs | 2 +- waspc/tests/Analyzer/EvaluatorTest.hs | 11 ++- .../tests/Analyzer/Parser/CST/TraverseTest.hs | 2 +- .../Analyzer/Parser/ConcreteParserTest.hs | 5 +- waspc/tests/Analyzer/Parser/ParseErrorTest.hs | 2 +- .../Analyzer/Parser/SourcePositionTest.hs | 2 +- waspc/tests/Analyzer/Parser/SourceSpanTest.hs | 2 +- waspc/tests/Analyzer/ParserTest.hs | 2 +- .../Analyzer/TypeChecker/InternalTest.hs | 2 +- waspc/tests/Analyzer/TypeCheckerTest.hs | 6 +- waspc/tests/Analyzer/ValidTest.hs | 2 +- waspc/tests/AnalyzerTest.hs | 2 +- waspc/tests/AppSpec/EntityTest.hs | 2 +- waspc/tests/AppSpec/FromJSONTest.hs | 4 +- waspc/tests/AppSpec/ValidTest.hs | 2 +- waspc/tests/EnvTest.hs | 2 +- waspc/tests/ErrorTest.hs | 5 +- waspc/tests/FilePath/ExtraTest.hs | 2 +- waspc/tests/Generator/AuthInjectionTest.hs | 2 +- waspc/tests/Generator/CommonTest.hs | 2 +- waspc/tests/Generator/CrudTest.hs | 2 +- waspc/tests/Generator/DbGeneratorTest.hs | 2 +- .../CopyAndModifyTextFileDraftTest.hs | 2 +- .../Generator/FileDraft/CopyFileDraftTest.hs | 2 +- .../FileDraft/TemplateFileDraftTest.hs | 2 +- waspc/tests/Generator/JsImportTest.hs | 2 +- waspc/tests/Generator/MockWriteableMonad.hs | 2 +- waspc/tests/Generator/NpmDependenciesTest.hs | 2 +- waspc/tests/Generator/WebAppGeneratorTest.hs | 8 +- waspc/tests/Generator/WriteFileDraftsTest.hs | 2 +- waspc/tests/JsImportTest.hs | 2 +- waspc/tests/Node/InternalTest.hs | 2 +- waspc/tests/Project/DbTest.hs | 2 +- waspc/tests/Project/WaspignoreTest.hs | 2 +- waspc/tests/Psl/Generator/ModelTest.hs | 2 +- waspc/tests/Psl/Generator/SchemaTest.hs | 4 +- waspc/tests/Psl/Generator/WithCtxTest.hs | 14 +-- waspc/tests/Psl/Parser/ArgumentTest.hs | 2 +- waspc/tests/Psl/Parser/AttributeTest.hs | 2 +- waspc/tests/Psl/Parser/ConfigBlockTest.hs | 2 +- waspc/tests/Psl/Parser/EnumTest.hs | 2 +- waspc/tests/Psl/Parser/ModelTest.hs | 2 +- waspc/tests/Psl/Parser/SchemaTest.hs | 14 +-- waspc/tests/Psl/Parser/TypeTest.hs | 2 +- waspc/tests/Psl/Parser/ViewTest.hs | 2 +- waspc/tests/Psl/Parser/WithCtxTest.hs | 46 ++++----- waspc/tests/Psl/ValidTest.hs | 2 +- .../tests/SemanticVersion/VersionBoundTest.hs | 2 +- waspc/tests/SemanticVersion/VersionTest.hs | 2 +- waspc/tests/SemanticVersionTest.hs | 2 +- waspc/tests/Util/Control/MonadTest.hs | 3 +- waspc/tests/Util/Diff.hs | 4 +- waspc/tests/Util/FibTest.hs | 2 +- waspc/tests/Util/FilePathTest.hs | 2 +- waspc/tests/Util/IO/RetryTest.hs | 2 +- waspc/tests/UtilTest.hs | 2 +- waspc/waspc.cabal | 67 ++++++------- waspc/waspls/src/Control/Monad/Log.hs | 2 +- waspc/waspls/src/Control/Monad/Log/Class.hs | 8 +- waspc/waspls/src/Language/LSP/Extra.hs | 64 +++++++++++++ .../src/Wasp/LSP/Commands/ScaffoldTsSymbol.hs | 4 +- waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs | 4 +- waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs | 2 +- waspc/waspls/src/Wasp/LSP/Server.hs | 11 ++- .../LSP/ServerMonads/HasProjectRootDir.hs | 2 +- waspc/waspls/src/Wasp/LSP/SignatureHelp.hs | 2 +- waspc/waspls/src/Wasp/LSP/Util.hs | 8 +- waspc/waspls/tests/Wasp/LSP/DebouncerTest.hs | 2 +- waspc/waspls/tests/Wasp/LSP/SyntaxTest.hs | 2 +- 138 files changed, 505 insertions(+), 350 deletions(-) create mode 100644 waspc/waspls/src/Language/LSP/Extra.hs diff --git a/.github/actions/setup-haskell/action.yaml b/.github/actions/setup-haskell/action.yaml index 434515cb05..b1b11e9225 100644 --- a/.github/actions/setup-haskell/action.yaml +++ b/.github/actions/setup-haskell/action.yaml @@ -9,7 +9,7 @@ inputs: description: | The version of GHC to install. required: false - default: "9.0.2" + default: "9.6.7" cabal-version: description: | diff --git a/.github/workflows/ci-waspc-build.yaml b/.github/workflows/ci-waspc-build.yaml index 6535e04431..b81657fa81 100644 --- a/.github/workflows/ci-waspc-build.yaml +++ b/.github/workflows/ci-waspc-build.yaml @@ -109,10 +109,6 @@ jobs: brew install --formula "$repo_name/$formula_name" brew link --force "$repo_name/$formula_name" - # Workaround for GHC 9.0.2 and Apple Silicon - # https://gitlab.haskell.org/ghc/ghc/-/issues/20592 - echo C_INCLUDE_PATH="`xcrun --show-sdk-path`/usr/include/ffi" >> $GITHUB_ENV - runs-on: ${{ matrix.env.runner }} container: ${{ matrix.env.container }} name: build (${{ matrix.env.name }}) diff --git a/waspc/cabal.project b/waspc/cabal.project index cd53a90e0c..dcc91a44d8 100644 --- a/waspc/cabal.project +++ b/waspc/cabal.project @@ -1,4 +1,4 @@ -with-compiler: ghc-9.0.2 +with-compiler: ghc-9.6.7 packages: . @@ -16,8 +16,15 @@ package waspc -- building multiple packages at once, for example external dependencies. jobs: $ncpus +-- We need this to for the HLS to work in tests. When e updated to GHC 9.2.8, +-- Cabal stopped including tests in the build plan. We don't fully understand +-- how Cabal chooses when it requires the flag to include tests, so we always +-- enable it. +-- Context: https://github.com/wasp-lang/wasp/pull/3286#discussion_r2490111116 +tests: True + -- Ensures that tests print their output to stdout as they execute. test-show-details: direct -- WARNING: Run cabal update if your local package index is older than this date. -index-state: 2023-03-09T09:15:49Z +index-state: 2025-11-01T14:29:27Z diff --git a/waspc/cli/src/Wasp/Cli/Command.hs b/waspc/cli/src/Wasp/Cli/Command.hs index 38d24297ef..804d535bad 100644 --- a/waspc/cli/src/Wasp/Cli/Command.hs +++ b/waspc/cli/src/Wasp/Cli/Command.hs @@ -40,9 +40,9 @@ runCommand cmd = do data CommandError = CommandError {_errorTitle :: !String, _errorMsg :: !String} data Requirement where - Requirement :: Requirable r => r -> Requirement + Requirement :: (Requirable r) => r -> Requirement -class Typeable r => Requirable r where +class (Typeable r) => Requirable r where -- | Check if the requirement is met and return a value representing that -- requirement. -- @@ -59,7 +59,7 @@ class Typeable r => Requirable r where -- do -- HasDbConnection <- require -- @ -require :: Requirable r => Command r +require :: (Requirable r) => Command r require = Command (gets (mapMaybe cast)) >>= \case (req : _) -> return req diff --git a/waspc/cli/src/Wasp/Cli/Command/Build.hs b/waspc/cli/src/Wasp/Cli/Command/Build.hs index fd5bc9ec0d..d0db8d4d3d 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Build.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Build.hs @@ -8,10 +8,10 @@ import Control.Monad (unless, when) import Control.Monad.Except (ExceptT (ExceptT), runExceptT, throwError) import Control.Monad.IO.Class (liftIO) import Data.Aeson (Value (..)) +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KM import Data.Aeson.Lens -import qualified Data.HashMap.Strict as HM import Data.List (isSuffixOf) -import Data.Text (Text, unpack) import StrongPath (Abs, Dir, Path', castRel, ()) import qualified System.FilePath as FP import Wasp.Cli.Command (Command, CommandError (..)) @@ -52,7 +52,8 @@ build = do InWaspProject waspProjectDir <- require let buildDir = - waspProjectDir dotWaspDirInWaspProjectDir + waspProjectDir + dotWaspDirInWaspProjectDir buildDirInDotWaspDir doesBuildDirExist <- liftIO $ doesDirectoryExist buildDir @@ -76,7 +77,8 @@ build = do liftIO $ printCompilationResult (warnings, errors) unless (null errors) $ throwError $ - CommandError "Building of wasp project failed" $ show (length errors) ++ " errors found." + CommandError "Building of wasp project failed" $ + show (length errors) ++ " errors found." liftIO (prepareFilesNecessaryForDockerBuild waspProjectDir buildDir) >>= \case Left err -> throwError $ CommandError "Failed to prepare files necessary for docker build" err @@ -148,12 +150,12 @@ build = do packageLockJsonObject & key "packages" . key "" %~ removeWaspConfigFromDevDependenciesArray & key "packages" . _Object - %~ HM.filterWithKey - (\packageLocation _ -> not $ isWaspConfigPackageLocation packageLocation) + %~ KM.filterWithKey + (\packageLocation _ -> not $ isWaspConfigPackageLocation (Key.toString packageLocation)) - isWaspConfigPackageLocation :: Text -> Bool + isWaspConfigPackageLocation :: String -> Bool isWaspConfigPackageLocation packageLocation = - (FP.pathSeparator : "wasp-config") `isSuffixOf` unpack packageLocation + (FP.pathSeparator : "wasp-config") `isSuffixOf` packageLocation removeWaspConfigFromDevDependenciesArray :: Value -> Value removeWaspConfigFromDevDependenciesArray original = diff --git a/waspc/cli/src/Wasp/Cli/Command/BuildStart/Config.hs b/waspc/cli/src/Wasp/Cli/Command/BuildStart/Config.hs index d9677ac33a..83ce87b477 100644 --- a/waspc/cli/src/Wasp/Cli/Command/BuildStart/Config.hs +++ b/waspc/cli/src/Wasp/Cli/Command/BuildStart/Config.hs @@ -12,7 +12,8 @@ module Wasp.Cli.Command.BuildStart.Config ) where -import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) +import Control.Monad.Except (MonadError (throwError)) +import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Char (toLower) import Data.List (intercalate) import StrongPath (()) diff --git a/waspc/cli/src/Wasp/Cli/Command/Common.hs b/waspc/cli/src/Wasp/Cli/Command/Common.hs index 21fae7622b..920f5753b3 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Common.hs @@ -29,7 +29,8 @@ readWaspCompileInfo waspDir = (return "No compile information found") where dotWaspInfoFile = - waspDir Project.Common.dotWaspDirInWaspProjectDir + waspDir + Project.Common.dotWaspDirInWaspProjectDir Project.Common.generatedCodeDirInDotWaspDir Project.Common.dotWaspInfoFileInGeneratedCodeDir diff --git a/waspc/cli/src/Wasp/Cli/Command/Compile.hs b/waspc/cli/src/Wasp/Cli/Command/Compile.hs index c1bb97d15f..b914c0146e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Compile.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Compile.hs @@ -47,7 +47,8 @@ compileWithOptions :: CompileOptions -> Command [CompileWarning] compileWithOptions options = do InWaspProject waspProjectDir <- require let outDir = - waspProjectDir dotWaspDirInWaspProjectDir + waspProjectDir + dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir cliSendMessageC $ Msg.Start "Compiling wasp project..." diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs index 7dc468bb1b..50d8bde545 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs @@ -7,7 +7,8 @@ where import Control.Arrow () import Control.Monad (unless) -import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) +import Control.Monad.Except (MonadError (throwError)) +import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Function ((&)) import Data.Functor ((<&>)) import Data.List (intercalate) @@ -50,41 +51,41 @@ createNewProjectInteractiveOnDisk waspProjectDir appName = do openAIApiKey <- getOpenAIApiKey appDescription <- liftIO $ Interactive.askForRequiredInput "Describe your app in a couple of sentences" (planningGptModel, codingGptModel) <- - liftIO $ - Interactive.askToChoose' + liftIO + $ Interactive.askToChoose' "Choose GPT model(s) you want to use:" - $ NE.fromList - [ Interactive.Option - "gpt-4o (planning + coding)" - (Just "Good results. Cheap and fast. Best cost/benefit ratio.") - (ChatGPT.GPT_4o, ChatGPT.GPT_4o), - Interactive.Option - "gpt-4 (planning) + gpt-4o (coding)" - (Just "Possibly better results, but somewhat slower and somewhat more expensive.") - (ChatGPT.GPT_4, ChatGPT.GPT_4o), - Interactive.Option - "gpt-4 (planning + coding)" - (Just "Possibly best results, but quite slower and quite more expensive.") - (ChatGPT.GPT_4, ChatGPT.GPT_4) - ] + $ NE.fromList + [ Interactive.Option + "gpt-4o (planning + coding)" + (Just "Good results. Cheap and fast. Best cost/benefit ratio.") + (ChatGPT.GPT_4o, ChatGPT.GPT_4o), + Interactive.Option + "gpt-4 (planning) + gpt-4o (coding)" + (Just "Possibly better results, but somewhat slower and somewhat more expensive.") + (ChatGPT.GPT_4, ChatGPT.GPT_4o), + Interactive.Option + "gpt-4 (planning + coding)" + (Just "Possibly best results, but quite slower and quite more expensive.") + (ChatGPT.GPT_4, ChatGPT.GPT_4) + ] temperature <- - liftIO $ - Interactive.askToChoose' + liftIO + $ Interactive.askToChoose' "Choose the creativity level (temperature):" - $ NE.fromList - [ Interactive.Option - "Balanced (0.7)" - (Just "Optimal trade-off between creativity and possible mistakes.") - 0.7, - Interactive.Option - "Conventional (0.4)" - (Just "Generates sensible code with minimal amount of mistakes.") - 0.4, - Interactive.Option - "Creative (1.0)" - (Just "Generates more creative code, but mistakes are more likely.") - 1.0 - ] + $ NE.fromList + [ Interactive.Option + "Balanced (0.7)" + (Just "Optimal trade-off between creativity and possible mistakes.") + 0.7, + Interactive.Option + "Conventional (0.4)" + (Just "Generates sensible code with minimal amount of mistakes.") + 0.4, + Interactive.Option + "Creative (1.0)" + (Just "Generates more creative code, but mistakes are more likely.") + 1.0 + ] let projectConfig = emptyNewProjectConfig { GNP.C.projectPlanningGptModel = Just planningGptModel, @@ -217,20 +218,20 @@ getOpenAIApiKey = validateKey k = Just k throwMissingOpenAIApiKeyEnvVarError = - throwError $ - CommandError + throwError + $ CommandError "Missing OPENAI_API_KEY environment variable" - $ unlines - [ "Wasp AI uses ChatGPT to generate your project, and therefore requires you to provide it with an OpenAI API key.", - "You can obtain this key via your OpenAI account's user settings (https://platform.openai.com/account/api-keys).", - "Then, set OPENAI_API_KEY env var to it and wasp CLI will read from it.", - "", - "To persist the OPENAI_API_KEY env var, add", - " export OPENAI_API_KEY=", - "to your .bash_profile (or .profile or .zprofile or whatever your machine is using), restart your shell, and you should be good to go.", - "", - "Alternatively, you can go to our Mage web app at https://usemage.ai and generate new Wasp app there for free, with no OpenAI API keys needed." - ] + $ unlines + [ "Wasp AI uses ChatGPT to generate your project, and therefore requires you to provide it with an OpenAI API key.", + "You can obtain this key via your OpenAI account's user settings (https://platform.openai.com/account/api-keys).", + "Then, set OPENAI_API_KEY env var to it and wasp CLI will read from it.", + "", + "To persist the OPENAI_API_KEY env var, add", + " export OPENAI_API_KEY=", + "to your .bash_profile (or .profile or .zprofile or whatever your machine is using), restart your shell, and you should be good to go.", + "", + "Alternatively, you can go to our Mage web app at https://usemage.ai and generate new Wasp app there for free, with no OpenAI API keys needed." + ] newProjectDetails :: NewProjectConfig -> String -> String -> NewProjectDetails newProjectDetails projectConfig webAppName webAppDescription = diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs index 8283945818..54810dc2e8 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs @@ -177,7 +177,8 @@ simpleGhReleaseArchiveTemplate (repoName, assetName) (tmplDisplayName, tmplDescr ( DirBasedTemplateMetadata { _name = tmplDisplayName, _description = tmplDescription, - _path = [reldir|.|], -- We assume that the archive contains files at its root. + -- We assume that the archive contains files at its root. + _path = [reldir|.|], _buildStartingInstructions = buildStartingInstructions } ) diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs index 176962f097..5937fe22a4 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Seed.hs @@ -75,7 +75,9 @@ obtainNameOfExistingSeedToRun maybeUserProvidedSeedName spec = do then return userProvidedSeedName else (E.throwError . CommandError "Invalid seed name") $ - "There is no seed with the name " <> userProvidedSeedName <> "." + "There is no seed with the name " + <> userProvidedSeedName + <> "." <> ("\nValid seed names are: " <> intercalate ", " (NE.toList seedNames) <> ".") getSeedsFromAppSpecOrThrowIfNone :: Command (NE.NonEmpty AS.ExtImport.ExtImport) diff --git a/waspc/cli/src/Wasp/Cli/Command/Info.hs b/waspc/cli/src/Wasp/Cli/Command/Info.hs index 0a5f567089..c162269f56 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Info.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Info.hs @@ -4,7 +4,7 @@ module Wasp.Cli.Command.Info where import Control.Arrow () -import Control.Monad.Except (MonadIO (liftIO)) +import Control.Monad.IO.Class (MonadIO (liftIO)) import StrongPath (Abs, Dir, Path', fromRelFile) import StrongPath.Operations () import System.Directory (getFileSize) diff --git a/waspc/cli/src/Wasp/Cli/Command/Studio.hs b/waspc/cli/src/Wasp/Cli/Command/Studio.hs index 321cf28763..27b480c724 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Studio.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Studio.hs @@ -122,7 +122,8 @@ studio = do ] let generatedProjectDir = - waspDir dotWaspDirInWaspProjectDir + waspDir + dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir let waspStudioDataJsonFilePath = generatedProjectDir [relfile|.wasp-studio-data.json|] diff --git a/waspc/cli/src/Wasp/Cli/FileSystem.hs b/waspc/cli/src/Wasp/Cli/FileSystem.hs index ff1ff29471..1698f05936 100644 --- a/waspc/cli/src/Wasp/Cli/FileSystem.hs +++ b/waspc/cli/src/Wasp/Cli/FileSystem.hs @@ -46,7 +46,7 @@ waspInstallationDirInHomeDir = [reldir|.local/share/wasp-lang|] waspExecutableInHomeDir :: Path' (Rel UserHomeDir) File' waspExecutableInHomeDir = [relfile|.local/bin/wasp|] -getAbsPathToDirInCwd :: MonadThrow m => String -> IO (m (Path' Abs (Dir d))) +getAbsPathToDirInCwd :: (MonadThrow m) => String -> IO (m (Path' Abs (Dir d))) getAbsPathToDirInCwd dirName = do absCwd <- SD.getCurrentDirectory return $ SP.parseAbsDir $ absCwd FP. dirName diff --git a/waspc/cli/src/Wasp/Cli/GithubRepo.hs b/waspc/cli/src/Wasp/Cli/GithubRepo.hs index 69d42324fa..5495ee720f 100644 --- a/waspc/cli/src/Wasp/Cli/GithubRepo.hs +++ b/waspc/cli/src/Wasp/Cli/GithubRepo.hs @@ -87,7 +87,7 @@ fetchFolderFromGithubRepoToDisk githubRepoRef folderInRepoRoot destinationOnDisk githubRepoArchiveRootFolderName :: Path' (Rel archiveRoot) (Dir archiveInnerDir) githubRepoArchiveRootFolderName = fromJust . SP.parseRelDir $ repoName ++ "-" ++ repoReferenceName -fetchRepoFileContents :: FromJSON a => GithubRepoRef -> String -> IO (Either String a) +fetchRepoFileContents :: (FromJSON a) => GithubRepoRef -> String -> IO (Either String a) fetchRepoFileContents githubRepo filePath = do try (HTTP.httpJSONEither ghRepoInfoRequest) <&> \case Right response -> either (Left . show) Right $ HTTP.getResponseBody response diff --git a/waspc/cli/src/Wasp/Cli/Interactive.hs b/waspc/cli/src/Wasp/Cli/Interactive.hs index 324272721d..20d90414d6 100644 --- a/waspc/cli/src/Wasp/Cli/Interactive.hs +++ b/waspc/cli/src/Wasp/Cli/Interactive.hs @@ -67,7 +67,7 @@ askForRequiredInput = repeatIfNull . askForInput askToChoose' :: String -> NonEmpty (Option o) -> IO o askToChoose' question options = oValue <$> askToChoose question options -askToChoose :: forall o. IsOption o => String -> NonEmpty o -> IO o +askToChoose :: forall o. (IsOption o) => String -> NonEmpty o -> IO o askToChoose _ (singleOption :| []) = return singleOption askToChoose question options = do putStrLn $ Term.applyStyles [Term.Bold] question @@ -128,7 +128,7 @@ askToChoose question options = do askForInput :: String -> IO String askForInput question = putStr (Term.applyStyles [Term.Bold] question) >> prompt -repeatIfNull :: Foldable t => IO (t a) -> IO (t a) +repeatIfNull :: (Foldable t) => IO (t a) -> IO (t a) repeatIfNull action = repeatUntil null "This field cannot be empty." action repeatUntil :: (a -> Bool) -> String -> IO a -> IO a diff --git a/waspc/cli/tests/DbMigrateTest.hs b/waspc/cli/tests/DbMigrateTest.hs index 232496b6a0..5f8ae88f1e 100644 --- a/waspc/cli/tests/DbMigrateTest.hs +++ b/waspc/cli/tests/DbMigrateTest.hs @@ -1,7 +1,7 @@ module DbMigrateTest where import Data.Either (isLeft) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Cli.Command.Db.Migrate (parseMigrateArgs) import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), defaultMigrateArgs) diff --git a/waspc/cli/tests/TerminalTest.hs b/waspc/cli/tests/TerminalTest.hs index 6f430ed89d..2bf9db7bb1 100644 --- a/waspc/cli/tests/TerminalTest.hs +++ b/waspc/cli/tests/TerminalTest.hs @@ -1,6 +1,6 @@ module TerminalTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Cli.Terminal ( asWaspMessage, ) diff --git a/waspc/cli/tests/Wasp/Cli/Command/Telemetry/ProjectTest.hs b/waspc/cli/tests/Wasp/Cli/Command/Telemetry/ProjectTest.hs index c37bbe24be..2d0ba1513c 100644 --- a/waspc/cli/tests/Wasp/Cli/Command/Telemetry/ProjectTest.hs +++ b/waspc/cli/tests/Wasp/Cli/Command/Telemetry/ProjectTest.hs @@ -1,6 +1,6 @@ module Wasp.Cli.Command.Telemetry.ProjectTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Cli.Command.Telemetry.Project ( checkIfEnvValueIsTruthy, ) diff --git a/waspc/cli/tests/Wasp/Cli/Util/EnvVarArgumentTest.hs b/waspc/cli/tests/Wasp/Cli/Util/EnvVarArgumentTest.hs index a4ed0aba46..33da13a747 100644 --- a/waspc/cli/tests/Wasp/Cli/Util/EnvVarArgumentTest.hs +++ b/waspc/cli/tests/Wasp/Cli/Util/EnvVarArgumentTest.hs @@ -1,6 +1,6 @@ module Wasp.Cli.Util.EnvVarArgumentTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Cli.Util.EnvVarArgument (envVarFromString) spec_envVarFromString :: Spec diff --git a/waspc/dev-tool.project b/waspc/dev-tool.project index 134477aaff..990d43779d 100644 --- a/waspc/dev-tool.project +++ b/waspc/dev-tool.project @@ -1,4 +1,4 @@ -with-compiler: ghc-9.0.2 +with-compiler: ghc-9.6.7 packages: . @@ -6,10 +6,10 @@ packages: . jobs: $ncpus constraints: - ormolu ==0.4.0.0, + ormolu ==0.7.2.0, stan ==0.2.1.0, - hlint ==3.5.0, + hlint ==3.6.1, prune-juice ==0.7 -- See cabal.project. -index-state: 2023-03-09T09:15:49Z +index-state: 2025-11-01T14:29:27Z diff --git a/waspc/e2e-tests/ShellCommands.hs b/waspc/e2e-tests/ShellCommands.hs index 7dbb420c2b..bef201db6b 100644 --- a/waspc/e2e-tests/ShellCommands.hs +++ b/waspc/e2e-tests/ShellCommands.hs @@ -63,8 +63,19 @@ appendToFile fileName content = replaceLineInFile :: FilePath -> Int -> String -> ShellCommandBuilder context ShellCommand replaceLineInFile fileName lineNumber line = return $ - "awk 'NR==" ++ show lineNumber ++ "{$0=" ++ show line ++ "}1' " ++ fileName ++ " > " ++ fileName ++ ".tmp" - ~&& "mv " ++ fileName ++ ".tmp " ++ fileName + "awk 'NR==" + ++ show lineNumber + ++ "{$0=" + ++ show line + ++ "}1' " + ++ fileName + ++ " > " + ++ fileName + ++ ".tmp" + ~&& "mv " + ++ fileName + ++ ".tmp " + ++ fileName waspCliNewMinimalStarter :: String -> ShellCommandBuilder context ShellCommand waspCliNewMinimalStarter appName = return $ "wasp-cli new " ++ appName ++ " -t minimal" diff --git a/waspc/e2e-tests/SnapshotTest/ShellCommands.hs b/waspc/e2e-tests/SnapshotTest/ShellCommands.hs index 2e1cd7f63d..e670806c9b 100644 --- a/waspc/e2e-tests/SnapshotTest/ShellCommands.hs +++ b/waspc/e2e-tests/SnapshotTest/ShellCommands.hs @@ -41,9 +41,11 @@ withInSnapshotWaspProjectDir :: withInSnapshotWaspProjectDir waspProjectCommandBuilders = do snapshotTestContext <- ask return $ - "cd " ++ fromAbsDir (_waspProjectDir $ _snapshotWaspProjectContext snapshotTestContext) - ~&& foldr1 (~&&) (snapshotWaspProjectCommands snapshotTestContext) - ~&& "cd " ++ fromAbsDir (_snapshotDir snapshotTestContext) + "cd " + ++ fromAbsDir (_waspProjectDir $ _snapshotWaspProjectContext snapshotTestContext) + ~&& foldr1 (~&&) (snapshotWaspProjectCommands snapshotTestContext) + ~&& "cd " + ++ fromAbsDir (_snapshotDir snapshotTestContext) where snapshotWaspProjectCommands :: SnapshotTestContext -> [ShellCommand] snapshotWaspProjectCommands snapshotTestContext = @@ -65,7 +67,8 @@ copyContentsOfGitTrackedDirToSnapshotWaspProjectDir srcDirFromGitRootDir = do copyFromSrcDirToSnapshotWaspProjectDir :: ShellCommand = "rsync -a --files-from=- " ++ fromRelDir (gitRootFromSnapshotDir srcDirFromGitRootDir) ++ " " ++ fromAbsDir snapshotWaspProjectDir in return $ - "mkdir -p " ++ fromAbsDir snapshotWaspProjectDir - ~&& listRelPathsOfGitTrackedFilesInSrcDir + "mkdir -p " + ++ fromAbsDir snapshotWaspProjectDir + ~&& listRelPathsOfGitTrackedFilesInSrcDir ~| stripSrcDirRelPrefixFromPaths ~| copyFromSrcDirToSnapshotWaspProjectDir diff --git a/waspc/e2e-tests/WaspProject/ShellCommands.hs b/waspc/e2e-tests/WaspProject/ShellCommands.hs index 5881794fe0..5dfef8ac49 100644 --- a/waspc/e2e-tests/WaspProject/ShellCommands.hs +++ b/waspc/e2e-tests/WaspProject/ShellCommands.hs @@ -54,10 +54,15 @@ validateWaspProjectDockerImageBuilds = do waspProjectDir = _waspProjectDir waspProjectContext in return $ "[ -z \"$WASP_E2E_TESTS_SKIP_DOCKER\" ]" - ~? "cd " ++ fromAbsDir (waspProjectDir dotWaspDirInWaspProjectDir buildDirInDotWaspDir) - ~&& "docker build --build-arg \"BUILDKIT_DOCKERFILE_CHECK=error=true\" -t " ++ dockerImageTag ++ " ." - ~&& "docker image rm " ++ dockerImageTag - ~&& "cd " ++ fromAbsDir waspProjectDir + ~? "cd " + ++ fromAbsDir (waspProjectDir dotWaspDirInWaspProjectDir buildDirInDotWaspDir) + ~&& "docker build --build-arg \"BUILDKIT_DOCKERFILE_CHECK=error=true\" -t " + ++ dockerImageTag + ++ " ." + ~&& "docker image rm " + ++ dockerImageTag + ~&& "cd " + ++ fromAbsDir waspProjectDir -- | We make the migration name deterministic by forcing it to be -- @no-date-@, instead of usual @-@. @@ -74,9 +79,10 @@ waspCliMigrate migrationName = do dbRootDirInProjectRootDir dbMigrationsDirInDbRootDir in return $ - "wasp-cli db migrate-dev --name " ++ migrationName - ~&& replaceMigrationDatePrefix (fromAbsDir waspMigrationsDir) - ~&& replaceMigrationDatePrefix (fromAbsDir waspOutMigrationsDir) + "wasp-cli db migrate-dev --name " + ++ migrationName + ~&& replaceMigrationDatePrefix (fromAbsDir waspMigrationsDir) + ~&& replaceMigrationDatePrefix (fromAbsDir waspOutMigrationsDir) where replaceMigrationDatePrefix migrationDirPath = "mv " ++ joinPath [migrationDirPath, "*" ++ migrationName] ++ " " ++ joinPath [migrationDirPath, "no-date-" ++ migrationName] diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json index 97de800161..d99245b0a4 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json @@ -1 +1 @@ -{"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"@node-rs/argon2","version":"^1.8.3"},{"name":"arctic","version":"^1.2.1"},{"name":"lucia","version":"^3.0.1"},{"name":"oslo","version":"^1.1.2"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0"},{"name":"nodemailer","version":"^6.9.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"socket.io-client","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"postcss","version":"^8.4.21"},{"name":"autoprefixer","version":"^10.4.13"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]},"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"chai","version":"^5.2.0"},{"name":"clsx","version":"^2.1.1"},{"name":"linebyline","version":"^1.3.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"tailwind-merge","version":"^2.6.0"},{"name":"wait-port","version":"^1.1.0"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@playwright/test","version":"1.51.1"},{"name":"@tailwindcss/forms","version":"^0.5.10"},{"name":"@tailwindcss/typography","version":"^0.5.16"},{"name":"@types/chai","version":"^5.2.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/uuid","version":"^9.0.8"},{"name":"@wasp.sh/wasp-app-runner","version":"^0.0.8"},{"name":"prisma","version":"5.19.1"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]},"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]}}} \ No newline at end of file +{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"chai","version":"^5.2.0"},{"name":"clsx","version":"^2.1.1"},{"name":"linebyline","version":"^1.3.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"tailwind-merge","version":"^2.6.0"},{"name":"wait-port","version":"^1.1.0"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@playwright/test","version":"1.51.1"},{"name":"@tailwindcss/forms","version":"^0.5.10"},{"name":"@tailwindcss/typography","version":"^0.5.16"},{"name":"@types/chai","version":"^5.2.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/uuid","version":"^9.0.8"},{"name":"@wasp.sh/wasp-app-runner","version":"^0.0.8"},{"name":"prisma","version":"5.19.1"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"@node-rs/argon2","version":"^1.8.3"},{"name":"arctic","version":"^1.2.1"},{"name":"lucia","version":"^3.0.1"},{"name":"oslo","version":"^1.1.2"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0"},{"name":"nodemailer","version":"^6.9.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"socket.io-client","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"postcss","version":"^8.4.21"},{"name":"autoprefixer","version":"^10.4.13"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/installedNpmDepsLog.json b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/installedNpmDepsLog.json index 56c14786f9..0e43b49f3c 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/installedNpmDepsLog.json +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/installedNpmDepsLog.json @@ -1 +1 @@ -{"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]},"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]},"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]}}} \ No newline at end of file +{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file diff --git a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/installedNpmDepsLog.json b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/installedNpmDepsLog.json index 56c14786f9..0e43b49f3c 100644 --- a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/installedNpmDepsLog.json +++ b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/installedNpmDepsLog.json @@ -1 +1 @@ -{"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]},"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]},"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]}}} \ No newline at end of file +{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file diff --git a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/installedNpmDepsLog.json b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/installedNpmDepsLog.json index 56c14786f9..0e43b49f3c 100644 --- a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/installedNpmDepsLog.json +++ b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/installedNpmDepsLog.json @@ -1 +1 @@ -{"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]},"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]},"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]}}} \ No newline at end of file +{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@types/react","version":"^18.0.37"},{"name":"prisma","version":"5.19.1"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file diff --git a/waspc/run b/waspc/run index 9f2ebd6973..f16d088817 100755 --- a/waspc/run +++ b/waspc/run @@ -78,7 +78,7 @@ RUN_CMD="cabal --project-dir=${PROJECT_ROOT} run wasp-cli -- ${ARGS[@]}" GHCID_CMD="ghcid --command=cabal repl" # HLS version below should be one that works with the GHC version from the # `with-compiler` field in `cabal.project` -GHCUP_SET="ghcup set hls 2.2.0.0" +GHCUP_SET="ghcup set hls 2.10.0.0" DEV_TOOLS_BIN="$PROJECT_ROOT/.bin" function install_dev_tool() { echo "cabal --project-file=$PROJECT_ROOT/dev-tool.project install $1 --installdir=$DEV_TOOLS_BIN --install-method=copy --overwrite-policy=always" diff --git a/waspc/src/FilePath/Extra.hs b/waspc/src/FilePath/Extra.hs index c328d83c0d..280dce4544 100644 --- a/waspc/src/FilePath/Extra.hs +++ b/waspc/src/FilePath/Extra.hs @@ -16,7 +16,8 @@ reversePosixPath path | null parts = "." | otherwise = assert (".." `notElem` parts) $ - FPP.joinPath $ map (const "..") parts + FPP.joinPath $ + map (const "..") parts where parts :: [String] parts = filter (/= ".") $ FPP.splitDirectories path diff --git a/waspc/src/Wasp/AI/CodeAgent.hs b/waspc/src/Wasp/AI/CodeAgent.hs index c22095bb26..1db3bbb228 100644 --- a/waspc/src/Wasp/AI/CodeAgent.hs +++ b/waspc/src/Wasp/AI/CodeAgent.hs @@ -61,7 +61,8 @@ runCodeAgent config codeAgent = Handler ( \(e :: SomeException) -> do _writeLog config $ - fromString $ "Code agent failed with the following error: " <> showShortException e + fromString $ + "Code agent failed with the following error: " <> showShortException e throwIO e ) ] @@ -77,10 +78,10 @@ runCodeAgent config codeAgent = then text else take maxLen text <> "..." - showShortException :: forall e. Exception e => e -> String + showShortException :: forall e. (Exception e) => e -> String showShortException = shortenWithEllipsisTo 30 . displayException -writeToLog :: IsString logMsg => logMsg -> CodeAgent logMsg () +writeToLog :: (IsString logMsg) => logMsg -> CodeAgent logMsg () writeToLog msg = asks _writeLog >>= \f -> liftIO $ f msg writeToFile :: FilePath -> (Maybe Text -> Text) -> CodeAgent logMsg () diff --git a/waspc/src/Wasp/AI/GenerateNewProject/Common.hs b/waspc/src/Wasp/AI/GenerateNewProject/Common.hs index a003436a87..f5052d5aa0 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/Common.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/Common.hs @@ -95,7 +95,7 @@ instance Aeson.FromJSON AuthProvider where -- TODO: Make these relative to WaspProjectDir, via StrongPath? type File = (FilePath, Text) -queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a +queryChatGPTForJSON :: (FromJSON a) => ChatGPTParams -> [ChatMessage] -> CodeAgent a queryChatGPTForJSON chatGPTParams initChatMsgs = doQueryForJSON 0 0 initChatMsgs where -- Retry logic here got a bit complex, here is a short explanation. diff --git a/waspc/src/Wasp/AI/GenerateNewProject/Plan.hs b/waspc/src/Wasp/AI/GenerateNewProject/Plan.hs index b202ee4eb0..3f679be4ae 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/Plan.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/Plan.hs @@ -44,7 +44,9 @@ type PlanRule = String generatePlan :: NewProjectDetails -> [PlanRule] -> CodeAgent Plan generatePlan newProjectDetails planRules = do writeToLog $ - "\n" <> L.styled L.Generating "Generating" <> " plan (slowest step, can take up to 90 seconds for slower models)" + "\n" + <> L.styled L.Generating "Generating" + <> " plan (slowest step, can take up to 90 seconds for slower models)" <> L.styled (L.Custom [Term.Blink]) "..." initialPlan <- queryChatGPTForJSON (planningChatGPTParams newProjectDetails) chatMessages writeToLog $ "Initial plan generated!\n" <> L.fromText (summarizePlan initialPlan) @@ -209,7 +211,9 @@ checkPlanForEntityIssues plan = expectedNumEntities = 2 in if numEntities < expectedNumEntities then - [ "There is only " <> show numEntities <> " entities in the plan," + [ "There is only " + <> show numEntities + <> " entities in the plan," <> (" I would expect at least " <> show expectedNumEntities <> " or more.") ] else [] @@ -222,7 +226,9 @@ checkPlanForEntityIssues plan = checkIfEntityBodyParses entity = case Psl.Parser.Model.parseBody (entityBodyPsl entity) of Left parseError -> - [ "Failed to parse PSL body of entity '" <> entityName entity <> "': " + [ "Failed to parse PSL body of entity '" + <> entityName entity + <> "': " <> show parseError ] Right _ -> [] @@ -267,7 +273,11 @@ checkPlanForOperationIssues opType plan = expectedNumOps = 1 in if numOps < expectedNumOps then - [ "There is only " <> show numOps <> " " <> caseOnOpType "queries" "actions" <> " in the plan," + [ "There is only " + <> show numOps + <> " " + <> caseOnOpType "queries" "actions" + <> " in the plan," <> (" I would expect at least " <> show expectedNumOps <> " or more.") ] else [] @@ -275,7 +285,10 @@ checkPlanForOperationIssues opType plan = checkOperationFnPath op = if not ("@src" `isPrefixOf` opFnPath op) then - [ "fn path of " <> caseOnOpType "query" "action" <> " '" <> opName op + [ "fn path of " + <> caseOnOpType "query" "action" + <> " '" + <> opName op <> "' must start with '@src'." ] else [] @@ -302,7 +315,9 @@ checkPlanForPageIssues plan = expectedNumPages = 1 in if numPages < expectedNumPages then - [ "There is only " <> show numPages <> " pages in the plan," + [ "There is only " + <> show numPages + <> " pages in the plan," <> (" I would expect at least " <> show expectedNumPages <> " or more.") ] else [] @@ -331,7 +346,7 @@ summarizePlan plan = - ${numPages} pages: ${pageNames} |] where - showT :: Show a => a -> Text + showT :: (Show a) => a -> Text showT = T.pack . show -- TODO: Alternative idea is to give quite more autonomy and equip it with tools (functions) it diff --git a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs index 0b2bef955b..2333c06654 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs @@ -60,7 +60,7 @@ bool = evaluation' . withCtx $ \ctx -> \case expr -> Left $ ER.mkEvaluationError ctx $ ER.ExpectedType T.BoolType (TypedAST.exprType expr) -- | An evaluation that expects a "Var" bound to a "Decl" of type "a". -declRef :: forall a. TD.IsDeclType a => TypedExprEvaluation (Ref a) +declRef :: forall a. (TD.IsDeclType a) => TypedExprEvaluation (Ref a) declRef = evaluation' . withCtx $ \ctx -> \case TypedAST.Var varName varType -> case varType of @@ -80,7 +80,7 @@ declRef = evaluation' . withCtx $ \ctx -> \case expectedType = T.DeclType expectedDeclTypeName -- | An evaluation that expects a "Var" bound to an "EnumType" for "a". -enum :: forall a. TD.IsEnumType a => TypedExprEvaluation a +enum :: forall a. (TD.IsEnumType a) => TypedExprEvaluation a enum = evaluation' . withCtx $ \ctx -> \case TypedAST.Var identifier _ -> case TD.enumEvaluate @a identifier of Nothing -> Left $ ER.mkEvaluationError ctx $ ER.InvalidEnumVariant enumName enumVariants identifier diff --git a/waspc/src/Wasp/Analyzer/Evaluator/EvaluationError.hs b/waspc/src/Wasp/Analyzer/Evaluator/EvaluationError.hs index 0186baf656..56180dce10 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/EvaluationError.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/EvaluationError.hs @@ -98,7 +98,8 @@ getErrorMsgAndErrorCtxMsgsAndParsingCtx (EvaluationError (WithCtx ctx evalError) UndefinedVariable varName -> makeMainMsg $ "Undefined variable " ++ varName InvalidEnumVariant enumType validEnumVariants actualEnumVariant -> makeMainMsg $ - "Expected value of enum type '" ++ enumType + "Expected value of enum type '" + ++ enumType ++ "' but got value '" ++ actualEnumVariant ++ "'\n" diff --git a/waspc/src/Wasp/Analyzer/Parser/AST/PrettyPrinter.hs b/waspc/src/Wasp/Analyzer/Parser/AST/PrettyPrinter.hs index 39f3ba93ec..d459b03944 100644 --- a/waspc/src/Wasp/Analyzer/Parser/AST/PrettyPrinter.hs +++ b/waspc/src/Wasp/Analyzer/Parser/AST/PrettyPrinter.hs @@ -18,7 +18,12 @@ prettyPrintAST (AST stmts) = "(AST\n" ++ indent 2 (concatMap prettyPrintStmt stm prettyPrintStmt :: WithCtx Stmt -> String prettyPrintStmt (WithCtx ctx (Decl typ name body)) = - prettyPrintWithCtx "(Decl" ctx ++ " type=" ++ typ ++ " name=" ++ name ++ "\n" + prettyPrintWithCtx "(Decl" ctx + ++ " type=" + ++ typ + ++ " name=" + ++ name + ++ "\n" ++ indent 2 (prettyPrintExpr body) ++ ")\n" @@ -54,7 +59,7 @@ prettyPrintExpr (WithCtx ctx expr) = "(" ++ prettyPrintWithCtx (exprName expr) c showExtImportName (ExtImportField name) = "field=" ++ name showExtImportName (ExtImportModule name) = "module=" ++ name - showLiteral :: Show a => a -> String + showLiteral :: (Show a) => a -> String showLiteral value = " value=" ++ show value prettyPrintWithCtx :: String -> Ctx -> String diff --git a/waspc/src/Wasp/Analyzer/Parser/ConcreteParser.hs b/waspc/src/Wasp/Analyzer/Parser/ConcreteParser.hs index 8dc245fcff..35e269a7f7 100644 --- a/waspc/src/Wasp/Analyzer/Parser/ConcreteParser.hs +++ b/waspc/src/Wasp/Analyzer/Parser/ConcreteParser.hs @@ -76,12 +76,17 @@ quoter = Quoter <$$> lquote <> quoterTail extImport :: GrammarRule extImport = ExtImport - <$$> kwImport <> extImportName <> kwFrom <> (T.String `as` ExtImportPath) + <$$> kwImport + <> extImportName + <> kwFrom + <> (T.String `as` ExtImportPath) where extImportName :: GrammarRule extImportName = - lcurly <> (T.Identifier `as` ExtImportField) <> rcurly - <|> (T.Identifier `as` ExtImportModule) + lcurly + <> (T.Identifier `as` ExtImportField) + <> rcurly + <|> (T.Identifier `as` ExtImportModule) -- | @listLike open value sep close@ parses list like structures in the form: -- diff --git a/waspc/src/Wasp/Analyzer/Parser/ConcreteParser/ParserLib.hs b/waspc/src/Wasp/Analyzer/Parser/ConcreteParser/ParserLib.hs index a1ad001230..4576b9e0c2 100644 --- a/waspc/src/Wasp/Analyzer/Parser/ConcreteParser/ParserLib.hs +++ b/waspc/src/Wasp/Analyzer/Parser/ConcreteParser/ParserLib.hs @@ -770,5 +770,5 @@ eof = EOF -- UTILITIES: -tryError :: MonadError e m => m () -> m (Maybe e) +tryError :: (MonadError e m) => m () -> m (Maybe e) tryError m = (m >> return Nothing) `catchError` (return . Just) diff --git a/waspc/src/Wasp/Analyzer/Parser/Lexer/Internal.hs b/waspc/src/Wasp/Analyzer/Parser/Lexer/Internal.hs index 2290acd81d..0a6cdb53b7 100644 --- a/waspc/src/Wasp/Analyzer/Parser/Lexer/Internal.hs +++ b/waspc/src/Wasp/Analyzer/Parser/Lexer/Internal.hs @@ -57,9 +57,9 @@ getInput = lstateInput <$> get updateInput :: Int -> Lexer () updateInput !consumed = do (LexInput _ _ remaining) <- getInput - let newInput = - let (prevChar : remaining') = drop (consumed - 1) remaining - in LexInput prevChar [] remaining' + let newInput = case drop (consumed - 1) remaining of + (prevChar : remaining') -> LexInput prevChar [] remaining' + [] -> error "This should never happen. Lexer should never attempt to consume more input than remaining, but it did." modify $ \s -> s {lstateInput = newInput} getStartCode :: Lexer LexerStartCode diff --git a/waspc/src/Wasp/Analyzer/TypeDefinitions/Class/IsEnumType.hs b/waspc/src/Wasp/Analyzer/TypeDefinitions/Class/IsEnumType.hs index e8f4c94369..eef09ae8f9 100644 --- a/waspc/src/Wasp/Analyzer/TypeDefinitions/Class/IsEnumType.hs +++ b/waspc/src/Wasp/Analyzer/TypeDefinitions/Class/IsEnumType.hs @@ -13,7 +13,7 @@ import Wasp.Analyzer.TypeDefinitions.Internal (EnumType) -- -- NOTE: If this Haskell type satisfies certain requirements, the IsEnumType instance for it -- can be automatically derived from its shape by using 'Analyzer.Evaluator.TH.makeEnumType'. -class Typeable a => IsEnumType a where +class (Typeable a) => IsEnumType a where enumType :: EnumType -- | Converts a string to a Haskell value of this type. diff --git a/waspc/src/Wasp/AppSpec.hs b/waspc/src/Wasp/AppSpec.hs index bb4b21a652..d9b659a2ea 100644 --- a/waspc/src/Wasp/AppSpec.hs +++ b/waspc/src/Wasp/AppSpec.hs @@ -99,7 +99,7 @@ data AppSpec = AppSpec -- Or @WithName@ or just @Named@. -- I like the best: `newtype Named a = Named (String, a)` -- I created a github issue for it: https://github.com/wasp-lang/wasp/issues/426 . -getDecls :: IsDecl a => AppSpec -> [(String, a)] +getDecls :: (IsDecl a) => AppSpec -> [(String, a)] getDecls = takeDecls . decls getEntities :: AppSpec -> [(String, Entity)] @@ -148,8 +148,8 @@ resolveRef spec ref = ++ "." ++ " This should never happen, as Analyzer should ensure all references in AppSpec are valid." ) - $ find ((== refName ref) . fst) $ - getDecls spec + $ find ((== refName ref) . fst) + $ getDecls spec asAbsWaspProjectDirFile :: AppSpec -> Path' (Rel WaspProjectDir) File' -> Path' Abs File' asAbsWaspProjectDirFile spec file = waspProjectDir spec file diff --git a/waspc/src/Wasp/AppSpec/App/Auth.hs b/waspc/src/Wasp/AppSpec/App/Auth.hs index 02b148b2a7..54006c2459 100644 --- a/waspc/src/Wasp/AppSpec/App/Auth.hs +++ b/waspc/src/Wasp/AppSpec/App/Auth.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedRecordDot #-} module Wasp.AppSpec.App.Auth ( Auth (..), @@ -113,13 +114,13 @@ isEmailAuthEnabled :: Auth -> Bool isEmailAuthEnabled = isJust . email . methods -- These helper functions are used to avoid ambiguity when using the --- `userSignupFields` function (otherwise we need to use the DuplicateRecordFields --- extension in each module that uses them). +-- `userSignupFields` function (otherwise we need to use DuplicateRecordFields +-- and OverloadedRecordDot extension in each module that uses them). userSignupFieldsForEmailAuth :: EmailAuthConfig -> Maybe ExtImport -userSignupFieldsForEmailAuth = userSignupFields +userSignupFieldsForEmailAuth = (.userSignupFields) userSignupFieldsForUsernameAuth :: UsernameAndPasswordConfig -> Maybe ExtImport -userSignupFieldsForUsernameAuth = userSignupFields +userSignupFieldsForUsernameAuth = (.userSignupFields) userSignupFieldsForExternalAuth :: ExternalAuthConfig -> Maybe ExtImport -userSignupFieldsForExternalAuth = userSignupFields +userSignupFieldsForExternalAuth = (.userSignupFields) diff --git a/waspc/src/Wasp/AppSpec/Core/Ref.hs b/waspc/src/Wasp/AppSpec/Core/Ref.hs index c2c4fbb64d..2d4c6d6bd9 100644 --- a/waspc/src/Wasp/AppSpec/Core/Ref.hs +++ b/waspc/src/Wasp/AppSpec/Core/Ref.hs @@ -19,9 +19,9 @@ import Wasp.AppSpec.Core.IsDecl (IsDecl (declTypeName)) data Ref a where Ref :: (IsDecl a) => String -> Ref a -deriving instance Eq a => Eq (Ref a) +deriving instance (Eq a) => Eq (Ref a) -deriving instance Show a => Show (Ref a) +deriving instance (Show a) => Show (Ref a) deriving instance (IsDecl a, Data a) => Data (Ref a) @@ -36,7 +36,8 @@ instance (IsDecl a) => FromJSON (Ref a) where then pure $ Ref name else fail $ - "Expected declType to be '" <> declTypeName @a + "Expected declType to be '" + <> declTypeName @a <> "', but it was '" <> declType <> "'." diff --git a/waspc/src/Wasp/AppSpec/Job.hs b/waspc/src/Wasp/AppSpec/Job.hs index 70ae76ee1e..e17f62716e 100644 --- a/waspc/src/Wasp/AppSpec/Job.hs +++ b/waspc/src/Wasp/AppSpec/Job.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedRecordDot #-} module Wasp.AppSpec.Job ( Job (..), @@ -72,12 +73,12 @@ jobExecutors = enumFrom minBound :: [JobExecutor] -- Helpers to disambiguate duplicate field `executorOptions`. performExecutorOptionsJson :: Job -> Maybe JSON performExecutorOptionsJson job = - executorOptionsJson (executor job) (executorOptions (perform job :: Perform)) + executorOptionsJson (executor job) job.perform.executorOptions scheduleExecutorOptionsJson :: Job -> Maybe JSON scheduleExecutorOptionsJson job = do s <- schedule job - executorOptionsJson (executor job) (executorOptions (s :: Schedule)) + executorOptionsJson (executor job) s.executorOptions executorOptionsJson :: JobExecutor -> Maybe ExecutorOptions -> Maybe JSON executorOptionsJson PgBoss (Just ExecutorOptions {pgBoss = Just json}) = Just json diff --git a/waspc/src/Wasp/AppSpec/Valid.hs b/waspc/src/Wasp/AppSpec/Valid.hs index 07f87f08c0..d82a99f3c3 100644 --- a/waspc/src/Wasp/AppSpec/Valid.hs +++ b/waspc/src/Wasp/AppSpec/Valid.hs @@ -278,7 +278,7 @@ validateUniqueDeclarationNames spec = checkIfDeclarationsAreUnique "job" (AS.getJobs spec) ] where - checkIfDeclarationsAreUnique :: IsDecl a => String -> [(String, a)] -> [ValidationError] + checkIfDeclarationsAreUnique :: (IsDecl a) => String -> [(String, a)] -> [ValidationError] checkIfDeclarationsAreUnique declTypeName decls = case duplicateDeclNames of [] -> [] (firstDuplicateDeclName : _) -> @@ -442,4 +442,7 @@ getIdFieldFromCrudEntity spec crud = fromJust $ Entity.getIdField crudEntity getLowestNodeVersionUserAllows :: AppSpec -> SV.Version getLowestNodeVersionUserAllows spec = fromMaybe (error "This should never happen: user Node version range lower bound is Inf") $ - SVB.versionFromBound $ fst $ SVB.versionBounds $ AS.userNodeVersionRange spec + SVB.versionFromBound $ + fst $ + SVB.versionBounds $ + AS.userNodeVersionRange spec diff --git a/waspc/src/Wasp/Generator/Crud.hs b/waspc/src/Wasp/Generator/Crud.hs index 139f68a3b1..03178c96af 100644 --- a/waspc/src/Wasp/Generator/Crud.hs +++ b/waspc/src/Wasp/Generator/Crud.hs @@ -11,9 +11,9 @@ where import Data.Aeson (object, (.=)) import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Key as Key import qualified Data.Aeson.Types as Aeson.Types import Data.Maybe (catMaybes, fromJust, fromMaybe) -import qualified Data.Text as T import StrongPath (File', Path', Rel) import qualified StrongPath as SP import qualified Wasp.AppSpec as AS @@ -68,4 +68,4 @@ crudDeclarationToOperationsList crud = makeCrudOperationKeyAndJsonPair :: AS.Crud.CrudOperation -> Aeson.Value -> Aeson.Types.Pair makeCrudOperationKeyAndJsonPair operation json = key .= json where - key = T.pack . show $ operation + key = Key.fromString . show $ operation diff --git a/waspc/src/Wasp/Generator/DbGenerator/Common.hs b/waspc/src/Wasp/Generator/DbGenerator/Common.hs index 25689f8c79..3d743c7d0d 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Common.hs @@ -65,10 +65,10 @@ dbSchemaFileInDbTemplatesDir = [relfile|schema.prisma|] dbSchemaFileInDbRootDir :: Path' (Rel DbRootDir) (File PrismaDbSchema) dbSchemaFileInDbRootDir = [relfile|schema.prisma|] -dbRootDirFromAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (Dir DbRootDir) +dbRootDirFromAppComponentDir :: (AppComponentRootDir d) => Path' (Rel d) (Dir DbRootDir) dbRootDirFromAppComponentDir = [reldir|../db|] -dbSchemaFileFromAppComponentDir :: AppComponentRootDir d => Path' (Rel d) (File PrismaDbSchema) +dbSchemaFileFromAppComponentDir :: (AppComponentRootDir d) => Path' (Rel d) (File PrismaDbSchema) dbSchemaFileFromAppComponentDir = dbRootDirFromAppComponentDir dbSchemaFileInDbRootDir dbSchemaFileInProjectRootDir :: Path' (Rel ProjectRootDir) (File PrismaDbSchema) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs index 1b18d8d207..9733968bfc 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs @@ -94,16 +94,17 @@ copyMigrationsBackToSourceIfTheyExist genProjectRootDirAbs dbMigrationsDirInWasp True -> copyMigrationsDir where copyMigrationsDir = - copyDirectoryRecursive genProjectMigrationsDir waspMigrationsDir >> return (Right ()) - `catch` (\e -> return $ Left $ show (e :: P.PathException)) - `catch` (\e -> return $ Left $ show (e :: IOError)) + copyDirectoryRecursive genProjectMigrationsDir waspMigrationsDir + >> return (Right ()) + `catch` (\e -> return $ Left $ show (e :: P.PathException)) + `catch` (\e -> return $ Left $ show (e :: IOError)) waspMigrationsDir = dbMigrationsDirInWaspProjectDirAbs genProjectMigrationsDir = genProjectRootDirAbs dbRootDirInProjectRootDir dbMigrationsDirInDbRootDir -- | This function assumes the DB schema has been generated, as it will attempt to read it from the generated code. writeDbSchemaChecksumToFile :: - DbSchemaChecksumFile f => + (DbSchemaChecksumFile f) => Path' Abs (Dir ProjectRootDir) -> Path' (Rel ProjectRootDir) (File f) -> IO () @@ -116,7 +117,7 @@ writeDbSchemaChecksumToFile genProjectRootDirAbs dbSchemaChecksumInProjectRootDi dbSchemaChecksumFile = genProjectRootDirAbs dbSchemaChecksumInProjectRootDir removeDbSchemaChecksumFile :: - DbSchemaChecksumFile f => + (DbSchemaChecksumFile f) => Path' Abs (Dir ProjectRootDir) -> Path' (Rel ProjectRootDir) (File f) -> IO () diff --git a/waspc/src/Wasp/Generator/FileDraft/TemplateFileDraft.hs b/waspc/src/Wasp/Generator/FileDraft/TemplateFileDraft.hs index 6407fa2ead..ca7fc33137 100644 --- a/waspc/src/Wasp/Generator/FileDraft/TemplateFileDraft.hs +++ b/waspc/src/Wasp/Generator/FileDraft/TemplateFileDraft.hs @@ -46,7 +46,7 @@ instance Writeable TemplateFileDraft where getDstPath draft = Left $ _dstPath draft -processBasedOnTmplDataExistence :: WriteableMonad m => TemplateFileDraft -> (Path' Abs File' -> m b) -> (Text -> m b) -> m b +processBasedOnTmplDataExistence :: (WriteableMonad m) => TemplateFileDraft -> (Path' Abs File' -> m b) -> (Text -> m b) -> m b processBasedOnTmplDataExistence draft onNoTmplData onTmplData = case _tmplData draft of Nothing -> do absDraftSrcPath <- getTemplateFileAbsPath (_srcPathInTmplDir draft) diff --git a/waspc/src/Wasp/Generator/JsImport.hs b/waspc/src/Wasp/Generator/JsImport.hs index 9df2fea89f..04c5b84d5a 100644 --- a/waspc/src/Wasp/Generator/JsImport.hs +++ b/waspc/src/Wasp/Generator/JsImport.hs @@ -21,7 +21,7 @@ import Wasp.JsImport ) extImportToJsImport :: - GeneratedSrcDir d => + (GeneratedSrcDir d) => Path Posix (Rel d) (Dir GeneratedExternalCodeDir) -> Path Posix (Rel importLocation) (Dir d) -> EI.ExtImport -> diff --git a/waspc/src/Wasp/Generator/NpmInstall.hs b/waspc/src/Wasp/Generator/NpmInstall.hs index 9d47ff7f99..64ba3dfee3 100644 --- a/waspc/src/Wasp/Generator/NpmInstall.hs +++ b/waspc/src/Wasp/Generator/NpmInstall.hs @@ -5,7 +5,8 @@ where import Control.Concurrent (Chan, newChan, readChan, threadDelay, writeChan) import Control.Concurrent.Async (concurrently) -import Control.Monad.Except (MonadError (throwError), runExceptT, when) +import Control.Monad (when) +import Control.Monad.Except (MonadError (throwError), runExceptT) import Control.Monad.IO.Class (liftIO) import Data.Function ((&)) import Data.Functor ((<&>)) diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Client/OperationsGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator/Client/OperationsGenerator.hs index 8e69d73d1f..7e1a771457 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Client/OperationsGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Client/OperationsGenerator.hs @@ -137,4 +137,5 @@ getOperationTypeData operation = tmplData makeSdkImportPath $ relDirToRelFileP $ fromJust $ - SP.relDirToPosix $ serverOperationsDirInSdkRootDir operation + SP.relDirToPosix $ + serverOperationsDirInSdkRootDir operation diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Server/JobGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator/Server/JobGenerator.hs index 1de303d215..f08db3f56d 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Server/JobGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Server/JobGenerator.hs @@ -54,11 +54,11 @@ genIndexTs jobs = return $ C.mkTmplFdWithData tmplFile tmplData genJob :: (String, Job) -> Generator FileDraft genJob (jobName, job) = - return $ - C.mkTmplFdWithDstAndData + return + $ C.mkTmplFdWithDstAndData tmplFile dstFile - $ Just tmplData + $ Just tmplData where tmplFile = [relfile|server/jobs/_job.ts|] dstFile = [reldir|server/jobs|] fromJust (SP.parseRelFile $ jobName ++ ".ts") diff --git a/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs index 50bdf2b212..e0ae6da86d 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs @@ -58,7 +58,8 @@ genRegisterJob (jobName, job) = jobPerformFn = SJI.extImportToImportJson relPathFromJobsDirToServerSrcDir $ - Just $ (J.fn . J.perform) job + Just $ + (J.fn . J.perform) job relPathFromJobsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir) relPathFromJobsDirToServerSrcDir = [reldirP|../|] diff --git a/waspc/src/Wasp/Project/Common.hs b/waspc/src/Wasp/Project/Common.hs index 1ca3fe5c51..00818eeb44 100644 --- a/waspc/src/Wasp/Project/Common.hs +++ b/waspc/src/Wasp/Project/Common.hs @@ -81,7 +81,7 @@ buildDirInDotWaspDir = [reldir|build|] -- TODO: This backwards relative path relies on multiple forward relative path -- definitions. We should find a better way to express it (e.g., by somehow -- calculating it from existing definitions) -waspProjectDirFromAppComponentDir :: G.Common.AppComponentRootDir d => Path' (Rel d) (Dir WaspProjectDir) +waspProjectDirFromAppComponentDir :: (G.Common.AppComponentRootDir d) => Path' (Rel d) (Dir WaspProjectDir) waspProjectDirFromAppComponentDir = [reldir|../../../|] -- | NOTE: This path is calculated from the values of @dotWaspDirInWaspProjectDir@, diff --git a/waspc/src/Wasp/Project/Waspignore.hs b/waspc/src/Wasp/Project/Waspignore.hs index 3bcd3e5897..1232cea61f 100644 --- a/waspc/src/Wasp/Project/Waspignore.hs +++ b/waspc/src/Wasp/Project/Waspignore.hs @@ -27,7 +27,7 @@ instance AffectedByWaspignoreFile SourceExternalCodeDir instance AffectedByWaspignoreFile SourceExternalPublicDir getNotIgnoredRelFilePaths :: - AffectedByWaspignoreFile d => + (AffectedByWaspignoreFile d) => Path' Abs File' -> Path' Abs (Dir d) -> IO [Path' (Rel d) File'] diff --git a/waspc/src/Wasp/Psl/Generator/Attribute.hs b/waspc/src/Wasp/Psl/Generator/Attribute.hs index 26c43686f7..7771463e9c 100644 --- a/waspc/src/Wasp/Psl/Generator/Attribute.hs +++ b/waspc/src/Wasp/Psl/Generator/Attribute.hs @@ -10,7 +10,8 @@ import Wasp.Psl.Generator.Common (PslSource) generateAttribute :: Psl.Attribute.Attribute -> PslSource generateAttribute attribute = - "@" ++ Psl.Attribute._attrName attribute + "@" + ++ Psl.Attribute._attrName attribute ++ if null (Psl.Attribute._attrArgs attribute) then "" else "(" ++ intercalate ", " (map generateArgument (Psl.Attribute._attrArgs attribute)) ++ ")" diff --git a/waspc/src/Wasp/Psl/Generator/Model.hs b/waspc/src/Wasp/Psl/Generator/Model.hs index 407fe1ad06..ce2f309a3a 100644 --- a/waspc/src/Wasp/Psl/Generator/Model.hs +++ b/waspc/src/Wasp/Psl/Generator/Model.hs @@ -13,7 +13,8 @@ generateModelBody (Psl.Model.Body elements) = unlines $ generateWithCtx generate generateModelElement :: Psl.Model.Element -> PslSource generateModelElement (Psl.Model.ElementField field) = - Psl.Model._name field ++ " " + Psl.Model._name field + ++ " " ++ generateModelFieldType (Psl.Model._type field) ++ concatMap generateModelFieldTypeModifier (Psl.Model._typeModifiers field) ++ concatMap ((" " ++) . generateAttribute) (Psl.Model._attrs field) diff --git a/waspc/src/Wasp/Psl/Parser/Argument.hs b/waspc/src/Wasp/Psl/Parser/Argument.hs index ebce922225..445451148b 100644 --- a/waspc/src/Wasp/Psl/Parser/Argument.hs +++ b/waspc/src/Wasp/Psl/Parser/Argument.hs @@ -59,7 +59,8 @@ stringExpr = Psl.Argument.StringExpr <$> stringLiteral funcCallExpr :: Parser Psl.Argument.Expression funcCallExpr = Psl.Argument.FuncExpr - <$> identifier <*> argumentList + <$> identifier + <*> argumentList arrayExpr :: Parser Psl.Argument.Expression arrayExpr = diff --git a/waspc/src/Wasp/TypeScript/Inspect/Exports.hs b/waspc/src/Wasp/TypeScript/Inspect/Exports.hs index 09c9ec935c..49421379cc 100644 --- a/waspc/src/Wasp/TypeScript/Inspect/Exports.hs +++ b/waspc/src/Wasp/TypeScript/Inspect/Exports.hs @@ -51,7 +51,8 @@ getExportsOfTsFiles requests = do groupExportRequestsByTsconfig :: [TsExportsRequest] -> [TsExportsRequest] groupExportRequestsByTsconfig requests = map (uncurry $ flip TsExportsRequest) $ - M.toList $ foldr insertRequest M.empty requests + M.toList $ + foldr insertRequest M.empty requests where insertRequest (TsExportsRequest names maybeTsconfig) grouped = M.insertWith (++) maybeTsconfig names grouped diff --git a/waspc/src/Wasp/Util.hs b/waspc/src/Wasp/Util.hs index 252b6709d0..340d9c7356 100644 --- a/waspc/src/Wasp/Util.hs +++ b/waspc/src/Wasp/Util.hs @@ -48,17 +48,17 @@ import Control.Applicative (liftA2) import Control.Monad (unless, when) import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KM import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.UTF8 as BSU import Data.Char (isSpace, isUpper, toLower, toUpper) -import qualified Data.HashMap.Strict as M import Data.List (group, intercalate, sort) import Data.List.Split (splitOn, wordsBy) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T -import qualified Data.Text as Text import qualified Data.Text.Encoding as TextEncoding import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TLE @@ -107,8 +107,8 @@ headSafe xs = Just (head xs) second3 :: (b -> d) -> (a, b, c) -> (a, d, c) second3 f (x, y, z) = (x, f y, z) -jsonSet :: Text.Text -> Aeson.Value -> Aeson.Value -> Aeson.Value -jsonSet key value (Aeson.Object o) = Aeson.Object $ M.insert key value o +jsonSet :: Key.Key -> Aeson.Value -> Aeson.Value -> Aeson.Value +jsonSet key value (Aeson.Object o) = Aeson.Object $ KM.insert key value o jsonSet _ _ _ = error "Input JSON must be an object" indent :: Int -> String -> String @@ -138,10 +138,11 @@ indent numSpaces = intercalate "\n" . map (toEmptyStringIfAllWhiteSpace . (inden -- Written to file bar.txt -- @ concatShortPrefixAndText :: String -> String -> String -concatShortPrefixAndText prefix "" = prefix concatShortPrefixAndText prefix text = - let (l : ls) = lines text - in prefix ++ l ++ if null ls then "" else "\n" ++ indent (length prefix) (intercalate "\n" ls) + prefix <> case lines text of + [] -> "" + [l] -> l + (l : ls) -> l <> "\n" <> indent (length prefix) (intercalate "\n" ls) -- | Given a prefix and text, concatenates them in the following manner: -- - If just one line of text: @@ -189,21 +190,21 @@ trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace infixr 5 <++> -(<++>) :: Applicative f => f [a] -> f [a] -> f [a] +(<++>) :: (Applicative f) => f [a] -> f [a] -> f [a] (<++>) = liftA2 (++) infixr 5 <:> -(<:>) :: Applicative f => f a -> f [a] -> f [a] +(<:>) :: (Applicative f) => f a -> f [a] -> f [a] (<:>) = liftA2 (:) -ifM :: Monad m => m Bool -> m a -> m a -> m a +ifM :: (Monad m) => m Bool -> m a -> m a -> m a ifM p x y = p >>= \b -> if b then x else y -whenM :: Monad m => m Bool -> m () -> m () +whenM :: (Monad m) => m Bool -> m () -> m () whenM ma mb = ma >>= (`when` mb) -unlessM :: Monad m => m Bool -> m () -> m () +unlessM :: (Monad m) => m Bool -> m () -> m () unlessM ma mb = ma >>= (`unless` mb) type Checksum = Hex @@ -272,5 +273,5 @@ textToLazyBS = TLE.encodeUtf8 . TL.fromStrict secondsToMicroSeconds :: Int -> Int secondsToMicroSeconds = (* 1000000) -findDuplicateElems :: Ord a => [a] -> [a] +findDuplicateElems :: (Ord a) => [a] -> [a] findDuplicateElems = map head . filter ((> 1) . length) . group . sort diff --git a/waspc/src/Wasp/Util/Aeson.hs b/waspc/src/Wasp/Util/Aeson.hs index 488417528d..659a60a596 100644 --- a/waspc/src/Wasp/Util/Aeson.hs +++ b/waspc/src/Wasp/Util/Aeson.hs @@ -12,11 +12,11 @@ import Data.Text (Text) import Data.Text.Lazy (toStrict) import Data.Text.Lazy.Builder (toLazyText) -encodeToText :: ToJSON a => a -> Text +encodeToText :: (ToJSON a) => a -> Text encodeToText = toStrict . toLazyText . encodeToTextBuilder -decodeFromString :: FromJSON a => String -> Either String a +decodeFromString :: (FromJSON a) => String -> Either String a decodeFromString = eitherDecode . BS.fromString -encodeToString :: ToJSON a => a -> String +encodeToString :: (ToJSON a) => a -> String encodeToString = BS.toString . encode diff --git a/waspc/src/Wasp/Util/Control/Monad.hs b/waspc/src/Wasp/Util/Control/Monad.hs index 5e8f1d67da..15777d5b55 100644 --- a/waspc/src/Wasp/Util/Control/Monad.hs +++ b/waspc/src/Wasp/Util/Control/Monad.hs @@ -46,7 +46,7 @@ foldM1 f (x :| xs) = foldM f x xs -- | Analogue of 'until'. @'untilM' p f b@ yields the result of applying @f@ -- until @p@ is true. -untilM :: Monad m => (a -> Bool) -> (a -> m a) -> a -> m a +untilM :: (Monad m) => (a -> Bool) -> (a -> m a) -> a -> m a untilM predicate f base | predicate base = return base | otherwise = f base >>= untilM predicate f diff --git a/waspc/src/Wasp/Util/Json.hs b/waspc/src/Wasp/Util/Json.hs index 6a2262a703..1d5f934233 100644 --- a/waspc/src/Wasp/Util/Json.hs +++ b/waspc/src/Wasp/Util/Json.hs @@ -16,7 +16,7 @@ import qualified Wasp.Util.IO as IOUtil -- | Uses Node.js to parse JSON with comments by treating it as a JavaScript object. -- We use this technique because Aeson can't read JSON with comments and we didn't want to write -- a custom parser. -parseJsonWithComments :: FromJSON a => String -> IO (Either String a) +parseJsonWithComments :: (FromJSON a) => String -> IO (Either String a) parseJsonWithComments jsonStr = do let evalScript = "const v = " ++ jsonStr ++ ";console.log(JSON.stringify(v));" let cp = P.proc "node" ["-e", evalScript] diff --git a/waspc/src/Wasp/Util/Network/HTTP.hs b/waspc/src/Wasp/Util/Network/HTTP.hs index fba0b7e8d4..eccef09642 100644 --- a/waspc/src/Wasp/Util/Network/HTTP.hs +++ b/waspc/src/Wasp/Util/Network/HTTP.hs @@ -15,7 +15,7 @@ import qualified Network.HTTP.Simple as HTTP import UnliftIO (MonadUnliftIO) import UnliftIO.Exception (catch, throwIO) -catchRetryableHttpException :: MonadUnliftIO m => m a -> (HTTP.HttpException -> m a) -> m a +catchRetryableHttpException :: (MonadUnliftIO m) => m a -> (HTTP.HttpException -> m a) -> m a catchRetryableHttpException action handle = action `catch` ( \e -> case e of @@ -45,6 +45,7 @@ httpJSONThatThrowsIfNot2xx request = do let statusCode = HTTP.getResponseStatusCode response when (statusCode < 200 || statusCode >= 300) $ - throwIO $ HTTP.HttpExceptionRequest request (HTTP.C.StatusCodeException (void response) "") + throwIO $ + HTTP.HttpExceptionRequest request (HTTP.C.StatusCodeException (void response) "") return $ Aeson.eitherDecode $ HTTP.getResponseBody response diff --git a/waspc/src/Wasp/Util/StrongPath.hs b/waspc/src/Wasp/Util/StrongPath.hs index 5a55755205..38ad9d821c 100644 --- a/waspc/src/Wasp/Util/StrongPath.hs +++ b/waspc/src/Wasp/Util/StrongPath.hs @@ -20,19 +20,19 @@ stripProperPrefix base file = SP.fromPathRelFile <$> P.stripProperPrefix (SP.toPathAbsDir base) (SP.toPathAbsFile file) -replaceExtension :: MonadThrow m => SP.Path' SP.Abs (SP.File a) -> String -> m (SP.Path' SP.Abs (SP.File a)) +replaceExtension :: (MonadThrow m) => SP.Path' SP.Abs (SP.File a) -> String -> m (SP.Path' SP.Abs (SP.File a)) replaceExtension path ext = SP.fromPathAbsFile <$> P.replaceExtension ext (SP.toPathAbsFile path) -replaceRelExtension :: MonadThrow m => SP.Path' (SP.Rel b) (SP.File a) -> String -> m (SP.Path' (SP.Rel b) (SP.File a)) +replaceRelExtension :: (MonadThrow m) => SP.Path' (SP.Rel b) (SP.File a) -> String -> m (SP.Path' (SP.Rel b) (SP.File a)) replaceRelExtension path ext = SP.fromPathRelFile <$> P.replaceExtension ext (SP.toPathRelFile path) -splitAbsExtension :: MonadThrow m => SP.Path' SP.Abs (SP.File a) -> m (SP.Path' SP.Abs (SP.File c), String) +splitAbsExtension :: (MonadThrow m) => SP.Path' SP.Abs (SP.File a) -> m (SP.Path' SP.Abs (SP.File c), String) splitAbsExtension path = first SP.fromPathAbsFile <$> P.splitExtension (SP.toPathAbsFile path) -splitRelExtension :: MonadThrow m => SP.Path' (SP.Rel b) (SP.File a) -> m (SP.Path' (SP.Rel b) (SP.File c), String) +splitRelExtension :: (MonadThrow m) => SP.Path' (SP.Rel b) (SP.File a) -> m (SP.Path' (SP.Rel b) (SP.File c), String) splitRelExtension path = first SP.fromPathRelFile <$> P.splitExtension (SP.toPathRelFile path) diff --git a/waspc/src/Wasp/Version.hs b/waspc/src/Wasp/Version.hs index 690cbc3b91..aca7cbcb0d 100644 --- a/waspc/src/Wasp/Version.hs +++ b/waspc/src/Wasp/Version.hs @@ -5,6 +5,6 @@ import qualified Paths_waspc import qualified Wasp.SemanticVersion as SV waspVersion :: SV.Version -waspVersion = SV.Version (toEnum major) (toEnum minor) (toEnum patch) - where - DV.Version [major, minor, patch] _ = Paths_waspc.version +waspVersion = case Paths_waspc.version of + DV.Version [major, minor, patch] _ -> SV.Version (toEnum major) (toEnum minor) (toEnum patch) + _ -> error "This should never happen. Wasp binary version must have exactly three digits, but it doesn't." diff --git a/waspc/testEnv.hs b/waspc/testEnv.hs index 34626ccdb4..b116af4ef5 100755 --- a/waspc/testEnv.hs +++ b/waspc/testEnv.hs @@ -22,5 +22,5 @@ import Parser.Common (runWaspParser) import Text.Pretty.Simple (pPrint) -- | Prints any ToJSON instance, useful when testing parser. -printJSON :: ToJSON a => a -> IO () +printJSON :: (ToJSON a) => a -> IO () printJSON = L.putStrLn . encodePretty diff --git a/waspc/tests/AI/GenerateNewProject/LogMsgTest.hs b/waspc/tests/AI/GenerateNewProject/LogMsgTest.hs index 6468c45fcf..8a47741ab7 100644 --- a/waspc/tests/AI/GenerateNewProject/LogMsgTest.hs +++ b/waspc/tests/AI/GenerateNewProject/LogMsgTest.hs @@ -1,6 +1,6 @@ module AI.GenerateNewProject.LogMsgTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.AI.GenerateNewProject.LogMsg import qualified Wasp.Util.Terminal as Term diff --git a/waspc/tests/Analyzer/Evaluation/EvaluationErrorTest.hs b/waspc/tests/Analyzer/Evaluation/EvaluationErrorTest.hs index 794472b3e2..6d0679afbe 100644 --- a/waspc/tests/Analyzer/Evaluation/EvaluationErrorTest.hs +++ b/waspc/tests/Analyzer/Evaluation/EvaluationErrorTest.hs @@ -2,7 +2,7 @@ module Analyzer.Evaluation.EvaluationErrorTest where import Analyzer.TestUtil (ctx) import Data.List (intercalate) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Analyzer.Evaluator.EvaluationError import Wasp.Analyzer.Parser.Ctx import Wasp.Analyzer.Type (Type (..)) diff --git a/waspc/tests/Analyzer/EvaluatorTest.hs b/waspc/tests/Analyzer/EvaluatorTest.hs index 0d47c0b7c8..3c2256dd56 100644 --- a/waspc/tests/Analyzer/EvaluatorTest.hs +++ b/waspc/tests/Analyzer/EvaluatorTest.hs @@ -10,7 +10,7 @@ import Data.Data (Data) import Data.List.Split (splitOn) import Data.Maybe (fromJust) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import Text.Read (readMaybe) import Wasp.Analyzer.Evaluator import qualified Wasp.Analyzer.Evaluator.Evaluation as E @@ -27,7 +27,7 @@ import Wasp.AppSpec.Core.Ref (Ref (..)) import Wasp.AppSpec.ExtImport (ExtImport (..), ExtImportName (..)) import Wasp.AppSpec.JSON (JSON (..)) -fromRight :: Show a => Either a b -> b +fromRight :: (Show a) => Either a b -> b fromRight (Right x) = x fromRight (Left e) = error $ show e @@ -178,7 +178,8 @@ spec_Evaluator = do let typeDefs = TD.addDeclType @Business $ TD.addEnumType @BusinessType $ - TD.addDeclType @Person $ TD.empty + TD.addDeclType @Person $ + TD.empty let source = [ "person Tim { name: \"Tim Stocker\", age: 40 }", "person John { name: \"John Cashier\", age: 23 }", @@ -226,7 +227,9 @@ spec_Evaluator = do " booleanValue: {=json false json=},", "}" ] - let Right [("Test", allJson)] = takeDecls <$> eval typeDefs source + let allJson = case takeDecls <$> eval typeDefs source of + Right [(_, aj)] -> aj + other -> error $ "Couldn't deconstroct value: " ++ show other show (objectValue allJson) `shouldBe` "{\"key\":1}" show (arrayValue allJson) `shouldBe` "[1,2,3]" show (stringValue allJson) `shouldBe` "\"hello\"" diff --git a/waspc/tests/Analyzer/Parser/CST/TraverseTest.hs b/waspc/tests/Analyzer/Parser/CST/TraverseTest.hs index ef1175b9b5..e5cb428050 100644 --- a/waspc/tests/Analyzer/Parser/CST/TraverseTest.hs +++ b/waspc/tests/Analyzer/Parser/CST/TraverseTest.hs @@ -2,7 +2,7 @@ module Analyzer.Parser.CST.TraverseTest where -import Test.Tasty.Hspec (Spec, it, shouldBe) +import Test.Hspec (Spec, it, shouldBe) import Wasp.Analyzer.Parser.CST (SyntaxKind (..), SyntaxNode) import Wasp.Analyzer.Parser.CST.Traverse import Wasp.Analyzer.Parser.ConcreteParser (parseCST) diff --git a/waspc/tests/Analyzer/Parser/ConcreteParserTest.hs b/waspc/tests/Analyzer/Parser/ConcreteParserTest.hs index aa36f514ae..79f6686180 100644 --- a/waspc/tests/Analyzer/Parser/ConcreteParserTest.hs +++ b/waspc/tests/Analyzer/Parser/ConcreteParserTest.hs @@ -4,7 +4,7 @@ module Analyzer.Parser.ConcreteParserTest where import Control.DeepSeq (deepseq) -import Test.Tasty.Hspec (Spec, describe, it) +import Test.Hspec (Spec, describe, it) import Test.Tasty.QuickCheck import Util.Diff import Wasp.Analyzer.Parser.CST @@ -265,7 +265,8 @@ spec_ConcreteParser = -- -- The point of this test is to ensure some sort of parse tree is always built it "never fails to parse" $ - property $ \source -> parseCST (L.lex source) `deepseq` True + property $ + \source -> parseCST (L.lex source) `deepseq` True instance Diffable SyntaxNode where toLines n = lines $ cstPrettyPrint n diff --git a/waspc/tests/Analyzer/Parser/ParseErrorTest.hs b/waspc/tests/Analyzer/Parser/ParseErrorTest.hs index 204f390a77..34e699c427 100644 --- a/waspc/tests/Analyzer/Parser/ParseErrorTest.hs +++ b/waspc/tests/Analyzer/Parser/ParseErrorTest.hs @@ -1,7 +1,7 @@ module Analyzer.Parser.ParseErrorTest where import Analyzer.TestUtil (ctx, pos, rgn, wctx) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Analyzer.Parser.ParseError import Wasp.Analyzer.Parser.Token import qualified Wasp.Analyzer.Parser.TokenSet as TokenSet diff --git a/waspc/tests/Analyzer/Parser/SourcePositionTest.hs b/waspc/tests/Analyzer/Parser/SourcePositionTest.hs index 23d495f143..8ccfa547b1 100644 --- a/waspc/tests/Analyzer/Parser/SourcePositionTest.hs +++ b/waspc/tests/Analyzer/Parser/SourcePositionTest.hs @@ -1,6 +1,6 @@ module Analyzer.Parser.SourcePositionTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Analyzer.Parser.SourcePosition spec_SourcePositionTest :: Spec diff --git a/waspc/tests/Analyzer/Parser/SourceSpanTest.hs b/waspc/tests/Analyzer/Parser/SourceSpanTest.hs index b12292cb2b..1c0e7aad9d 100644 --- a/waspc/tests/Analyzer/Parser/SourceSpanTest.hs +++ b/waspc/tests/Analyzer/Parser/SourceSpanTest.hs @@ -1,7 +1,7 @@ module Analyzer.Parser.SourceSpanTest where +import Test.Hspec import Test.QuickCheck -import Test.Tasty.Hspec import Wasp.Analyzer.Parser.SourceSpan (SourceSpan (SourceSpan), spansOverlap) spec_SourceSpanTest :: Spec diff --git a/waspc/tests/Analyzer/ParserTest.hs b/waspc/tests/Analyzer/ParserTest.hs index aa84a7d37e..714853fdfd 100644 --- a/waspc/tests/Analyzer/ParserTest.hs +++ b/waspc/tests/Analyzer/ParserTest.hs @@ -3,9 +3,9 @@ module Analyzer.ParserTest where import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy.Char8 as BSC import System.FilePath (replaceExtension, takeBaseName) +import Test.Hspec import Test.Tasty (TestTree, testGroup) import Test.Tasty.Golden (findByExtension, goldenVsStringDiff) -import Test.Tasty.Hspec import Wasp.Analyzer.Parser hiding (withCtx) import Wasp.Analyzer.Parser.PrettyPrinter (prettyPrintParserResult) diff --git a/waspc/tests/Analyzer/TypeChecker/InternalTest.hs b/waspc/tests/Analyzer/TypeChecker/InternalTest.hs index 57871b50d9..9989fad205 100644 --- a/waspc/tests/Analyzer/TypeChecker/InternalTest.hs +++ b/waspc/tests/Analyzer/TypeChecker/InternalTest.hs @@ -4,7 +4,7 @@ import Analyzer.TestUtil (ctx, fromWithCtx) import Data.Either (isLeft) import qualified Data.HashMap.Strict as H import Data.List.NonEmpty (NonEmpty ((:|))) -import Test.Tasty.Hspec +import Test.Hspec import Test.Tasty.QuickCheck import qualified Wasp.Analyzer.Parser as P import Wasp.Analyzer.Type diff --git a/waspc/tests/Analyzer/TypeCheckerTest.hs b/waspc/tests/Analyzer/TypeCheckerTest.hs index 2776fc4e97..f2fdc774eb 100644 --- a/waspc/tests/Analyzer/TypeCheckerTest.hs +++ b/waspc/tests/Analyzer/TypeCheckerTest.hs @@ -1,9 +1,9 @@ module Analyzer.TypeCheckerTest where import Analyzer.TestUtil (ctx) -import Data.Either (isRight) +import Data.Either (fromRight, isRight) import qualified Data.HashMap.Strict as H -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.Analyzer.Parser as P import Wasp.Analyzer.Type import Wasp.Analyzer.TypeChecker @@ -68,7 +68,7 @@ spec_TypeChecker = do it "Properly hoists declarations" $ do let mAst = P.parseStatements "llnode Head { value: 2, next: Tail } llnode Tail { value: 3 }" mAst `shouldSatisfy` isRight - let (Right ast) = mAst + let ast = fromRight (error "Impossible, this should have been checked in the previous step") mAst let llnodeArgType = DictType $ H.fromList diff --git a/waspc/tests/Analyzer/ValidTest.hs b/waspc/tests/Analyzer/ValidTest.hs index 3b8575d520..0bd6cff4e7 100644 --- a/waspc/tests/Analyzer/ValidTest.hs +++ b/waspc/tests/Analyzer/ValidTest.hs @@ -1,7 +1,7 @@ module Analyzer.ValidTest where import Data.Either (fromRight, isRight) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Analyzer.Parser hiding (withCtx) import qualified Wasp.Analyzer.Parser as P import Wasp.Analyzer.Parser.Valid (validateAst) diff --git a/waspc/tests/AnalyzerTest.hs b/waspc/tests/AnalyzerTest.hs index 314ed646b7..e6f4705cc1 100644 --- a/waspc/tests/AnalyzerTest.hs +++ b/waspc/tests/AnalyzerTest.hs @@ -9,7 +9,7 @@ import Data.List (intercalate) import Data.Maybe (fromJust) import NeatInterpolation (trimming) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import qualified Util.Prisma as Util import Wasp.Analyzer import Wasp.Analyzer.Parser (Ctx) diff --git a/waspc/tests/AppSpec/EntityTest.hs b/waspc/tests/AppSpec/EntityTest.hs index 84c42ea19b..11ca64eb1d 100644 --- a/waspc/tests/AppSpec/EntityTest.hs +++ b/waspc/tests/AppSpec/EntityTest.hs @@ -1,6 +1,6 @@ module AppSpec.EntityTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.AppSpec.Entity (getIdField) import qualified Wasp.AppSpec.Entity as Entity import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute diff --git a/waspc/tests/AppSpec/FromJSONTest.hs b/waspc/tests/AppSpec/FromJSONTest.hs index 7c356129cb..428161c17a 100644 --- a/waspc/tests/AppSpec/FromJSONTest.hs +++ b/waspc/tests/AppSpec/FromJSONTest.hs @@ -7,7 +7,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as TE import NeatInterpolation (trimming) import StrongPath (relfileP) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.AppSpec (Ref) import qualified Wasp.AppSpec.Action as Action import qualified Wasp.AppSpec.App.Db as Db @@ -317,7 +317,7 @@ spec_AppSpecFromJSON = do barEntityRef = [trimming| { "name": "bar", "declType": "Entity" }|] pageRef = [trimming| { "name": "foo", "declType": "Page" }|] - decodeJson :: FromJSON a => T.Text -> Maybe a + decodeJson :: (FromJSON a) => T.Text -> Maybe a decodeJson = Aeson.decodeStrict . TE.encodeUtf8 shouldDecodeTo json expectedValue = do diff --git a/waspc/tests/AppSpec/ValidTest.hs b/waspc/tests/AppSpec/ValidTest.hs index ed1ccf7b64..3fe0f38b4c 100644 --- a/waspc/tests/AppSpec/ValidTest.hs +++ b/waspc/tests/AppSpec/ValidTest.hs @@ -9,7 +9,7 @@ import Fixtures (systemSPRoot) import NeatInterpolation (trimming) import StrongPath (relfile) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import qualified Util.Prisma as Util import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec.Action as AS.Action diff --git a/waspc/tests/EnvTest.hs b/waspc/tests/EnvTest.hs index 5eb651d995..941720e10d 100644 --- a/waspc/tests/EnvTest.hs +++ b/waspc/tests/EnvTest.hs @@ -2,7 +2,7 @@ module EnvTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Env ( envVarsToDotEnvContent, formatEnvVarValue, diff --git a/waspc/tests/ErrorTest.hs b/waspc/tests/ErrorTest.hs index c3fc703497..8635edd8be 100644 --- a/waspc/tests/ErrorTest.hs +++ b/waspc/tests/ErrorTest.hs @@ -4,7 +4,7 @@ import Data.List (intercalate) import Data.Maybe (fromJust) import Fixtures (systemSPRoot) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Analyzer.Parser.Ctx (ctxFromRgn) import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..)) import Wasp.Error @@ -72,7 +72,8 @@ spec_WaspError = do [ SP.fromAbsFile waspFilePath ++ " @ 1:5-12", " " ++ errMsg, "", - " " ++ T.applyStyles [T.Yellow] " 1 | " + " " + ++ T.applyStyles [T.Yellow] " 1 | " ++ "app " ++ T.applyStyles [T.Red] "$TestApp" ++ " { title: \"Test App\" }" diff --git a/waspc/tests/FilePath/ExtraTest.hs b/waspc/tests/FilePath/ExtraTest.hs index 3acc2b6ab8..e6b18417e4 100644 --- a/waspc/tests/FilePath/ExtraTest.hs +++ b/waspc/tests/FilePath/ExtraTest.hs @@ -2,7 +2,7 @@ module FilePath.ExtraTest where import qualified FilePath.Extra as PE import StrongPath (reldirP, toFilePath) -import Test.Tasty.Hspec +import Test.Hspec spec_reversePosixPath :: Spec spec_reversePosixPath = do diff --git a/waspc/tests/Generator/AuthInjectionTest.hs b/waspc/tests/Generator/AuthInjectionTest.hs index 47cf73d924..82bc66c3b7 100644 --- a/waspc/tests/Generator/AuthInjectionTest.hs +++ b/waspc/tests/Generator/AuthInjectionTest.hs @@ -2,7 +2,7 @@ module Generator.AuthInjectionTest where import Data.Maybe (maybeToList) import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import Util.Prisma (getPrismaModelBody) import qualified Wasp.AppSpec.Entity as AS.Entity import Wasp.Generator.DbGenerator.Auth (injectAuth) diff --git a/waspc/tests/Generator/CommonTest.hs b/waspc/tests/Generator/CommonTest.hs index 50ee8a4596..2f1b295df2 100644 --- a/waspc/tests/Generator/CommonTest.hs +++ b/waspc/tests/Generator/CommonTest.hs @@ -1,6 +1,6 @@ module Generator.CommonTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Generator.Common (makeJsArrayFromHaskellList) spec_GeneratorCommonTest :: Spec diff --git a/waspc/tests/Generator/CrudTest.hs b/waspc/tests/Generator/CrudTest.hs index 3506d24868..1479c6b407 100644 --- a/waspc/tests/Generator/CrudTest.hs +++ b/waspc/tests/Generator/CrudTest.hs @@ -6,7 +6,7 @@ import Data.Aeson (KeyValue ((.=)), Value, object) import Data.Aeson.Types (Pair) import StrongPath (relfileP) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.AppSpec.Core.Ref as AS.Core.Ref import qualified Wasp.AppSpec.Crud as AS.Crud import qualified Wasp.AppSpec.ExtImport as AS.ExtImport diff --git a/waspc/tests/Generator/DbGeneratorTest.hs b/waspc/tests/Generator/DbGeneratorTest.hs index bc3a12c0fd..1ce4079276 100644 --- a/waspc/tests/Generator/DbGeneratorTest.hs +++ b/waspc/tests/Generator/DbGeneratorTest.hs @@ -1,7 +1,7 @@ module Generator.DbGeneratorTest where import qualified Data.Text as T -import Test.Tasty.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe) import Wasp.Generator.DbGenerator.Common ( MigrateArgs (..), defaultMigrateArgs, diff --git a/waspc/tests/Generator/FileDraft/CopyAndModifyTextFileDraftTest.hs b/waspc/tests/Generator/FileDraft/CopyAndModifyTextFileDraftTest.hs index 1a63400bfa..8d03ad7a19 100644 --- a/waspc/tests/Generator/FileDraft/CopyAndModifyTextFileDraftTest.hs +++ b/waspc/tests/Generator/FileDraft/CopyAndModifyTextFileDraftTest.hs @@ -3,7 +3,7 @@ module Generator.FileDraft.CopyAndModifyTextFileDraftTest where import Fixtures (systemSPRoot) import qualified Generator.MockWriteableMonad as Mock import StrongPath (parent, reldir, relfile, toFilePath, ()) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Generator.FileDraft spec_CopyAndModifyTextFileDraft :: Spec diff --git a/waspc/tests/Generator/FileDraft/CopyFileDraftTest.hs b/waspc/tests/Generator/FileDraft/CopyFileDraftTest.hs index 9b45672c4f..36c4b5acf7 100644 --- a/waspc/tests/Generator/FileDraft/CopyFileDraftTest.hs +++ b/waspc/tests/Generator/FileDraft/CopyFileDraftTest.hs @@ -3,7 +3,7 @@ module Generator.FileDraft.CopyFileDraftTest where import Fixtures (systemSPRoot) import qualified Generator.MockWriteableMonad as Mock import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Generator.FileDraft spec_CopyFileDraft :: Spec diff --git a/waspc/tests/Generator/FileDraft/TemplateFileDraftTest.hs b/waspc/tests/Generator/FileDraft/TemplateFileDraftTest.hs index f5d753a4ee..f046d9927c 100644 --- a/waspc/tests/Generator/FileDraft/TemplateFileDraftTest.hs +++ b/waspc/tests/Generator/FileDraft/TemplateFileDraftTest.hs @@ -5,7 +5,7 @@ import Data.Text (Text) import Fixtures (systemSPRoot) import qualified Generator.MockWriteableMonad as Mock import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Generator.FileDraft spec_TemplateFileDraft :: Spec diff --git a/waspc/tests/Generator/JsImportTest.hs b/waspc/tests/Generator/JsImportTest.hs index f0c13e8cfd..05cd72d779 100644 --- a/waspc/tests/Generator/JsImportTest.hs +++ b/waspc/tests/Generator/JsImportTest.hs @@ -2,7 +2,7 @@ module Generator.JsImportTest where import StrongPath (Dir, Path, Posix, Rel) import qualified StrongPath as SP -import Test.Tasty.Hspec +import Test.Hspec import Wasp.AppSpec.ExtImport import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.JsImport diff --git a/waspc/tests/Generator/MockWriteableMonad.hs b/waspc/tests/Generator/MockWriteableMonad.hs index e1209a1641..c45a2eae66 100644 --- a/waspc/tests/Generator/MockWriteableMonad.hs +++ b/waspc/tests/Generator/MockWriteableMonad.hs @@ -91,7 +91,7 @@ instance WriteableMonad MockWriteableMonad where instance MonadIO MockWriteableMonad where liftIO = undefined -modifyLogs :: MonadState (a, b) m => (a -> a) -> m () +modifyLogs :: (MonadState (a, b) m) => (a -> a) -> m () modifyLogs f = modify (first f) newtype MockWriteableMonad a = MockWriteableMonad diff --git a/waspc/tests/Generator/NpmDependenciesTest.hs b/waspc/tests/Generator/NpmDependenciesTest.hs index 1fe331e0cf..6f4ad38a11 100644 --- a/waspc/tests/Generator/NpmDependenciesTest.hs +++ b/waspc/tests/Generator/NpmDependenciesTest.hs @@ -1,6 +1,6 @@ module Generator.NpmDependenciesTest where -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.ExternalConfig.Npm.Dependency as D import Wasp.Generator.NpmDependencies diff --git a/waspc/tests/Generator/WebAppGeneratorTest.hs b/waspc/tests/Generator/WebAppGeneratorTest.hs index f8b1767c73..e02e1568d4 100644 --- a/waspc/tests/Generator/WebAppGeneratorTest.hs +++ b/waspc/tests/Generator/WebAppGeneratorTest.hs @@ -1,12 +1,13 @@ module Generator.WebAppGeneratorTest where +import Data.Either (fromRight) import qualified Data.Map as M import qualified Data.Set as S import Fixtures import StrongPath (relfile) import qualified StrongPath as SP import System.FilePath (()) -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App.Wasp as AS.Wasp @@ -98,7 +99,10 @@ spec_WebAppGenerator = do -- that they will successfully be written, it checks only that their -- destinations are correct. it "Given a simple AppSpec, creates file drafts at expected destinations" $ do - let (_, Right fileDrafts) = runGenerator $ genWebApp testAppSpec + let fileDrafts = + fromRight + (error "Expected Right but got Left") + (snd $ runGenerator $ genWebApp testAppSpec) let expectedFileDraftDstPaths = map (SP.toFilePath Common.webAppRootDirInProjectRootDir ) $ concat diff --git a/waspc/tests/Generator/WriteFileDraftsTest.hs b/waspc/tests/Generator/WriteFileDraftsTest.hs index 57a3da22a6..a2f800fd3f 100644 --- a/waspc/tests/Generator/WriteFileDraftsTest.hs +++ b/waspc/tests/Generator/WriteFileDraftsTest.hs @@ -6,7 +6,7 @@ import Data.Bifunctor (Bifunctor (first)) import Data.Maybe (fromJust) import Data.Text (pack) import qualified StrongPath as SP -import Test.Tasty.Hspec (Spec, anyErrorCall, describe, it, shouldBe, shouldMatchList, shouldReturn, shouldThrow) +import Test.Hspec (Spec, anyErrorCall, describe, it, shouldBe, shouldMatchList, shouldReturn, shouldThrow) import Wasp.Generator.FileDraft (FileDraft (FileDraftTextFd), Writeable (getDstPath)) import Wasp.Generator.FileDraft.TextFileDraft (TextFileDraft) import qualified Wasp.Generator.FileDraft.TextFileDraft as TextFD diff --git a/waspc/tests/JsImportTest.hs b/waspc/tests/JsImportTest.hs index 3c0331b665..c59fa86817 100644 --- a/waspc/tests/JsImportTest.hs +++ b/waspc/tests/JsImportTest.hs @@ -2,7 +2,7 @@ module JsImportTest where import StrongPath (()) import qualified StrongPath as SP -import Test.Tasty.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe) import Wasp.JsImport spec_JsImportTest :: Spec diff --git a/waspc/tests/Node/InternalTest.hs b/waspc/tests/Node/InternalTest.hs index 315e5aa776..42b9fb70f2 100644 --- a/waspc/tests/Node/InternalTest.hs +++ b/waspc/tests/Node/InternalTest.hs @@ -1,6 +1,6 @@ module Node.InternalTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Node.Internal (parseVersionFromCommandOutput) import Wasp.SemanticVersion.Version (Version (..)) diff --git a/waspc/tests/Project/DbTest.hs b/waspc/tests/Project/DbTest.hs index b44556cf47..97633fdbf7 100644 --- a/waspc/tests/Project/DbTest.hs +++ b/waspc/tests/Project/DbTest.hs @@ -1,7 +1,7 @@ module Project.DbTest where import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Util.Prisma as Util import qualified Wasp.AppSpec.App.Db as AS.App.Db import Wasp.Project.Db diff --git a/waspc/tests/Project/WaspignoreTest.hs b/waspc/tests/Project/WaspignoreTest.hs index eedd110100..91b3e97d07 100644 --- a/waspc/tests/Project/WaspignoreTest.hs +++ b/waspc/tests/Project/WaspignoreTest.hs @@ -1,6 +1,6 @@ module Project.WaspignoreTest where -import Test.Tasty.Hspec +import Test.Hspec import Test.Tasty.QuickCheck (arbitraryPrintableChar, forAll, listOf, property) import Wasp.Project.Waspignore (ignores, parseWaspignoreFile) diff --git a/waspc/tests/Psl/Generator/ModelTest.hs b/waspc/tests/Psl/Generator/ModelTest.hs index 908207e31f..a9db84cfe9 100644 --- a/waspc/tests/Psl/Generator/ModelTest.hs +++ b/waspc/tests/Psl/Generator/ModelTest.hs @@ -4,7 +4,7 @@ module Psl.Generator.ModelTest where import Psl.Common.ModelTest (sampleBodyAst) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Model as Psl.Model import qualified Wasp.Psl.Ast.Schema as Psl.Schema diff --git a/waspc/tests/Psl/Generator/SchemaTest.hs b/waspc/tests/Psl/Generator/SchemaTest.hs index 9fda4a56db..e543c5e772 100644 --- a/waspc/tests/Psl/Generator/SchemaTest.hs +++ b/waspc/tests/Psl/Generator/SchemaTest.hs @@ -3,7 +3,7 @@ module Psl.Generator.SchemaTest where -import Test.Tasty.Hspec +import Test.Hspec import Test.Tasty.QuickCheck import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument @@ -162,7 +162,7 @@ instance Arbitrary Psl.ConfigBlock.ConfigBlock where instance Arbitrary Psl.ConfigBlock.KeyValuePair where arbitrary = Psl.ConfigBlock.KeyValuePair <$> arbitraryIdentifier <*> arbitrary -instance Arbitrary a => Arbitrary (Psl.WithCtx.WithCtx a) where +instance (Arbitrary a) => Arbitrary (Psl.WithCtx.WithCtx a) where arbitrary = arbitrary >>= withArbitraryCtx withArbitraryCtx :: node -> Gen (Psl.WithCtx.WithCtx node) diff --git a/waspc/tests/Psl/Generator/WithCtxTest.hs b/waspc/tests/Psl/Generator/WithCtxTest.hs index 582bbceee4..990ca3453b 100644 --- a/waspc/tests/Psl/Generator/WithCtxTest.hs +++ b/waspc/tests/Psl/Generator/WithCtxTest.hs @@ -4,7 +4,7 @@ module Psl.Generator.WithCtxTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute import qualified Wasp.Psl.Ast.Model as Psl.Model @@ -46,12 +46,12 @@ spec_generatePslWithCtx = do [ " Multiline leading comments", " For prop3" ] - $ Psl.Model.ElementField $ - Psl.Model.Field - "prop3" - Psl.Model.String - [] - [Psl.Attribute.Attribute "unique" []] + $ Psl.Model.ElementField + $ Psl.Model.Field + "prop3" + Psl.Model.String + [] + [Psl.Attribute.Attribute "unique" []] ] ) ] diff --git a/waspc/tests/Psl/Parser/ArgumentTest.hs b/waspc/tests/Psl/Parser/ArgumentTest.hs index eab9c8c800..caccb3afdf 100644 --- a/waspc/tests/Psl/Parser/ArgumentTest.hs +++ b/waspc/tests/Psl/Parser/ArgumentTest.hs @@ -1,6 +1,6 @@ module Psl.Parser.ArgumentTest where -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import Wasp.Psl.Parser.Argument (argument) diff --git a/waspc/tests/Psl/Parser/AttributeTest.hs b/waspc/tests/Psl/Parser/AttributeTest.hs index 9e5655998b..5fcdadf1c0 100644 --- a/waspc/tests/Psl/Parser/AttributeTest.hs +++ b/waspc/tests/Psl/Parser/AttributeTest.hs @@ -1,6 +1,6 @@ module Psl.Parser.AttributeTest where -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute diff --git a/waspc/tests/Psl/Parser/ConfigBlockTest.hs b/waspc/tests/Psl/Parser/ConfigBlockTest.hs index ce774029c8..b19dcad957 100644 --- a/waspc/tests/Psl/Parser/ConfigBlockTest.hs +++ b/waspc/tests/Psl/Parser/ConfigBlockTest.hs @@ -2,7 +2,7 @@ module Psl.Parser.ConfigBlockTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.ConfigBlock as Psl.ConfigBlock diff --git a/waspc/tests/Psl/Parser/EnumTest.hs b/waspc/tests/Psl/Parser/EnumTest.hs index 56cd9fd6b8..7674d4cc49 100644 --- a/waspc/tests/Psl/Parser/EnumTest.hs +++ b/waspc/tests/Psl/Parser/EnumTest.hs @@ -2,7 +2,7 @@ module Psl.Parser.EnumTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute diff --git a/waspc/tests/Psl/Parser/ModelTest.hs b/waspc/tests/Psl/Parser/ModelTest.hs index eb765293b0..a2a6e81f16 100644 --- a/waspc/tests/Psl/Parser/ModelTest.hs +++ b/waspc/tests/Psl/Parser/ModelTest.hs @@ -4,7 +4,7 @@ import Data.Either (isLeft) import qualified Data.Text as T import NeatInterpolation (trimming) import Psl.Common.ModelTest (sampleBodyAst, sampleBodySchema) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute diff --git a/waspc/tests/Psl/Parser/SchemaTest.hs b/waspc/tests/Psl/Parser/SchemaTest.hs index 526ea22d17..b791668a41 100644 --- a/waspc/tests/Psl/Parser/SchemaTest.hs +++ b/waspc/tests/Psl/Parser/SchemaTest.hs @@ -4,7 +4,7 @@ module Psl.Parser.SchemaTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute import qualified Wasp.Psl.Ast.ConfigBlock as Psl.ConfigBlock @@ -318,13 +318,13 @@ spec_parsePslSchema = do ] ] ), - Psl.Schema.EnumBlock $ - Psl.Enum.Enum + Psl.Schema.EnumBlock + $ Psl.Enum.Enum "Role" - $ Psl.WithCtx.empty - <$> [ Psl.Enum.ElementValue "USER" [], - Psl.Enum.ElementValue "ADMIN" [] - ], + $ Psl.WithCtx.empty + <$> [ Psl.Enum.ElementValue "USER" [], + Psl.Enum.ElementValue "ADMIN" [] + ], Psl.Schema.ViewBlock $ Psl.View.View "UserInfo" diff --git a/waspc/tests/Psl/Parser/TypeTest.hs b/waspc/tests/Psl/Parser/TypeTest.hs index fb9abf8840..4c7c6f36a3 100644 --- a/waspc/tests/Psl/Parser/TypeTest.hs +++ b/waspc/tests/Psl/Parser/TypeTest.hs @@ -2,7 +2,7 @@ module Psl.Parser.TypeTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute diff --git a/waspc/tests/Psl/Parser/ViewTest.hs b/waspc/tests/Psl/Parser/ViewTest.hs index 9ae98083a0..447a935856 100644 --- a/waspc/tests/Psl/Parser/ViewTest.hs +++ b/waspc/tests/Psl/Parser/ViewTest.hs @@ -2,7 +2,7 @@ module Psl.Parser.ViewTest where import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Text.Megaparsec as Megaparsec import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute import qualified Wasp.Psl.Ast.Model as Psl.Model diff --git a/waspc/tests/Psl/Parser/WithCtxTest.hs b/waspc/tests/Psl/Parser/WithCtxTest.hs index d54178ab07..81e53dc831 100644 --- a/waspc/tests/Psl/Parser/WithCtxTest.hs +++ b/waspc/tests/Psl/Parser/WithCtxTest.hs @@ -5,7 +5,7 @@ module Psl.Parser.WithCtxTest where import Data.Either (isLeft) import qualified Data.Text as T import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Wasp.Psl.Ast.Argument as Psl.Argument import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute import qualified Wasp.Psl.Ast.Model as Psl.Model @@ -60,12 +60,12 @@ spec_parsePslWithCtx = do [ " Multiline comments", " For prop3" ] - $ Psl.Model.ElementField $ - Psl.Model.Field - "prop3" - Psl.Model.String - [] - [Psl.Attribute.Attribute "unique" []] + $ Psl.Model.ElementField + $ Psl.Model.Field + "prop3" + Psl.Model.String + [] + [Psl.Attribute.Attribute "unique" []] ] ) ] @@ -100,26 +100,26 @@ spec_parsePslWithCtx = do [ " The unique identifier for the post", " @zod.uuid()" ] - $ Psl.Model.ElementField $ - Psl.Model.Field - "id" - Psl.Model.String - [] - [ Psl.Attribute.Attribute "id" [], - Psl.Attribute.Attribute - "default" - [Psl.Argument.ArgUnnamed $ Psl.Argument.FuncExpr "uuid" []] - ], + $ Psl.Model.ElementField + $ Psl.Model.Field + "id" + Psl.Model.String + [] + [ Psl.Attribute.Attribute "id" [], + Psl.Attribute.Attribute + "default" + [Psl.Argument.ArgUnnamed $ Psl.Argument.FuncExpr "uuid" []] + ], commentedNode [ " A brief title that describes the contents of the post", " @zod.max(255, { message: \"The title must be shorter than 256 characters\" })" ] - $ Psl.Model.ElementField $ - Psl.Model.Field - "title" - Psl.Model.String - [] - [], + $ Psl.Model.ElementField + $ Psl.Model.Field + "title" + Psl.Model.String + [] + [], commentedNode [" @zod.max(10240)"] $ Psl.Model.ElementField $ Psl.Model.Field diff --git a/waspc/tests/Psl/ValidTest.hs b/waspc/tests/Psl/ValidTest.hs index a4a0647ec7..e8de3e8726 100644 --- a/waspc/tests/Psl/ValidTest.hs +++ b/waspc/tests/Psl/ValidTest.hs @@ -1,7 +1,7 @@ module Psl.ValidTest where import NeatInterpolation (trimming) -import Test.Tasty.Hspec +import Test.Hspec import qualified Util.Prisma as Util import qualified Wasp.Psl.Valid as PslV import qualified Wasp.Valid as Valid diff --git a/waspc/tests/SemanticVersion/VersionBoundTest.hs b/waspc/tests/SemanticVersion/VersionBoundTest.hs index a1a8bf8731..600efafdc7 100644 --- a/waspc/tests/SemanticVersion/VersionBoundTest.hs +++ b/waspc/tests/SemanticVersion/VersionBoundTest.hs @@ -1,6 +1,6 @@ module SemanticVersion.VersionBoundTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.SemanticVersion.Version import Wasp.SemanticVersion.VersionBound diff --git a/waspc/tests/SemanticVersion/VersionTest.hs b/waspc/tests/SemanticVersion/VersionTest.hs index e83ac9b08a..0399b5dc59 100644 --- a/waspc/tests/SemanticVersion/VersionTest.hs +++ b/waspc/tests/SemanticVersion/VersionTest.hs @@ -1,7 +1,7 @@ module SemanticVersion.VersionTest where import Data.Either (isLeft) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.SemanticVersion.Version spec_SemanticVersion_Version :: Spec diff --git a/waspc/tests/SemanticVersionTest.hs b/waspc/tests/SemanticVersionTest.hs index 6757f239e7..e00ffc98ac 100644 --- a/waspc/tests/SemanticVersionTest.hs +++ b/waspc/tests/SemanticVersionTest.hs @@ -1,7 +1,7 @@ module SemanticVersionTest where import Numeric.Natural -import Test.Tasty.Hspec +import Test.Hspec import Wasp.SemanticVersion import Wasp.SemanticVersion.Version (v) import Wasp.SemanticVersion.VersionBound (HasVersionBounds (versionBounds), vi) diff --git a/waspc/tests/Util/Control/MonadTest.hs b/waspc/tests/Util/Control/MonadTest.hs index 0dba35ef64..00460112fc 100644 --- a/waspc/tests/Util/Control/MonadTest.hs +++ b/waspc/tests/Util/Control/MonadTest.hs @@ -2,7 +2,7 @@ module Util.Control.MonadTest where -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Util.Control.Monad data Identity a = Identity {runIdentity :: a} deriving (Functor) @@ -12,7 +12,6 @@ instance Applicative Identity where Identity f <*> Identity x = Identity (f x) instance Monad Identity where - return = Identity Identity x >>= f = f x spec_untilM :: Spec diff --git a/waspc/tests/Util/Diff.hs b/waspc/tests/Util/Diff.hs index 45c98096be..c3ba92ad67 100644 --- a/waspc/tests/Util/Diff.hs +++ b/waspc/tests/Util/Diff.hs @@ -7,7 +7,7 @@ where import Data.Algorithm.Diff (Diff, PolyDiff (Both, First, Second), getDiff) import Data.List (intercalate) -import Test.Tasty.Hspec (Expectation, expectationFailure) +import Test.Hspec (Expectation, expectationFailure) shouldBeWithDiff :: (Eq a, Diffable a) => a -> a -> Expectation shouldBeWithDiff actual expected @@ -32,7 +32,7 @@ class Diffable a where instance Diffable Char where toLines c = [[c]] -instance Diffable a => Diffable [a] where +instance (Diffable a) => Diffable [a] where toLines xs = concatMap toLines xs instance (Diffable a, Diffable b) => Diffable (a, b) where diff --git a/waspc/tests/Util/FibTest.hs b/waspc/tests/Util/FibTest.hs index 9293eccf40..097d45298b 100644 --- a/waspc/tests/Util/FibTest.hs +++ b/waspc/tests/Util/FibTest.hs @@ -1,6 +1,6 @@ module Util.FibTest where -import Test.Tasty.Hspec +import Test.Hspec import Test.Tasty.QuickCheck import Wasp.Util.Fib diff --git a/waspc/tests/Util/FilePathTest.hs b/waspc/tests/Util/FilePathTest.hs index 519b2e4a23..626da8f080 100644 --- a/waspc/tests/Util/FilePathTest.hs +++ b/waspc/tests/Util/FilePathTest.hs @@ -1,7 +1,7 @@ module Util.FilePathTest where import qualified System.FilePath as FP -import Test.Tasty.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe) import Wasp.Util.FilePath (removePathPrefix) spec_FilePathTest :: Spec diff --git a/waspc/tests/Util/IO/RetryTest.hs b/waspc/tests/Util/IO/RetryTest.hs index 1f61525432..414d17eb12 100644 --- a/waspc/tests/Util/IO/RetryTest.hs +++ b/waspc/tests/Util/IO/RetryTest.hs @@ -5,7 +5,7 @@ module Util.IO.RetryTest where import Control.Monad (forM_) import Control.Monad.State (MonadState (get), State, modify, runState) import Numeric.Natural (Natural) -import Test.Tasty.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe) import qualified Wasp.Util.IO.Retry as R spec_RetryTest :: Spec diff --git a/waspc/tests/UtilTest.hs b/waspc/tests/UtilTest.hs index 0753aabaa5..41f4a8b482 100644 --- a/waspc/tests/UtilTest.hs +++ b/waspc/tests/UtilTest.hs @@ -4,7 +4,7 @@ import Control.DeepSeq import Control.Exception (evaluate) import Data.Aeson (object, toJSON, (.=)) import qualified Data.Aeson as Aeson -import Test.Tasty.Hspec +import Test.Hspec import Wasp.Util spec_camelToKebabCase :: Spec diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index df49436abc..f85238bf6a 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -115,20 +115,20 @@ library build-tool-depends: alex:alex build-depends: - , base >= 4.7 && < 5 + , base >= 4.16 && < 5 , Glob ^>= 0.10.2 , containers ^>= 0.6.5 , directory ^>= 1.3.6 && < 1.4 , dir-traverse ^>= 0.2.3 , filepath ^>= 1.4.2 , time ^>= 1.9.3 - , bytestring ^>= 0.10.12 - , aeson ^>= 1.5.6 + , bytestring ^>= 0.11 + , aeson ^>= 2.2 , aeson-pretty ^>= 0.8 , text ^>= 1.2.4 - , template-haskell ^>= 2.17.0 + , template-haskell ^>= 2.20.0 , unordered-containers ^>= 0.2.16 - , mtl ^>= 2.2.2 + , mtl ^>= 2.3 , async ^>= 2.2.4 , conduit ^>= 1.3.4 , exceptions ^>= 0.10.4 @@ -136,16 +136,16 @@ library , conduit-extra ^>= 1.3.5 , process ^>= 1.6.17 , cryptohash-sha256 ^>= 0.11.102 - , mustache ^>= 2.3.2 + , mustache ^>= 2.4 , parsec ^>= 3.1.14 , path ^>= 0.9.2 , path-io ^>= 1.6.3 , pretty-simple ^>= 4.1.2 , regex-tdfa ^>= 1.3.1 - , strong-path ^>= 1.1.4 + , strong-path ^>= 1.2.0 , unliftio ^>= 0.2.20 , utf8-string ^>= 1.0.2 - , cryptonite ^>= 0.29 + , cryptonite ^>= 0.30 , fsnotify ^>= 0.3.0 , http-conduit ^>= 2.3.8 , uuid ^>= 1.3.15 @@ -461,6 +461,7 @@ library waspls exposed-modules: Control.Monad.Log Control.Monad.Log.Class + Language.LSP.Extra Wasp.LSP.Analysis Wasp.LSP.CodeActions Wasp.LSP.Commands @@ -497,21 +498,21 @@ library waspls , deepseq , data-default ^>=0.7.1.1 , hslogger ^>=1.3.1.0 - , lens ^>=5.1 - , lsp ^>=1.4.0.0 - , lsp-types ^>=1.4.0.1 + , lens ^>=5.2 + , lsp ^>=1.6 + , lsp-types ^>=1.6 , stm ^>=2.5.1.0 - , stm-containers ^>=1.2 - , hashable ^>=1.3.5.0 + , stm-containers ^>=1.2.1 + , hashable ^>=1.4 , unordered-containers , strong-path , path , async ^>=2.2.4 - , mustache ^>=2.3.2 + , mustache ^>=2.4 , unliftio-core , mtl , text - , transformers ^>=0.5.6.2 + , transformers ^>=0.6.1 , utf8-string , unordered-containers , waspc @@ -531,8 +532,8 @@ library cli-lib , mtl , async , exceptions - , lens ^>=5.1 - , lens-aeson ^>=1.1.3 + , lens ^>=5.2 + , lens-aeson ^>=1.2 , cryptonite , fsnotify , http-conduit @@ -550,7 +551,7 @@ library cli-lib , neat-interpolation , unliftio ^>= 0.2.20 , unordered-containers ^>= 0.2.16 - , bytestring ^>= 0.10.12 + , bytestring ^>= 0.11 , tar ^>=0.5.1.1 , zlib ^>=0.6.3.0 , temporary ^>=1.3 @@ -651,9 +652,9 @@ test-suite waspc-tests , bytestring , waspc , QuickCheck ^>= 2.14 - , tasty ^>= 1.4.2 - -- tasty-hspec 1.1.7 introduces breaking changes, which is why we have < 1.1.7 . - , tasty-hspec >= 1.1 && < 1.1.7 + , tasty ^>= 1.5 + , hspec >= 2.11 + , tasty-hspec >= 1.2 , tasty-quickcheck ^>= 0.10 , tasty-golden ^>= 2.3.5 other-modules: @@ -732,14 +733,14 @@ test-suite waspls-tests , base , waspc , waspls - , lens ^>=5.1 - , lsp ^>=1.4.0.0 - , lsp-types ^>=1.4.0.1 + , lens ^>=5.2 + , lsp ^>=1.6 + , lsp-types ^>=1.6 , mtl , QuickCheck ^>= 2.14 - , tasty ^>= 1.4.2 - -- tasty-hspec 1.1.7 introduces breaking changes, which is why we have < 1.1.7 . - , tasty-hspec >= 1.1 && < 1.1.7 + , tasty ^>= 1.5 + , hspec >= 2.11 + , tasty-hspec >= 1.2 , tasty-golden ^>= 2.3.5 , bytestring , filepath @@ -760,9 +761,9 @@ test-suite wasp-cli-tests , waspc , cli-lib , QuickCheck ^>= 2.14 - , tasty ^>= 1.4.2 - -- tasty-hspec 1.1.7 introduces breaking changes, which is why we have < 1.1.7 . - , tasty-hspec >= 1.1 && < 1.1.7 + , tasty ^>= 1.5 + , hspec >= 2.11 + , tasty-hspec >= 1.2 , tasty-quickcheck ^>= 0.10 other-modules: DbMigrateTest @@ -790,9 +791,9 @@ test-suite waspc-e2e-tests , dir-traverse , aeson-pretty , process - , tasty ^>= 1.4.2 - -- tasty-hspec 1.1.7 introduces breaking changes, which is why we have < 1.1.7 . - , tasty-hspec >= 1.1 && < 1.1.7 + , tasty ^>= 1.5 + , hspec >= 2.11 + , tasty-hspec >= 1.2 , tasty-golden ^>= 2.3.5 other-modules: FileSystem diff --git a/waspc/waspls/src/Control/Monad/Log.hs b/waspc/waspls/src/Control/Monad/Log.hs index 8485534336..12669e3523 100644 --- a/waspc/waspls/src/Control/Monad/Log.hs +++ b/waspc/waspls/src/Control/Monad/Log.hs @@ -26,5 +26,5 @@ newtype LogT m a = LogT (WriterT [String] m a) runLogT :: LogT m a -> m (a, [String]) runLogT (LogT m) = runWriterT m -instance Monad m => MonadLog (LogT m) where +instance (Monad m) => MonadLog (LogT m) where logM msg = LogT $ tell [msg] diff --git a/waspc/waspls/src/Control/Monad/Log/Class.hs b/waspc/waspls/src/Control/Monad/Log/Class.hs index 47c847dae3..298fb6b719 100644 --- a/waspc/waspls/src/Control/Monad/Log/Class.hs +++ b/waspc/waspls/src/Control/Monad/Log/Class.hs @@ -8,15 +8,15 @@ import Control.Monad.Reader (ReaderT) import Control.Monad.State.Strict (StateT) import Control.Monad.Trans.Class (lift) -class Monad m => MonadLog m where +class (Monad m) => MonadLog m where -- | Write a message to the log logM :: String -> m () -instance MonadLog m => MonadLog (ExceptT e m) where +instance (MonadLog m) => MonadLog (ExceptT e m) where logM = lift . logM -instance MonadLog m => MonadLog (StateT s m) where +instance (MonadLog m) => MonadLog (StateT s m) where logM = lift . logM -instance MonadLog m => MonadLog (ReaderT r m) where +instance (MonadLog m) => MonadLog (ReaderT r m) where logM = lift . logM diff --git a/waspc/waspls/src/Language/LSP/Extra.hs b/waspc/waspls/src/Language/LSP/Extra.hs new file mode 100644 index 0000000000..4de3505caa --- /dev/null +++ b/waspc/waspls/src/Language/LSP/Extra.hs @@ -0,0 +1,64 @@ +module Language.LSP.Extra + ( setupLogger, + ) +where + +import qualified Control.Exception as E +import Control.Monad (forM_) +import System.IO (Handle, IOMode (AppendMode), hClose, hPutStr, hSetEncoding, openFile, stderr, utf8) +import qualified System.Log.Formatter as L +import qualified System.Log.Handler as LH +import qualified System.Log.Handler.Simple as LHS +import System.Log.Logger (Priority) +import qualified System.Log.Logger as L + +-- | This is a stub identical to the @setupLogger@ function from lsp 1.4.0.0 +-- +-- Updating GHC to 9.6.7 forced us to update lsp to a version newer than +-- 1.5.0.0. +-- +-- Lsp 1.5.0.0 drops support for @hslogger@ and no longer exposes the +-- @setupLogger@ function. To properly accomodate the change, we would have to +-- update our entire logging architecture in the LSP. +-- +-- We soon plan to move Wasp to the TS Spec. Spending time on the LSP therefore +-- makes little sense, so we've decided to stick with the "old approach" by +-- copying over the @setupLogger@ function verbatim and putting it here. +-- Reference: +-- https://hackage.haskell.org/package/lsp-1.4.0.0/docs/src/Language.LSP.Server.Core.html#setupLogger +-- +-- To avoid our logs clashing with LSP's new default logs, we've also had to +-- disable the new logging mechanism in @Wasp.LSP.Server@. +-- +-- These two small changes (creating a stub + disabling default logging) allow +-- us to keep the rest of the code completely intact without compromising any +-- functionality. +setupLogger :: Maybe FilePath -> [String] -> Priority -> IO () +setupLogger mLogFile extraLogNames level = do + logStream <- case mLogFile of + Just logFile -> openFile logFile AppendMode `E.catch` handleIOException logFile + Nothing -> return stderr + hSetEncoding logStream utf8 + + logH <- LHS.streamHandler logStream level + + let logHandle = logH {LHS.closeFunc = hClose} + logFormatter = L.tfLogFormatter logDateFormat logFormat + logHandler = LH.setFormatter logHandle logFormatter + + L.updateGlobalLogger L.rootLoggerName $ L.setHandlers ([] :: [LHS.GenericHandler Handle]) + L.updateGlobalLogger "haskell-lsp" $ L.setHandlers [logHandler] + L.updateGlobalLogger "haskell-lsp" $ L.setLevel level + + -- Also route the additional log names to the same log + forM_ extraLogNames $ \logName -> do + L.updateGlobalLogger logName $ L.setHandlers [logHandler] + L.updateGlobalLogger logName $ L.setLevel level + where + logFormat = "$time [$tid] $prio $loggername:\t$msg" + logDateFormat = "%Y-%m-%d %H:%M:%S%Q" + +handleIOException :: FilePath -> E.IOException -> IO Handle +handleIOException logFile _ = do + hPutStr stderr $ "Couldn't open log file " ++ logFile ++ "; falling back to stderr logging" + return stderr diff --git a/waspc/waspls/src/Wasp/LSP/Commands/ScaffoldTsSymbol.hs b/waspc/waspls/src/Wasp/LSP/Commands/ScaffoldTsSymbol.hs index db4ab272b1..15bf98f768 100644 --- a/waspc/waspls/src/Wasp/LSP/Commands/ScaffoldTsSymbol.hs +++ b/waspc/waspls/src/Wasp/LSP/Commands/ScaffoldTsSymbol.hs @@ -230,7 +230,7 @@ hasTemplateForArgs Args {..} = case P.fileExtension $ SP.toPathAbsFile filepath -- | @getTemplateFor pathToExtImport extension@ finds the mustache template in -- @data/Lsp/templates/ts@ and compiles it. -getTemplateFor :: MonadIO m => ExprPath -> String -> m (Either String Mustache.Template) +getTemplateFor :: (MonadIO m) => ExprPath -> String -> m (Either String Mustache.Template) getTemplateFor exprPath ext = runExceptT $ do templatesDir <- liftIO getTemplatesDir templateFile <- (templatesDir SP.) <$> templateFileFor exprPath ext @@ -271,7 +271,7 @@ getTemplatesDir :: IO (SP.Path' SP.Abs (SP.Dir TemplatesDir)) getTemplatesDir = (SP. templatesDirInDataDir) <$> Wasp.Data.getAbsDataDirPath templateFileFor :: - MonadError String m => + (MonadError String m) => -- | Path to the external import that the scaffold request came from. ExprPath -> -- | Extension of the file that the request is scaffolding code in. diff --git a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs index 83625f99d5..a6b6556c90 100644 --- a/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs +++ b/waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs @@ -64,7 +64,7 @@ waspStylePathToCachePath (WaspStyleExtFilePath waspStylePath) = Just (relPathWithoutExt, ext) -> ExtFileCachePath relPathWithoutExt (widenExtension ext) -absPathToCachePath :: HasProjectRootDir m => SP.Path' SP.Abs (SP.File a) -> m (Maybe ExtFileCachePath) +absPathToCachePath :: (HasProjectRootDir m) => SP.Path' SP.Abs (SP.File a) -> m (Maybe ExtFileCachePath) absPathToCachePath absFile = do -- Makes the path relative to src/ and deletes the extension. maybeProjectDir <- getProjectRootDir @@ -78,7 +78,7 @@ absPathToCachePath absFile = do Nothing -> pure Nothing Just (fileWithoutExt, ext) -> pure $ Just $ ExtFileCachePath fileWithoutExt (DotExact ext) -cachePathToAbsPathWithoutExt :: HasProjectRootDir m => ExtFileCachePath -> m (Maybe (SP.Path' SP.Abs (SP.File ExtensionlessExtFile))) +cachePathToAbsPathWithoutExt :: (HasProjectRootDir m) => ExtFileCachePath -> m (Maybe (SP.Path' SP.Abs (SP.File ExtensionlessExtFile))) cachePathToAbsPathWithoutExt (ExtFileCachePath cachePath _) = do -- Converts to an absolute path, but does not add any extension. maybeProjectDir <- getProjectRootDir diff --git a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs index 520d831590..7a4f8b4b20 100644 --- a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs +++ b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs @@ -1,7 +1,7 @@ module Wasp.LSP.Prisma.Analyze where import Control.Lens ((.~)) -import Control.Monad.Cont (liftIO) +import Control.Monad.IO.Class (liftIO) import Control.Monad.Log.Class (logM) import StrongPath (Abs, Dir, Path') import Wasp.LSP.ServerMonads (ServerM, modify) diff --git a/waspc/waspls/src/Wasp/LSP/Server.hs b/waspc/waspls/src/Wasp/LSP/Server.hs index 45a17fbeaa..adc147e646 100644 --- a/waspc/waspls/src/Wasp/LSP/Server.hs +++ b/waspc/waspls/src/Wasp/LSP/Server.hs @@ -15,9 +15,11 @@ import qualified Data.Aeson as Aeson import Data.Default (Default (def)) import qualified Data.HashMap.Strict as M import qualified Data.Text as Text +import Language.LSP.Extra (setupLogger) import qualified Language.LSP.Server as LSP import qualified Language.LSP.Types as LSP import System.Exit (ExitCode (ExitFailure), exitWith) +import System.IO (stdin, stdout) import qualified System.Log.Logger import qualified Wasp.LSP.Commands as Commands import Wasp.LSP.Debouncer (newDebouncerIO) @@ -95,7 +97,10 @@ serve maybeLogFile = do LSP.runLspT env $ runRLspM stateTVar handler exitCode <- - LSP.runServer + -- Setting the first two arguments to mempty disables LSP's default + -- logging. We are instead using our own logger for reasons explained in + -- @Language.LSP.Extra@. + LSP.runServerWithHandles mempty mempty stdin stdout $ LSP.ServerDefinition { defaultConfig = def :: ServerConfig, onConfigurationChange = lspServerUpdateConfig, @@ -121,8 +126,8 @@ serve maybeLogFile = do -- @setupLspLogger (Just filepath)@ writes log messages to the path given setupLspLogger :: Maybe FilePath -> IO () setupLspLogger Nothing = pure () -setupLspLogger (Just "[OUTPUT]") = LSP.setupLogger Nothing [] System.Log.Logger.DEBUG -setupLspLogger file = LSP.setupLogger file [] System.Log.Logger.DEBUG +setupLspLogger (Just "[OUTPUT]") = setupLogger Nothing [] System.Log.Logger.DEBUG +setupLspLogger file = setupLogger file [] System.Log.Logger.DEBUG -- | Returns either a JSON parsing error message or the updated "ServerConfig". lspServerUpdateConfig :: ServerConfig -> Aeson.Value -> Either Text.Text ServerConfig diff --git a/waspc/waspls/src/Wasp/LSP/ServerMonads/HasProjectRootDir.hs b/waspc/waspls/src/Wasp/LSP/ServerMonads/HasProjectRootDir.hs index 1ba7c70cd6..d2cb60d32f 100644 --- a/waspc/waspls/src/Wasp/LSP/ServerMonads/HasProjectRootDir.hs +++ b/waspc/waspls/src/Wasp/LSP/ServerMonads/HasProjectRootDir.hs @@ -6,5 +6,5 @@ where import qualified StrongPath as SP import Wasp.Project (WaspProjectDir) -class Monad m => HasProjectRootDir m where +class (Monad m) => HasProjectRootDir m where getProjectRootDir :: m (Maybe (SP.Path' SP.Abs (SP.Dir WaspProjectDir))) diff --git a/waspc/waspls/src/Wasp/LSP/SignatureHelp.hs b/waspc/waspls/src/Wasp/LSP/SignatureHelp.hs index c03a61f772..70b0506ef2 100644 --- a/waspc/waspls/src/Wasp/LSP/SignatureHelp.hs +++ b/waspc/waspls/src/Wasp/LSP/SignatureHelp.hs @@ -207,7 +207,7 @@ signatureToFragments signature = case signatureType signature of in concat [["("], fields, [")"]] typ -> [fromString (show typ)] where - showInnerType :: IsString s => Type -> s + showInnerType :: (IsString s) => Type -> s showInnerType (Type.DictType _) = "{ ... }" showInnerType typ = fromString (show typ) diff --git a/waspc/waspls/src/Wasp/LSP/Util.hs b/waspc/waspls/src/Wasp/LSP/Util.hs index 197b0a7daf..ce12285e77 100644 --- a/waspc/waspls/src/Wasp/LSP/Util.hs +++ b/waspc/waspls/src/Wasp/LSP/Util.hs @@ -40,20 +40,20 @@ waspPositionToLspPosition (W.SourcePosition ln col) = } -- | Check if all the supplied predicates are true. -allP :: Foldable f => f (a -> Bool) -> a -> Bool +allP :: (Foldable f) => f (a -> Bool) -> a -> Bool allP preds x = all ($ x) preds -- | Check if any of the supplied predicates are true. -anyP :: Foldable f => f (a -> Bool) -> a -> Bool +anyP :: (Foldable f) => f (a -> Bool) -> a -> Bool anyP preds x = any ($ x) preds -- | Lift a 'Maybe' into a 'MaybeT' monad transformer. -hoistMaybe :: Applicative m => Maybe a -> MaybeT m a +hoistMaybe :: (Applicative m) => Maybe a -> MaybeT m a hoistMaybe = MaybeT . pure -- | @getPathRelativeToProjectDir file@ finds the path to @file@ if it is inside the -- project root directory. -getPathRelativeToProjectDir :: HasProjectRootDir m => SP.Path' SP.Abs (SP.File a) -> m (Maybe (SP.Path' (SP.Rel WaspProjectDir) (SP.File a))) +getPathRelativeToProjectDir :: (HasProjectRootDir m) => SP.Path' SP.Abs (SP.File a) -> m (Maybe (SP.Path' (SP.Rel WaspProjectDir) (SP.File a))) getPathRelativeToProjectDir file = do maybeProjectRootDir <- getProjectRootDir case maybeProjectRootDir of diff --git a/waspc/waspls/tests/Wasp/LSP/DebouncerTest.hs b/waspc/waspls/tests/Wasp/LSP/DebouncerTest.hs index 0f2f535d76..88aa5cf03d 100644 --- a/waspc/waspls/tests/Wasp/LSP/DebouncerTest.hs +++ b/waspc/waspls/tests/Wasp/LSP/DebouncerTest.hs @@ -7,7 +7,7 @@ import Control.Concurrent (newEmptyMVar, readMVar, tryPutMVar) import Control.Monad (void) import Data.Maybe (isJust) import System.Timeout (timeout) -import Test.Tasty.Hspec +import Test.Hspec import Wasp.LSP.Debouncer (debounce, newDebouncerIO) -- | Debounce time for all tests in microseconds. diff --git a/waspc/waspls/tests/Wasp/LSP/SyntaxTest.hs b/waspc/waspls/tests/Wasp/LSP/SyntaxTest.hs index 465be48ed4..9f583f143b 100644 --- a/waspc/waspls/tests/Wasp/LSP/SyntaxTest.hs +++ b/waspc/waspls/tests/Wasp/LSP/SyntaxTest.hs @@ -1,8 +1,8 @@ module Wasp.LSP.SyntaxTest where import qualified Language.LSP.Types as LSP +import Test.Hspec import Test.QuickCheck -import Test.Tasty.Hspec import Wasp.Analyzer.Parser.SourceSpan (SourceSpan (SourceSpan)) import Wasp.LSP.Syntax From 1f1103f482be0f4329f8730e7e3d06cdbff609d1 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Thu, 6 Nov 2025 16:06:52 +0100 Subject: [PATCH 02/92] Fix secret name (#3313) --- .github/workflows/automation-pr-label-external.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/automation-pr-label-external.yaml b/.github/workflows/automation-pr-label-external.yaml index 3d65792105..78e022ad07 100644 --- a/.github/workflows/automation-pr-label-external.yaml +++ b/.github/workflows/automation-pr-label-external.yaml @@ -39,7 +39,7 @@ jobs: AUTHOR_LOGIN: ${{ github.event.sender.login }} GITHUB_ORG: ${{ github.repository_owner }} TEAM_NAME: ${{ env.internal_team_name }} - GH_TOKEN: ${{ secrets.WASP_LANG_READ_MEMBERS }} + GH_TOKEN: ${{ secrets.GH_TOKEN_READ_WASP_LANG_MEMBERS }} - name: Label external PR if: steps.check_pr.outputs.pr_is_internal == 'false' From 7a55e9817c02a92ae1f26a797a0fce5dacedbee8 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Thu, 6 Nov 2025 16:31:54 +0100 Subject: [PATCH 03/92] Add fetch-nightly-cli GitHub Action documentation (#3312) Co-authored-by: Claude --- .github/actions/fetch-nightly-cli/README.md | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/actions/fetch-nightly-cli/README.md diff --git a/.github/actions/fetch-nightly-cli/README.md b/.github/actions/fetch-nightly-cli/README.md new file mode 100644 index 0000000000..04e081b5e8 --- /dev/null +++ b/.github/actions/fetch-nightly-cli/README.md @@ -0,0 +1,53 @@ +# `fetch-nightly-cli` action + +A GitHub Action that fetches the latest nightly CLI build from Wasp's CI runs. This action enables testing against the latest version of Wasp from `main` or from a specific branch/PR. + +> [!TIP] +> This action is intended for internal usage only. If you have a Wasp app and you're just looking to install Wasp in your CI, you can use this step instead: +> +> ```yaml +> - run: curl -sSL https://get.wasp.sh/installer.sh | sh -- -v [version] +> ``` + +## Background + +This action provides an internal "nightly" system for Wasp, allowing other repositories and workflows to download and install the binary from the latest successful CI run. The action: + +- Fetches the most recent successful build from the specified branch (defaults to `main`). +- Downloads the appropriate binary for the current OS and architecture. +- Can be used to test against PRs by specifying a different branch. + +## Usage + +### Basic example + +Fetch the latest nightly build from `main` and install it: + +```yaml +- uses: wasp-lang/wasp/.github/actions/fetch-nightly-cli@main +- run: curl -sSL https://get.wasp.sh/installer.sh | sh -- -f wasp-*.tar.gz +``` + +The wildcard `wasp-*.tar.gz` is used because the artifact has a different name depending on the OS and architecture. The `-f` flag tells the installer to use the locally downloaded file. + +### Fetch from a specific branch + +To test against a specific branch or PR: + +```yaml +- uses: wasp-lang/wasp/.github/actions/fetch-nightly-cli@main + with: + branch: my-feature-branch +- run: curl -sSL https://get.wasp.sh/installer.sh | sh -- -f wasp-*.tar.gz +``` + +### Custom output directory + +Download the CLI package to a specific directory: + +```yaml +- uses: wasp-lang/wasp/.github/actions/fetch-nightly-cli@main + with: + output-dir: ./bin +- run: curl -sSL https://get.wasp.sh/installer.sh | sh -- -f bin/wasp-*.tar.gz +``` From 0b04a040a06aca4e09e2330f8a6172d54033d9e0 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Thu, 6 Nov 2025 16:33:38 +0100 Subject: [PATCH 04/92] New PR template (#3255) --- .github/pull_request_template.md | 87 +++++++++++++++----------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 09d5d2f4e8..61a8ae3604 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,62 +1,57 @@ -### Description + -> Describe your PR! If this PR closes an issue, use “Fixes #(issue_number)" syntax so GitHub will auto-close it when merged. +## Description -### Select what type of change this PR introduces: +Replace this message here, and write a high-level overview with any additional +context (motivation, trade-offs, approaches considered, concerns, ...). -1. [ ] **Just code/docs improvement** (no functional change). -2. [ ] **Bug fix** (non-breaking change which fixes an issue). -3. [ ] **New feature** (non-breaking change which adds functionality). -4. [ ] **Breaking change** (fix or feature that would cause existing functionality to not work as expected). +## Type of change -### Update Waspc ChangeLog and version if needed + -If you did a **bug fix, new feature, or breaking change**, that affects `waspc`, make sure you satisfy the following: +- [ ] **🔧 Just code/docs improvement** +- [ ] **🐞 Bug fix** +- [ ] **🚀 New/improved feature** +- [ ] **💥 Breaking change** -1. [ ] I updated [`ChangeLog.md`](https://github.com/wasp-lang/wasp/blob/main/waspc/ChangeLog.md) with description of the change this PR introduces. -2. [ ] I bumped `waspc` version in [`waspc.cabal`](https://github.com/wasp-lang/wasp/blob/main/waspc/waspc.cabal) to reflect changes I introduced, with regards to the version of the latest wasp release, if the bump was needed. +## Checklist -### Add a regression test if needed + -If you did a **bug fix**, make sure you satisfy the following: +- [ ] I tested my change in a Wasp app to verify that it works as intended. -1. [ ] I added a regression test that reproduces the bug and verifies the fix. +- 🧪 Tests: -If you're unable to add a regression test, please explain why. -This likely indicates that our current testing setup needs improvement. + - [ ] I added **unit tests** for my change. + - [ ] _(if you fixed a bug)_ I added a **regression test** for the bug I fixed. + - [ ] _(if you added/updated a feature)_ I added/updated **e2e tests** at `examples/kitchen-sink/e2e-tests`. + - [ ] _(if you added/updated a feature)_ I updated the **starter templates** at `waspc/data/Cli/templates`, as needed. -### Test Coverage +- 📜 Documentation: -Please ensure your changes are adequately tested: + - [ ] _(if you added/updated a feature)_ I **added/updated the documentation** in `web/docs/`. -1. [ ] **My changes are covered by tests** (unit, integration, or e2e tests as appropriate). +- 🆕 Changelog: _(if change is more than just code/docs improvement)_ + - [ ] I updated `waspc/ChangeLog.md` with a **user-friendly** description of the change. + - [ ] _(if you did a breaking change)_ I added a step to the current **migration guide** at `web/docs/migration-guides/`. + - [ ] I **bumped the `version`** in `waspc/waspc.cabal` to reflect the changes I introduced. -If you're unable to add tests or if coverage is partial, please explain why below: + + We still haven't reached 1.0, so the version bumping follows these rules: + - Bug fix: 0.X.+1 (e.g. 0.16.3 bumps to 0.16.4) + - New feature: 0.X.+1 (e.g. 0.16.3 bumps to 0.16.4) + - Breaking change: 0.+1.0 (e.g. 0.16.3 bumps to 0.17.0) -### Update example apps if needed - -If you did code changes and **added a new feature**, make sure you satisfy the following: - -1. [ ] I updated [`waspc/examples/todoApp`](https://github.com/wasp-lang/wasp/tree/main/waspc/examples/todoApp) and its e2e tests as needed and manually checked it works correctly. - -If you did code changes and **updated an existing feature**, make sure you satisfy the following: - -1. [ ] I updated [`waspc/examples/todoApp`](https://github.com/wasp-lang/wasp/tree/main/waspc/examples/todoApp) and its e2e tests as needed and manually checked it works correctly. - -### Update starter apps if needed - -If you did code changes and **updated an existing feature**, make sure you satisfy the following: - -1. [ ] I updated [starter skeleton](https://github.com/wasp-lang/wasp/tree/main/waspc/data/Cli/templates/skeleton) as needed and manually checked it works correctly. -2. [ ] I updated [`basic` starter](https://github.com/wasp-lang/wasp/tree/main/waspc/data/Cli/templates/basic) as needed and manually checked it works correctly. -3. [ ] I updated [`todo-ts` starter](https://github.com/wasp-lang/starters/tree/dev/todo-ts) as needed and manually checked it works correctly. -4. [ ] I updated [`embeddings` starter](https://github.com/wasp-lang/starters/tree/dev/embeddings) as needed and manually checked it works correctly. -5. [ ] I updated [`saas` starter](https://github.com/wasp-lang/open-saas/tree/main/template) as needed and manually checked it works correctly. - -### Update e2e tests if needed - -If you did code changes and changed Wasp's code generation logic, make sure you satisfy the following: - -1. [] I updated [e2e tests](https://github.com/wasp-lang/wasp/tree/main/waspc#end-to-end-e2e-tests) as needed and manually checked they are correct. + If the version has already been bumped on `main` since the last release, skip this. +--> From 7f67577d3e8a8b2990b12b81dd20fac177e5d293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0o=C5=A1i=C4=87?= Date: Thu, 6 Nov 2025 22:00:38 +0100 Subject: [PATCH 05/92] Fix wrong names in ci-starters-test.yaml (copy/paste mistake) (#3319) --- .github/workflows/ci-starters-test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-starters-test.yaml b/.github/workflows/ci-starters-test.yaml index a4ac1e61f3..844e620324 100644 --- a/.github/workflows/ci-starters-test.yaml +++ b/.github/workflows/ci-starters-test.yaml @@ -1,4 +1,4 @@ -name: "Test - E2E Starters" +name: "Test - Starters" on: workflow_call: @@ -7,8 +7,8 @@ env: WASP_TELEMETRY_DISABLE: 1 jobs: - test-wasp-config: - name: Test wasp-config + test-starters: + name: Test starters runs-on: ubuntu-22.04 steps: - uses: "actions/checkout@v5" From 4a7d6857d0dfd7d2e6db2df9ca512c3b7ede7db4 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Thu, 6 Nov 2025 22:13:38 +0100 Subject: [PATCH 06/92] Remove LLVM workaround (#3317) --- .github/workflows/ci-waspc-build.yaml | 25 +------------------------ waspc/README.md | 6 ------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/.github/workflows/ci-waspc-build.yaml b/.github/workflows/ci-waspc-build.yaml index b81657fa81..49986f565e 100644 --- a/.github/workflows/ci-waspc-build.yaml +++ b/.github/workflows/ci-waspc-build.yaml @@ -84,30 +84,6 @@ jobs: - name: macos-aarch64 runner: macos-15 # By default an Apple Silicon-based runner. static: false # Check the comment above for why we can't statically link on macOS - install-deps: | - # We need to install llvm@13 for building on Apple Silicon (prebuilt libraries - # are only available for x86_64). The llvm@13 formula is not available in - # Homebrew by default, but we can edit it and comment out the `disable!` line. - - # Homebrew disabled the ability to install formulae from URL or from bare local - # files, so we will download it into a local repository (a.k.a "tap", in Homebrew - # parlance). - # https://github.com/Homebrew/brew/issues/20441#issuecomment-3186032595 - - formula_url="https://raw.githubusercontent.com/Homebrew/homebrew-core/74572f47ce6a2463c19d7fa164ab9fb8c91bbe61/Formula/l/llvm%4013.rb" - formula_name="llvm@13" - repo_name="wasp/repo" # This is a custom name, not an actual GitHub repo - formula_dir="$(brew --repository "$repo_name")/Formula" # Use a local repo for the formula - formula_file="$formula_dir/$formula_name.rb" - - # Fetch formula and comment out the `disable!` line - mkdir -p "$formula_dir" - curl -fsSL "$formula_url" > "$formula_file" - sed -i '' 's/disable!/# disable!/' "$formula_file" - - # Install and link the formula - brew install --formula "$repo_name/$formula_name" - brew link --force "$repo_name/$formula_name" runs-on: ${{ matrix.env.runner }} container: ${{ matrix.env.container }} @@ -117,6 +93,7 @@ jobs: - uses: actions/checkout@v5 - name: Install dependencies + if: ${{ matrix.env.install-deps }} run: ${{ matrix.env.install-deps }} - uses: ./.github/actions/setup-haskell diff --git a/waspc/README.md b/waspc/README.md index 5fa2ae3b78..1e11f0b4fb 100644 --- a/waspc/README.md +++ b/waspc/README.md @@ -81,12 +81,6 @@ to build the whole `waspc` project. This might take a while (e.g. 10 mins) if you are doing it for the very first time, due to having to download all the dependencies (which will later be cached). If that is the case, relax and feel free to get yourself a cup of coffee! When somebody asks what you are doing, you can finally rightfully say "compiling!" 😀. -> [!NOTE] -> If you are on Mac and get "Couldn't figure out LLVM version!" error message while building, make sure you have LLVM installed and that it is correctly exposed via env vars (PATH, LDFLAGS, CPPFLAGS). -> The easiest way to do it is by just running `brew install llvm@13`, this should install LLVM and also set up env vars. -> -> If the LLVM error persists even after its installation, you may need to manually add it your PATH. To do this, you should add the following to end of your shell rc file (e.g. _~/.bashrc_ or _~/.zshrc_): `export PATH="/opt/homebrew/opt/llvm@13/bin:$PATH"`. - ### Run tests ```sh From 39b885cecf238dc9653dda21c7b0e9342de7a9e1 Mon Sep 17 00:00:00 2001 From: Franjo Mindek <84568328+FranjoMindek@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:27:02 +0100 Subject: [PATCH 07/92] Update outdated Wasp DSL import statements (#3315) --- examples/kitchen-sink/main.wasp | 14 +++---- examples/websockets-realtime-voting/main.wasp | 10 ++--- mage/main.wasp | 40 +++++++++---------- waspc/data/Cli/starters/basic/main.wasp | 14 +++---- .../wasp-app/.wasp/out/.waspchecksums | 4 +- .../.wasp/out/server/src/auth/hooks.ts | 10 ++--- .../src/auth/providers/config/github.ts | 4 +- .../kitchen-sink-golden/wasp-app/main.wasp | 14 +++---- .../AI/GenerateNewProject/Common/Prompts.hs | 16 ++++---- .../AI/GenerateNewProject/InitialFiles.hs | 6 +-- .../Wasp/AI/GenerateNewProject/Operation.hs | 8 ++-- waspc/src/Wasp/AI/GenerateNewProject/Page.hs | 6 +-- .../Wasp/AI/GenerateNewProject/WaspFile.hs | 6 +-- .../externalImports.golden | 4 +- .../parseStatementsTests/externalImports.wasp | 4 +- waspc/tests/Analyzer/ValidTest.hs | 6 +-- waspc/tests/AnalyzerTest.hs | 38 +++++++++--------- .../Wasp/LSP/completionTests/at_dict_key.wasp | 2 +- .../completionTests/dict_key_duplicate.wasp | 2 +- .../Wasp/LSP/completionTests/route_to.wasp | 2 +- web/docs/auth/email.md | 32 +++++++-------- web/docs/auth/entities/entities.md | 4 +- web/docs/auth/ui.md | 20 +++++----- web/docs/auth/username-and-pass.md | 12 +++--- web/docs/data-model/crud.md | 14 +++---- web/docs/data-model/operations/actions.md | 24 +++++------ web/docs/data-model/operations/queries.md | 24 +++++------ web/docs/general/language.md | 2 +- web/docs/project/client-config.md | 24 +++++------ web/docs/project/server-config.md | 16 ++++---- web/src/components/Hero.jsx | 4 +- 31 files changed, 193 insertions(+), 193 deletions(-) diff --git a/examples/kitchen-sink/main.wasp b/examples/kitchen-sink/main.wasp index 44b066b3da..de56f1d12f 100644 --- a/examples/kitchen-sink/main.wasp +++ b/examples/kitchen-sink/main.wasp @@ -27,8 +27,8 @@ app KitchenSink { userSignupFields: import { userSignupFields } from "@src/features/auth/providers/google" }, gitHub: { - configFn: import { config } from "@src/features/auth/providers/github.js", - userSignupFields: import { userSignupFields } from "@src/features/auth/providers/github.js" + configFn: import { config } from "@src/features/auth/providers/github", + userSignupFields: import { userSignupFields } from "@src/features/auth/providers/github" }, // keycloak: {}, email: { @@ -49,11 +49,11 @@ app KitchenSink { }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/", - onBeforeSignup: import { onBeforeSignup } from "@src/features/auth/hooks.js", - onAfterSignup: import { onAfterSignup } from "@src/features/auth/hooks.js", - onAfterEmailVerified: import { onAfterEmailVerified } from "@src/features/auth/hooks.js", - onBeforeLogin: import { onBeforeLogin } from "@src/features/auth/hooks.js", - onAfterLogin: import { onAfterLogin } from "@src/features/auth/hooks.js", + onBeforeSignup: import { onBeforeSignup } from "@src/features/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/features/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/features/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/features/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/features/auth/hooks", }, server: { setupFn: import { setup } from "@src/serverSetup", diff --git a/examples/websockets-realtime-voting/main.wasp b/examples/websockets-realtime-voting/main.wasp index 60d94c0be3..30f67d0c15 100644 --- a/examples/websockets-realtime-voting/main.wasp +++ b/examples/websockets-realtime-voting/main.wasp @@ -4,7 +4,7 @@ app whereDoWeEat { }, title: "where-do-we-eat", client: { - rootComponent: import { Layout } from "@src/Layout.jsx", + rootComponent: import { Layout } from "@src/Layout", }, auth: { userEntity: User, @@ -14,22 +14,22 @@ app whereDoWeEat { } }, webSocket: { - fn: import { webSocketFn } from "@src/ws-server.js", + fn: import { webSocketFn } from "@src/ws-server", } } route RootRoute { path: "/", to: MainPage } page MainPage { - component: import Main from "@src/pages/MainPage.tsx", + component: import Main from "@src/pages/MainPage", authRequired: true } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/pages/LoginPage.jsx" + component: import { LoginPage } from "@src/pages/LoginPage" } route RegisterRoute { path: "/signup", to: RegisterPage } page RegisterPage { - component: import { SignupPage } from "@src/pages/SignupPage.jsx" + component: import { SignupPage } from "@src/pages/SignupPage" } diff --git a/mage/main.wasp b/mage/main.wasp index ed11011749..0e03acd652 100644 --- a/mage/main.wasp +++ b/mage/main.wasp @@ -17,7 +17,7 @@ app waspAi { "", ], client: { - rootComponent: import { RootComponent } from "@src/client/RootComponent.jsx", + rootComponent: import { RootComponent } from "@src/client/RootComponent", }, auth: { userEntity: User, @@ -38,93 +38,93 @@ app waspAi { route RootRoute { path: "/", to: MainPage } page MainPage { - component: import Main from "@src/client/pages/MainPage.jsx" + component: import Main from "@src/client/pages/MainPage" } route ResultRoute { path: "/result/:appId", to: ResultPage } page ResultPage { - component: import { ResultPage } from "@src/client/pages/ResultPage.jsx" + component: import { ResultPage } from "@src/client/pages/ResultPage" } route UserRoute { path: "/user", to: UserPage } page UserPage { - component: import { UserPage } from "@src/client/pages/UserPage.jsx", + component: import { UserPage } from "@src/client/pages/UserPage", authRequired: true } route StatsRoute { path: "/stats", to: StatsPage } page StatsPage { - component: import { Stats } from "@src/client/pages/StatsPage.jsx", + component: import { Stats } from "@src/client/pages/StatsPage", authRequired: true } route FeedbackRoute { path: "/feedback", to: FeedbackPage } page FeedbackPage { - component: import { Feedback } from "@src/client/pages/FeedbackPage.jsx", + component: import { Feedback } from "@src/client/pages/FeedbackPage", authRequired: true } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/client/pages/LoginPage.jsx", + component: import { LoginPage } from "@src/client/pages/LoginPage", } action startGeneratingNewApp { - fn: import { startGeneratingNewApp } from "@src/server/operations.js", + fn: import { startGeneratingNewApp } from "@src/server/operations", entities: [ Project, ] } action registerZipDownload { - fn: import { registerZipDownload } from "@src/server/operations.js", + fn: import { registerZipDownload } from "@src/server/operations", entities: [Project] } action createFeedback { - fn: import { createFeedback } from "@src/server/operations.js", + fn: import { createFeedback } from "@src/server/operations", entities: [Feedback] } action deleteMyself { - fn: import { deleteMyself } from "@src/server/operations.js", + fn: import { deleteMyself } from "@src/server/operations", entities: [User, Project, File, Log] } query getFeedback { - fn: import { getFeedback } from "@src/server/operations.js", + fn: import { getFeedback } from "@src/server/operations", entities: [Feedback] } query getProjectsByUser { - fn: import { getProjectsByUser } from "@src/server/operations.js", + fn: import { getProjectsByUser } from "@src/server/operations", entities: [Project] } query getAppGenerationResult { - fn: import { getAppGenerationResult } from "@src/server/operations.js", + fn: import { getAppGenerationResult } from "@src/server/operations", entities: [ Project ] } query getProjects { - fn: import { getProjects } from "@src/server/operations.js", + fn: import { getProjects } from "@src/server/operations", entities: [ Project ] } query getStats { - fn: import { getStats } from "@src/server/operations.js", + fn: import { getStats } from "@src/server/operations", entities: [ Project ] } query getNumProjects { - fn: import { getNumProjects } from "@src/server/operations.js", + fn: import { getNumProjects } from "@src/server/operations", entities: [ Project ] @@ -136,7 +136,7 @@ job checkPendingAppsJob { cron: "* * * * *", }, perform: { - fn: import { checkForPendingApps } from "@src/server/jobs/checkForPendingApps.js" + fn: import { checkForPendingApps } from "@src/server/jobs/checkForPendingApps" }, entities: [Project] } @@ -147,7 +147,7 @@ job failStaleAppsJobs { cron: "* * * * *", }, perform: { - fn: import { failStaleGenerations } from "@src/server/jobs/failStaleGenerations.js", + fn: import { failStaleGenerations } from "@src/server/jobs/failStaleGenerations", }, entities: [Project, Log] } @@ -155,7 +155,7 @@ job failStaleAppsJobs { job generateAppJob { executor: PgBoss, perform: { - fn: import { generateApp } from "@src/server/jobs/generateApp.js", + fn: import { generateApp } from "@src/server/jobs/generateApp", }, entities: [ Project, diff --git a/waspc/data/Cli/starters/basic/main.wasp b/waspc/data/Cli/starters/basic/main.wasp index 2fb408f4fa..a11719ba7c 100644 --- a/waspc/data/Cli/starters/basic/main.wasp +++ b/waspc/data/Cli/starters/basic/main.wasp @@ -30,34 +30,34 @@ app __waspAppName__ { provider: Dummy, }, client: { - rootComponent: import { App } from "@src/App.tsx", + rootComponent: import { App } from "@src/App", } } // #region Auth route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/auth/email/LoginPage.tsx" + component: import { LoginPage } from "@src/auth/email/LoginPage" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@src/auth/email/SignupPage.tsx" + component: import { SignupPage } from "@src/auth/email/SignupPage" } route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { - component: import { RequestPasswordResetPage } from "@src/auth/email/RequestPasswordResetPage.tsx", + component: import { RequestPasswordResetPage } from "@src/auth/email/RequestPasswordResetPage", } route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { - component: import { PasswordResetPage } from "@src/auth/email/PasswordResetPage.tsx", + component: import { PasswordResetPage } from "@src/auth/email/PasswordResetPage", } route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { - component: import { EmailVerificationPage } from "@src/auth/email/EmailVerificationPage.tsx", + component: import { EmailVerificationPage } from "@src/auth/email/EmailVerificationPage", } // #endregion Auth @@ -65,7 +65,7 @@ page EmailVerificationPage { route TasksRoute { path: "/", to: TasksPage } page TasksPage { authRequired: true, - component: import { TasksPage } from "@src/tasks/TasksPage.tsx" + component: import { TasksPage } from "@src/tasks/TasksPage" } query getTasks { diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums index 37c2d9e11d..4507d91eac 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums @@ -1677,7 +1677,7 @@ "file", "server/src/auth/hooks.ts" ], - "83e95b9635cf80d5a9ea8061560bcd632c2f03ffd4ab4aa68a35674a1d28ed51" + "c7bf9640c1fccbd84f9d5564d6e727f4b49c1051091fc785cfe78fa2f5911454" ], [ [ @@ -1698,7 +1698,7 @@ "file", "server/src/auth/providers/config/github.ts" ], - "1324f6edb08c6389025aaaaadcd51d345aa70d734a634cd31eee5396d91b46c0" + "c945f0d28b33c4c5683cb343fc52dedb9789c3058454896889d2a5e4d55751f3" ], [ [ diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/hooks.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/hooks.ts index 4a8df138ae..0f18af225d 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/hooks.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/hooks.ts @@ -8,11 +8,11 @@ import type { OnAfterLoginHook, InternalAuthHookParams, } from 'wasp/server/auth' -import { onBeforeSignup as onBeforeSignupHook_ext } from '../../../../../src/features/auth/hooks.js' -import { onAfterSignup as onAfterSignupHook_ext } from '../../../../../src/features/auth/hooks.js' -import { onAfterEmailVerified as onAfterEmailVerifiedHook_ext } from '../../../../../src/features/auth/hooks.js' -import { onBeforeLogin as onBeforeLoginHook_ext } from '../../../../../src/features/auth/hooks.js' -import { onAfterLogin as onAfterLoginHook_ext } from '../../../../../src/features/auth/hooks.js' +import { onBeforeSignup as onBeforeSignupHook_ext } from '../../../../../src/features/auth/hooks' +import { onAfterSignup as onAfterSignupHook_ext } from '../../../../../src/features/auth/hooks' +import { onAfterEmailVerified as onAfterEmailVerifiedHook_ext } from '../../../../../src/features/auth/hooks' +import { onBeforeLogin as onBeforeLoginHook_ext } from '../../../../../src/features/auth/hooks' +import { onAfterLogin as onAfterLoginHook_ext } from '../../../../../src/features/auth/hooks' /* These are "internal hook functions" based on the user defined hook functions. diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/providers/config/github.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/providers/config/github.ts index 3cdb0e6dff..677663757a 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/providers/config/github.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/server/src/auth/providers/config/github.ts @@ -4,9 +4,9 @@ import { github } from "wasp/server/auth"; import { mergeDefaultAndUserConfig } from "../oauth/config.js"; import { createOAuthProviderRouter } from "../oauth/handler.js"; -import { userSignupFields } from '../../../../../../../src/features/auth/providers/github.js' +import { userSignupFields } from '../../../../../../../src/features/auth/providers/github' const _waspUserSignupFields = userSignupFields -import { config } from '../../../../../../../src/features/auth/providers/github.js' +import { config } from '../../../../../../../src/features/auth/providers/github' const _waspUserDefinedConfigFn = config const _waspConfig: ProviderConfig = { diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp index 44b066b3da..de56f1d12f 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp @@ -27,8 +27,8 @@ app KitchenSink { userSignupFields: import { userSignupFields } from "@src/features/auth/providers/google" }, gitHub: { - configFn: import { config } from "@src/features/auth/providers/github.js", - userSignupFields: import { userSignupFields } from "@src/features/auth/providers/github.js" + configFn: import { config } from "@src/features/auth/providers/github", + userSignupFields: import { userSignupFields } from "@src/features/auth/providers/github" }, // keycloak: {}, email: { @@ -49,11 +49,11 @@ app KitchenSink { }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/", - onBeforeSignup: import { onBeforeSignup } from "@src/features/auth/hooks.js", - onAfterSignup: import { onAfterSignup } from "@src/features/auth/hooks.js", - onAfterEmailVerified: import { onAfterEmailVerified } from "@src/features/auth/hooks.js", - onBeforeLogin: import { onBeforeLogin } from "@src/features/auth/hooks.js", - onAfterLogin: import { onAfterLogin } from "@src/features/auth/hooks.js", + onBeforeSignup: import { onBeforeSignup } from "@src/features/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/features/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/features/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/features/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/features/auth/hooks", }, server: { setupFn: import { setup } from "@src/serverSetup", diff --git a/waspc/src/Wasp/AI/GenerateNewProject/Common/Prompts.hs b/waspc/src/Wasp/AI/GenerateNewProject/Common/Prompts.hs index e6eeaa06ab..ebdd1bb504 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/Common/Prompts.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/Common/Prompts.hs @@ -76,43 +76,43 @@ waspFileExample = onAuthFailedRedirectTo: "/login" }, client: { - rootComponent: import { Layout } from "@src/Layout.jsx", + rootComponent: import { Layout } from "@src/Layout", }, } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import Signup from "@src/pages/auth/Signup.jsx" + component: import Signup from "@src/pages/auth/Signup" } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import Login from "@src/pages/auth/Login.jsx" + component: import Login from "@src/pages/auth/Login" } route DashboardRoute { path: "/", to: DashboardPage } page DashboardPage { authRequired: true, - component: import Dashboard from "@src/pages/Dashboard.jsx" + component: import Dashboard from "@src/pages/Dashboard" } query getUser { - fn: import { getUser } from "@src/queries.js", + fn: import { getUser } from "@src/queries", entities: [User] // Entities that this query operates on. } query getTasks { - fn: import { getTasks } from "@src/queries.js", + fn: import { getTasks } from "@src/queries", entities: [Task] } action createTask { - fn: import { createTask } from "@src/actions.js", + fn: import { createTask } from "@src/actions", entities: [Task] } action updateTask { - fn: import { updateTask } from "@src/actions.js", + fn: import { updateTask } from "@src/actions", entities: [Task] } ``` diff --git a/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs b/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs index d54073cf0b..8763761272 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs @@ -100,18 +100,18 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules) "", ], client: { - rootComponent: import { Layout } from "@src/Layout.jsx", + rootComponent: import { Layout } from "@src/Layout", }, ${appAuth} } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import Login from "@src/pages/auth/Login.jsx" + component: import Login from "@src/pages/auth/Login" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import Signup from "@src/pages/auth/Signup.jsx" + component: import Signup from "@src/pages/auth/Signup" } |] diff --git a/waspc/src/Wasp/AI/GenerateNewProject/Operation.hs b/waspc/src/Wasp/AI/GenerateNewProject/Operation.hs index c9139247cf..dc9128010d 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/Operation.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/Operation.hs @@ -160,7 +160,7 @@ actionDocPrompt = - Wasp declaration: ```wasp action updateTask { - fn: import { updateTask } from "@src/taskActions.js", + fn: import { updateTask } from "@src/taskActions", entities: [Task] // Entities that action mutates. } ``` @@ -189,7 +189,7 @@ actionDocPrompt = ```wasp action deleteList { - fn: import { deleteList } from "@src/actions.js", + fn: import { deleteList } from "@src/actions", entities: [List, Card] } ``` @@ -232,7 +232,7 @@ queryDocPrompt = - Wasp declaration: ```wasp query fetchFilteredTasks { - fn: import { getFilteredTasks } from "@src/taskQueries.js", + fn: import { getFilteredTasks } from "@src/taskQueries", entities: [Task] // Entities that query uses. } ``` @@ -259,7 +259,7 @@ queryDocPrompt = - Wasp declaration: ```wasp query getAuthor { - fn: import { getAuthor } from "@src/author/queries.js", + fn: import { getAuthor } from "@src/author/queries", entities: [Author] } ``` diff --git a/waspc/src/Wasp/AI/GenerateNewProject/Page.hs b/waspc/src/Wasp/AI/GenerateNewProject/Page.hs index c5d88b270a..a00b175a7d 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/Page.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/Page.hs @@ -109,7 +109,7 @@ generatePage newProjectDetails entityPlans queries actions pPlan = do Example of such JSON: { - "pageWaspDecl": "route ExampleRoute { path: \"/\", to: ExamplePage }\npage ExamplePage {\n component: import ExamplePage from \"@src/ExamplePage.jsx\",\n authRequired: true\n}", + "pageWaspDecl": "route ExampleRoute { path: \"/\", to: ExamplePage }\npage ExamplePage {\n component: import ExamplePage from \"@src/ExamplePage\",\n authRequired: true\n}", "pageJsImpl": "JS imports + React component implementing the page.", } There should be no other text in the response. @@ -135,7 +135,7 @@ makePageDocPrompt = ```wasp route TasksRoute { path: "/", to: ExamplePage } page TasksPage { - component: import Tasks from "@src/Tasks.jsx", + component: import Tasks from "@src/Tasks", authRequired: true } ``` @@ -211,7 +211,7 @@ makePageDocPrompt = ```wasp route DashboardRoute { path: "/dashboard", to: DashboardPage } page DashboardPage { - component: import Dashboard from "@src/Dashboard.jsx", + component: import Dashboard from "@src/Dashboard", authRequired: true } ``` diff --git a/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs b/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs index c164015a1e..d60271898b 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs @@ -102,7 +102,7 @@ fixWaspFile newProjectDetails waspFilePath plan = do For example, the following is missing ',' after the component field: ```wasp page ExamplePage { - component: import ExamplePage from "@src/pages/ExamplePage.jsx" // <- missing ',' + component: import ExamplePage from "@src/pages/ExamplePage" // <- missing ',' authRequired: true } ``` @@ -110,8 +110,8 @@ fixWaspFile newProjectDetails waspFilePath plan = do Fix these by replacing them with actual implementation. - Strings in Wasp must use double quotes, not single quotes. - Value of `fn:` field in `query` or `action` not having correct import syntax, - for example it might have invalid syntax, e.g. `fn: @src/actions.js`. - Fix these by replacing it with correct syntax, e.g. `fn: import { actionName } from "@src/actions.js"`. + for example it might have invalid syntax, e.g. `fn: @src/actions`. + Fix these by replacing it with correct syntax, e.g. `fn: import { actionName } from "@src/actions"`. - I noticed that you sometimes by accident add redundant "}" at the end of the Wasp file while fixing it. Be careful not to do that. - We are using SQLite as a database for Prisma, so we can't use scalar arrays in PSL, like `String[]`, diff --git a/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.golden b/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.golden index 249612a8e4..bbdd7eaf61 100644 --- a/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.golden +++ b/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.golden @@ -2,10 +2,10 @@ (Decl@1:1-4:1 type=test name=Imports (Dict@1:14-4:1 (DictEntry key=module - (ExtImport@2:11-37 module=Page path="page.jsx") + (ExtImport@2:11-33 module=Page path="page") ) (DictEntry key=field - (ExtImport@3:10-40 field=Page path="page.jsx") + (ExtImport@3:10-36 field=Page path="page") ) ) ) diff --git a/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.wasp b/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.wasp index 8c5d8b8bf2..83b44f19b9 100644 --- a/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.wasp +++ b/waspc/tests/Analyzer/ParserTest/parseStatementsTests/externalImports.wasp @@ -1,4 +1,4 @@ test Imports { - module: import Page from "page.jsx", - field: import { Page } from "page.jsx" + module: import Page from "page", + field: import { Page } from "page" } diff --git a/waspc/tests/Analyzer/ValidTest.hs b/waspc/tests/Analyzer/ValidTest.hs index 0bd6cff4e7..fd02274a8a 100644 --- a/waspc/tests/Analyzer/ValidTest.hs +++ b/waspc/tests/Analyzer/ValidTest.hs @@ -38,7 +38,7 @@ spec_ValidateAst = do " userEntity: User,", " methods: {", " usernameAndPassword: {", - " userSignupFields: import { getUserFields } from \"@src/auth/signup.js\",", + " userSignupFields: import { getUserFields } from \"@src/auth/signup\",", " }", " },", " onAuthFailedRedirectTo: \"/\",", @@ -52,12 +52,12 @@ spec_ValidateAst = do "route HomeRoute { path: \"/\", to: HomePage }", "", "query getUsers {", - " fn: import { getAllUsers } from \"@src/foo.js\",", + " fn: import { getAllUsers } from \"@src/foo\",", " entities: [User]", "}", "", "action updateUser {", - " fn: import { updateUser } from \"@src/foo.js\",", + " fn: import { updateUser } from \"@src/foo\",", " entities: [User],", " auth: true", "}" diff --git a/waspc/tests/AnalyzerTest.hs b/waspc/tests/AnalyzerTest.hs index e6f4705cc1..d152e9a840 100644 --- a/waspc/tests/AnalyzerTest.hs +++ b/waspc/tests/AnalyzerTest.hs @@ -57,22 +57,22 @@ spec_Analyzer = do " userEntity: User,", " methods: {", " usernameAndPassword: {", - " userSignupFields: import { getUserFields } from \"@src/auth/signup.js\",", + " userSignupFields: import { getUserFields } from \"@src/auth/signup\",", " }", " },", " onAuthFailedRedirectTo: \"/\",", " },", " server: {", - " setupFn: import { setupServer } from \"@src/bar.js\"", + " setupFn: import { setupServer } from \"@src/bar\"", " },", " client: {", - " rootComponent: import { App } from \"@src/App.jsx\",", - " setupFn: import { setupClient } from \"@src/baz.js\",", + " rootComponent: import { App } from \"@src/App\",", + " setupFn: import { setupClient } from \"@src/baz\",", " baseDir: \"/\"", " },", " db: {", - " seeds: [ import { devSeedSimple } from \"@src/dbSeeds.js\" ],", - " prismaSetupFn: import { setUpPrisma } from \"@src/setUpPrisma.js\",", + " seeds: [ import { devSeedSimple } from \"@src/dbSeeds\" ],", + " prismaSetupFn: import { setUpPrisma } from \"@src/setUpPrisma\",", " },", " emailSender: {", " provider: SendGrid,", @@ -95,12 +95,12 @@ spec_Analyzer = do "route HomeRoute { path: \"/\", to: HomePage }", "", "query getUsers {", - " fn: import { getAllUsers } from \"@src/foo.js\",", + " fn: import { getAllUsers } from \"@src/foo\",", " entities: [User]", "}", "", "action updateUser {", - " fn: import { updateUser } from \"@src/foo.js\",", + " fn: import { updateUser } from \"@src/foo\",", " entities: [User],", " auth: true", "}", @@ -108,7 +108,7 @@ spec_Analyzer = do "job BackgroundJob {", " executor: PgBoss,", " perform: {", - " fn: import { backgroundJob } from \"@src/jobs/baz.js\",", + " fn: import { backgroundJob } from \"@src/jobs/baz\",", " executorOptions: {", " pgBoss: {=json { \"retryLimit\": 1 } json=}", " }", @@ -141,7 +141,7 @@ spec_Analyzer = do { Auth.usernameAndPassword = Just Auth.UsernameAndPasswordConfig - { Auth.userSignupFields = Just $ ExtImport (ExtImportField "getUserFields") (fromJust $ SP.parseRelFileP "auth/signup.js") + { Auth.userSignupFields = Just $ ExtImport (ExtImportField "getUserFields") (fromJust $ SP.parseRelFileP "auth/signup") }, Auth.slack = Nothing, Auth.discord = Nothing, @@ -166,7 +166,7 @@ spec_Analyzer = do Just $ ExtImport (ExtImportField "setupServer") - (fromJust $ SP.parseRelFileP "bar.js"), + (fromJust $ SP.parseRelFileP "bar"), Server.middlewareConfigFn = Nothing, Server.envValidationSchema = Nothing }, @@ -175,10 +175,10 @@ spec_Analyzer = do Client.Client { Client.setupFn = Just $ - ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js"), + ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz"), Client.rootComponent = Just $ - ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx"), + ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App"), Client.baseDir = Just "/", Client.envValidationSchema = Nothing }, @@ -189,13 +189,13 @@ spec_Analyzer = do Just [ ExtImport (ExtImportField "devSeedSimple") - (fromJust $ SP.parseRelFileP "dbSeeds.js") + (fromJust $ SP.parseRelFileP "dbSeeds") ], Db.prismaSetupFn = Just $ ExtImport (ExtImportField "setUpPrisma") - (fromJust $ SP.parseRelFileP "setUpPrisma.js") + (fromJust $ SP.parseRelFileP "setUpPrisma") }, App.emailSender = Just @@ -249,7 +249,7 @@ spec_Analyzer = do { Query.fn = ExtImport (ExtImportField "getAllUsers") - (fromJust $ SP.parseRelFileP "foo.js"), + (fromJust $ SP.parseRelFileP "foo"), Query.entities = Just [Ref "User"], Query.auth = Nothing } @@ -263,7 +263,7 @@ spec_Analyzer = do { Action.fn = ExtImport (ExtImportField "updateUser") - (fromJust $ SP.parseRelFileP "foo.js"), + (fromJust $ SP.parseRelFileP "foo"), Action.entities = Just [Ref "User"], Action.auth = Just True } @@ -275,7 +275,7 @@ spec_Analyzer = do Job.Perform ( ExtImport (ExtImportField "backgroundJob") - (fromJust $ SP.parseRelFileP "jobs/baz.js") + (fromJust $ SP.parseRelFileP "jobs/baz") ) ( Just $ Job.ExecutorOptions @@ -332,7 +332,7 @@ spec_Analyzer = do let source = unlines [ "route HomeRoute { path: \"/\", to: HomePage }", - "page HomePage { component: import Home from \"@src/HomePage.js\" }" + "page HomePage { component: import Home from \"@src/HomePage\" }" ] isRight (analyze prismaSchema source) `shouldBe` True diff --git a/waspc/waspls/tests/Wasp/LSP/completionTests/at_dict_key.wasp b/waspc/waspls/tests/Wasp/LSP/completionTests/at_dict_key.wasp index 4a7a87b35a..1e0af5fe4c 100644 --- a/waspc/waspls/tests/Wasp/LSP/completionTests/at_dict_key.wasp +++ b/waspc/waspls/tests/Wasp/LSP/completionTests/at_dict_key.wasp @@ -9,5 +9,5 @@ app todoApp { route MainRoute { | } ^ page MainPage { - component: import { MainPage } from "@client/MainPage.jsx", + component: import { MainPage } from "@client/MainPage", } diff --git a/waspc/waspls/tests/Wasp/LSP/completionTests/dict_key_duplicate.wasp b/waspc/waspls/tests/Wasp/LSP/completionTests/dict_key_duplicate.wasp index f30881f317..8c7554042c 100644 --- a/waspc/waspls/tests/Wasp/LSP/completionTests/dict_key_duplicate.wasp +++ b/waspc/waspls/tests/Wasp/LSP/completionTests/dict_key_duplicate.wasp @@ -10,5 +10,5 @@ app todoApp { route MainRoute { path: "/", | } ^ page MainPage { - component: import { MainPage } from "@client/MainPage.jsx", + component: import { MainPage } from "@client/MainPage", } diff --git a/waspc/waspls/tests/Wasp/LSP/completionTests/route_to.wasp b/waspc/waspls/tests/Wasp/LSP/completionTests/route_to.wasp index 873b3af383..300d3f47ab 100644 --- a/waspc/waspls/tests/Wasp/LSP/completionTests/route_to.wasp +++ b/waspc/waspls/tests/Wasp/LSP/completionTests/route_to.wasp @@ -9,5 +9,5 @@ app todoApp { route MainRoute { path: "/", to: | } ^ page MainPage { - component: import { MainPage } from "@client/MainPage.jsx", + component: import { MainPage } from "@client/MainPage", } diff --git a/web/docs/auth/email.md b/web/docs/auth/email.md index 4de90d86da..32fe729731 100644 --- a/web/docs/auth/email.md +++ b/web/docs/auth/email.md @@ -163,27 +163,27 @@ Add the following to the `main.wasp` file: route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { Login } from "@src/pages/auth.jsx" + component: import { Login } from "@src/pages/auth" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { Signup } from "@src/pages/auth.jsx" + component: import { Signup } from "@src/pages/auth" } route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { - component: import { RequestPasswordReset } from "@src/pages/auth.jsx", + component: import { RequestPasswordReset } from "@src/pages/auth", } route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { - component: import { PasswordReset } from "@src/pages/auth.jsx", + component: import { PasswordReset } from "@src/pages/auth", } route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { - component: import { EmailVerification } from "@src/pages/auth.jsx", + component: import { EmailVerification } from "@src/pages/auth", } ``` @@ -194,27 +194,27 @@ Add the following to the `main.wasp` file: route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { Login } from "@src/pages/auth.tsx" + component: import { Login } from "@src/pages/auth" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { Signup } from "@src/pages/auth.tsx" + component: import { Signup } from "@src/pages/auth" } route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { - component: import { RequestPasswordReset } from "@src/pages/auth.tsx", + component: import { RequestPasswordReset } from "@src/pages/auth", } route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { - component: import { PasswordReset } from "@src/pages/auth.tsx", + component: import { PasswordReset } from "@src/pages/auth", } route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { - component: import { EmailVerification } from "@src/pages/auth.tsx", + component: import { EmailVerification } from "@src/pages/auth", } ``` @@ -672,18 +672,18 @@ Let's go over the options we can specify when using email authentication. userEntity: User, methods: { email: { - userSignupFields: import { userSignupFields } from "@src/auth.js", + userSignupFields: import { userSignupFields } from "@src/auth", fromField: { name: "My App", email: "hello@itsme.com" }, emailVerification: { clientRoute: EmailVerificationRoute, - getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js", + getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", }, passwordReset: { clientRoute: PasswordResetRoute, - getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js", + getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", }, }, }, @@ -704,18 +704,18 @@ Let's go over the options we can specify when using email authentication. userEntity: User, methods: { email: { - userSignupFields: import { userSignupFields } from "@src/auth.js", + userSignupFields: import { userSignupFields } from "@src/auth", fromField: { name: "My App", email: "hello@itsme.com" }, emailVerification: { clientRoute: EmailVerificationRoute, - getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js", + getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", }, passwordReset: { clientRoute: PasswordResetRoute, - getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js", + getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", }, }, }, diff --git a/web/docs/auth/entities/entities.md b/web/docs/auth/entities/entities.md index a202467fea..f85e9c337a 100644 --- a/web/docs/auth/entities/entities.md +++ b/web/docs/auth/entities/entities.md @@ -598,7 +598,7 @@ Below is a simplified version of a custom signup action which you probably would // ... action customSignup { - fn: import { signup } from "@src/auth/signup.js", + fn: import { signup } from "@src/auth/signup", entities: [User] } ``` @@ -666,7 +666,7 @@ Below is a simplified version of a custom signup action which you probably would // ... action customSignup { - fn: import { signup } from "@src/auth/signup.js", + fn: import { signup } from "@src/auth/signup", entities: [User] } ``` diff --git a/web/docs/auth/ui.md b/web/docs/auth/ui.md index b1273a5359..d3e0712037 100644 --- a/web/docs/auth/ui.md +++ b/web/docs/auth/ui.md @@ -123,7 +123,7 @@ You can use the `LoginForm` component to build your login page: route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/LoginPage.jsx" + component: import { LoginPage } from "@src/LoginPage" } ``` @@ -143,7 +143,7 @@ You can use the `LoginForm` component to build your login page: route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/LoginPage.tsx" + component: import { LoginPage } from "@src/LoginPage" } ``` @@ -175,7 +175,7 @@ You can use the `SignupForm` component to build your signup page: route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@src/SignupPage.jsx" + component: import { SignupPage } from "@src/SignupPage" } ``` @@ -195,7 +195,7 @@ You can use the `SignupForm` component to build your signup page: route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@src/SignupPage.tsx" + component: import { SignupPage } from "@src/SignupPage" } ``` @@ -231,7 +231,7 @@ You can use the `ForgotPasswordForm` component to build your own forgot password route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { - component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage.jsx" + component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage" } ``` @@ -251,7 +251,7 @@ You can use the `ForgotPasswordForm` component to build your own forgot password route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { - component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage.tsx" + component: import { ForgotPasswordPage } from "@src/ForgotPasswordPage" } ``` @@ -283,7 +283,7 @@ You can use the `ResetPasswordForm` component to build your reset password page: route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { - component: import { ResetPasswordPage } from "@src/ResetPasswordPage.jsx" + component: import { ResetPasswordPage } from "@src/ResetPasswordPage" } ``` @@ -303,7 +303,7 @@ You can use the `ResetPasswordForm` component to build your reset password page: route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { - component: import { ResetPasswordPage } from "@src/ResetPasswordPage.tsx" + component: import { ResetPasswordPage } from "@src/ResetPasswordPage" } ``` @@ -335,7 +335,7 @@ You can use the `VerifyEmailForm` component to build your email verification pag route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { - component: import { VerifyEmailPage } from "@src/VerifyEmailPage.jsx" + component: import { VerifyEmailPage } from "@src/VerifyEmailPage" } ``` @@ -355,7 +355,7 @@ You can use the `VerifyEmailForm` component to build your email verification pag route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { - component: import { VerifyEmailPage } from "@src/VerifyEmailPage.tsx" + component: import { VerifyEmailPage } from "@src/VerifyEmailPage" } ``` diff --git a/web/docs/auth/username-and-pass.md b/web/docs/auth/username-and-pass.md index 129b31defc..2525e884d3 100644 --- a/web/docs/auth/username-and-pass.md +++ b/web/docs/auth/username-and-pass.md @@ -128,11 +128,11 @@ Add the following to the `main.wasp` file: // ... route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { Login } from "@src/pages/auth.jsx" + component: import { Login } from "@src/pages/auth" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { Signup } from "@src/pages/auth.jsx" + component: import { Signup } from "@src/pages/auth" } ``` @@ -142,11 +142,11 @@ Add the following to the `main.wasp` file: // ... route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { Login } from "@src/pages/auth.tsx" + component: import { Login } from "@src/pages/auth" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { Signup } from "@src/pages/auth.tsx" + component: import { Signup } from "@src/pages/auth" } ``` @@ -342,7 +342,7 @@ When you receive the `user` object [on the client or the server](./overview.md#a userEntity: User, methods: { usernameAndPassword: { - userSignupFields: import { userSignupFields } from "@src/auth/email.js", + userSignupFields: import { userSignupFields } from "@src/auth/email", }, }, onAuthFailedRedirectTo: "/login" @@ -363,7 +363,7 @@ When you receive the `user` object [on the client or the server](./overview.md#a userEntity: User, methods: { usernameAndPassword: { - userSignupFields: import { userSignupFields } from "@src/auth/email.js", + userSignupFields: import { userSignupFields } from "@src/auth/email", }, }, onAuthFailedRedirectTo: "/login" diff --git a/web/docs/data-model/crud.md b/web/docs/data-model/crud.md index 941c6313d2..377841432c 100644 --- a/web/docs/data-model/crud.md +++ b/web/docs/data-model/crud.md @@ -41,7 +41,7 @@ crud Tasks { }, get: {}, create: { - overrideFn: import { createTask } from "@src/tasks.js", + overrideFn: import { createTask } from "@src/tasks", }, update: {}, }, @@ -90,18 +90,18 @@ app tasksCrudApp { // Tasks app routes route RootRoute { path: "/", to: MainPage } page MainPage { - component: import { MainPage } from "@src/MainPage.jsx", + component: import { MainPage } from "@src/MainPage", authRequired: true, } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { - component: import { LoginPage } from "@src/LoginPage.jsx", + component: import { LoginPage } from "@src/LoginPage", } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { - component: import { SignupPage } from "@src/SignupPage.jsx", + component: import { SignupPage } from "@src/SignupPage", } ``` @@ -137,7 +137,7 @@ crud Tasks { operations: { getAll: {}, create: { - overrideFn: import { createTask } from "@src/tasks.js", + overrideFn: import { createTask } from "@src/tasks", }, }, } @@ -597,7 +597,7 @@ Here's an example of a more complex CRUD declaration: }, get: {}, create: { - overrideFn: import { createTask } from "@src/tasks.js", // optional + overrideFn: import { createTask } from "@src/tasks", // optional }, update: {}, }, @@ -615,7 +615,7 @@ Here's an example of a more complex CRUD declaration: }, get: {}, create: { - overrideFn: import { createTask } from "@src/tasks.js", // optional + overrideFn: import { createTask } from "@src/tasks", // optional }, update: {}, }, diff --git a/web/docs/data-model/operations/actions.md b/web/docs/data-model/operations/actions.md index ee220171e7..774d57d703 100644 --- a/web/docs/data-model/operations/actions.md +++ b/web/docs/data-model/operations/actions.md @@ -43,11 +43,11 @@ To create an Action in Wasp, we begin with an `action` declaration. Let's declar // ... action createTask { - fn: import { createTask } from "@src/actions.js" + fn: import { createTask } from "@src/actions" } action markTaskAsDone { - fn: import { markTaskAsDone } from "@src/actions.js" + fn: import { markTaskAsDone } from "@src/actions" } ``` @@ -58,11 +58,11 @@ To create an Action in Wasp, we begin with an `action` declaration. Let's declar // ... action createTask { - fn: import { createTask } from "@src/actions.js" + fn: import { createTask } from "@src/actions" } action markTaskAsDone { - fn: import { markTaskAsDone } from "@src/actions.js" + fn: import { markTaskAsDone } from "@src/actions" } ``` @@ -452,12 +452,12 @@ To use an Entity in your Action, add it to the `action` declaration in Wasp: ```wasp {4,9} title="main.wasp" action createTask { - fn: import { createTask } from "@src/actions.js", + fn: import { createTask } from "@src/actions", entities: [Task] } action markTaskAsDone { - fn: import { markTaskAsDone } from "@src/actions.js", + fn: import { markTaskAsDone } from "@src/actions", entities: [Task] } ``` @@ -467,12 +467,12 @@ To use an Entity in your Action, add it to the `action` declaration in Wasp: ```wasp {4,9} title="main.wasp" action createTask { - fn: import { createTask } from "@src/actions.js", + fn: import { createTask } from "@src/actions", entities: [Task] } action markTaskAsDone { - fn: import { markTaskAsDone } from "@src/actions.js", + fn: import { markTaskAsDone } from "@src/actions", entities: [Task] } ``` @@ -591,7 +591,7 @@ The `action` declaration supports the following fields: ```wasp query createFoo { - fn: import { createFoo } from "@src/actions.js" + fn: import { createFoo } from "@src/actions" entities: [Foo] } ``` @@ -612,7 +612,7 @@ The `action` declaration supports the following fields: ```wasp query createFoo { - fn: import { createFoo } from "@src/actions.js" + fn: import { createFoo } from "@src/actions" entities: [Foo] } ``` @@ -678,7 +678,7 @@ Since both arguments are positional, you can name the parameters however you wan ```wasp action createFoo { - fn: import { createFoo } from "@src/actions.js" + fn: import { createFoo } from "@src/actions" entities: [Foo] } ``` @@ -697,7 +697,7 @@ Since both arguments are positional, you can name the parameters however you wan ```wasp action createFoo { - fn: import { createFoo } from "@src/actions.js" + fn: import { createFoo } from "@src/actions" entities: [Foo] } ``` diff --git a/web/docs/data-model/operations/queries.md b/web/docs/data-model/operations/queries.md index e5a1c5b049..8f6afb7e04 100644 --- a/web/docs/data-model/operations/queries.md +++ b/web/docs/data-model/operations/queries.md @@ -44,11 +44,11 @@ Let's declare two Queries - one to fetch all tasks, and another to fetch tasks b // ... query getAllTasks { - fn: import { getAllTasks } from "@src/queries.js" + fn: import { getAllTasks } from "@src/queries" } query getFilteredTasks { - fn: import { getFilteredTasks } from "@src/queries.js" + fn: import { getFilteredTasks } from "@src/queries" } ``` @@ -58,11 +58,11 @@ Let's declare two Queries - one to fetch all tasks, and another to fetch tasks b // ... query getAllTasks { - fn: import { getAllTasks } from "@src/queries.js" + fn: import { getAllTasks } from "@src/queries" } query getFilteredTasks { - fn: import { getFilteredTasks } from "@src/queries.js" + fn: import { getFilteredTasks } from "@src/queries" } ``` @@ -469,12 +469,12 @@ To use an Entity in your Query, add it to the `query` declaration in Wasp: ```wasp {4,9} title="main.wasp" query getAllTasks { - fn: import { getAllTasks } from "@src/queries.js", + fn: import { getAllTasks } from "@src/queries", entities: [Task] } query getFilteredTasks { - fn: import { getFilteredTasks } from "@src/queries.js", + fn: import { getFilteredTasks } from "@src/queries", entities: [Task] } ``` @@ -484,12 +484,12 @@ To use an Entity in your Query, add it to the `query` declaration in Wasp: ```wasp {4,9} title="main.wasp" query getAllTasks { - fn: import { getAllTasks } from "@src/queries.js", + fn: import { getAllTasks } from "@src/queries", entities: [Task] } query getFilteredTasks { - fn: import { getFilteredTasks } from "@src/queries.js", + fn: import { getFilteredTasks } from "@src/queries", entities: [Task] } ``` @@ -561,7 +561,7 @@ The `query` declaration supports the following fields: ```wasp query getFoo { - fn: import { getFoo } from "@src/queries.js" + fn: import { getFoo } from "@src/queries" entities: [Foo] } ``` @@ -584,7 +584,7 @@ The `query` declaration supports the following fields: ```wasp query getFoo { - fn: import { getFoo } from "@src/queries.js" + fn: import { getFoo } from "@src/queries" entities: [Foo] } ``` @@ -650,7 +650,7 @@ Since both arguments are positional, you can name the parameters however you wan ```wasp query getFoo { - fn: import { getFoo } from "@src/queries.js" + fn: import { getFoo } from "@src/queries" entities: [Foo] } ``` @@ -669,7 +669,7 @@ Since both arguments are positional, you can name the parameters however you wan ```wasp query getFoo { - fn: import { getFoo } from "@src/queries.js" + fn: import { getFoo } from "@src/queries" entities: [Foo] } ``` diff --git a/web/docs/general/language.md b/web/docs/general/language.md index c817700bdd..89fd2f4ee9 100644 --- a/web/docs/general/language.md +++ b/web/docs/general/language.md @@ -24,7 +24,7 @@ app MyApp { route RootRoute { path: "/", to: DashboardPage } page DashboardPage { - component: import { DashboardPage } from "@src/Dashboard.jsx" + component: import { DashboardPage } from "@src/Dashboard" } ``` diff --git a/web/docs/project/client-config.md b/web/docs/project/client-config.md index 4bdd2dac68..f3c36e08b6 100644 --- a/web/docs/project/client-config.md +++ b/web/docs/project/client-config.md @@ -15,8 +15,8 @@ You can configure the client using the `client` field inside the `app` declarati title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.jsx", - setupFn: import mySetupFunction from "@src/myClientSetupCode.js" + rootComponent: import Root from "@src/Root", + setupFn: import mySetupFunction from "@src/myClientSetupCode" } } ``` @@ -28,8 +28,8 @@ You can configure the client using the `client` field inside the `app` declarati title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.tsx", - setupFn: import mySetupFunction from "@src/myClientSetupCode.ts" + rootComponent: import Root from "@src/Root", + setupFn: import mySetupFunction from "@src/myClientSetupCode" } } ``` @@ -56,7 +56,7 @@ Let's define a common layout for your application: title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.jsx", + rootComponent: import Root from "@src/Root", } } ``` @@ -87,7 +87,7 @@ Let's define a common layout for your application: title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.tsx", + rootComponent: import Root from "@src/Root", } } ``` @@ -126,7 +126,7 @@ This is how to set up various providers that your application needs: title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.jsx", + rootComponent: import Root from "@src/Root", } } ``` @@ -152,7 +152,7 @@ This is how to set up various providers that your application needs: title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.tsx", + rootComponent: import Root from "@src/Root", } } ``` @@ -298,8 +298,8 @@ router will work correctly, and all the assets will be served from title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.jsx", - setupFn: import mySetupFunction from "@src/myClientSetupCode.js" + rootComponent: import Root from "@src/Root", + setupFn: import mySetupFunction from "@src/myClientSetupCode" } } ``` @@ -311,8 +311,8 @@ router will work correctly, and all the assets will be served from title: "My app", // ... client: { - rootComponent: import Root from "@src/Root.tsx", - setupFn: import mySetupFunction from "@src/myClientSetupCode.ts", + rootComponent: import Root from "@src/Root", + setupFn: import mySetupFunction from "@src/myClientSetupCode", baseDir: "/my-app", } } diff --git a/web/docs/project/server-config.md b/web/docs/project/server-config.md index 076238d655..605eeafbec 100644 --- a/web/docs/project/server-config.md +++ b/web/docs/project/server-config.md @@ -13,8 +13,8 @@ You can configure the behavior of the server via the `server` field of `app` dec title: "My app", // ... server: { - setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js", - middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js" + setupFn: import { mySetupFunction } from "@src/myServerSetupCode", + middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode" } } ``` @@ -26,8 +26,8 @@ You can configure the behavior of the server via the `server` field of `app` dec title: "My app", // ... server: { - setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js", - middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js" + setupFn: import { mySetupFunction } from "@src/myServerSetupCode", + middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode" } } ``` @@ -167,8 +167,8 @@ Read more about [middleware config function](#middlewareconfigfn-extimport) belo title: "My app", // ... server: { - setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js", - middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js" + setupFn: import { mySetupFunction } from "@src/myServerSetupCode", + middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode" } } ``` @@ -180,8 +180,8 @@ Read more about [middleware config function](#middlewareconfigfn-extimport) belo title: "My app", // ... server: { - setupFn: import { mySetupFunction } from "@src/myServerSetupCode.js", - middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode.js" + setupFn: import { mySetupFunction } from "@src/myServerSetupCode", + middlewareConfigFn: import { myMiddlewareConfigFn } from "@src/myServerSetupCode" } } ``` diff --git a/web/src/components/Hero.jsx b/web/src/components/Hero.jsx index 115d3af9f5..909a16165f 100644 --- a/web/src/components/Hero.jsx +++ b/web/src/components/Hero.jsx @@ -74,11 +74,11 @@ const Hero = () => { route RootRoute { path: "/", to: MainPage } page MainPage { authRequired: true, // Limit access to logged in users. - component: import Main from "@client/Main.tsx" // Your React code. + component: import Main from "@client/Main" // Your React code. } query getTasks { - fn: import { getTasks } from "@server/tasks.js", // Your Node.js code. + fn: import { getTasks } from "@server/tasks", // Your Node.js code. entities: [Task] // Automatic cache invalidation. }`; From 03008934c5f7e08c47f9fad7816086bbc5428aab Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Fri, 7 Nov 2025 14:54:39 +0100 Subject: [PATCH 08/92] Fix `wasp start db` to use Postgres 18 explicitly (#3323) --- examples/tutorials/TodoApp/main.wasp | 2 +- examples/tutorials/TodoApp/package-lock.json | 18 ++++---- examples/tutorials/TodoAppTs/main.wasp | 2 +- examples/waspello/main.wasp.ts | 2 +- examples/waspleau/main.wasp | 2 +- examples/waspleau/package-lock.json | 14 +++--- examples/websockets-realtime-voting/main.wasp | 2 +- mage/main.wasp | 2 +- waspc/ChangeLog.md | 6 +++ waspc/cli/src/Wasp/Cli/Command/Start/Db.hs | 46 +++++++++++-------- .../waspBuild-golden/waspBuild/main.wasp | 2 +- .../waspCompile-golden/waspCompile/main.wasp | 2 +- .../waspComplexTest/main.wasp | 2 +- .../waspJob-golden/waspJob/main.wasp | 2 +- .../waspMigrate-golden/waspMigrate/main.wasp | 2 +- .../waspNew-golden/waspNew/main.wasp | 2 +- waspc/examples/todoApp/main.wasp | 2 +- waspc/waspc.cabal | 2 +- 18 files changed, 63 insertions(+), 49 deletions(-) diff --git a/examples/tutorials/TodoApp/main.wasp b/examples/tutorials/TodoApp/main.wasp index c52f88adb0..5359050717 100644 --- a/examples/tutorials/TodoApp/main.wasp +++ b/examples/tutorials/TodoApp/main.wasp @@ -1,6 +1,6 @@ app TodoApp { wasp: { - version: "^0.18.1" // Pins the version of Wasp to use. + version: "^0.18.2" // Pins the version of Wasp to use. }, title: "TodoApp", // Used as the browser tab title. Note that all strings in Wasp are double quoted! head: [ diff --git a/examples/tutorials/TodoApp/package-lock.json b/examples/tutorials/TodoApp/package-lock.json index f2da03db5b..c3e85e6f40 100644 --- a/examples/tutorials/TodoApp/package-lock.json +++ b/examples/tutorials/TodoApp/package-lock.json @@ -102,9 +102,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", "license": "MIT", "optional": true, "dependencies": { @@ -113,9 +113,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", "license": "MIT", "optional": true, "dependencies": { @@ -1777,9 +1777,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "license": "MIT", "optional": true, "dependencies": { diff --git a/examples/tutorials/TodoAppTs/main.wasp b/examples/tutorials/TodoAppTs/main.wasp index 3206d6fc7e..610cda5e1c 100644 --- a/examples/tutorials/TodoAppTs/main.wasp +++ b/examples/tutorials/TodoAppTs/main.wasp @@ -1,6 +1,6 @@ app TodoApp { wasp: { - version: "^0.18.1" // Pins the version of Wasp to use. + version: "^0.18.2" // Pins the version of Wasp to use. }, title: "TodoApp", // Used as the browser tab title. Note that all strings in Wasp are double quoted! head: [ diff --git a/examples/waspello/main.wasp.ts b/examples/waspello/main.wasp.ts index ccd9109b77..6ad23765a5 100644 --- a/examples/waspello/main.wasp.ts +++ b/examples/waspello/main.wasp.ts @@ -2,7 +2,7 @@ import { ActionConfig, App, ExtImport } from "wasp-config"; const app = new App("waspello", { title: "Waspello", - wasp: { version: "^0.18.1" }, + wasp: { version: "^0.18.2" }, }); app.auth({ diff --git a/examples/waspleau/main.wasp b/examples/waspleau/main.wasp index 357b212dd4..77f30a9ed9 100644 --- a/examples/waspleau/main.wasp +++ b/examples/waspleau/main.wasp @@ -1,6 +1,6 @@ app waspleau { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "Waspleau", diff --git a/examples/waspleau/package-lock.json b/examples/waspleau/package-lock.json index 312d596479..0a48935fb6 100644 --- a/examples/waspleau/package-lock.json +++ b/examples/waspleau/package-lock.json @@ -1188,21 +1188,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/examples/websockets-realtime-voting/main.wasp b/examples/websockets-realtime-voting/main.wasp index 2703a5206f..60d94c0be3 100644 --- a/examples/websockets-realtime-voting/main.wasp +++ b/examples/websockets-realtime-voting/main.wasp @@ -1,6 +1,6 @@ app whereDoWeEat { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "where-do-we-eat", client: { diff --git a/mage/main.wasp b/mage/main.wasp index 845509d998..ed11011749 100644 --- a/mage/main.wasp +++ b/mage/main.wasp @@ -1,6 +1,6 @@ app waspAi { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "MAGE - GPT Web App Generator ✨", head: [ diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index 3fc9c53b00..10d3817967 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -1,5 +1,11 @@ # Changelog +## 0.18.2 + +### 🐞 Bug fixes + +- Wasp dev database (`wasp start db`) now uses a pinned PostgreSQL version (`postgres:18`) with the correct volume mount path. ([#3323](https://github.com/wasp-lang/wasp/pull/3323)) + ## 0.18.1 ### 🐞 Bug fixes diff --git a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs index ac5bb63cd0..c430187164 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs @@ -107,31 +107,24 @@ startPostgreDevDb waspProjectDir appName = do -- only when initializing the database -> if it already exists, they will be ignored. -- This is how the postgres Docker image works. let command = - printf - ( unwords - [ "docker run", - "--name %s", - "--rm", - "--publish %d:5432", - "-v %s:/var/lib/postgresql/data", - "--env POSTGRES_PASSWORD=%s", - "--env POSTGRES_USER=%s", - "--env POSTGRES_DB=%s", - "postgres" - ] - ) - dockerContainerName - Dev.Postgres.defaultDevPort - dockerVolumeName - Dev.Postgres.defaultDevPass - Dev.Postgres.defaultDevUser - dbName + unwords + [ "docker run", + printf "--name %s" dockerContainerName, + "--rm", + printf "--publish %d:5432" Dev.Postgres.defaultDevPort, + printf "-v %s:%s" dockerVolumeName postgresDockerVolumeMountPath, + printf "--env POSTGRES_PASSWORD=%s" Dev.Postgres.defaultDevPass, + printf "--env POSTGRES_USER=%s" Dev.Postgres.defaultDevUser, + printf "--env POSTGRES_DB=%s" dbName, + postgresDockerImage + ] liftIO $ callCommand command where dockerVolumeName = makeWaspDevDbDockerVolumeName waspProjectDir appName dockerContainerName = makeWaspDevDbDockerContainerName waspProjectDir appName dbName = Dev.Postgres.makeDevDbName waspProjectDir appName connectionUrl = Dev.Postgres.makeDevConnectionUrl waspProjectDir appName + (postgresDockerImage, postgresDockerVolumeMountPath) = waspDevDbPostgresDockerImageSpec throwIfDevDbPortIsAlreadyInUse :: Command () throwIfDevDbPortIsAlreadyInUse = do @@ -171,3 +164,18 @@ makeWaspDevDbDockerContainerName waspProjectDir appName = maxDockerContainerNameLength :: Int maxDockerContainerNameLength = 63 + +type PostgresDockerImage = String + +type PostgresDockerVolumeMountPath = String + +-- | We pin the Postgres Docker image to avoid issues when a new major version of Postgres +-- is released. We aim to occasionally update this version in Wasp releases. +-- If you bump the Postgres version here, also check if `postgresDockerVolumeMountPath` +-- is still correct. +waspDevDbPostgresDockerImageSpec :: (PostgresDockerImage, PostgresDockerVolumeMountPath) +waspDevDbPostgresDockerImageSpec = ("postgres:18", postgresDockerVolumeMountPath) + where + -- Path inside the Postgres Docker container where the database files are stored. + postgresDockerVolumeMountPath :: PostgresDockerVolumeMountPath + postgresDockerVolumeMountPath = "/var/lib/postgresql" diff --git a/waspc/e2e-tests/test-outputs/waspBuild-golden/waspBuild/main.wasp b/waspc/e2e-tests/test-outputs/waspBuild-golden/waspBuild/main.wasp index 2a771a7b3d..f40d0fd480 100644 --- a/waspc/e2e-tests/test-outputs/waspBuild-golden/waspBuild/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspBuild-golden/waspBuild/main.wasp @@ -1,6 +1,6 @@ app waspBuild { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "waspBuild", head: [ diff --git a/waspc/e2e-tests/test-outputs/waspCompile-golden/waspCompile/main.wasp b/waspc/e2e-tests/test-outputs/waspCompile-golden/waspCompile/main.wasp index f2682c32e6..cce670600c 100644 --- a/waspc/e2e-tests/test-outputs/waspCompile-golden/waspCompile/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspCompile-golden/waspCompile/main.wasp @@ -1,6 +1,6 @@ app waspCompile { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "waspCompile", head: [ diff --git a/waspc/e2e-tests/test-outputs/waspComplexTest-golden/waspComplexTest/main.wasp b/waspc/e2e-tests/test-outputs/waspComplexTest-golden/waspComplexTest/main.wasp index 5912c0b489..a7642a8265 100644 --- a/waspc/e2e-tests/test-outputs/waspComplexTest-golden/waspComplexTest/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspComplexTest-golden/waspComplexTest/main.wasp @@ -1,6 +1,6 @@ app waspComplexTest { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, auth: { userEntity: User, diff --git a/waspc/e2e-tests/test-outputs/waspJob-golden/waspJob/main.wasp b/waspc/e2e-tests/test-outputs/waspJob-golden/waspJob/main.wasp index 26bd53aceb..fbb3911d2d 100644 --- a/waspc/e2e-tests/test-outputs/waspJob-golden/waspJob/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspJob-golden/waspJob/main.wasp @@ -1,6 +1,6 @@ app waspJob { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "waspJob", head: [ diff --git a/waspc/e2e-tests/test-outputs/waspMigrate-golden/waspMigrate/main.wasp b/waspc/e2e-tests/test-outputs/waspMigrate-golden/waspMigrate/main.wasp index be9fd0b71e..c7e9fe9b52 100644 --- a/waspc/e2e-tests/test-outputs/waspMigrate-golden/waspMigrate/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspMigrate-golden/waspMigrate/main.wasp @@ -1,6 +1,6 @@ app waspMigrate { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "waspMigrate", head: [ diff --git a/waspc/e2e-tests/test-outputs/waspNew-golden/waspNew/main.wasp b/waspc/e2e-tests/test-outputs/waspNew-golden/waspNew/main.wasp index 2aaf70b8bb..f83ac33c5e 100644 --- a/waspc/e2e-tests/test-outputs/waspNew-golden/waspNew/main.wasp +++ b/waspc/e2e-tests/test-outputs/waspNew-golden/waspNew/main.wasp @@ -1,6 +1,6 @@ app waspNew { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "waspNew", head: [ diff --git a/waspc/examples/todoApp/main.wasp b/waspc/examples/todoApp/main.wasp index f4f83e00ae..276a97303a 100644 --- a/waspc/examples/todoApp/main.wasp +++ b/waspc/examples/todoApp/main.wasp @@ -1,6 +1,6 @@ app todoApp { wasp: { - version: "^0.18.1" + version: "^0.18.2" }, title: "ToDo App", // head: [], diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 7de46673bf..125d5f992a 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -6,7 +6,7 @@ cabal-version: 2.4 -- Consider using hpack, or maybe even hpack-dhall. name: waspc -version: 0.18.1 +version: 0.18.2 description: Please see the README on GitHub at homepage: https://github.com/wasp-lang/wasp/waspc#readme bug-reports: https://github.com/wasp-lang/wasp/issues From b9471de4094a7b4ecce6492818c8a42168a39572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Fri, 7 Nov 2025 15:09:43 +0100 Subject: [PATCH 09/92] Trigger CI on all PRs, not just on main (#3325) --- .github/workflows/ci.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 22208b3466..9aa1b38677 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,9 +8,6 @@ on: - main - release pull_request: - branches: - - main - - release workflow_call: workflow_dispatch: From 367591051bb552176d1a7c8f2b8cef9058aa9de0 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Fri, 7 Nov 2025 15:50:18 +0100 Subject: [PATCH 10/92] Bumps the version to 0.18.3 after hotfix --- examples/kitchen-sink/main.wasp | 2 +- examples/tutorials/TodoApp/main.wasp | 2 +- examples/tutorials/TodoAppTs/main.wasp | 2 +- examples/waspello/main.wasp.ts | 2 +- examples/waspleau/main.wasp | 2 +- examples/websockets-realtime-voting/main.wasp | 2 +- mage/Dockerfile | 2 +- mage/main.wasp | 2 +- .../e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp | 2 +- waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/main.wasp | 2 +- .../e2e-tests/snapshots/wasp-compile-golden/wasp-app/main.wasp | 2 +- .../e2e-tests/snapshots/wasp-migrate-golden/wasp-app/main.wasp | 2 +- waspc/e2e-tests/snapshots/wasp-new-golden/wasp-app/main.wasp | 2 +- waspc/waspc.cabal | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/kitchen-sink/main.wasp b/examples/kitchen-sink/main.wasp index de56f1d12f..59eec97a12 100644 --- a/examples/kitchen-sink/main.wasp +++ b/examples/kitchen-sink/main.wasp @@ -1,6 +1,6 @@ app KitchenSink { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "Wasp Kitchen Sink", // head: [], diff --git a/examples/tutorials/TodoApp/main.wasp b/examples/tutorials/TodoApp/main.wasp index 5359050717..1cb3590728 100644 --- a/examples/tutorials/TodoApp/main.wasp +++ b/examples/tutorials/TodoApp/main.wasp @@ -1,6 +1,6 @@ app TodoApp { wasp: { - version: "^0.18.2" // Pins the version of Wasp to use. + version: "^0.18.3" // Pins the version of Wasp to use. }, title: "TodoApp", // Used as the browser tab title. Note that all strings in Wasp are double quoted! head: [ diff --git a/examples/tutorials/TodoAppTs/main.wasp b/examples/tutorials/TodoAppTs/main.wasp index a9aa5881d8..50c48f6af4 100644 --- a/examples/tutorials/TodoAppTs/main.wasp +++ b/examples/tutorials/TodoAppTs/main.wasp @@ -1,6 +1,6 @@ app TodoApp { wasp: { - version: "^0.18.2" // Pins the version of Wasp to use. + version: "^0.18.3" // Pins the version of Wasp to use. }, title: "TodoApp", // Used as the browser tab title. Note that all strings in Wasp are double quoted! head: [ diff --git a/examples/waspello/main.wasp.ts b/examples/waspello/main.wasp.ts index 6ad23765a5..df739bff85 100644 --- a/examples/waspello/main.wasp.ts +++ b/examples/waspello/main.wasp.ts @@ -2,7 +2,7 @@ import { ActionConfig, App, ExtImport } from "wasp-config"; const app = new App("waspello", { title: "Waspello", - wasp: { version: "^0.18.2" }, + wasp: { version: "^0.18.3" }, }); app.auth({ diff --git a/examples/waspleau/main.wasp b/examples/waspleau/main.wasp index 77f30a9ed9..7acdced13f 100644 --- a/examples/waspleau/main.wasp +++ b/examples/waspleau/main.wasp @@ -1,6 +1,6 @@ app waspleau { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "Waspleau", diff --git a/examples/websockets-realtime-voting/main.wasp b/examples/websockets-realtime-voting/main.wasp index 30f67d0c15..9505a7235d 100644 --- a/examples/websockets-realtime-voting/main.wasp +++ b/examples/websockets-realtime-voting/main.wasp @@ -1,6 +1,6 @@ app whereDoWeEat { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "where-do-we-eat", client: { diff --git a/mage/Dockerfile b/mage/Dockerfile index d9a10aeed4..c61c3b4646 100644 --- a/mage/Dockerfile +++ b/mage/Dockerfile @@ -39,7 +39,7 @@ RUN mkdir -p /app/.wasp/build/server/node_modules # TODO: Use pm2? # TODO: Use non-root user (node). FROM base AS server-production -RUN curl -sSL https://get.wasp.sh/installer.sh | sh -s -- -v 0.18.2 +RUN curl -sSL https://get.wasp.sh/installer.sh | sh -s -- -v 0.18.3 ENV PATH "$PATH:/root/.local/bin" ENV NODE_ENV production WORKDIR /app diff --git a/mage/main.wasp b/mage/main.wasp index 0e03acd652..d51fe411b4 100644 --- a/mage/main.wasp +++ b/mage/main.wasp @@ -1,6 +1,6 @@ app waspAi { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "MAGE - GPT Web App Generator ✨", head: [ diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp index de56f1d12f..59eec97a12 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/main.wasp @@ -1,6 +1,6 @@ app KitchenSink { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "Wasp Kitchen Sink", // head: [], diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/main.wasp index fc8fa0e321..2589826237 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/main.wasp @@ -1,6 +1,6 @@ app waspApp { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "wasp-app", head: [ diff --git a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/main.wasp index fc8fa0e321..2589826237 100644 --- a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/main.wasp @@ -1,6 +1,6 @@ app waspApp { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "wasp-app", head: [ diff --git a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/main.wasp index fc8fa0e321..2589826237 100644 --- a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/main.wasp @@ -1,6 +1,6 @@ app waspApp { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "wasp-app", head: [ diff --git a/waspc/e2e-tests/snapshots/wasp-new-golden/wasp-app/main.wasp b/waspc/e2e-tests/snapshots/wasp-new-golden/wasp-app/main.wasp index fc8fa0e321..2589826237 100644 --- a/waspc/e2e-tests/snapshots/wasp-new-golden/wasp-app/main.wasp +++ b/waspc/e2e-tests/snapshots/wasp-new-golden/wasp-app/main.wasp @@ -1,6 +1,6 @@ app waspApp { wasp: { - version: "^0.18.2" + version: "^0.18.3" }, title: "wasp-app", head: [ diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index f85238bf6a..da228e0843 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -6,7 +6,7 @@ cabal-version: 2.4 -- Consider using hpack, or maybe even hpack-dhall. name: waspc -version: 0.18.2 +version: 0.18.3 description: Please see the README on GitHub at homepage: https://github.com/wasp-lang/wasp/waspc#readme bug-reports: https://github.com/wasp-lang/wasp/issues From dcf9a4cafc68175dac6e4a2f4ac4fe756a584a2c Mon Sep 17 00:00:00 2001 From: Matija Sosic Date: Fri, 7 Nov 2025 20:50:27 +0100 Subject: [PATCH 11/92] web: removed design-ai-thon navbars. --- web/docusaurus.config.ts | 2 +- web/src/components/Nav/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/docusaurus.config.ts b/web/docusaurus.config.ts index 21ee77d3cf..6a6277a24d 100644 --- a/web/docusaurus.config.ts +++ b/web/docusaurus.config.ts @@ -32,7 +32,7 @@ const config: Config = { announcementBar: { id: "design-aithon", content: - '🎨 Wasp Design-AI-Thon is live! Oct 10 - Oct 19 Join now →', + 'Have a Wasp app in production? 🐝 We\'ll send you some swag! 👕', backgroundColor: "#8b5cf6", textColor: "#fff", isCloseable: false, diff --git a/web/src/components/Nav/index.jsx b/web/src/components/Nav/index.jsx index 0389677947..562c88a5dc 100644 --- a/web/src/components/Nav/index.jsx +++ b/web/src/components/Nav/index.jsx @@ -105,7 +105,7 @@ const Nav = () => { return ( <> - + {/* */}
- ) -} + ); +}; // ... TaskList, NewTaskForm ... ``` + Awesome! You can now mark this task as done. diff --git a/web/docs/tutorial/07-auth.md b/web/docs/tutorial/07-auth.md index 5928e6eddc..f70cbb49ab 100644 --- a/web/docs/tutorial/07-auth.md +++ b/web/docs/tutorial/07-auth.md @@ -4,6 +4,7 @@ title: 7. Adding Authentication import useBaseUrl from '@docusaurus/useBaseUrl'; import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'; +import { TutorialAction } from './TutorialAction'; Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support. @@ -23,6 +24,8 @@ Since Wasp manages authentication, it will create [the auth related entities](.. You must only add the `User` Entity to keep track of who owns which tasks: + + ```prisma title="schema.prisma" // ... @@ -30,11 +33,14 @@ model User { id Int @id @default(autoincrement()) } ``` + ## Adding Auth to the Project Next, tell Wasp to use full-stack [authentication](../auth/overview): + + ```wasp title="main.wasp" app TodoApp { wasp: { @@ -60,9 +66,12 @@ app TodoApp { // ... ``` + Don't forget to update the database schema by running: + + ```sh wasp db migrate-dev ``` @@ -82,6 +91,8 @@ Wasp also supports authentication using [Google](../auth/social-auth/google), [G Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file: + + ```wasp title="main.wasp" // ... @@ -95,46 +106,53 @@ page LoginPage { component: import { LoginPage } from "@src/LoginPage" } ``` + Great, Wasp now knows these pages exist! Here's the React code for the pages you've just imported: + + ```tsx title="src/LoginPage.tsx" auto-js -import { Link } from 'react-router-dom' -import { LoginForm } from 'wasp/client/auth' +import { Link } from "react-router-dom"; +import { LoginForm } from "wasp/client/auth"; export const LoginPage = () => { return ( -
+

I don't have an account yet (go to signup).
- ) -} + ); +}; ``` + The signup page is very similar to the login page: + + ```tsx title="src/SignupPage.tsx" auto-js -import { Link } from 'react-router-dom' -import { SignupForm } from 'wasp/client/auth' +import { Link } from "react-router-dom"; +import { SignupForm } from "wasp/client/auth"; export const SignupPage = () => { return ( -
+

I already have an account (go to login).
- ) -} + ); +}; ``` + :::tip Type-safe links @@ -146,6 +164,8 @@ export const SignupPage = () => { We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in: + + ```wasp title="main.wasp" // ... @@ -155,20 +175,24 @@ page MainPage { component: import { MainPage } from "@src/MainPage" } ``` + Now that auth is required for this page, unauthenticated users will be redirected to `/login`, as we specified with `app.auth.onAuthFailedRedirectTo`. Additionally, when `authRequired` is `true`, the page's React component will be provided a `user` object as prop. + + ```tsx title="src/MainPage.tsx" auto-js -import type { AuthUser } from 'wasp/auth' +import type { AuthUser } from "wasp/auth"; // highlight-next-line export const MainPage = ({ user }: { user: AuthUser }) => { // Do something with the user // ... -} +}; ``` + Ok, time to test this out. Navigate to the main page (`/`) of the app. You'll get redirected to `/login`, where you'll be asked to authenticate. @@ -194,6 +218,8 @@ However, you will notice that if you try logging in as different users and creat First, let's define a one-to-many relation between users and tasks (check the [Prisma docs on relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)): + + ```prisma title="schema.prisma" // ... @@ -213,9 +239,11 @@ model Task { userId Int? } ``` + As always, you must migrate the database after changing the Entities: + ```sh wasp db migrate-dev ``` @@ -232,41 +260,46 @@ Instead, we would do a data migration to take care of those tasks, even if it me Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks: + + ```ts title="src/queries.ts" auto-js -import type { Task } from 'wasp/entities' +import type { Task } from "wasp/entities"; // highlight-next-line -import { HttpError } from 'wasp/server' -import type { GetTasks } from 'wasp/server/operations' +import { HttpError } from "wasp/server"; +import type { GetTasks } from "wasp/server/operations"; export const getTasks: GetTasks = async (args, context) => { // highlight-start if (!context.user) { - throw new HttpError(401) + throw new HttpError(401); } // highlight-end return context.entities.Task.findMany({ // highlight-next-line where: { user: { id: context.user.id } }, - orderBy: { id: 'asc' }, - }) -} + orderBy: { id: "asc" }, + }); +}; ``` + + + ```ts title="src/actions.ts" auto-js -import type { Task } from 'wasp/entities' +import type { Task } from "wasp/entities"; // highlight-next-line -import { HttpError } from 'wasp/server' -import type { CreateTask, UpdateTask } from 'wasp/server/operations' +import { HttpError } from "wasp/server"; +import type { CreateTask, UpdateTask } from "wasp/server/operations"; -type CreateTaskPayload = Pick +type CreateTaskPayload = Pick; export const createTask: CreateTask = async ( args, - context + context, ) => { // highlight-start if (!context.user) { - throw new HttpError(401) + throw new HttpError(401); } // highlight-end return context.entities.Task.create({ @@ -275,10 +308,10 @@ export const createTask: CreateTask = async ( // highlight-next-line user: { connect: { id: context.user.id } }, }, - }) -} + }); +}; -type UpdateTaskPayload = Pick +type UpdateTaskPayload = Pick; export const updateTask: UpdateTask< UpdateTaskPayload, @@ -286,15 +319,16 @@ export const updateTask: UpdateTask< > = async (args, context) => { // highlight-start if (!context.user) { - throw new HttpError(401) + throw new HttpError(401); } // highlight-end return context.entities.Task.updateMany({ where: { id: args.id, user: { id: context.user.id } }, data: { isDone: args.isDone }, - }) -} + }); +}; ``` + :::note Due to how Prisma works, we had to convert `update` to `updateMany` in `updateTask` action to be able to specify the user id in `where`. @@ -316,10 +350,12 @@ You will see that each user has their tasks, just as we specified in our code! Last, but not least, let's add the logout functionality: + + ```tsx title="src/MainPage.tsx" auto-js with-hole // ... // highlight-next-line -import { logout } from 'wasp/client/auth' +import { logout } from "wasp/client/auth"; //... const MainPage = () => { @@ -330,9 +366,10 @@ const MainPage = () => { // highlight-next-line
- ) -} + ); +}; ``` +
This is it, we have a working authentication system, and our Todo app is multi-user! diff --git a/web/docs/tutorial/TutorialAction.tsx b/web/docs/tutorial/TutorialAction.tsx new file mode 100644 index 0000000000..c2ef586562 --- /dev/null +++ b/web/docs/tutorial/TutorialAction.tsx @@ -0,0 +1,146 @@ +import { useEffect, useRef, useState } from "react"; +/* +`TutorialAction` component is related to the Tutorial Actions Executor (TACTE) which you can find in the `web/tutorial-actions-executor` folder. + +Its main purpose is to provide metadata on how to execute tutorial actions programmatically (using TACTE) +Additionally, it renders tutorial action names during development for easier debugging. + +`TutorialAction` component is used in the `web/docs/tutorial/*.md` files to annotate specific tutorial actions. +*/ + +// IMPORTANT: If you change actions here, make sure to also update the types in `web/tutorial-actions-executor/src/actions/actions.ts`. +type ActionProps = { + id: string; +} & ( + | { + action: "INIT_APP"; + starterTemplateName: string; + } + | { + action: "APPLY_PATCH"; + } + | { + action: "MIGRATE_DB"; + } +); + +export function TutorialAction(props: React.PropsWithChildren) { + const isDevelopment = process.env.NODE_ENV !== "production"; + + return isDevelopment ? ( + + ) : ( + props.children + ); +} + +function TutorialActionDebugInfo({ + id, + action, + children, +}: React.PropsWithChildren) { + const { copied, handleCopy } = useCopy(id); + + return ( +
+
+
tutorial action: {action}
+
+ id: {id} + +
+
+ {children &&
{children}
} +
+ ); +} + +function useCopy(textToCopy: string): { + copied: boolean; + handleCopy: () => void; +} { + const [copied, setCopied] = useState(false); + const timeoutRef = useRef(null); + + const handleCopy = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + navigator.clipboard.writeText(textToCopy); + setCopied(true); + + timeoutRef.current = setTimeout(() => { + setCopied(false); + timeoutRef.current = null; + }, 1500); + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + return { copied, handleCopy }; +} + +const containerStyle: React.CSSProperties = { + marginBottom: "1.5rem", +}; + +const headerStyle: React.CSSProperties = { + display: "flex", + gap: "0.5rem", + marginBottom: "0.5rem", + alignItems: "center", +}; + +const idTextStyle: React.CSSProperties = { + marginRight: "0.25rem", +}; + +const copyButtonStyle: React.CSSProperties = { + fontSize: "0.65rem", + cursor: "pointer", + background: "rgba(255, 255, 255, 0.2)", + border: "1px solid rgba(255, 255, 255, 0.3)", + borderRadius: "0.25rem", + padding: "0.125rem 0.375rem", + color: "white", + transition: "all 0.2s ease", +}; + +const childrenContainerStyle: React.CSSProperties = { + border: "1px dashed #ef4444", + padding: "1rem", + borderRadius: "0.5rem", +}; + +const pillStyle: React.CSSProperties = { + borderRadius: "0.375rem", + paddingLeft: "0.625rem", + paddingRight: "0.625rem", + paddingTop: "0.375rem", + paddingBottom: "0.375rem", + fontSize: "0.75rem", + fontWeight: "600", + color: "white", + boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)", +}; + +const tutorialActionPillStyle: React.CSSProperties = { + ...pillStyle, + backgroundColor: "#6b7280", +}; + +const actionPillStyle: React.CSSProperties = { + ...pillStyle, + backgroundColor: "#ef4444", + display: "flex", + alignItems: "center", +}; diff --git a/web/docs/tutorial/patches/03-pages__prepare-project.patch b/web/docs/tutorial/patches/03-pages__prepare-project.patch new file mode 100644 index 0000000000..09902d6e10 --- /dev/null +++ b/web/docs/tutorial/patches/03-pages__prepare-project.patch @@ -0,0 +1,162 @@ +diff --git a/src/Main.css b/src/Main.css +deleted file mode 100644 +index 9e93c7a..0000000 +--- a/src/Main.css ++++ /dev/null +@@ -1,103 +0,0 @@ +-* { +- -webkit-font-smoothing: antialiased; +- -moz-osx-font-smoothing: grayscale; +- box-sizing: border-box; +- margin: 0; +- padding: 0; +-} +- +-body { +- font-family: +- -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", +- "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +-} +- +-#root { +- min-height: 100vh; +- display: flex; +- flex-direction: column; +- justify-content: center; +- align-items: center; +-} +- +-.container { +- margin: 3rem 3rem 10rem 3rem; +- max-width: 726px; +- display: flex; +- flex-direction: column; +- justify-content: center; +- align-items: center; +- text-align: center; +-} +- +-.logo { +- max-height: 200px; +- margin-bottom: 1rem; +-} +- +-.title { +- font-size: 4rem; +- font-weight: 700; +- margin-bottom: 1rem; +-} +- +-.content { +- font-size: 1.2rem; +- font-weight: 400; +- line-height: 2; +- margin-bottom: 3rem; +-} +- +-.buttons { +- display: flex; +- flex-direction: row; +- gap: 1rem; +-} +- +-.button { +- font-size: 1.2rem; +- font-weight: 700; +- text-decoration: none; +- padding: 1.2rem 1.5rem; +- border-radius: 10px; +-} +- +-.button-filled { +- color: black; +- background-color: #ffcc00; +- border: 2px solid #ffcc00; +- +- transition: all 0.2s ease-in-out; +-} +- +-.button-filled:hover { +- filter: brightness(0.95); +-} +- +-.button-outlined { +- color: black; +- background-color: transparent; +- border: 2px solid #ffcc00; +- +- transition: all 0.2s ease-in-out; +-} +- +-.button-outlined:hover { +- filter: brightness(0.95); +-} +- +-code { +- border-radius: 5px; +- border: 1px solid #ffcc00; +- padding: 0.2rem; +- background: #ffcc0044; +- font-family: +- Menlo, +- Monaco, +- Lucida Console, +- Liberation Mono, +- DejaVu Sans Mono, +- Bitstream Vera Sans Mono, +- Courier New, +- monospace; +-} +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index bdb62b3..144163a 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,37 +1,3 @@ +-import Logo from "./assets/logo.svg"; +-import "./Main.css"; +- +-export function MainPage() { +- return ( +-
+- wasp +- +-

Welcome to Wasp!

+- +-

+- This is page MainPage located at route /. +-
+- Open src/MainPage.tsx to edit it. +-

+- +-
+-
+- ); +-} ++export const MainPage = () => { ++ return
Hello world!
; ++}; +diff --git a/src/assets/logo.svg b/src/assets/logo.svg +deleted file mode 100644 +index faa99ae..0000000 +--- a/src/assets/logo.svg ++++ /dev/null +@@ -1 +0,0 @@ +- +\ No newline at end of file diff --git a/web/docs/tutorial/patches/04-entities__prisma-task.patch b/web/docs/tutorial/patches/04-entities__prisma-task.patch new file mode 100644 index 0000000000..6e0d47c74a --- /dev/null +++ b/web/docs/tutorial/patches/04-entities__prisma-task.patch @@ -0,0 +1,14 @@ +diff --git a/schema.prisma b/schema.prisma +index 190e2a8..65d7d30 100644 +--- a/schema.prisma ++++ b/schema.prisma +@@ -8,3 +8,9 @@ datasource db { + generator client { + provider = "prisma-client-js" + } ++ ++model Task { ++ id Int @id @default(autoincrement()) ++ description String ++ isDone Boolean @default(false) ++} diff --git a/web/docs/tutorial/patches/05-queries__main-page-tasks.patch b/web/docs/tutorial/patches/05-queries__main-page-tasks.patch new file mode 100644 index 0000000000..7bd4028566 --- /dev/null +++ b/web/docs/tutorial/patches/05-queries__main-page-tasks.patch @@ -0,0 +1,42 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index 144163a..addc7ba 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,3 +1,36 @@ ++import { getTasks, useQuery } from "wasp/client/operations"; ++import type { Task } from "wasp/entities"; ++ + export const MainPage = () => { +- return
Hello world!
; ++ const { data: tasks, isLoading, error } = useQuery(getTasks); ++ ++ return ( ++
++ {tasks && } ++ ++ {isLoading && "Loading..."} ++ {error && "Error: " + error} ++
++ ); ++}; ++ ++const TaskView = ({ task }: { task: Task }) => { ++ return ( ++
++ ++ {task.description} ++
++ ); ++}; ++ ++const TasksList = ({ tasks }: { tasks: Task[] }) => { ++ if (!tasks?.length) return
No tasks
; ++ ++ return ( ++
++ {tasks.map((task, idx) => ( ++ ++ ))} ++
++ ); + }; diff --git a/web/docs/tutorial/patches/05-queries__query-get-tasks-impl.patch b/web/docs/tutorial/patches/05-queries__query-get-tasks-impl.patch new file mode 100644 index 0000000000..6b0c04357c --- /dev/null +++ b/web/docs/tutorial/patches/05-queries__query-get-tasks-impl.patch @@ -0,0 +1,14 @@ +diff --git a/src/queries.ts b/src/queries.ts +new file mode 100644 +index 0000000..4e4ba1b +--- /dev/null ++++ b/src/queries.ts +@@ -0,0 +1,8 @@ ++import type { Task } from "wasp/entities"; ++import type { GetTasks } from "wasp/server/operations"; ++ ++export const getTasks: GetTasks = async (args, context) => { ++ return context.entities.Task.findMany({ ++ orderBy: { id: "asc" }, ++ }); ++}; diff --git a/web/docs/tutorial/patches/05-queries__query-get-tasks.patch b/web/docs/tutorial/patches/05-queries__query-get-tasks.patch new file mode 100644 index 0000000000..9248897f55 --- /dev/null +++ b/web/docs/tutorial/patches/05-queries__query-get-tasks.patch @@ -0,0 +1,18 @@ +diff --git a/main.wasp b/main.wasp +index a5f1899..c3df1d1 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -9,3 +9,13 @@ route RootRoute { path: "/", to: MainPage } + page MainPage { + component: import { MainPage } from "@src/MainPage" + } ++ ++query getTasks { ++ // Specifies where the implementation for the query function is. ++ // The path `@src/queries` resolves to `src/queries.ts`. ++ // No need to specify an extension. ++ fn: import { getTasks } from "@src/queries", ++ // Tell Wasp that this query reads from the `Task` entity. Wasp will ++ // automatically update the results of this query when tasks are modified. ++ entities: [Task] ++} diff --git a/web/docs/tutorial/patches/06-actions__action-create-task-impl.patch b/web/docs/tutorial/patches/06-actions__action-create-task-impl.patch new file mode 100644 index 0000000000..f28f25bc5d --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__action-create-task-impl.patch @@ -0,0 +1,19 @@ +diff --git a/src/actions.ts b/src/actions.ts +new file mode 100644 +index 0000000..b8b2026 +--- /dev/null ++++ b/src/actions.ts +@@ -0,0 +1,13 @@ ++import type { Task } from "wasp/entities"; ++import type { CreateTask } from "wasp/server/operations"; ++ ++type CreateTaskPayload = Pick; ++ ++export const createTask: CreateTask = async ( ++ args, ++ context, ++) => { ++ return context.entities.Task.create({ ++ data: { description: args.description }, ++ }); ++}; diff --git a/web/docs/tutorial/patches/06-actions__action-create-task.patch b/web/docs/tutorial/patches/06-actions__action-create-task.patch new file mode 100644 index 0000000000..c0952faf52 --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__action-create-task.patch @@ -0,0 +1,13 @@ +diff --git a/main.wasp b/main.wasp +index c3df1d1..9b3c9d8 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -19,3 +19,8 @@ query getTasks { + // automatically update the results of this query when tasks are modified. + entities: [Task] + } ++ ++action createTask { ++ fn: import { createTask } from "@src/actions", ++ entities: [Task] ++} diff --git a/web/docs/tutorial/patches/06-actions__action-update-task-impl.patch b/web/docs/tutorial/patches/06-actions__action-update-task-impl.patch new file mode 100644 index 0000000000..96085a3d01 --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__action-update-task-impl.patch @@ -0,0 +1,29 @@ +diff --git a/src/actions.ts b/src/actions.ts +index b8b2026..973d821 100644 +--- a/src/actions.ts ++++ b/src/actions.ts +@@ -1,5 +1,5 @@ + import type { Task } from "wasp/entities"; +-import type { CreateTask } from "wasp/server/operations"; ++import type { CreateTask, UpdateTask } from "wasp/server/operations"; + + type CreateTaskPayload = Pick; + +@@ -11,3 +11,17 @@ export const createTask: CreateTask = async ( + data: { description: args.description }, + }); + }; ++ ++type UpdateTaskPayload = Pick; ++ ++export const updateTask: UpdateTask = async ( ++ { id, isDone }, ++ context, ++) => { ++ return context.entities.Task.update({ ++ where: { id }, ++ data: { ++ isDone: isDone, ++ }, ++ }); ++}; diff --git a/web/docs/tutorial/patches/06-actions__action-update-task.patch b/web/docs/tutorial/patches/06-actions__action-update-task.patch new file mode 100644 index 0000000000..47c9e59df5 --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__action-update-task.patch @@ -0,0 +1,13 @@ +diff --git a/main.wasp b/main.wasp +index 9b3c9d8..eed193e 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -24,3 +24,8 @@ action createTask { + fn: import { createTask } from "@src/actions", + entities: [Task] + } ++ ++action updateTask { ++ fn: import { updateTask } from "@src/actions", ++ entities: [Task] ++} diff --git a/web/docs/tutorial/patches/06-actions__main-page-create-task-impl-form.patch b/web/docs/tutorial/patches/06-actions__main-page-create-task-impl-form.patch new file mode 100644 index 0000000000..af1e804b0d --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__main-page-create-task-impl-form.patch @@ -0,0 +1,36 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index addc7ba..3f1732f 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,4 +1,5 @@ +-import { getTasks, useQuery } from "wasp/client/operations"; ++import type { FormEvent } from "react"; ++import { createTask, getTasks, useQuery } from "wasp/client/operations"; + import type { Task } from "wasp/entities"; + + export const MainPage = () => { +@@ -34,3 +35,24 @@ const TasksList = ({ tasks }: { tasks: Task[] }) => { +
+ ); + }; ++ ++const NewTaskForm = () => { ++ const handleSubmit = async (event: FormEvent) => { ++ event.preventDefault(); ++ try { ++ const target = event.target as HTMLFormElement; ++ const description = target.description.value; ++ target.reset(); ++ await createTask({ description }); ++ } catch (err: any) { ++ window.alert("Error: " + err.message); ++ } ++ }; ++ ++ return ( ++
++ ++ ++
++ ); ++}; diff --git a/web/docs/tutorial/patches/06-actions__main-page-create-task-use-form.patch b/web/docs/tutorial/patches/06-actions__main-page-create-task-use-form.patch new file mode 100644 index 0000000000..2f6b8b3dea --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__main-page-create-task-use-form.patch @@ -0,0 +1,12 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index eb804b9..cd11965 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -8,6 +8,7 @@ export const MainPage = () => { + + return ( +
++ + {tasks && } + + {isLoading && "Loading..."} diff --git a/web/docs/tutorial/patches/06-actions__main-page-update-task.patch b/web/docs/tutorial/patches/06-actions__main-page-update-task.patch new file mode 100644 index 0000000000..de0210db83 --- /dev/null +++ b/web/docs/tutorial/patches/06-actions__main-page-update-task.patch @@ -0,0 +1,44 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index 44c52c8..ec559d7 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,5 +1,10 @@ +-import type { FormEvent } from "react"; +-import { createTask, getTasks, useQuery } from "wasp/client/operations"; ++import type { ChangeEvent, FormEvent } from "react"; ++import { ++ createTask, ++ getTasks, ++ updateTask, ++ useQuery, ++} from "wasp/client/operations"; + import type { Task } from "wasp/entities"; + + export const MainPage = () => { +@@ -17,9 +22,25 @@ export const MainPage = () => { + }; + + const TaskView = ({ task }: { task: Task }) => { ++ const handleIsDoneChange = async (event: ChangeEvent) => { ++ try { ++ await updateTask({ ++ id: task.id, ++ isDone: event.target.checked, ++ }); ++ } catch (error: any) { ++ window.alert("Error while updating task: " + error.message); ++ } ++ }; ++ + return ( +
+- ++ + {task.description} +
+ ); diff --git a/web/docs/tutorial/patches/07-auth__action-add-auth.patch b/web/docs/tutorial/patches/07-auth__action-add-auth.patch new file mode 100644 index 0000000000..307737476b --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__action-add-auth.patch @@ -0,0 +1,49 @@ +diff --git a/src/actions.ts b/src/actions.ts +index 973d821..b4a52e8 100644 +--- a/src/actions.ts ++++ b/src/actions.ts +@@ -1,4 +1,5 @@ + import type { Task } from "wasp/entities"; ++import { HttpError } from "wasp/server"; + import type { CreateTask, UpdateTask } from "wasp/server/operations"; + + type CreateTaskPayload = Pick; +@@ -7,21 +8,28 @@ export const createTask: CreateTask = async ( + args, + context, + ) => { ++ if (!context.user) { ++ throw new HttpError(401); ++ } + return context.entities.Task.create({ +- data: { description: args.description }, ++ data: { ++ description: args.description, ++ user: { connect: { id: context.user.id } }, ++ }, + }); + }; + + type UpdateTaskPayload = Pick; + +-export const updateTask: UpdateTask = async ( +- { id, isDone }, +- context, +-) => { +- return context.entities.Task.update({ +- where: { id }, +- data: { +- isDone: isDone, +- }, ++export const updateTask: UpdateTask< ++ UpdateTaskPayload, ++ { count: number } ++> = async (args, context) => { ++ if (!context.user) { ++ throw new HttpError(401); ++ } ++ return context.entities.Task.updateMany({ ++ where: { id: args.id, user: { id: context.user.id } }, ++ data: { isDone: args.isDone }, + }); + }; diff --git a/web/docs/tutorial/patches/07-auth__login-page-initial.patch b/web/docs/tutorial/patches/07-auth__login-page-initial.patch new file mode 100644 index 0000000000..10251db19d --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__login-page-initial.patch @@ -0,0 +1,20 @@ +diff --git a/src/LoginPage.tsx b/src/LoginPage.tsx +new file mode 100644 +index 0000000..e2bfb6d +--- /dev/null ++++ b/src/LoginPage.tsx +@@ -0,0 +1,14 @@ ++import { Link } from "react-router-dom"; ++import { LoginForm } from "wasp/client/auth"; ++ ++export const LoginPage = () => { ++ return ( ++
++ ++
++ ++ I don't have an account yet (go to signup). ++ ++
++ ); ++}; diff --git a/web/docs/tutorial/patches/07-auth__main-page-add-auth.patch b/web/docs/tutorial/patches/07-auth__main-page-add-auth.patch new file mode 100644 index 0000000000..301f9f5fda --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__main-page-add-auth.patch @@ -0,0 +1,19 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index ec559d7..bc435c6 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,4 +1,5 @@ + import type { ChangeEvent, FormEvent } from "react"; ++import type { AuthUser } from "wasp/auth"; + import { + createTask, + getTasks, +@@ -7,7 +8,7 @@ import { + } from "wasp/client/operations"; + import type { Task } from "wasp/entities"; + +-export const MainPage = () => { ++export const MainPage = ({ user }: { user: AuthUser }) => { + const { data: tasks, isLoading, error } = useQuery(getTasks); + + return ( diff --git a/web/docs/tutorial/patches/07-auth__main-page-add-logout.patch b/web/docs/tutorial/patches/07-auth__main-page-add-logout.patch new file mode 100644 index 0000000000..c498325864 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__main-page-add-logout.patch @@ -0,0 +1,19 @@ +diff --git a/src/MainPage.tsx b/src/MainPage.tsx +index bc435c6..b93386a 100644 +--- a/src/MainPage.tsx ++++ b/src/MainPage.tsx +@@ -1,5 +1,6 @@ + import type { ChangeEvent, FormEvent } from "react"; + import type { AuthUser } from "wasp/auth"; ++import { logout } from "wasp/client/auth"; + import { + createTask, + getTasks, +@@ -18,6 +19,7 @@ export const MainPage = ({ user }: { user: AuthUser }) => { + + {isLoading && "Loading..."} + {error && "Error: " + error} ++ +
+ ); + }; diff --git a/web/docs/tutorial/patches/07-auth__prisma-connect-task-user.patch b/web/docs/tutorial/patches/07-auth__prisma-connect-task-user.patch new file mode 100644 index 0000000000..fd50fa7d72 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__prisma-connect-task-user.patch @@ -0,0 +1,24 @@ +diff --git a/schema.prisma b/schema.prisma +index 76c3741..aad0a3c 100644 +--- a/schema.prisma ++++ b/schema.prisma +@@ -9,12 +9,15 @@ generator client { + provider = "prisma-client-js" + } + ++model User { ++ id Int @id @default(autoincrement()) ++ tasks Task[] ++} ++ + model Task { + id Int @id @default(autoincrement()) + description String + isDone Boolean @default(false) +-} +- +-model User { +- id Int @id @default(autoincrement()) ++ user User? @relation(fields: [userId], references: [id]) ++ userId Int? + } diff --git a/web/docs/tutorial/patches/07-auth__prisma-user.patch b/web/docs/tutorial/patches/07-auth__prisma-user.patch new file mode 100644 index 0000000000..0966ebfcd3 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__prisma-user.patch @@ -0,0 +1,12 @@ +diff --git a/schema.prisma b/schema.prisma +index 65d7d30..76c3741 100644 +--- a/schema.prisma ++++ b/schema.prisma +@@ -14,3 +14,7 @@ model Task { + description String + isDone Boolean @default(false) + } ++ ++model User { ++ id Int @id @default(autoincrement()) ++} diff --git a/web/docs/tutorial/patches/07-auth__query-add-auth.patch b/web/docs/tutorial/patches/07-auth__query-add-auth.patch new file mode 100644 index 0000000000..58918dcded --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__query-add-auth.patch @@ -0,0 +1,18 @@ +diff --git a/src/queries.ts b/src/queries.ts +index 4e4ba1b..92a513e 100644 +--- a/src/queries.ts ++++ b/src/queries.ts +@@ -1,8 +1,13 @@ + import type { Task } from "wasp/entities"; ++import { HttpError } from "wasp/server"; + import type { GetTasks } from "wasp/server/operations"; + + export const getTasks: GetTasks = async (args, context) => { ++ if (!context.user) { ++ throw new HttpError(401); ++ } + return context.entities.Task.findMany({ ++ where: { user: { id: context.user.id } }, + orderBy: { id: "asc" }, + }); + }; diff --git a/web/docs/tutorial/patches/07-auth__signup-page-initial.patch b/web/docs/tutorial/patches/07-auth__signup-page-initial.patch new file mode 100644 index 0000000000..8f87fcf87c --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__signup-page-initial.patch @@ -0,0 +1,20 @@ +diff --git a/src/SignupPage.tsx b/src/SignupPage.tsx +new file mode 100644 +index 0000000..ee62723 +--- /dev/null ++++ b/src/SignupPage.tsx +@@ -0,0 +1,14 @@ ++import { Link } from "react-router-dom"; ++import { SignupForm } from "wasp/client/auth"; ++ ++export const SignupPage = () => { ++ return ( ++
++ ++
++ ++ I already have an account (go to login). ++ ++
++ ); ++}; diff --git a/web/docs/tutorial/patches/07-auth__wasp-file-auth-required.patch b/web/docs/tutorial/patches/07-auth__wasp-file-auth-required.patch new file mode 100644 index 0000000000..458b674c84 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__wasp-file-auth-required.patch @@ -0,0 +1,12 @@ +diff --git a/main.wasp b/main.wasp +index f2aa7ca..aa5eef1 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -17,6 +17,7 @@ app TodoApp { + + route RootRoute { path: "/", to: MainPage } + page MainPage { ++ authRequired: true, + component: import { MainPage } from "@src/MainPage" + } + diff --git a/web/docs/tutorial/patches/07-auth__wasp-file-auth-routes.patch b/web/docs/tutorial/patches/07-auth__wasp-file-auth-routes.patch new file mode 100644 index 0000000000..07d56c73b3 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__wasp-file-auth-routes.patch @@ -0,0 +1,18 @@ +diff --git a/main.wasp b/main.wasp +index 5592563..f2aa7ca 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -39,3 +39,13 @@ action updateTask { + fn: import { updateTask } from "@src/actions", + entities: [Task] + } ++ ++route SignupRoute { path: "/signup", to: SignupPage } ++page SignupPage { ++ component: import { SignupPage } from "@src/SignupPage" ++} ++ ++route LoginRoute { path: "/login", to: LoginPage } ++page LoginPage { ++ component: import { LoginPage } from "@src/LoginPage" ++} diff --git a/web/docs/tutorial/patches/07-auth__wasp-file-auth.patch b/web/docs/tutorial/patches/07-auth__wasp-file-auth.patch new file mode 100644 index 0000000000..38b0ae4d48 --- /dev/null +++ b/web/docs/tutorial/patches/07-auth__wasp-file-auth.patch @@ -0,0 +1,23 @@ +diff --git a/main.wasp b/main.wasp +index f761afe..d4798a4 100644 +--- a/main.wasp ++++ b/main.wasp +@@ -5,7 +5,17 @@ app TodoApp { + title: "TodoApp", + head: [ + "", +- ] ++ ], ++ auth: { ++ // Tells Wasp which entity to use for storing users. ++ userEntity: User, ++ methods: { ++ // Enable username and password auth. ++ usernameAndPassword: {} ++ }, ++ // We'll see how this is used in a bit. ++ onAuthFailedRedirectTo: "/login" ++ } + } + + route RootRoute { path: "/", to: MainPage } diff --git a/web/tutorial-actions-executor/.gitignore b/web/tutorial-actions-executor/.gitignore new file mode 100644 index 0000000000..e89b2210df --- /dev/null +++ b/web/tutorial-actions-executor/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.result/ +e2e-tests/.result/ diff --git a/web/tutorial-actions-executor/README.md b/web/tutorial-actions-executor/README.md new file mode 100644 index 0000000000..d3e27cb418 --- /dev/null +++ b/web/tutorial-actions-executor/README.md @@ -0,0 +1,151 @@ +# Tutorial Actions Executor (tacte) + +## What is this CLI? + +Wasp docs have a tutorial that walks users through building a complete Wasp application step-by-step. + +Next to the text that explains each step, we added `` components that define machine-executable actions, +like "create a new Wasp app", "add authentication", "create a Task entity", etc. +This CLI tool reads those tutorial files, extracts the actions, and executes them in sequence +to create a fully functional Wasp application. + +## Commands + +The CLI provides three commands: + +### 1. Generate App (`npm run generate-app`) + +Creates a complete Wasp application by executing all tutorial actions in sequence. + +```bash +npm run generate-app +# Optional: pass a custom Wasp CLI binary/command +npm run generate-app -- --wasp-cli-command wasp +``` + +This command: + +- Reads all tutorial files (numbered like `01-setup.md`, `02-auth.md`, etc.). +- Extracts actions from `` components in each tutorial file. +- Applies each action in sequence (e.g. initialize app, apply patches, migrate DB). +- The result of applying all of the actions in a fully functional application. + +One of the action types is to apply a Git patch that modifies a source file. If applying +a patch fails due to conflicts, `generate-app` command pauses and allows you +to resolve the conflicts manually. + +### 2. Edit Patch Action (`npm run edit-patch-action`) + +Allows you to modify a specific patch action and automatically reapplies all subsequent actions. + +```bash +# Non-interactive (direct by ID): +npm run edit-patch-action -- --action-id "create-task-entity" + +# Interactive (pick an action from a list): +npm run edit-patch-action + +# Optional flags: +# - skip generating app before editing +npm run edit-patch-action -- --skip-generating-app +# - pass a custom Wasp CLI +npm run edit-patch-action -- --wasp-cli-command wasp +``` + +This command: + +- Generates the app from scratch using generate-app command. + - Generating the apps results in having each action as a separate commit in Git history. +- "Rewinds" the app to the commit of the specified action and allows you to make changes + to its code as needed. +- Asks you to confirm when you are done, updates the commit of the action with the changes + you did, and reapplies all subsequent commits on top of it. + - There might be conflicts between your new changes and subsequent commits. + If so, you will need to resolve them manually. + +### 3. List Actions (`npm run list-actions`) + +Displays all available tutorial actions organized by source file. + +```bash +npm run list-actions +``` + +Shows actions grouped by tutorial file, including each action's `id` and `kind`. + +### Required Command Options + +All commands require the following options to set up the tutorial app configuration: + +- `--app-name `: Name of the app to generate. +- `--output-dir `: Directory where the app will be generated (default: `./.result`) +- `--tutorial-dir `: Directory containing the tutorial files. + +For example: + +```bash +npm run generate-app -- --app-name MyApp --output-dir ./custom-output --tutorial-dir ./my-tutorial +``` + +### Patch File Management + +- Patch files need to exist in the `patches` dir in the configured tutorial dir +- Files are named using the source file and action ID. +- Each patch file contains a Git diff for that specific action. + +### Tutorial File Format + +Tutorial actions are defined in MDX files using JSX components: + +````mdx +# Step 4: Create Task Entity + +In this action, we'll create the Task entity: + + +```prisma +model Task { + id Int @id @default(autoincrement()) +} +``` + +```` + +The `` component should wrap the part of the tutorial text that it is associated with. + +The tool extracts these components and uses: + +- `id`: Unique identifier for the action (becomes commit message), +- `action`: Type of action (`INIT_APP`, `APPLY_PATCH`, `MIGRATE_DB`). + +## Testing + +The project includes both unit tests and end-to-end (e2e) snapshot tests. + +You can run all tests using: + +```bash +npm run test +``` + +### E2E Snapshot Tests + +E2E tests verify the entire tutorial action execution flow using snapshot testing. + +Snapshot testing captures the output of the tutorial action executor (generated files, git history, etc.) +and stores it as a "snapshot". On subsequent test runs, the output is compared against the stored snapshot +to ensure nothing has changed unexpectedly. + +#### E2E Test Structure + +- Test fixtures: `e2e-tests/fixtures/tutorial/` contains minimal tutorial files used for testing. +- Generated output: `e2e-tests/.result/`. +- Snapshots: `e2e-tests/__snapshots__/`. + +#### Updating Snapshots + +When you intentionally change the tutorial action executor behavior, run the tests in update mode: + +```bash +$ npm run test --update +``` diff --git a/web/tutorial-actions-executor/e2e-tests/__snapshots__/generate-app.test.ts.snap b/web/tutorial-actions-executor/e2e-tests/__snapshots__/generate-app.test.ts.snap new file mode 100644 index 0000000000..537f85ed4b --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/__snapshots__/generate-app.test.ts.snap @@ -0,0 +1,79 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generate-app e2e > should generate app from tutorial actions and match snapshots > file-structure 1`] = ` +[ + "main.wasp", + "migrations/NORMALIZED_TIMESTAMP_E2E_migrate_db/migration.sql", + "migrations/migration_lock.toml", + "package-lock.json", + "package.json", + "public/favicon.ico", + "schema.prisma", + "src/Main.css", + "src/MainPage.tsx", + "src/assets/logo.svg", + "src/testUtils.ts", + "src/vite-env.d.ts", + "tsconfig.json", + "vite.config.ts", +] +`; + +exports[`generate-app e2e > should generate app from tutorial actions and match snapshots > git-commits 1`] = ` +"migrate-db + +migrations/NORMALIZED_TIMESTAMP_E2E_migrate_db/migration.sql +migrations/migration_lock.toml +package-lock.json +add-model + +schema.prisma +add-test-file + +src/testUtils.ts +create-test-app + +.gitignore +.waspignore +.wasproot +main.wasp +package.json +public/.gitkeep +public/favicon.ico +schema.prisma +src/Main.css +src/MainPage.tsx +src/assets/logo.svg +src/vite-env.d.ts +tsconfig.json +vite.config.ts +" +`; + +exports[`generate-app e2e > should generate app from tutorial actions and match snapshots > prisma-schema 1`] = ` +"datasource db { + provider = "sqlite" + // Wasp requires that the url is set to the DATABASE_URL environment variable. + url = env("DATABASE_URL") +} + +// Wasp requires the \`prisma-client-js\` generator to be present. +generator client { + provider = "prisma-client-js" +} + +model Post { + id Int @id @default(autoincrement()) + title String + content String? + createdAt DateTime @default(now()) +} +" +`; + +exports[`generate-app e2e > should generate app from tutorial actions and match snapshots > test-utils 1`] = ` +"export function testHelper() { + return "test helper function"; +} +" +`; diff --git a/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/01-init.md b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/01-init.md new file mode 100644 index 0000000000..0c81d0b665 --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/01-init.md @@ -0,0 +1,9 @@ +--- +title: 1. Initialize Test App +--- + +import { TutorialAction } from './TutorialAction'; + +## Create a New Wasp App + + diff --git a/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/02-patch.md b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/02-patch.md new file mode 100644 index 0000000000..961a17c972 --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/02-patch.md @@ -0,0 +1,19 @@ +--- +title: 2. Apply a Patch +--- + +import { TutorialAction } from './TutorialAction'; + +## Add a Test File + +Let's add a new file `src/testUtils.ts` to our Wasp app: + + + +```ts title="src/testUtils.ts" +export function testHelper() { + return "test helper function"; +} +``` + + diff --git a/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/03-migrate.md b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/03-migrate.md new file mode 100644 index 0000000000..a50ac956bd --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/03-migrate.md @@ -0,0 +1,25 @@ +--- +title: 3. Migrate DB +--- + +import { TutorialAction } from './TutorialAction'; + +## Migrate DB + + +Add a model to `schema.prisma`: + +```prisma +model Post { + id Int @id @default(autoincrement()) + title String + content String? + createdAt DateTime @default(now()) +} +``` + + + +Now, migrate the database with `wasp db migrate-dev`. + + diff --git a/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/02-patch__add-test-file.patch b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/02-patch__add-test-file.patch new file mode 100644 index 0000000000..58313f4768 --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/02-patch__add-test-file.patch @@ -0,0 +1,9 @@ +diff --git a/src/testUtils.ts b/src/testUtils.ts +new file mode 100644 +index 0000000..afe13c3 +--- /dev/null ++++ b/src/testUtils.ts +@@ -0,0 +1,3 @@ ++export function testHelper() { ++ return "test helper function"; ++} diff --git a/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/03-migrate__add-model.patch b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/03-migrate__add-model.patch new file mode 100644 index 0000000000..ff69a3d29d --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/fixtures/tutorial/patches/03-migrate__add-model.patch @@ -0,0 +1,15 @@ +diff --git a/schema.prisma b/schema.prisma +index 190e2a8..5dc8402 100644 +--- a/schema.prisma ++++ b/schema.prisma +@@ -8,3 +8,10 @@ datasource db { + generator client { + provider = "prisma-client-js" + } ++ ++model Post { ++ id Int @id @default(autoincrement()) ++ title String ++ content String? ++ createdAt DateTime @default(now()) ++} diff --git a/web/tutorial-actions-executor/e2e-tests/generate-app.test.ts b/web/tutorial-actions-executor/e2e-tests/generate-app.test.ts new file mode 100644 index 0000000000..45b4e78528 --- /dev/null +++ b/web/tutorial-actions-executor/e2e-tests/generate-app.test.ts @@ -0,0 +1,79 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; +import { $, glob } from "zx"; + +const currentDirPath = import.meta.dirname; + +const TEST_APP_NAME = "e2e-test-app"; +const TEST_OUTPUT_DIR = path.join(currentDirPath, ".result"); +const TEST_TUTORIAL_DIR = path.join(currentDirPath, "fixtures", "tutorial"); +const WASP_CLI_COMMAND = process.env.WASP_CLI_COMMAND ?? "wasp-cli"; + +describe("generate-app e2e", () => { + beforeAll(async () => { + await fs.rm(TEST_OUTPUT_DIR, { recursive: true, force: true }); + }); + + it( + "should generate app from tutorial actions and match snapshots", + async () => { + const generateAppOptions = [ + "--app-name", + TEST_APP_NAME, + "--output-dir", + TEST_OUTPUT_DIR, + "--tutorial-dir", + TEST_TUTORIAL_DIR, + "--wasp-cli-command", + WASP_CLI_COMMAND, + ]; + await $`npm run tacte -- generate-app ${generateAppOptions}`.verbose(); + + const projectDirPath = path.join(TEST_OUTPUT_DIR, TEST_APP_NAME); + + expect(await getProjectFileList(projectDirPath)).toMatchSnapshot( + "file-structure", + ); + expect( + await getProjectFileContent(projectDirPath, "src/testUtils.ts"), + ).toMatchSnapshot("test-utils"); + expect( + await getProjectFileContent(projectDirPath, "schema.prisma"), + ).toMatchSnapshot("prisma-schema"); + expect(await getGitLog(projectDirPath)).toMatchSnapshot("git-commits"); + }, + 60 * 1000, // Test timeout + ); +}); + +function getProjectFileContent( + projectDirPath: string, + pathInProject: string, +): Promise { + return fs.readFile(path.join(projectDirPath, pathInProject), "utf-8"); +} + +async function getProjectFileList(projectDirPath: string): Promise { + const files = await glob("**/*", { + cwd: projectDirPath, + ignore: ["node_modules/**", ".wasp/**", ".git/**"], + onlyFiles: true, + }); + return files.map(normalizeTimestamps).sort(); +} + +async function getGitLog(projectDirPath: string): Promise { + const gitLogResult = + await $`git -C ${projectDirPath} log --format="%s%b" --name-only`.quiet(); + + return normalizeTimestamps(gitLogResult.text()); +} + +/** + * Normalizes timestamps in the given content by replacing them with a placeholder. + * Prisma migration files contain timestamps which change between test runs. + */ +function normalizeTimestamps(content: string): string { + return content.replace(/\d{14}/g, "NORMALIZED_TIMESTAMP_E2E"); +} diff --git a/web/tutorial-actions-executor/package-lock.json b/web/tutorial-actions-executor/package-lock.json new file mode 100644 index 0000000000..6c7d639304 --- /dev/null +++ b/web/tutorial-actions-executor/package-lock.json @@ -0,0 +1,3355 @@ +{ + "name": "tutorial-app-generator", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tutorial-app-generator", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "@inquirer/prompts": "^7.8.0", + "acorn": "8.15.0", + "commander": "^14.0.0", + "dedent": "^1.6.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-markdown": "^2.1.2", + "micromark-extension-mdx-jsx": "^3.0.2", + "parse-git-diff": "0.0.19", + "remark-comment": "^1.0.0", + "tsx": "^4.20.3", + "unist-util-visit": "^5.0.0", + "zx": "^8.5.3" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/mdast": "^4.0.4", + "vitest": "^3.2.4" + } + }, + "node_modules/@commander-js/extra-typings": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", + "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==", + "license": "MIT", + "peerDependencies": { + "commander": "~14.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-git-diff": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/parse-git-diff/-/parse-git-diff-0.0.19.tgz", + "integrity": "sha512-oh3giwKzsPlOhekiDDyd/pfFKn04IZoTjEThquhfKigwiUHymiP/Tp6AN5nGIwXQdWuBTQvz9AaRdN5TBsJ8MA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-k8YPo5MGvl8l4gGxOH6Zk4Fa2AhDACN5eqKnKZcHDORZQS15hlnezlBHj2lqyDiqzApNmYOMTibkEJbMSKU25w==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/remark-comment/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-comment/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-comment/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-comment/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/zx": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.7.1.tgz", + "integrity": "sha512-28u1w2LlIfvyvJvYe6pmCipesk8oL5AFMVp+P/U445LcaPgzrU5lNDtAPd6nJvWmoCNyXZz37R/xKOGokccjsw==", + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } + } + } +} diff --git a/web/tutorial-actions-executor/package.json b/web/tutorial-actions-executor/package.json new file mode 100644 index 0000000000..d713344dd3 --- /dev/null +++ b/web/tutorial-actions-executor/package.json @@ -0,0 +1,36 @@ +{ + "name": "tutorial-app-generator", + "version": "0.0.1", + "type": "module", + "license": "MIT", + "scripts": { + "edit-patch-action": "npm run tacte -- edit-patch-action --app-name TodoApp --tutorial-dir ../docs/tutorial", + "generate-app": "npm run tacte -- generate-app --app-name TodoApp --tutorial-dir ../docs/tutorial", + "list-actions": "npm run tacte -- list-actions --app-name TodoApp --tutorial-dir ../docs/tutorial", + "tacte": "tsx ./src/index.ts", + "test": "vitest run" + }, + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "@inquirer/prompts": "^7.8.0", + "acorn": "8.15.0", + "commander": "^14.0.0", + "dedent": "^1.6.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-markdown": "^2.1.2", + "micromark-extension-mdx-jsx": "^3.0.2", + "parse-git-diff": "0.0.19", + "remark-comment": "^1.0.0", + "tsx": "^4.20.3", + "unist-util-visit": "^5.0.0", + "zx": "^8.5.3" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/mdast": "^4.0.4", + "vitest": "^3.2.4" + } +} diff --git a/web/tutorial-actions-executor/src/actions/actions.ts b/web/tutorial-actions-executor/src/actions/actions.ts new file mode 100644 index 0000000000..9928416231 --- /dev/null +++ b/web/tutorial-actions-executor/src/actions/actions.ts @@ -0,0 +1,28 @@ +import type { Branded } from "../brandedTypes"; + +// If you modify or add new action kinds, make sure to also update the type in `web/docs/tutorial/TutorialAction.tsx`. +export type Action = InitAppAction | ApplyPatchAction | MigrateDbAction; + +export interface InitAppAction extends BaseAction { + kind: "INIT_APP"; + waspStarterTemplateName: string; +} + +export interface ApplyPatchAction extends BaseAction { + kind: "APPLY_PATCH"; + displayName: string; + patchFilePath: PatchFilePath; +} + +export interface MigrateDbAction extends BaseAction { + kind: "MIGRATE_DB"; +} + +export interface BaseAction { + id: ActionId; + sourceTutorialFilePath: MdxFilePath; +} + +export type ActionId = Branded; +export type PatchFilePath = Branded; +export type MdxFilePath = Branded; diff --git a/web/tutorial-actions-executor/src/actions/git.ts b/web/tutorial-actions-executor/src/actions/git.ts new file mode 100644 index 0000000000..8e78d91fdb --- /dev/null +++ b/web/tutorial-actions-executor/src/actions/git.ts @@ -0,0 +1,118 @@ +import { confirm } from "@inquirer/prompts"; +import parseGitDiff from "parse-git-diff"; +import { fs } from "zx"; + +import { askToOpenTutorialAppInEditor } from "../editor"; +import { + applyPatch, + commitAllChanges, + createBranchFromRevision, + findCommitSHAForExactMessage, + generatePatchFromAllChanges, + generatePatchFromRevision, +} from "../git"; +import { log } from "../log"; +import type { AppDirPath } from "../tutorialApp"; +import type { Action, ApplyPatchAction } from "./actions"; + +export function commitActionChanges({ + appDir, + action, +}: { + appDir: AppDirPath; + action: Action; +}) { + return commitAllChanges(appDir, action.id); +} + +export function getActionCommitSHA({ + appDir, + action, +}: { + appDir: AppDirPath; + action: Action; +}): Promise { + return findCommitSHAForExactMessage(appDir, action.id); +} + +export async function generatePatchForAction({ + appDir, + action, +}: { + appDir: AppDirPath; + action: ApplyPatchAction; +}): Promise { + const actionCommitSha = await getActionCommitSHA({ appDir, action }); + return generatePatchFromRevision(appDir, actionCommitSha); +} + +export async function applyPatchForAction({ + appDir, + action, +}: { + appDir: AppDirPath; + action: ApplyPatchAction; +}): Promise { + await applyPatch(appDir, action.patchFilePath); +} + +export async function regeneratePatchForAction({ + appDir, + action, +}: { + appDir: AppDirPath; + action: ApplyPatchAction; +}): Promise { + log("info", `Trying to fix patch for action: ${action.displayName}`); + + if (await fs.pathExists(action.patchFilePath)) { + log("info", `Removing existing patch file: ${action.patchFilePath}`); + await fs.unlink(action.patchFilePath); + } + + await askUserToEditAndCreatePatch({ appDir, action }); +} + +export async function askUserToEditAndCreatePatch({ + appDir, + action, +}: { + appDir: AppDirPath; + action: ApplyPatchAction; +}) { + await askToOpenTutorialAppInEditor(appDir); + await confirm({ + message: `Update the app according to action ${action.displayName} and press Enter`, + }); + + const patch = await generatePatchFromAllChanges(appDir); + assertValidPatch(patch); + await fs.writeFile(action.patchFilePath, patch, "utf-8"); + + log("success", `Patch file created: ${action.patchFilePath}`); +} + +export async function assertValidPatch(patch: string): Promise { + const parsedPatch = parseGitDiff(patch); + + if (parsedPatch.files.length === 0 || parsedPatch.files[0] === undefined) { + throw new Error("Invalid patch: no changes found"); + } +} + +export async function createBranchFromActionCommit({ + appDir, + branchName, + action, +}: { + appDir: AppDirPath; + branchName: string; + action: ApplyPatchAction; +}): Promise { + const actionCommitSha = await getActionCommitSHA({ appDir, action }); + await createBranchFromRevision({ + gitRepoDirPath: appDir, + branchName, + revision: actionCommitSha, + }); +} diff --git a/web/tutorial-actions-executor/src/actions/index.ts b/web/tutorial-actions-executor/src/actions/index.ts new file mode 100644 index 0000000000..16c22c666b --- /dev/null +++ b/web/tutorial-actions-executor/src/actions/index.ts @@ -0,0 +1,54 @@ +import path, { basename } from "path"; + +import { getFileNameWithoutExtension } from "../files"; +import type { PatchesDirPath } from "../tutorialApp"; +import type { + ActionId, + ApplyPatchAction, + BaseAction, + InitAppAction, + MdxFilePath, + MigrateDbAction, + PatchFilePath, +} from "./actions"; + +export function createInitAppAction( + commonData: BaseAction, + waspStarterTemplateName: string, +): InitAppAction { + return { + ...commonData, + kind: "INIT_APP", + waspStarterTemplateName, + }; +} + +export function createMigrateDbAction(commonData: BaseAction): MigrateDbAction { + return { + ...commonData, + kind: "MIGRATE_DB", + }; +} + +export function createApplyPatchAction( + commonData: BaseAction, + patchesDirPath: PatchesDirPath, +): ApplyPatchAction { + const patchFilename = getPatchFilename( + commonData.id, + commonData.sourceTutorialFilePath, + ); + return { + ...commonData, + kind: "APPLY_PATCH", + displayName: `${basename(commonData.sourceTutorialFilePath)} / ${commonData.id}`, + patchFilePath: path.resolve(patchesDirPath, patchFilename) as PatchFilePath, + }; +} + +function getPatchFilename( + id: ActionId, + sourceTutorialFilePath: MdxFilePath, +): string { + return `${getFileNameWithoutExtension(sourceTutorialFilePath)}__${id}.patch`; +} diff --git a/web/tutorial-actions-executor/src/actions/init.ts b/web/tutorial-actions-executor/src/actions/init.ts new file mode 100644 index 0000000000..90eec021e8 --- /dev/null +++ b/web/tutorial-actions-executor/src/actions/init.ts @@ -0,0 +1,31 @@ +import { fs } from "zx"; + +import { initGitRepo } from "../git"; +import { log } from "../log"; +import { type AppDirPath, type AppName, type OutputDir } from "../tutorialApp"; +import { waspNew, type WaspCliCommand } from "../waspCli"; + +export async function initWaspAppWithGitRepo({ + waspCliCommand, + appName, + appDirPath, + outputDir, + mainBranchName, +}: { + waspCliCommand: WaspCliCommand; + appName: AppName; + outputDir: OutputDir; + appDirPath: AppDirPath; + mainBranchName: string; +}): Promise { + await fs.ensureDir(outputDir); + await fs.remove(appDirPath); + + await waspNew({ + waspCliCommand, + appName, + outputDir, + }); + await initGitRepo(appDirPath, mainBranchName); + log("info", `Tutorial app has been initialized in ${appDirPath}`); +} diff --git a/web/tutorial-actions-executor/src/assert.ts b/web/tutorial-actions-executor/src/assert.ts new file mode 100644 index 0000000000..0432db3aa3 --- /dev/null +++ b/web/tutorial-actions-executor/src/assert.ts @@ -0,0 +1,3 @@ +export function assertUnreachable(_x: never, message: string): never { + throw new Error(message); +} diff --git a/web/tutorial-actions-executor/src/brandedTypes.ts b/web/tutorial-actions-executor/src/brandedTypes.ts new file mode 100644 index 0000000000..3e9401f1fb --- /dev/null +++ b/web/tutorial-actions-executor/src/brandedTypes.ts @@ -0,0 +1,3 @@ +declare const __brand: unique symbol; +type Brand = { [__brand]: B }; +export type Branded = T & Brand; diff --git a/web/tutorial-actions-executor/src/commands/commonOptions.ts b/web/tutorial-actions-executor/src/commands/commonOptions.ts new file mode 100644 index 0000000000..7a55debd4e --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/commonOptions.ts @@ -0,0 +1,10 @@ +import { Option } from "@commander-js/extra-typings"; + +import type { WaspCliCommand } from "../waspCli"; + +export const waspCliCommandOption = new Option( + "--wasp-cli-command ", + "Wasp CLI command to use", +) + .default("wasp" as WaspCliCommand) + .argParser((value) => value as WaspCliCommand); diff --git a/web/tutorial-actions-executor/src/commands/edit-patch-action/index.ts b/web/tutorial-actions-executor/src/commands/edit-patch-action/index.ts new file mode 100644 index 0000000000..e16bbd7130 --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/edit-patch-action/index.ts @@ -0,0 +1,178 @@ +import fs from "fs/promises"; + +import { Option } from "@commander-js/extra-typings"; +import { ProcessOutput } from "zx"; + +import { confirm, select } from "@inquirer/prompts"; +import type { Action, ApplyPatchAction } from "../../actions/actions"; +import { + applyPatchForAction, + askUserToEditAndCreatePatch, + commitActionChanges, + createBranchFromActionCommit, + generatePatchForAction, +} from "../../actions/git"; +import { getActionsFromTutorialFiles } from "../../extract-actions"; +import { + mainBranchName, + moveLastCommitChangesToStaging, + rebaseBranch, +} from "../../git"; +import { log } from "../../log"; +import { + createTutorialApp, + type AppDirPath, + type TutorialApp, +} from "../../tutorialApp"; +import type { WaspCliCommand } from "../../waspCli"; +import { waspCliCommandOption } from "../commonOptions"; +import { generateApp } from "../generate-app"; +import { createTacteCommand } from "../tacteCommand"; + +export const editPatchActionCommand = createTacteCommand("edit-patch-action") + .description("Edit a patch action in the tutorial app") + .addOption(new Option("--action-id ", "ID of the action to edit")) + .addOption( + new Option( + "--skip-generating-app", + "Skip generating app before editing action", + ), + ) + .addOption(waspCliCommandOption) + .action(async (args) => { + const { + actionId, + skipGeneratingApp, + waspCliCommand, + appName, + outputDir, + tutorialDir, + } = args; + const tutorialApp = createTutorialApp({ + appName, + outputDir, + tutorialDir, + }); + const actions = await getActionsFromTutorialFiles(tutorialApp); + log("info", `Found ${actions.length} actions in tutorial files.`); + + const action = await ensureAction({ + actions, + actionIdOptionValue: actionId, + }); + + if (!skipGeneratingApp) { + log("info", "Generating app before editing action..."); + await generateApp(actions, waspCliCommand as WaspCliCommand, tutorialApp); + } else { + log( + "info", + `Skipping app generation, using existing app in ${tutorialApp.appDirPath}`, + ); + } + + log("info", `Editing action ${action.displayName}...`); + + await editPatchActionPatch({ appDir: tutorialApp.appDirPath, action }); + + await extractCommitsIntoPatches(actions, tutorialApp); + + log("success", `Edit completed for action ${action.displayName}!`); + }); + +async function editPatchActionPatch({ + appDir, + action, +}: { + appDir: AppDirPath; + action: ApplyPatchAction; +}): Promise { + const fixesBranchName = "fixes"; + + await createBranchFromActionCommit({ + appDir, + branchName: fixesBranchName, + action, + }); + + await moveLastCommitChangesToStaging(appDir); + await askUserToEditAndCreatePatch({ appDir, action }); + await applyPatchForAction({ appDir, action }); + await commitActionChanges({ appDir, action }); + + try { + await rebaseBranch({ + gitRepoDirPath: appDir, + branchName: fixesBranchName, + baseBranchName: mainBranchName, + }); + } catch (error: unknown) { + if ( + error instanceof ProcessOutput && + error.stderr.includes("git rebase --continue") + ) { + await confirm({ + message: `Resolve rebase issues and press Enter to continue`, + }); + } else { + throw error; + } + } +} + +async function extractCommitsIntoPatches( + actions: Action[], + tutorialApp: TutorialApp, +): Promise { + const applyPatchActions = actions.filter( + (action) => action.kind === "APPLY_PATCH", + ); + + for (const action of applyPatchActions) { + log("info", `Updating patch for action ${action.displayName}`); + const patch = await generatePatchForAction({ + appDir: tutorialApp.appDirPath, + action, + }); + await fs.writeFile(action.patchFilePath, patch, "utf-8"); + } +} + +async function ensureAction({ + actions, + actionIdOptionValue, +}: { + actions: Action[]; + actionIdOptionValue: string | undefined; +}): Promise { + const applyPatchActions = actions.filter( + (action) => action.kind === "APPLY_PATCH", + ); + + if (!actionIdOptionValue) { + return askUserToSelectAction(applyPatchActions); + } + + const action = applyPatchActions.find((a) => a.id === actionIdOptionValue); + if (!action) { + throw new Error( + `Apply patch action with ID "${actionIdOptionValue}" not found.`, + ); + } + return action; +} + +async function askUserToSelectAction( + actions: ApplyPatchAction[], +): Promise { + const selectedActionId = await select({ + message: "Select a action to edit", + choices: actions.map((action) => ({ + name: action.displayName, + value: action.id, + })), + }); + return actions.find( + (a) => a.kind === "APPLY_PATCH" && a.id === selectedActionId, + )!; +} diff --git a/web/tutorial-actions-executor/src/commands/generate-app/execute-actions.ts b/web/tutorial-actions-executor/src/commands/generate-app/execute-actions.ts new file mode 100644 index 0000000000..c529aa30b3 --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/generate-app/execute-actions.ts @@ -0,0 +1,69 @@ +import { chalk, fs, spinner } from "zx"; + +import type { Action } from "../../actions/actions"; +import { + applyPatchForAction, + commitActionChanges, + regeneratePatchForAction, +} from "../../actions/git"; +import { initWaspAppWithGitRepo } from "../../actions/init"; +import { assertUnreachable } from "../../assert"; +import { mainBranchName } from "../../git"; +import { log } from "../../log"; +import type { TutorialApp } from "../../tutorialApp"; +import { waspDbMigrate, type WaspCliCommand } from "../../waspCli"; + +export async function executeActions({ + tutorialApp, + actions, + waspCliCommand, +}: { + tutorialApp: TutorialApp; + actions: Action[]; + waspCliCommand: WaspCliCommand; +}): Promise { + for (const action of actions) { + log("info", `${chalk.bold(`[action ${action.id}]`)} ${action.kind}`); + + await fs.ensureDir(tutorialApp.docsTutorialPatchesPath); + + switch (action.kind) { + case "INIT_APP": + await spinner("Initializing the tutorial app...", () => + initWaspAppWithGitRepo({ + waspCliCommand, + appName: tutorialApp.name, + outputDir: tutorialApp.outputDir, + appDirPath: tutorialApp.appDirPath, + mainBranchName, + }), + ); + break; + case "APPLY_PATCH": + try { + await applyPatchForAction({ appDir: tutorialApp.appDirPath, action }); + } catch (err) { + log( + "error", + `Failed to apply patch for action ${action.displayName}:\n${err}`, + ); + await regeneratePatchForAction({ + appDir: tutorialApp.appDirPath, + action, + }); + await applyPatchForAction({ appDir: tutorialApp.appDirPath, action }); + } + break; + case "MIGRATE_DB": + await waspDbMigrate({ + waspCliCommand, + appDir: tutorialApp.appDirPath, + migrationName: action.id, + }); + break; + default: + assertUnreachable(action, `Unknown action '${action}'`); + } + await commitActionChanges({ appDir: tutorialApp.appDirPath, action }); + } +} diff --git a/web/tutorial-actions-executor/src/commands/generate-app/index.ts b/web/tutorial-actions-executor/src/commands/generate-app/index.ts new file mode 100644 index 0000000000..c1ef7690a2 --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/generate-app/index.ts @@ -0,0 +1,40 @@ +import type { Action } from "../../actions/actions"; +import { getActionsFromTutorialFiles } from "../../extract-actions"; +import { log } from "../../log"; +import { createTutorialApp, type TutorialApp } from "../../tutorialApp"; +import type { WaspCliCommand } from "../../waspCli"; +import { waspCliCommandOption } from "../commonOptions"; +import { createTacteCommand } from "../tacteCommand"; +import { executeActions } from "./execute-actions"; + +export const generateAppCommand = createTacteCommand("generate-app") + .description("Generate a new Wasp app based on the tutorial actions") + .addOption(waspCliCommandOption) + .action(async ({ waspCliCommand, appName, outputDir, tutorialDir }) => { + const tutorialApp = createTutorialApp({ + appName, + outputDir, + tutorialDir, + }); + + const actions = await getActionsFromTutorialFiles(tutorialApp); + log("info", `Found ${actions.length} actions in tutorial files.`); + + await generateApp(actions, waspCliCommand, tutorialApp); + }); + +export async function generateApp( + actions: Action[], + waspCliCommand: WaspCliCommand, + tutorialApp: TutorialApp, +): Promise { + await executeActions({ + waspCliCommand, + tutorialApp, + actions, + }); + log( + "success", + `Tutorial app has been successfully generated in ${tutorialApp.appDirPath}`, + ); +} diff --git a/web/tutorial-actions-executor/src/commands/list-actions/index.ts b/web/tutorial-actions-executor/src/commands/list-actions/index.ts new file mode 100644 index 0000000000..e3e6716c95 --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/list-actions/index.ts @@ -0,0 +1,66 @@ +import { basename } from "path"; + +import { chalk } from "zx"; + +import type { Action } from "../../actions/actions"; +import { getActionsFromTutorialFiles } from "../../extract-actions"; +import { createTutorialApp } from "../../tutorialApp"; +import { createTacteCommand } from "../tacteCommand"; + +type SourceFileName = string; +type ActionsGroupedByFile = Map; + +export const listActionsCommand = createTacteCommand("list-actions") + .description("List all actions in the tutorial") + .action(async ({ appName, outputDir, tutorialDir }) => { + const tutorialApp = createTutorialApp({ + appName, + outputDir, + tutorialDir, + }); + const actions = await getActionsFromTutorialFiles(tutorialApp); + const actionsGroupedByFile = groupActionsBySourceFile(actions); + displayGroupedActions(actionsGroupedByFile); + }); + +export function groupActionsBySourceFile( + actions: Action[], +): ActionsGroupedByFile { + const groupedActions = new Map(); + + for (const action of actions) { + const filename = basename(action.sourceTutorialFilePath); + const existingActions = groupedActions.get(filename) ?? []; + groupedActions.set(filename, [...existingActions, action]); + } + + return groupedActions; +} + +function displayGroupedActions( + actionsGroupedByFile: ActionsGroupedByFile, +): void { + for (const [filename, fileActions] of actionsGroupedByFile) { + displayFileHeader(filename); + displayActionsForFile(fileActions); + console.log(); + } +} + +function displayFileHeader(filename: string): void { + console.log(chalk.bold.magenta(filename)); + console.log(); +} + +function displayActionsForFile(actions: Action[]): void { + const kindColorMap: Record string> = { + INIT_APP: chalk.yellow, + APPLY_PATCH: chalk.green, + MIGRATE_DB: chalk.blue, + }; + + actions.forEach((action) => { + const kindColorFn = kindColorMap[action.kind]; + console.log(`- ${chalk.bold(action.id)} (${kindColorFn(action.kind)})`); + }); +} diff --git a/web/tutorial-actions-executor/src/commands/tacteCommand.ts b/web/tutorial-actions-executor/src/commands/tacteCommand.ts new file mode 100644 index 0000000000..be41fc59ee --- /dev/null +++ b/web/tutorial-actions-executor/src/commands/tacteCommand.ts @@ -0,0 +1,23 @@ +import { Command, Option } from "@commander-js/extra-typings"; + +export function createTacteCommand(commandName: string) { + return new Command(commandName) + .addOption( + new Option( + "--app-name ", + "Name of the app to generate", + ).makeOptionMandatory(), + ) + .addOption( + new Option( + "--output-dir ", + "Directory where the app will be generated", + ).default("./.result"), + ) + .addOption( + new Option( + "--tutorial-dir ", + "Directory containing the tutorial MDX files", + ).makeOptionMandatory(), + ); +} diff --git a/web/tutorial-actions-executor/src/editor.ts b/web/tutorial-actions-executor/src/editor.ts new file mode 100644 index 0000000000..93e4ed5c20 --- /dev/null +++ b/web/tutorial-actions-executor/src/editor.ts @@ -0,0 +1,19 @@ +import { confirm } from "@inquirer/prompts"; +import { $ } from "zx"; + +import type { AppDirPath } from "./tutorialApp"; + +export async function askToOpenTutorialAppInEditor( + appDirPath: AppDirPath, +): Promise { + const editor = process.env.EDITOR; + if (!editor) { + return; + } + const wantsToOpenVSCode = await confirm({ + message: `Do you want to open the tutorial app in ${editor}? (Found in $EDITOR env variable)`, + }); + if (wantsToOpenVSCode) { + await $`$EDITOR ${appDirPath}`; + } +} diff --git a/web/tutorial-actions-executor/src/extract-actions/astTraversal.ts b/web/tutorial-actions-executor/src/extract-actions/astTraversal.ts new file mode 100644 index 0000000000..95b81662e5 --- /dev/null +++ b/web/tutorial-actions-executor/src/extract-actions/astTraversal.ts @@ -0,0 +1,65 @@ +import type { Root } from "mdast"; +import type { MdxJsxFlowElement } from "mdast-util-mdx-jsx"; +import { visit } from "unist-util-visit"; + +import type { Action, ActionId } from "../actions/actions.js"; + +export const TUTORIAL_ACTION_NODE_NAME = "TutorialAction"; + +export type TutorialActionNode = { + id: ActionId; + action: Action["kind"]; + starterTemplateName: string | null; +}; + +export function extractTutorialActionNodes(ast: Root): TutorialActionNode[] { + const nodes: TutorialActionNode[] = []; + + visit(ast, "mdxJsxFlowElement", (node) => { + if (node.name === TUTORIAL_ACTION_NODE_NAME) { + nodes.push( + createTutorialActionNode( + getAttributeValue(node, "id"), + getAttributeValue(node, "action"), + getAttributeValue(node, "starterTemplateName"), + ), + ); + } + }); + + return nodes; +} + +export function createTutorialActionNode( + id: string | null, + action: string | null, + starterTemplateName: string | null, +): TutorialActionNode { + if (!id) { + throw new Error(`TutorialAction component requires the 'id' attribute.`); + } + + if (!action) { + throw new Error( + `TutorialAction component requires the 'action' attribute.`, + ); + } + + return { + id: id as ActionId, + action: action as Action["kind"], + starterTemplateName, + }; +} + +export function getAttributeValue( + node: MdxJsxFlowElement, + attributeName: string, +): string | null { + const attribute = node.attributes.find( + (attr) => attr.type === "mdxJsxAttribute" && attr.name === attributeName, + ); + return attribute && typeof attribute.value === "string" + ? attribute.value + : null; +} diff --git a/web/tutorial-actions-executor/src/extract-actions/fileOperations.ts b/web/tutorial-actions-executor/src/extract-actions/fileOperations.ts new file mode 100644 index 0000000000..3060303673 --- /dev/null +++ b/web/tutorial-actions-executor/src/extract-actions/fileOperations.ts @@ -0,0 +1,36 @@ +import fs from "fs/promises"; +import path from "path"; + +import type { MdxFilePath } from "../actions/actions.js"; +import type { TutorialDirPath } from "../tutorialApp.js"; + +const SUPPORTED_TUTORIAL_FILE_EXTENSIONS = ["md", "mdx"] as const; + +export async function getTutorialFilePaths( + tutorialDir: TutorialDirPath, +): Promise { + const files = await fs.readdir(tutorialDir); + return sortFileNamesByNumberedPrefix(filterValidTutorialFileNames(files)).map( + (file) => path.resolve(tutorialDir, file) as MdxFilePath, + ); +} + +export function filterValidTutorialFileNames(fileNames: string[]): string[] { + return fileNames.filter((fileName) => { + const lowerFileName = fileName.toLowerCase(); + return SUPPORTED_TUTORIAL_FILE_EXTENSIONS.some((ext) => + lowerFileName.endsWith(`.${ext}`), + ); + }); +} + +/** + * Sorts a list of files which are named "01-something.md" by their numeric prefix. + */ +export function sortFileNamesByNumberedPrefix(fileNames: string[]): string[] { + return fileNames.sort((a, b) => { + const aNumber = parseInt(a.split("-")[0]!, 10); + const bNumber = parseInt(b.split("-")[0]!, 10); + return aNumber - bNumber; + }); +} diff --git a/web/tutorial-actions-executor/src/extract-actions/index.ts b/web/tutorial-actions-executor/src/extract-actions/index.ts new file mode 100644 index 0000000000..0eb894e203 --- /dev/null +++ b/web/tutorial-actions-executor/src/extract-actions/index.ts @@ -0,0 +1,23 @@ +import type { Action } from "../actions/actions.js"; +import type { TutorialApp } from "../tutorialApp.js"; +import { getTutorialFilePaths } from "./fileOperations.js"; +import { getActionsFromMdxFile } from "./mdxParsing.js"; + +export async function getActionsFromTutorialFiles( + tutorialApp: TutorialApp, +): Promise { + const tutorialFilePaths = await getTutorialFilePaths( + tutorialApp.docsTutorialDirPath, + ); + const actions: Action[] = []; + + for (const filePath of tutorialFilePaths) { + const fileActions = await getActionsFromMdxFile( + filePath, + tutorialApp.docsTutorialPatchesPath, + ); + actions.push(...fileActions); + } + + return actions; +} diff --git a/web/tutorial-actions-executor/src/extract-actions/mdxParsing.ts b/web/tutorial-actions-executor/src/extract-actions/mdxParsing.ts new file mode 100644 index 0000000000..da6d86a0d6 --- /dev/null +++ b/web/tutorial-actions-executor/src/extract-actions/mdxParsing.ts @@ -0,0 +1,52 @@ +import fs from "fs/promises"; +import path from "path"; + +import * as acorn from "acorn"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import { mdxJsxFromMarkdown } from "mdast-util-mdx-jsx"; +import { mdxJsx } from "micromark-extension-mdx-jsx"; + +import type { Action, MdxFilePath } from "../actions/actions.js"; +import type { Branded } from "../brandedTypes.js"; +import type { PatchesDirPath } from "../tutorialApp.js"; +import { extractTutorialActionNodes } from "./astTraversal.js"; +import { mapTutorialActionNodeToAction } from "./nodeMapping.js"; + +type MdxContent = Branded, "MdxContent">; + +export async function getActionsFromMdxFile( + sourceTutorialFilePath: MdxFilePath, + patchesPath: PatchesDirPath, +): Promise { + const fileContent = await fs.readFile(path.resolve(sourceTutorialFilePath)); + + try { + return getActionsFromMdxContent( + fileContent as MdxContent, + sourceTutorialFilePath, + patchesPath, + ); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`${error.message} (File: ${sourceTutorialFilePath})`); + } + throw error; + } +} + +export async function getActionsFromMdxContent( + fileContent: MdxContent, + sourceTutorialFilePath: MdxFilePath, + patchesPath: PatchesDirPath, +): Promise { + const ast = fromMarkdown(fileContent, { + extensions: [mdxJsx({ acorn, addResult: true })], + mdastExtensions: [mdxJsxFromMarkdown()], + }); + + const tutorialActionNodes = extractTutorialActionNodes(ast); + + return tutorialActionNodes.map((node) => + mapTutorialActionNodeToAction(node, sourceTutorialFilePath, patchesPath), + ); +} diff --git a/web/tutorial-actions-executor/src/extract-actions/nodeMapping.ts b/web/tutorial-actions-executor/src/extract-actions/nodeMapping.ts new file mode 100644 index 0000000000..58ae02206d --- /dev/null +++ b/web/tutorial-actions-executor/src/extract-actions/nodeMapping.ts @@ -0,0 +1,39 @@ +import type { Action, BaseAction, MdxFilePath } from "../actions/actions.js"; +import { + createApplyPatchAction, + createInitAppAction, + createMigrateDbAction, +} from "../actions/index.js"; +import { assertUnreachable } from "../assert.js"; +import type { PatchesDirPath } from "../tutorialApp.js"; +import type { TutorialActionNode } from "./astTraversal.js"; + +export function mapTutorialActionNodeToAction( + node: TutorialActionNode, + sourceTutorialFilePath: MdxFilePath, + patchesPath: PatchesDirPath, +): Action { + const commonData: BaseAction = { + id: node.id, + sourceTutorialFilePath, + }; + + switch (node.action) { + case "INIT_APP": + if (node.starterTemplateName === null) { + throw new Error( + `TutorialAction with action 'INIT_APP' requires the 'starterTemplateName' attribute.`, + ); + } + return createInitAppAction(commonData, node.starterTemplateName); + case "APPLY_PATCH": + return createApplyPatchAction(commonData, patchesPath); + case "MIGRATE_DB": + return createMigrateDbAction(commonData); + default: + assertUnreachable( + node.action, + `Unknown action '${node.action}' in TutorialAction component.`, + ); + } +} diff --git a/web/tutorial-actions-executor/src/files.ts b/web/tutorial-actions-executor/src/files.ts new file mode 100644 index 0000000000..26f52fe037 --- /dev/null +++ b/web/tutorial-actions-executor/src/files.ts @@ -0,0 +1,5 @@ +import path from "path"; + +export function getFileNameWithoutExtension(filePath: string): string { + return path.basename(filePath, path.extname(filePath)); +} diff --git a/web/tutorial-actions-executor/src/git.ts b/web/tutorial-actions-executor/src/git.ts new file mode 100644 index 0000000000..8a30ba91db --- /dev/null +++ b/web/tutorial-actions-executor/src/git.ts @@ -0,0 +1,120 @@ +import { $ } from "zx"; + +export const mainBranchName = "main"; + +export async function generatePatchFromAllChanges( + gitRepoDirPath: string, +): Promise { + await commitAllChanges(gitRepoDirPath, "temporary-commit"); + const patch = await generatePatchFromRevision(gitRepoDirPath, "HEAD"); + await removeLastCommit(gitRepoDirPath); + return patch; +} + +export async function applyPatch(gitRepoDirPath: string, patchPath: string) { + await $({ cwd: gitRepoDirPath })`git apply ${patchPath} --verbose`.quiet( + true, + ); +} + +export async function findCommitSHAForExactMessage( + gitRepoDirPath: string, + message: string, +): Promise { + const commits = await grepGitCommitsByMessage(gitRepoDirPath, message); + + const commit = commits.find((commit) => commit.message === message); + if (!commit) { + throw new Error(`No commit found with message: "${message}"`); + } + + return commit.sha; +} + +interface GitCommit { + message: string; + sha: string; +} + +async function grepGitCommitsByMessage( + gitRepoDirPath: string, + message: string, +): Promise { + const format = `{"message":"%s","sha":"%H"}`; + const { stdout } = await $({ + cwd: gitRepoDirPath, + })`git log --branches --format=${format} --grep=${message}`; + + const commits = stdout.split("\n").filter((line) => line.trim() !== ""); + return commits.map((commit) => { + const parsed = JSON.parse(commit); + return { + message: parsed.message, + sha: parsed.sha, + }; + }); +} + +export async function commitAllChanges( + gitRepoDirPath: string, + message: string, +): Promise { + await $({ cwd: gitRepoDirPath })`git add .`; + await $({ cwd: gitRepoDirPath })`git commit -m ${message}`; +} + +async function removeLastCommit(gitRepoDirPath: string): Promise { + await $({ cwd: gitRepoDirPath })`git reset --hard HEAD~1`; +} + +export async function generatePatchFromRevision( + gitRepoDirPath: string, + gitRevision: string, +): Promise { + const { stdout: patch } = await $({ + cwd: gitRepoDirPath, + })`git show ${gitRevision} --format=`; + + return patch; +} + +export async function initGitRepo( + gitRepoDirPath: string, + mainBranchName: string, +): Promise { + await $({ cwd: gitRepoDirPath })`git init`.quiet(true); + await $({ cwd: gitRepoDirPath })`git branch -m ${mainBranchName}`; +} + +export async function createBranchFromRevision({ + gitRepoDirPath, + branchName, + revision, +}: { + gitRepoDirPath: string; + branchName: string; + revision: string; +}): Promise { + await $({ + cwd: gitRepoDirPath, + })`git switch --force-create ${branchName} ${revision}`; +} + +export async function moveLastCommitChangesToStaging( + gitRepoDirPath: string, +): Promise { + await $({ cwd: gitRepoDirPath })`git reset --soft HEAD~1`; +} + +export async function rebaseBranch({ + gitRepoDirPath, + branchName, + baseBranchName, +}: { + gitRepoDirPath: string; + branchName: string; + baseBranchName: string; +}): Promise { + await $({ cwd: gitRepoDirPath })`git switch ${baseBranchName}`; + await $({ cwd: gitRepoDirPath })`git rebase ${branchName}`; +} diff --git a/web/tutorial-actions-executor/src/index.ts b/web/tutorial-actions-executor/src/index.ts new file mode 100644 index 0000000000..9373f3ffab --- /dev/null +++ b/web/tutorial-actions-executor/src/index.ts @@ -0,0 +1,12 @@ +import { program } from "@commander-js/extra-typings"; + +import { editPatchActionCommand } from "./commands/edit-patch-action"; +import { generateAppCommand } from "./commands/generate-app"; +import { listActionsCommand } from "./commands/list-actions"; + +program + .addCommand(generateAppCommand) + .addCommand(editPatchActionCommand) + .addCommand(listActionsCommand) + .parse(process.argv) + .opts(); diff --git a/web/tutorial-actions-executor/src/log.ts b/web/tutorial-actions-executor/src/log.ts new file mode 100644 index 0000000000..da1e86379e --- /dev/null +++ b/web/tutorial-actions-executor/src/log.ts @@ -0,0 +1,13 @@ +import { chalk } from "zx"; + +const levels = { + info: { color: chalk.blue }, + success: { color: chalk.green }, + error: { color: chalk.red }, +}; + +export function log(level: keyof typeof levels, message: string) { + console.log( + levels[level].color(`[${level.toUpperCase()}] ${chalk.reset(message)}`), + ); +} diff --git a/web/tutorial-actions-executor/src/tutorialApp.ts b/web/tutorial-actions-executor/src/tutorialApp.ts new file mode 100644 index 0000000000..457165f45e --- /dev/null +++ b/web/tutorial-actions-executor/src/tutorialApp.ts @@ -0,0 +1,42 @@ +import path from "path"; + +import type { Branded } from "./brandedTypes"; + +export type AppName = Branded; +export type AppDirPath = Branded; +export type OutputDir = Branded; +export type TutorialDirPath = Branded; +export type PatchesDirPath = Branded; + +export interface TutorialApp { + name: AppName; + appDirPath: AppDirPath; + outputDir: OutputDir; + docsTutorialDirPath: TutorialDirPath; + docsTutorialPatchesPath: PatchesDirPath; +} + +export function createTutorialApp(options: { + appName: string; + outputDir: string; + tutorialDir: string; +}): TutorialApp { + const appName = options.appName as AppName; + const outputDir = path.resolve(options.outputDir) as OutputDir; + const appDirPath = path.resolve(outputDir, appName) as AppDirPath; + const docsTutorialDirPath = path.resolve( + options.tutorialDir, + ) as TutorialDirPath; + const docsTutorialPatchesPath = path.resolve( + docsTutorialDirPath, + "patches", + ) as PatchesDirPath; + + return { + name: appName, + outputDir, + appDirPath, + docsTutorialDirPath, + docsTutorialPatchesPath, + }; +} diff --git a/web/tutorial-actions-executor/src/waspCli.ts b/web/tutorial-actions-executor/src/waspCli.ts new file mode 100644 index 0000000000..419bab1fac --- /dev/null +++ b/web/tutorial-actions-executor/src/waspCli.ts @@ -0,0 +1,39 @@ +import { $ } from "zx"; + +import type { Branded } from "./brandedTypes"; +import type { AppDirPath, AppName, OutputDir } from "./tutorialApp"; + +export type WaspCliCommand = Branded; + +export async function waspDbMigrate({ + waspCliCommand, + appDir, + migrationName, +}: { + waspCliCommand: WaspCliCommand; + appDir: AppDirPath; + migrationName: string; +}): Promise { + await $({ + // We ignore stdin to avoid hangs in non-interactive environments e.g. e2e tests when + // `wasp db migrate-dev` runs. When `stdin` is not ignored, the command waits forever + // for user input. We don't need any input since we are providing the migration name + // via the --name flag. + stdio: ["ignore", "pipe", "pipe"], + cwd: appDir, + })`${waspCliCommand} db migrate-dev --name ${migrationName}`; +} + +export async function waspNew({ + waspCliCommand, + appName, + outputDir, +}: { + waspCliCommand: WaspCliCommand; + appName: AppName; + outputDir: OutputDir; +}): Promise { + await $({ + cwd: outputDir, + })`${waspCliCommand} new ${appName} -t minimal`; +} diff --git a/web/tutorial-actions-executor/tests/actions/git.fixtures.ts b/web/tutorial-actions-executor/tests/actions/git.fixtures.ts new file mode 100644 index 0000000000..417c6ac1ad --- /dev/null +++ b/web/tutorial-actions-executor/tests/actions/git.fixtures.ts @@ -0,0 +1,56 @@ +export const singleFilePatch = `diff --git a/test.js b/test.js +index 1234567..abcdefg 100644 +--- a/test.js ++++ b/test.js +@@ -1,3 +1,4 @@ + console.log('hello'); ++console.log('world'); + export const x = 1; + export const y = 2;`; + +export const multiFilePatch = `diff --git a/file1.js b/file1.js +index 1234567..abcdefg 100644 +--- a/file1.js ++++ b/file1.js +@@ -1,2 +1,3 @@ + export const a = 1; ++export const b = 2; + export { a }; +diff --git a/file2.js b/file2.js +index 2345678..bcdefgh 100644 +--- a/file2.js ++++ b/file2.js +@@ -1,2 +1,3 @@ + export const c = 3; ++export const d = 4; + export { c };`; + +export const deletionPatch = `diff --git a/deleted-file.js b/deleted-file.js +deleted file mode 100644 +index 1234567..0000000 +--- a/deleted-file.js ++++ /dev/null +@@ -1,3 +0,0 @@ +-console.log('deleted'); +-export const x = 1; +-export { x };`; + +export const additionPatch = `diff --git a/new-file.js b/new-file.js +new file mode 100644 +index 0000000..1234567 +--- /dev/null ++++ b/new-file.js +@@ -0,0 +1,3 @@ ++console.log('new file'); ++export const y = 2; ++export { y };`; + +export const binaryPatch = `diff --git a/image.png b/image.png +index 1234567..abcdefg 100644 +Binary files a/image.png and b/image.png differ`; + +export const noFilesPatch = `commit abc123 +Author: Test User +Date: Mon Jan 1 00:00:00 2023 +0000 + + Some commit message`; diff --git a/web/tutorial-actions-executor/tests/actions/git.test.ts b/web/tutorial-actions-executor/tests/actions/git.test.ts new file mode 100644 index 0000000000..cf08a312b9 --- /dev/null +++ b/web/tutorial-actions-executor/tests/actions/git.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import { assertValidPatch } from "../../src/actions/git"; +import { + additionPatch, + binaryPatch, + deletionPatch, + multiFilePatch, + noFilesPatch, + singleFilePatch, +} from "./git.fixtures"; + +describe("assertValidPatch", () => { + expectValidPatchNotToThrow(singleFilePatch); + expectValidPatchNotToThrow(multiFilePatch); + expectValidPatchNotToThrow(deletionPatch); + expectValidPatchNotToThrow(additionPatch); + expectValidPatchNotToThrow(binaryPatch); + + expectInvalidPatchToThrow(noFilesPatch, "Invalid patch: no changes found"); +}); + +function expectValidPatchNotToThrow(patch: string) { + it("should not throw for valid patch", async () => { + await expect(assertValidPatch(patch)).resolves.not.toThrow(); + }); +} + +function expectInvalidPatchToThrow(patch: string, errorMessage: string) { + it(`should throw for invalid patch: ${errorMessage}`, async () => { + await expect(assertValidPatch(patch)).rejects.toThrow(errorMessage); + }); +} diff --git a/web/tutorial-actions-executor/tests/actions/index.test.ts b/web/tutorial-actions-executor/tests/actions/index.test.ts new file mode 100644 index 0000000000..bec7dadbf2 --- /dev/null +++ b/web/tutorial-actions-executor/tests/actions/index.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; + +import type { + ActionId, + BaseAction, + MdxFilePath, +} from "../../src/actions/actions"; +import { + createApplyPatchAction, + createInitAppAction, + createMigrateDbAction, +} from "../../src/actions/index"; +import type { PatchesDirPath } from "../../src/tutorialApp"; + +describe("createInitAppAction", () => { + it("should create InitAppAction with correct properties", () => { + const baseAction: BaseAction = { + id: "test-init" as ActionId, + sourceTutorialFilePath: "/docs/01-intro.md" as MdxFilePath, + }; + + const result = createInitAppAction(baseAction, "basic"); + + expect(result).toEqual({ + id: "test-init", + sourceTutorialFilePath: "/docs/01-intro.md", + kind: "INIT_APP", + waspStarterTemplateName: "basic", + }); + }); +}); + +describe("createMigrateDbAction", () => { + it("should create MigrateDbAction with correct properties", () => { + const baseAction: BaseAction = { + id: "db-migrate" as ActionId, + sourceTutorialFilePath: "/docs/02-database.md" as MdxFilePath, + }; + + const result = createMigrateDbAction(baseAction); + + expect(result).toEqual({ + id: "db-migrate", + sourceTutorialFilePath: "/docs/02-database.md", + kind: "MIGRATE_DB", + }); + }); +}); + +describe("createApplyPatchAction", () => { + it("should create ApplyPatchAction with correct properties", () => { + const baseAction: BaseAction = { + id: "add-auth" as ActionId, + sourceTutorialFilePath: "/docs/03-auth.md" as MdxFilePath, + }; + const patchesDirPath = "/patches" as PatchesDirPath; + + const result = createApplyPatchAction(baseAction, patchesDirPath); + + expect(result).toEqual({ + id: "add-auth", + sourceTutorialFilePath: "/docs/03-auth.md", + kind: "APPLY_PATCH", + displayName: "03-auth.md / add-auth", + patchFilePath: "/patches/03-auth__add-auth.patch", + }); + }); +}); diff --git a/web/tutorial-actions-executor/tests/commands/list-actions.test.ts b/web/tutorial-actions-executor/tests/commands/list-actions.test.ts new file mode 100644 index 0000000000..9b71eee3f1 --- /dev/null +++ b/web/tutorial-actions-executor/tests/commands/list-actions.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import type { Action, ActionId, MdxFilePath } from "../../src/actions/actions"; +import { groupActionsBySourceFile } from "../../src/commands/list-actions/index"; + +describe("groupActionsBySourceFile", () => { + it("should group actions by source file basename", () => { + const actions: Action[] = [ + { + id: "action1" as ActionId, + sourceTutorialFilePath: "/path/to/01-intro.md" as MdxFilePath, + kind: "INIT_APP", + waspStarterTemplateName: "basic", + }, + { + id: "action2" as ActionId, + sourceTutorialFilePath: "/path/to/01-intro.md" as MdxFilePath, + kind: "APPLY_PATCH", + displayName: "Add auth", + patchFilePath: "/patches/auth.patch" as any, + }, + { + id: "action3" as ActionId, + sourceTutorialFilePath: "/path/to/02-setup.md" as MdxFilePath, + kind: "MIGRATE_DB", + }, + ]; + + const result = groupActionsBySourceFile(actions); + + expect(result.size).toBe(2); + expect(result.get("01-intro.md")).toEqual([actions[0], actions[1]]); + expect(result.get("02-setup.md")).toEqual([actions[2]]); + }); + + it("should handle empty actions array", () => { + const actions: Action[] = []; + const result = groupActionsBySourceFile(actions); + expect(result.size).toBe(0); + }); +}); diff --git a/web/tutorial-actions-executor/tests/extract-actions/astTraversal.test.ts b/web/tutorial-actions-executor/tests/extract-actions/astTraversal.test.ts new file mode 100644 index 0000000000..cf404b5521 --- /dev/null +++ b/web/tutorial-actions-executor/tests/extract-actions/astTraversal.test.ts @@ -0,0 +1,219 @@ +import type { Root } from "mdast"; +import type { MdxJsxAttribute, MdxJsxFlowElement } from "mdast-util-mdx-jsx"; +import { describe, expect, it } from "vitest"; + +import type { ActionId } from "../../src/actions/actions"; +import { + createTutorialActionNode, + extractTutorialActionNodes, + getAttributeValue, + TUTORIAL_ACTION_NODE_NAME, + type TutorialActionNode, +} from "../../src/extract-actions/astTraversal"; + +describe("getAttributeValue", () => { + it("should return correct value when attributes exist", () => { + const mockNode = { + attributes: [ + { + type: "mdxJsxAttribute", + name: "action", + value: "APPLY_PATCH", + }, + { + type: "mdxJsxAttribute", + name: "id", + value: "test-action", + }, + ], + } as MdxJsxFlowElement; + + expect(getAttributeValue(mockNode, "id")).toBe("test-action"); + expect(getAttributeValue(mockNode, "action")).toBe("APPLY_PATCH"); + }); + + it("should return null when attribute does not exist", () => { + const mockNode = { + attributes: [] as MdxJsxAttribute[], + } as MdxJsxFlowElement; + + expect(getAttributeValue(mockNode, "id")).toBe(null); + }); +}); + +describe("createTutorialActionNode", () => { + it("should create node with all attributes", () => { + const result = createTutorialActionNode("test-id", "APPLY_PATCH", "basic"); + expect(result).toEqual({ + id: "test-id", + action: "APPLY_PATCH", + starterTemplateName: "basic", + }); + }); + + it("should create node without starterTemplateName", () => { + const result = createTutorialActionNode("test-id", "MIGRATE_DB", null); + expect(result).toEqual({ + id: "test-id", + action: "MIGRATE_DB", + starterTemplateName: null, + }); + }); + + it("should throw error when id is null", () => { + expect(() => createTutorialActionNode(null, "APPLY_PATCH", null)).toThrow( + "TutorialAction component requires the 'id' attribute", + ); + }); + + it("should throw error when id is empty string", () => { + expect(() => createTutorialActionNode("", "APPLY_PATCH", null)).toThrow( + "TutorialAction component requires the 'id' attribute", + ); + }); + + it("should throw error when action is null", () => { + expect(() => createTutorialActionNode("test-id", null, null)).toThrow( + "TutorialAction component requires the 'action' attribute", + ); + }); + + it("should throw error when action is empty string", () => { + expect(() => createTutorialActionNode("test-id", "", null)).toThrow( + "TutorialAction component requires the 'action' attribute", + ); + }); +}); + +describe("extractTutorialActionNodes", () => { + function createMockTutorialActionNode( + id: string, + action: string, + starterTemplateName?: string, + ): MdxJsxFlowElement { + const attributes: MdxJsxAttribute[] = [ + { type: "mdxJsxAttribute", name: "id", value: id }, + { type: "mdxJsxAttribute", name: "action", value: action }, + ]; + if (starterTemplateName) { + attributes.push({ + type: "mdxJsxAttribute", + name: "starterTemplateName", + value: starterTemplateName, + }); + } + return { + type: "mdxJsxFlowElement", + name: TUTORIAL_ACTION_NODE_NAME, + attributes, + children: [], + } as MdxJsxFlowElement; + } + + function createMockNonTutorialActionNode(name: string): MdxJsxFlowElement { + return { + type: "mdxJsxFlowElement", + name, + attributes: [], + children: [], + } as MdxJsxFlowElement; + } + + function createMockAst(children: unknown[]): Root { + return { + type: "root", + children, + } as Root; + } + + function testExtractingTutorialActionNodes( + testName: string, + ast: Root, + expectedResult: TutorialActionNode[], + ) { + it(testName, () => { + const result = extractTutorialActionNodes(ast); + expect(result).toEqual(expectedResult); + }); + } + + testExtractingTutorialActionNodes( + "should return empty array for empty AST", + createMockAst([]), + [], + ); + + testExtractingTutorialActionNodes( + "should extract single TutorialAction node", + createMockAst([createMockTutorialActionNode("test-id", "APPLY_PATCH")]), + [ + { + id: "test-id" as ActionId, + action: "APPLY_PATCH", + starterTemplateName: null, + }, + ], + ); + + testExtractingTutorialActionNodes( + "should extract multiple TutorialAction nodes", + createMockAst([ + createMockTutorialActionNode("init", "INIT_APP", "basic"), + createMockTutorialActionNode("patch", "APPLY_PATCH"), + createMockTutorialActionNode("migrate", "MIGRATE_DB"), + ]), + [ + { + id: "init" as ActionId, + action: "INIT_APP", + starterTemplateName: "basic", + }, + { + id: "patch" as ActionId, + action: "APPLY_PATCH", + starterTemplateName: null, + }, + { + id: "migrate" as ActionId, + action: "MIGRATE_DB", + starterTemplateName: null, + }, + ], + ); + + testExtractingTutorialActionNodes( + "should ignore non-TutorialAction MDX components", + createMockAst([ + createMockNonTutorialActionNode("SomeComponent"), + createMockTutorialActionNode("valid", "MIGRATE_DB"), + createMockNonTutorialActionNode("AnotherComponent"), + ]), + [ + { + id: "valid" as ActionId, + action: "MIGRATE_DB", + starterTemplateName: null, + }, + ], + ); + + testExtractingTutorialActionNodes( + "should extract TutorialAction with optional starterTemplateName", + createMockAst([ + createMockTutorialActionNode("with-template", "INIT_APP", "advanced"), + createMockTutorialActionNode("without-template", "APPLY_PATCH"), + ]), + [ + { + id: "with-template" as ActionId, + action: "INIT_APP", + starterTemplateName: "advanced", + }, + { + id: "without-template" as ActionId, + action: "APPLY_PATCH", + starterTemplateName: null, + }, + ], + ); +}); diff --git a/web/tutorial-actions-executor/tests/extract-actions/fileOperations.test.ts b/web/tutorial-actions-executor/tests/extract-actions/fileOperations.test.ts new file mode 100644 index 0000000000..fa424a2aa5 --- /dev/null +++ b/web/tutorial-actions-executor/tests/extract-actions/fileOperations.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { + filterValidTutorialFileNames, + sortFileNamesByNumberedPrefix, +} from "../../src/extract-actions/fileOperations"; + +describe("filterValidTutorialFileNames", () => { + function testFilteringTutorialFileNames( + testName: string, + inputFiles: string[], + expectedFiles: string[], + ) { + it(testName, () => { + expect(filterValidTutorialFileNames(inputFiles)).toEqual(expectedFiles); + }); + } + + testFilteringTutorialFileNames( + "should return empty array when no markdown files are present", + ["file.txt", "image.png", "document.pdf"], + [], + ); + + testFilteringTutorialFileNames( + "should filter files ending with .md or .mdx", + [ + "01-intro.md", + "02-setup.mdx", + "03-advanced.md", + "README.txt", + "package.json", + ], + ["01-intro.md", "02-setup.mdx", "03-advanced.md"], + ); + + testFilteringTutorialFileNames( + "should handle case-insensitive file extensions", + ["README.MD", "guide.MDX", "tutorial.Md", "docs.mDx", "file.txt"], + ["README.MD", "guide.MDX", "tutorial.Md", "docs.mDx"], + ); + + testFilteringTutorialFileNames( + "should not match extensions in the middle of filename", + ["file.md.txt", "file.mdx.backup", "README.md"], + ["README.md"], + ); +}); + +describe("sortFileNamesByNumberedPrefix", () => { + it("should sort files by numeric prefix", () => { + const files = ["03-advanced.md", "01-intro.md", "02-setup.md"]; + + const result = sortFileNamesByNumberedPrefix(files); + + expect(result).toEqual(["01-intro.md", "02-setup.md", "03-advanced.md"]); + }); +}); diff --git a/web/tutorial-actions-executor/tests/extract-actions/mdxFileFixtures.ts b/web/tutorial-actions-executor/tests/extract-actions/mdxFileFixtures.ts new file mode 100644 index 0000000000..4fa9daac0c --- /dev/null +++ b/web/tutorial-actions-executor/tests/extract-actions/mdxFileFixtures.ts @@ -0,0 +1,78 @@ +export const mdxWithInitAppAction = ` +# Tutorial + + + +Some content here. +`; + +export const mdxWithApplyPatchAction = ` +# Tutorial + + + +\`\`\` +const someCode = "" +\`\`\` + + +Some content here. +`; + +export const mdxWithMigrateDbAction = ` +# Tutorial + + + +Some content here. +`; + +export const mdxWithMultipleActions = ` +# Tutorial + + + +Some content here. + + +\`\`\` +const someCode = "" +\`\`\` + + +More content. + + +`; + +export const mdxWithNoActions = ` +# Tutorial + +Just some regular content without any tutorial actions. +`; + +export const mdxWithNonTutorialActionComponents = ` +# Tutorial + + + + + + +`; + +export const mdxWithMissingActionAttribute = ` + +`; + +export const mdxWithMissingIdAttribute = ` + +`; + +export const mdxWithMissingStarterTemplateName = ` + +`; + +export const mdxWithUnknownActionType = ` + +`; diff --git a/web/tutorial-actions-executor/tests/extract-actions/mdxParsing.test.ts b/web/tutorial-actions-executor/tests/extract-actions/mdxParsing.test.ts new file mode 100644 index 0000000000..105baf39e6 --- /dev/null +++ b/web/tutorial-actions-executor/tests/extract-actions/mdxParsing.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from "vitest"; + +import type { MdxFilePath } from "../../src/actions/actions"; +import { getActionsFromMdxContent } from "../../src/extract-actions/mdxParsing"; +import type { PatchesDirPath } from "../../src/tutorialApp"; +import { + mdxWithApplyPatchAction, + mdxWithInitAppAction, + mdxWithMigrateDbAction, + mdxWithMissingActionAttribute, + mdxWithMissingIdAttribute, + mdxWithMissingStarterTemplateName, + mdxWithMultipleActions, + mdxWithNoActions, + mdxWithNonTutorialActionComponents, + mdxWithUnknownActionType, +} from "./mdxFileFixtures"; + +describe("getActionsFromMdxContent", () => { + const context = { + filePath: "/test/tutorial/01-intro.md" as MdxFilePath, + patchesPath: "/test/tutorial/patches" as PatchesDirPath, + }; + + function testExtractingActions( + testName: string, + mdxContent: string, + expectedActions: unknown[], + ) { + it(`should extract actions: ${testName}`, async () => { + const actions = await getActionsFromMdxContent( + Buffer.from(mdxContent), + context.filePath, + context.patchesPath, + ); + expect(actions).toEqual(expectedActions); + }); + } + + function testExtractingActionsThrows( + testName: string, + mdxContent: string, + errorMessage: string, + ) { + it(`should fail extracting actions: ${testName}`, async () => { + await expect( + getActionsFromMdxContent( + Buffer.from(mdxContent), + context.filePath, + context.patchesPath, + ), + ).rejects.toThrow(errorMessage); + }); + } + + testExtractingActions("INIT_APP action", mdxWithInitAppAction, [ + { + id: "init-project", + sourceTutorialFilePath: context.filePath, + kind: "INIT_APP", + waspStarterTemplateName: "basic", + }, + ]); + + testExtractingActions("APPLY_PATCH action", mdxWithApplyPatchAction, [ + { + id: "add-feature", + sourceTutorialFilePath: context.filePath, + kind: "APPLY_PATCH", + displayName: "01-intro.md / add-feature", + patchFilePath: "/test/tutorial/patches/01-intro__add-feature.patch", + }, + ]); + + testExtractingActions("MIGRATE_DB action", mdxWithMigrateDbAction, [ + { + id: "migrate-schema", + sourceTutorialFilePath: context.filePath, + kind: "MIGRATE_DB", + }, + ]); + + testExtractingActions("multiple actions", mdxWithMultipleActions, [ + { + id: "init-project", + sourceTutorialFilePath: context.filePath, + kind: "INIT_APP", + waspStarterTemplateName: "basic", + }, + { + id: "add-feature", + sourceTutorialFilePath: context.filePath, + kind: "APPLY_PATCH", + displayName: "01-intro.md / add-feature", + patchFilePath: "/test/tutorial/patches/01-intro__add-feature.patch", + }, + { + id: "migrate-schema", + sourceTutorialFilePath: context.filePath, + kind: "MIGRATE_DB", + }, + ]); + + testExtractingActions("no actions present", mdxWithNoActions, []); + + testExtractingActions( + "ignore non-TutorialAction MDX components", + mdxWithNonTutorialActionComponents, + [ + { + id: "valid-action", + sourceTutorialFilePath: context.filePath, + kind: "MIGRATE_DB", + }, + ], + ); + + testExtractingActionsThrows( + "when action attribute is missing", + mdxWithMissingActionAttribute, + "TutorialAction component requires the 'action' attribute", + ); + + testExtractingActionsThrows( + "when id attribute is missing", + mdxWithMissingIdAttribute, + "TutorialAction component requires the 'id' attribute", + ); + + testExtractingActionsThrows( + "when INIT_APP action is missing starterTemplateName", + mdxWithMissingStarterTemplateName, + "TutorialAction with action 'INIT_APP' requires the 'starterTemplateName' attribute", + ); + + testExtractingActionsThrows( + "for unknown action type", + mdxWithUnknownActionType, + "Unknown action 'UNKNOWN_ACTION' in TutorialAction component", + ); +}); diff --git a/web/tutorial-actions-executor/tests/extract-actions/nodeMapping.test.ts b/web/tutorial-actions-executor/tests/extract-actions/nodeMapping.test.ts new file mode 100644 index 0000000000..9b0861b1a3 --- /dev/null +++ b/web/tutorial-actions-executor/tests/extract-actions/nodeMapping.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; + +import type { + Action, + ActionId, + MdxFilePath, + PatchFilePath, +} from "../../src/actions/actions"; +import type { TutorialActionNode } from "../../src/extract-actions/astTraversal"; +import { mapTutorialActionNodeToAction } from "../../src/extract-actions/nodeMapping"; +import type { PatchesDirPath } from "../../src/tutorialApp"; + +describe("mapTutorialActionNodeToAction", () => { + const testContext = { + filePath: "/test/tutorial/01-intro.md" as MdxFilePath, + patchesPath: "/test/tutorial/patches" as PatchesDirPath, + }; + + function createNode( + id: string, + action: Action["kind"], + starterTemplateName: string | null = null, + ): TutorialActionNode { + return { + id: id as ActionId, + action, + starterTemplateName, + }; + } + + function testMappingNode( + testName: string, + node: TutorialActionNode, + expectedResult: Action, + ) { + it(testName, () => { + const result = mapTutorialActionNodeToAction( + node, + testContext.filePath, + testContext.patchesPath, + ); + expect(result).toEqual(expectedResult); + }); + } + + function testMappingNodeThrows( + testName: string, + node: TutorialActionNode, + errorMessage: string, + ) { + it(testName, () => { + expect(() => + mapTutorialActionNodeToAction( + node, + testContext.filePath, + testContext.patchesPath, + ), + ).toThrow(errorMessage); + }); + } + + testMappingNode( + "should map INIT_APP action with starterTemplateName", + createNode("init-project", "INIT_APP", "basic"), + { + id: "init-project" as ActionId, + sourceTutorialFilePath: testContext.filePath, + kind: "INIT_APP", + waspStarterTemplateName: "basic", + }, + ); + + testMappingNodeThrows( + "should throw error when INIT_APP action is missing starterTemplateName", + createNode("init-project", "INIT_APP", null), + "TutorialAction with action 'INIT_APP' requires the 'starterTemplateName' attribute", + ); + + testMappingNode( + "should map APPLY_PATCH action", + createNode("add-feature", "APPLY_PATCH"), + { + id: "add-feature" as ActionId, + sourceTutorialFilePath: testContext.filePath, + kind: "APPLY_PATCH", + displayName: "01-intro.md / add-feature", + patchFilePath: + "/test/tutorial/patches/01-intro__add-feature.patch" as PatchFilePath, + }, + ); + + testMappingNode( + "should map MIGRATE_DB action", + createNode("migrate-schema", "MIGRATE_DB"), + { + id: "migrate-schema" as ActionId, + sourceTutorialFilePath: testContext.filePath, + kind: "MIGRATE_DB", + }, + ); + + testMappingNodeThrows( + "should throw error for unknown action type", + createNode("test-action", "UNKNOWN_ACTION" as Action["kind"]), + "Unknown action 'UNKNOWN_ACTION' in TutorialAction component", + ); +}); diff --git a/web/tutorial-actions-executor/tests/files.test.ts b/web/tutorial-actions-executor/tests/files.test.ts new file mode 100644 index 0000000000..e82df04d60 --- /dev/null +++ b/web/tutorial-actions-executor/tests/files.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { getFileNameWithoutExtension } from "../src/files"; + +describe("getFileNameWithoutExtension", () => { + testGettingFileNameWithoutExtension("test.txt", "test"); + testGettingFileNameWithoutExtension("test.config.js", "test.config"); + testGettingFileNameWithoutExtension("test", "test"); + testGettingFileNameWithoutExtension("/path/to/test.txt", "test"); + testGettingFileNameWithoutExtension("./src/test.ts", "test"); + testGettingFileNameWithoutExtension("", ""); +}); + +function testGettingFileNameWithoutExtension( + filePath: string, + expectedFileName: string, +) { + it(`should return "${expectedFileName}" for "${filePath}"`, () => { + expect(getFileNameWithoutExtension(filePath)).toBe(expectedFileName); + }); +} diff --git a/web/tutorial-actions-executor/tsconfig.json b/web/tutorial-actions-executor/tsconfig.json new file mode 100644 index 0000000000..589c7f2011 --- /dev/null +++ b/web/tutorial-actions-executor/tsconfig.json @@ -0,0 +1,22 @@ +// Based on https://www.totaltypescript.com/tsconfig-cheat-sheet +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "module": "preserve", + "noEmit": true, + + "lib": ["es2022"] + } +} diff --git a/web/tutorial-actions-executor/vitest.config.ts b/web/tutorial-actions-executor/vitest.config.ts new file mode 100644 index 0000000000..150b70e566 --- /dev/null +++ b/web/tutorial-actions-executor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts", "e2e-tests/**/*.test.ts"], + environment: "node", + }, +}); From b7a442c7fb3c3a26be98ba57fbda1dff15925ddc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:37:24 +0100 Subject: [PATCH 38/92] Update Tailwind CSS to latest v3 in docs, migration guides, AI generator, and SDK generator (#3380) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mihovil Ilakovac Co-authored-by: Carlos Precioso --- waspc/ChangeLog.md | 2 ++ .../kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums | 2 +- .../wasp-app/.wasp/out/installedNpmDepsLog.json | 2 +- .../wasp-app/.wasp/out/sdk/wasp/package.json | 2 +- waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs | 2 +- waspc/src/Wasp/Generator/SdkGenerator/DepVersions.hs | 2 +- web/docs/migration-guides/migrate-from-0-17-to-0-18.md | 6 ++++++ web/docs/project/css-frameworks.md | 2 +- .../migration-guides/migrate-from-0-17-to-0-18.md | 6 ++++++ web/versioned_docs/version-0.18.0/project/css-frameworks.md | 2 +- 10 files changed, 21 insertions(+), 7 deletions(-) diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index 00fa1ddc3b..05b2ace230 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -20,6 +20,7 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guide - Fixed a type error in the generated server app when `process.env.NODE_ENV` is also declared by another dependency in the project. ([#3189](https://github.com/wasp-lang/wasp/pull/3189)) - Fixed an incompatibility between `wasp deploy fly` and Fly CLI v0.3.214. ([#3372](https://github.com/wasp-lang/wasp/pull/3372)) +- Raised the internal requirement of Tailwind CSS from `^3.2.7` to `^3.4.17` to ensure compatibility with ESM config files. ([#3380](https://github.com/wasp-lang/wasp/issues/3380)) ### 🔧 Small improvements @@ -28,6 +29,7 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guide ### 📖 Documentation +- Updated Tailwind CSS installation command in docs from exact version `3.2.7` to `3` (latest v3) to ensure compatibility with Wasp 0.18.0+ ESM config files. ([#3377](https://github.com/wasp-lang/wasp/issues/3377)) - Added note for SMTP ports being blocked by some hosting providers (by @Vickram-T-G). ([#3109](https://github.com/wasp-lang/wasp/pull/3109)) - Added documentation on how to setup the Chrome DevTools workspace mapping with Wasp (by @0xTaneja). ([#3103](https://github.com/wasp-lang/wasp/pull/3103)) diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums index 77b708960e..a6b90848c1 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums @@ -641,7 +641,7 @@ "file", "../out/sdk/wasp/package.json" ], - "815dd64c1b6ab2c8b86e43ebd4f69ea3829944ba9fce6b3a5d5745383fa10f9e" + "1591f79fc9e60bcf4708bcb11d1ead6042def086e0523dce8089512ee67d73e1" ], [ [ diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json index d99245b0a4..f7327bd1e4 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/installedNpmDepsLog.json @@ -1 +1 @@ -{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"chai","version":"^5.2.0"},{"name":"clsx","version":"^2.1.1"},{"name":"linebyline","version":"^1.3.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"tailwind-merge","version":"^2.6.0"},{"name":"wait-port","version":"^1.1.0"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@playwright/test","version":"1.51.1"},{"name":"@tailwindcss/forms","version":"^0.5.10"},{"name":"@tailwindcss/typography","version":"^0.5.16"},{"name":"@types/chai","version":"^5.2.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/uuid","version":"^9.0.8"},{"name":"@wasp.sh/wasp-app-runner","version":"^0.0.8"},{"name":"prisma","version":"5.19.1"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"@node-rs/argon2","version":"^1.8.3"},{"name":"arctic","version":"^1.2.1"},{"name":"lucia","version":"^3.0.1"},{"name":"oslo","version":"^1.1.2"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0"},{"name":"nodemailer","version":"^6.9.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"socket.io-client","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"postcss","version":"^8.4.21"},{"name":"autoprefixer","version":"^10.4.13"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file +{"_userNpmDeps":{"fromUser":{"dependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"chai","version":"^5.2.0"},{"name":"clsx","version":"^2.1.1"},{"name":"linebyline","version":"^1.3.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"tailwind-merge","version":"^2.6.0"},{"name":"wait-port","version":"^1.1.0"},{"name":"wasp","version":"file:.wasp/out/sdk/wasp"}],"devDependencies":[{"name":"@playwright/test","version":"1.51.1"},{"name":"@tailwindcss/forms","version":"^0.5.10"},{"name":"@tailwindcss/typography","version":"^0.5.16"},{"name":"@types/chai","version":"^5.2.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/uuid","version":"^9.0.8"},{"name":"@wasp.sh/wasp-app-runner","version":"^0.0.8"},{"name":"prisma","version":"5.19.1"},{"name":"tailwindcss","version":"^3.2.7"},{"name":"typescript","version":"5.8.2"},{"name":"vite","version":"^7.0.6"}],"peerDependencies":[]}},"_waspFrameworkNpmDeps":{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~5.1.0"},{"name":"morgan","version":"~1.10.0"},{"name":"dotenv","version":"^16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"superjson","version":"^2.2.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"typescript","version":"5.8.2"},{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"},{"name":"@types/node","version":"^22.0.0"},{"name":"@tsconfig/node22","version":"latest"},{"name":"@types/cors","version":"^2.8.5"},{"name":"rollup","version":"^4.9.6"},{"name":"rollup-plugin-esbuild","version":"^6.1.1"},{"name":"@rollup/plugin-node-resolve","version":"^16.0.0"}],"peerDependencies":[]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"~4.41.0"},{"name":"react-router-dom","version":"^6.26.2"}],"devDependencies":[{"name":"typescript","version":"5.8.2"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@vitejs/plugin-react","version":"^4.7.0"},{"name":"@tsconfig/vite-react","version":"^7.0.0"}],"peerDependencies":[]}},"_waspSdkNpmDeps":{"dependencies":[{"name":"@prisma/client","version":"5.19.1"},{"name":"prisma","version":"5.19.1"},{"name":"axios","version":"^1.4.0"},{"name":"express","version":"~5.1.0"},{"name":"mitt","version":"3.0.0"},{"name":"react","version":"^18.2.0"},{"name":"react-router-dom","version":"^6.26.2"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"superjson","version":"^2.2.1"},{"name":"@node-rs/argon2","version":"^1.8.3"},{"name":"arctic","version":"^1.2.1"},{"name":"lucia","version":"^3.0.1"},{"name":"oslo","version":"^1.1.2"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0"},{"name":"nodemailer","version":"^6.9.1"},{"name":"socket.io","version":"^4.6.1"},{"name":"socket.io-client","version":"^4.6.1"},{"name":"@socket.io/component-emitter","version":"^4.0.0"},{"name":"vitest","version":"^1.2.1"},{"name":"@vitest/ui","version":"^1.2.1"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.1.2"},{"name":"@testing-library/jest-dom","version":"^6.3.0"},{"name":"msw","version":"^1.1.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"tailwindcss","version":"^3.4.17"},{"name":"postcss","version":"^8.4.21"},{"name":"autoprefixer","version":"^10.4.13"},{"name":"zod","version":"^3.23.8"}],"devDependencies":[{"name":"@types/express","version":"^5.0.0"},{"name":"@types/express-serve-static-core","version":"^5.0.0"}],"peerDependencies":[{"name":"@tanstack/react-query","version":"~4.41.0"}]}} \ No newline at end of file diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/package.json b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/package.json index 12d8b03305..daee4b4558 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/package.json +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/package.json @@ -26,7 +26,7 @@ "socket.io": "^4.6.1", "socket.io-client": "^4.6.1", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, diff --git a/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs b/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs index d18e08c4f9..6e95b431c8 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/InitialFiles.hs @@ -160,7 +160,7 @@ generatePackageJson newProjectDetails = "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.2", - "tailwindcss": "^3.2.7" + "tailwindcss": "^3.4.17" }, "devDependencies": { "typescript": "5.8.2", diff --git a/waspc/src/Wasp/Generator/SdkGenerator/DepVersions.hs b/waspc/src/Wasp/Generator/SdkGenerator/DepVersions.hs index 1a0edcaafa..e366c2755a 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/DepVersions.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/DepVersions.hs @@ -6,4 +6,4 @@ where import qualified Wasp.SemanticVersion as SV tailwindCssVersion :: SV.ComparatorSet -tailwindCssVersion = SV.backwardsCompatibleWith $ SV.Version 3 2 7 +tailwindCssVersion = SV.backwardsCompatibleWith $ SV.Version 3 4 17 diff --git a/web/docs/migration-guides/migrate-from-0-17-to-0-18.md b/web/docs/migration-guides/migrate-from-0-17-to-0-18.md index 8a4df25841..1fe5098042 100644 --- a/web/docs/migration-guides/migrate-from-0-17-to-0-18.md +++ b/web/docs/migration-guides/migrate-from-0-17-to-0-18.md @@ -134,6 +134,12 @@ Update the Tailwind configuration files' extensions from `.cjs` to `.js`: - `tailwind.config.cjs` ➝ `tailwind.config.js` - `postcss.config.cjs` ➝ `postcss.config.js` +Make sure to update to the latest v3 to ensure compatibility with the new ESM configuration: + +```bash +npm install -D tailwindcss@3 +``` + ### 5. Check your compatibility with Vite 7 Wasp now uses Vite 7 for better performance and stability. This includes some breaking changes, but we don't expect Wasp apps to be affected by them. If you are using Vite features directly in your app, you should check the migration guides for [v5](https://v5.vite.dev/guide/migration.html), [v6](https://v6.vite.dev/guide/migration.html), and [v7](https://v7.vite.dev/guide/migration.html). We expect most Wasp apps to be unaffected by these changes. diff --git a/web/docs/project/css-frameworks.md b/web/docs/project/css-frameworks.md index 6bf513ba50..4a7a1f8780 100644 --- a/web/docs/project/css-frameworks.md +++ b/web/docs/project/css-frameworks.md @@ -13,7 +13,7 @@ Wasp works great with [Tailwind CSS](https://v3.tailwindcss.com/), a utility-fir 1. Install Tailwind as a development dependency. ```bash -npm install -D tailwindcss@3.2.7 +npm install -D tailwindcss@3 ``` 2. Add `./tailwind.config.js`. diff --git a/web/versioned_docs/version-0.18.0/migration-guides/migrate-from-0-17-to-0-18.md b/web/versioned_docs/version-0.18.0/migration-guides/migrate-from-0-17-to-0-18.md index 8a4df25841..1fe5098042 100644 --- a/web/versioned_docs/version-0.18.0/migration-guides/migrate-from-0-17-to-0-18.md +++ b/web/versioned_docs/version-0.18.0/migration-guides/migrate-from-0-17-to-0-18.md @@ -134,6 +134,12 @@ Update the Tailwind configuration files' extensions from `.cjs` to `.js`: - `tailwind.config.cjs` ➝ `tailwind.config.js` - `postcss.config.cjs` ➝ `postcss.config.js` +Make sure to update to the latest v3 to ensure compatibility with the new ESM configuration: + +```bash +npm install -D tailwindcss@3 +``` + ### 5. Check your compatibility with Vite 7 Wasp now uses Vite 7 for better performance and stability. This includes some breaking changes, but we don't expect Wasp apps to be affected by them. If you are using Vite features directly in your app, you should check the migration guides for [v5](https://v5.vite.dev/guide/migration.html), [v6](https://v6.vite.dev/guide/migration.html), and [v7](https://v7.vite.dev/guide/migration.html). We expect most Wasp apps to be unaffected by these changes. diff --git a/web/versioned_docs/version-0.18.0/project/css-frameworks.md b/web/versioned_docs/version-0.18.0/project/css-frameworks.md index 6bf513ba50..4a7a1f8780 100644 --- a/web/versioned_docs/version-0.18.0/project/css-frameworks.md +++ b/web/versioned_docs/version-0.18.0/project/css-frameworks.md @@ -13,7 +13,7 @@ Wasp works great with [Tailwind CSS](https://v3.tailwindcss.com/), a utility-fir 1. Install Tailwind as a development dependency. ```bash -npm install -D tailwindcss@3.2.7 +npm install -D tailwindcss@3 ``` 2. Add `./tailwind.config.js`. From 2cf791499edbac4e5be857839d2b4096bd361798 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Mon, 17 Nov 2025 19:38:09 +0100 Subject: [PATCH 39/92] Bump all `tacte` deps (#3383) --- .../package-lock.json | 956 ++++++++---------- web/tutorial-actions-executor/package.json | 12 +- 2 files changed, 404 insertions(+), 564 deletions(-) diff --git a/web/tutorial-actions-executor/package-lock.json b/web/tutorial-actions-executor/package-lock.json index 6c7d639304..fa7073a47f 100644 --- a/web/tutorial-actions-executor/package-lock.json +++ b/web/tutorial-actions-executor/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "@commander-js/extra-typings": "^14.0.0", - "@inquirer/prompts": "^7.8.0", + "@inquirer/prompts": "^8.0.1", "acorn": "8.15.0", - "commander": "^14.0.0", - "dedent": "^1.6.0", + "commander": "^14.0.2", + "dedent": "^1.7.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-mdx": "^3.0.0", @@ -22,14 +22,14 @@ "micromark-extension-mdx-jsx": "^3.0.2", "parse-git-diff": "0.0.19", "remark-comment": "^1.0.0", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "unist-util-visit": "^5.0.0", - "zx": "^8.5.3" + "zx": "^8.8.5" }, "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/mdast": "^4.0.4", - "vitest": "^3.2.4" + "vitest": "^4.0.10" } }, "node_modules/@commander-js/extra-typings": { @@ -457,20 +457,28 @@ "node": ">=18" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.1.tgz", + "integrity": "sha512-QAZUk6BBncv/XmSEZTscd8qazzjV3E0leUMrEPjxCd51QBgCKmprUGLex5DTsNtURm7LMzv+CLcd6S86xvBfYg==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, "node_modules/@inquirer/checkbox": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", - "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.1.tgz", + "integrity": "sha512-5VPFBK8jKdsjMK3DTFOlbR0+Kkd4q0AWB7VhWQn6ppv44dr3b7PU8wSJQTC5oA0f/aGW7v/ZozQJAY9zx6PKig==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^2.0.1", + "@inquirer/core": "^11.0.1", + "@inquirer/figures": "^2.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -482,16 +490,16 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", - "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.1.tgz", + "integrity": "sha512-wD+pM7IxLn1TdcQN12Q6wcFe5VpyCuh/I2sSmqO5KjWH2R4v+GkUToHb+PsDGobOe1MtAlXMwGNkZUPc2+L6NA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -503,22 +511,21 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.0.1.tgz", + "integrity": "sha512-Tpf49h50e4KYffVUCXzkx4gWMafUi3aDQDwfVAAGBNnVcXiwJIj4m2bKlZ7Kgyf6wjt1eyXH1wDGXcAokm4Ssw==", "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^2.0.1", + "@inquirer/figures": "^2.0.1", + "@inquirer/type": "^4.0.1", "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", + "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "wrap-ansi": "^9.0.2" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -530,17 +537,17 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", - "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.1.tgz", + "integrity": "sha512-zDKobHI7Ry++4noiV9Z5VfYgSVpPZoMApviIuGwLOMciQaP+dGzCO+1fcwI441riklRiZg4yURWyEoX0Zy2zZw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "external-editor": "^3.1.0" + "@inquirer/core": "^11.0.1", + "@inquirer/external-editor": "^2.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -552,17 +559,37 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", - "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.1.tgz", + "integrity": "sha512-TBrTpAB6uZNnGQHtSEkbvJZIQ3dXZOrwqQSO9uUbwct3G2LitwBCE5YZj98MbQ5nzihzs5pRjY1K9RRLH4WgoA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.1.tgz", + "integrity": "sha512-BPYWJXCAK9w6R+pb2s3WyxUz9ts9SP/LDOUwA9fu7LeuyYgojz83i0DSRwezu736BgMwz14G63Xwj70hSzHohQ==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -574,25 +601,25 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.1.tgz", + "integrity": "sha512-KtMxyjLCuDFqAWHmCY9qMtsZ09HnjMsm8H3OvpSIpfhHdfw3/AiGWHNrfRwbyvHPtOJpumm8wGn5fkhtvkWRsg==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" } }, "node_modules/@inquirer/input": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", - "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.1.tgz", + "integrity": "sha512-cEhEUohCpE2BCuLKtFFZGp4Ief05SEcqeAOq9NxzN5ThOQP8Rl5N/Nt9VEDORK1bRb2Sk/zoOyQYfysPQwyQtA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -604,16 +631,16 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", - "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.1.tgz", + "integrity": "sha512-4//zgBGHe8Q/FfCoUXZUrUHyK/q5dyqiwsePz3oSSPSmw1Ijo35ZkjaftnxroygcUlLYfXqm+0q08lnB5hd49A==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -625,17 +652,17 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", - "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.1.tgz", + "integrity": "sha512-UJudHpd7Ia30Q+x+ctYqI9Nh6SyEkaBscpa7J6Ts38oc1CNSws0I1hJEdxbQBlxQd65z5GEJPM4EtNf6tzfWaQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2" + "@inquirer/ansi": "^2.0.1", + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -647,24 +674,24 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", - "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.0.1.tgz", + "integrity": "sha512-MURRu/cyvLm9vchDDaVZ9u4p+ADnY0Mz3LQr0KTgihrrvuKZlqcWwlBC4lkOMvd0KKX4Wz7Ww9+uA7qEpQaqjg==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.0", - "@inquirer/confirm": "^5.1.14", - "@inquirer/editor": "^4.2.15", - "@inquirer/expand": "^4.0.17", - "@inquirer/input": "^4.2.1", - "@inquirer/number": "^3.0.17", - "@inquirer/password": "^4.0.17", - "@inquirer/rawlist": "^4.1.5", - "@inquirer/search": "^3.1.0", - "@inquirer/select": "^4.3.1" + "@inquirer/checkbox": "^5.0.1", + "@inquirer/confirm": "^6.0.1", + "@inquirer/editor": "^5.0.1", + "@inquirer/expand": "^5.0.1", + "@inquirer/input": "^5.0.1", + "@inquirer/number": "^4.0.1", + "@inquirer/password": "^5.0.1", + "@inquirer/rawlist": "^5.0.1", + "@inquirer/search": "^4.0.1", + "@inquirer/select": "^5.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -676,17 +703,16 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", - "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.0.1.tgz", + "integrity": "sha512-vVfVHKUgH6rZmMlyd0jOuGZo0Fw1jfcOqZF96lMwlgavx7g0x7MICe316bV01EEoI+c68vMdbkTTawuw3O+Fgw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^11.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -698,18 +724,17 @@ } }, "node_modules/@inquirer/search": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", - "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.0.1.tgz", + "integrity": "sha512-XwiaK5xBvr31STX6Ji8iS3HCRysBXfL/jUbTzufdWTS6LTGtvDQA50oVETt1BJgjKyQBp9vt0VU6AmU/AnOaGA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^11.0.1", + "@inquirer/figures": "^2.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -721,19 +746,18 @@ } }, "node_modules/@inquirer/select": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", - "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.1.tgz", + "integrity": "sha512-gPByrgYoezGyKMq5KjV7Tuy1JU2ArIy6/sI8sprw0OpXope3VGQwP5FK1KD4eFFqEhKu470Dwe6/AyDPmGRA0Q==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^2.0.1", + "@inquirer/core": "^11.0.1", + "@inquirer/figures": "^2.0.1", + "@inquirer/type": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -745,12 +769,12 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.1.tgz", + "integrity": "sha512-odO8YwoQAw/eVu/PSPsDDVPmqO77r/Mq7zcoF5VduVqIu2wSRWUgmYb5K9WH1no0SjLnOe8MDKtDL++z6mfo2g==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -769,9 +793,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", - "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -783,9 +807,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", - "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -797,9 +821,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", - "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -811,9 +835,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", - "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -825,9 +849,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", - "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -839,9 +863,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", - "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -853,9 +877,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", - "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -867,9 +891,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", - "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -881,9 +905,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", - "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -895,9 +919,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", - "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -909,9 +933,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", - "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -923,9 +947,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", - "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -937,9 +961,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", - "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -951,9 +975,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", - "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -965,9 +989,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", - "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -979,9 +1003,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", - "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -993,9 +1017,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", - "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1007,9 +1031,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", - "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "cpu": [ "arm64" ], @@ -1021,9 +1045,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", - "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1035,9 +1059,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", - "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1049,9 +1073,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", - "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "cpu": [ "x64" ], @@ -1063,9 +1087,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", - "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1076,14 +1100,22 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/debug": { @@ -1179,39 +1211,40 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.10.tgz", + "integrity": "sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.10.tgz", + "integrity": "sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.0.10", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1223,42 +1256,41 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.10.tgz", + "integrity": "sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.10.tgz", + "integrity": "sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.0.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.10.tgz", + "integrity": "sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.0.10", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1266,28 +1298,24 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.10.tgz", + "integrity": "sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.10.tgz", + "integrity": "sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.0.10", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1305,40 +1333,25 @@ "node": ">=0.4.0" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1354,16 +1367,6 @@ "node": ">=12" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1375,18 +1378,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -1432,20 +1428,10 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "license": "MIT" - }, - "node_modules/check-error": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" }, "node_modules/cli-width": { "version": "4.1.0", @@ -1456,37 +1442,19 @@ "node": ">= 12" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "license": "MIT", "engines": { "node": ">=20" } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1514,9 +1482,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -1527,16 +1495,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1560,9 +1518,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/es-module-lexer": { @@ -1669,20 +1627,6 @@ "node": ">=12.0.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fault": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", @@ -1736,6 +1680,18 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -1749,15 +1705,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/is-alphabetical": { @@ -1794,15 +1754,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -1813,13 +1764,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -1830,17 +1774,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2553,12 +2490,12 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -2580,15 +2517,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -2627,16 +2555,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2779,9 +2697,9 @@ } }, "node_modules/rollup": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", - "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -2795,28 +2713,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.3", - "@rollup/rollup-android-arm64": "4.52.3", - "@rollup/rollup-darwin-arm64": "4.52.3", - "@rollup/rollup-darwin-x64": "4.52.3", - "@rollup/rollup-freebsd-arm64": "4.52.3", - "@rollup/rollup-freebsd-x64": "4.52.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", - "@rollup/rollup-linux-arm-musleabihf": "4.52.3", - "@rollup/rollup-linux-arm64-gnu": "4.52.3", - "@rollup/rollup-linux-arm64-musl": "4.52.3", - "@rollup/rollup-linux-loong64-gnu": "4.52.3", - "@rollup/rollup-linux-ppc64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-musl": "4.52.3", - "@rollup/rollup-linux-s390x-gnu": "4.52.3", - "@rollup/rollup-linux-x64-gnu": "4.52.3", - "@rollup/rollup-linux-x64-musl": "4.52.3", - "@rollup/rollup-openharmony-arm64": "4.52.3", - "@rollup/rollup-win32-arm64-msvc": "4.52.3", - "@rollup/rollup-win32-ia32-msvc": "4.52.3", - "@rollup/rollup-win32-x64-gnu": "4.52.3", - "@rollup/rollup-win32-x64-msvc": "4.52.3", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -2863,24 +2781,27 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/stringify-entities": { @@ -2898,28 +2819,18 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/tinybench": { @@ -2953,52 +2864,20 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tsx": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", - "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "license": "MIT", "dependencies": { "esbuild": "~0.25.0", @@ -3014,18 +2893,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -3116,9 +2983,9 @@ } }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3190,65 +3057,39 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.10.tgz", + "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.0.10", + "@vitest/mocker": "4.0.10", + "@vitest/pretty-format": "4.0.10", + "@vitest/runner": "4.0.10", + "@vitest/snapshot": "4.0.10", + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -3256,9 +3097,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.10", + "@vitest/browser-preview": "4.0.10", + "@vitest/browser-webdriverio": "4.0.10", + "@vitest/ui": "4.0.10", "happy-dom": "*", "jsdom": "*" }, @@ -3272,7 +3115,13 @@ "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { @@ -3304,29 +3153,20 @@ } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "license": "MIT", "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/zwitch": { @@ -3340,9 +3180,9 @@ } }, "node_modules/zx": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.7.1.tgz", - "integrity": "sha512-28u1w2LlIfvyvJvYe6pmCipesk8oL5AFMVp+P/U445LcaPgzrU5lNDtAPd6nJvWmoCNyXZz37R/xKOGokccjsw==", + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", "license": "Apache-2.0", "bin": { "zx": "build/cli.js" diff --git a/web/tutorial-actions-executor/package.json b/web/tutorial-actions-executor/package.json index d713344dd3..6d15288454 100644 --- a/web/tutorial-actions-executor/package.json +++ b/web/tutorial-actions-executor/package.json @@ -12,10 +12,10 @@ }, "dependencies": { "@commander-js/extra-typings": "^14.0.0", - "@inquirer/prompts": "^7.8.0", + "@inquirer/prompts": "^8.0.1", "acorn": "8.15.0", - "commander": "^14.0.0", - "dedent": "^1.6.0", + "commander": "^14.0.2", + "dedent": "^1.7.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-mdx": "^3.0.0", @@ -24,13 +24,13 @@ "micromark-extension-mdx-jsx": "^3.0.2", "parse-git-diff": "0.0.19", "remark-comment": "^1.0.0", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "unist-util-visit": "^5.0.0", - "zx": "^8.5.3" + "zx": "^8.8.5" }, "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/mdast": "^4.0.4", - "vitest": "^3.2.4" + "vitest": "^4.0.10" } } From 1869b65a7a5b7f4dd9826f276fe7677f22f2fe34 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:42:23 +0100 Subject: [PATCH 40/92] Merge pull request #3369 from wasp-lang/copilot/update-wasp-app-runner-postgres-18 --- wasp-app-runner/src/db/postgres.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wasp-app-runner/src/db/postgres.ts b/wasp-app-runner/src/db/postgres.ts index b18ca50719..1e9d0454b0 100644 --- a/wasp-app-runner/src/db/postgres.ts +++ b/wasp-app-runner/src/db/postgres.ts @@ -9,7 +9,7 @@ import { Branded } from "../types.js"; import type { AppName } from "../waspCli.js"; import type { SetupDbResult } from "./types.js"; -export const defaultPostgresDbImage = "postgres:16" as DockerImageName; +export const defaultPostgresDbImage = "postgres:18" as DockerImageName; type DatabaseConnectionUrl = Branded; From c2aaebc82bc19f27988783247ea5214ec07ff173 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Tue, 18 Nov 2025 13:48:07 +0100 Subject: [PATCH 41/92] Remove automation for labelling PRs as external (#3379) --- .../automation-pr-label-external.yaml | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/automation-pr-label-external.yaml diff --git a/.github/workflows/automation-pr-label-external.yaml b/.github/workflows/automation-pr-label-external.yaml deleted file mode 100644 index d54ada9604..0000000000 --- a/.github/workflows/automation-pr-label-external.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: "Automation - Label external PRs" - -############################################################ -# CAUTION: This workflow should not check out the PR code! # -############################################################ -# The `pull_request_target` event is only intended for simple automations on the -# PRs themselves (e.g., labeling, commenting). If we checked out the PR code here, -# we would be running untrusted code and giving it access to our repository -# secrets, which is a major security risk. -# -# More info at: -# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ - -on: - pull_request_target: - types: - - opened - - reopened - -env: - external_label_name: "external" - internal_author_associations_json: - # Possible values here: - # https://docs.github.com/en/graphql/reference/enums#commentauthorassociation - '[ "MEMBER", "COLLABORATOR", "OWNER" ]' - -jobs: - label-external-pr: - name: Label external PRs - - runs-on: ubuntu-latest - - permissions: - pull-requests: write - - steps: - - name: Label external PR - if: ${{ !contains(fromJSON(env.internal_author_associations_json), github.event.pull_request.author_association) }} - run: gh pr edit "$PR_NUMBER" --add-label "$LABEL_NAME" - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - LABEL_NAME: ${{ env.external_label_name }} - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} From ae72bbd4803601b341410f7dbe6ff2251ab51967 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Tue, 18 Nov 2025 13:54:18 +0100 Subject: [PATCH 42/92] Bump and publish wasp-app-runner --- wasp-app-runner/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wasp-app-runner/package.json b/wasp-app-runner/package.json index 664737e9db..0e3eb3f7d8 100644 --- a/wasp-app-runner/package.json +++ b/wasp-app-runner/package.json @@ -1,6 +1,6 @@ { "name": "@wasp.sh/wasp-app-runner", - "version": "0.0.9", + "version": "0.0.10", "type": "module", "description": "CLI tool for running Wasp applications for e2e testing", "repository": { From 7927c5e0bb40725d2e17f4668a6b2860c944e48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Wed, 19 Nov 2025 13:54:28 +0100 Subject: [PATCH 43/92] Fix CORS when client is deployed to subdir (#3339) Co-authored-by: Carlos Precioso --- examples/kitchen-sink/src/serverSetup.ts | 2 +- waspc/ChangeLog.md | 4 ++++ .../Generator/templates/sdk/wasp/server/config.ts | 14 +++++++------- .../Generator/templates/sdk/wasp/universal/url.ts | 4 ++++ .../wasp-app/.wasp/out/.waspchecksums | 6 +++--- .../wasp-app/.wasp/out/sdk/wasp/server/config.ts | 14 +++++++------- .../wasp-app/.wasp/out/sdk/wasp/src/serverSetup.ts | 2 +- .../wasp-app/.wasp/out/sdk/wasp/universal/url.ts | 4 ++++ .../wasp-app/src/serverSetup.ts | 2 +- .../wasp-app/.wasp/build/.waspchecksums | 4 ++-- .../wasp-app/.wasp/build/sdk/wasp/server/config.ts | 14 +++++++------- .../wasp-app/.wasp/build/sdk/wasp/universal/url.ts | 4 ++++ .../wasp-app/.wasp/out/sdk/wasp/server/config.ts | 14 +++++++------- .../wasp-app/.wasp/out/sdk/wasp/universal/url.ts | 4 ++++ .../wasp-app/.wasp/out/.waspchecksums | 4 ++-- .../wasp-app/.wasp/out/sdk/wasp/server/config.ts | 14 +++++++------- .../wasp-app/.wasp/out/sdk/wasp/universal/url.ts | 4 ++++ .../wasp-app/.wasp/out/.waspchecksums | 4 ++-- .../wasp-app/.wasp/out/sdk/wasp/server/config.ts | 14 +++++++------- .../wasp-app/.wasp/out/sdk/wasp/universal/url.ts | 4 ++++ web/docs/advanced/middleware-config.md | 6 +++--- .../migration-guides/migrate-from-0-18-to-0-19.md | 11 ++++++++++- 22 files changed, 95 insertions(+), 58 deletions(-) diff --git a/examples/kitchen-sink/src/serverSetup.ts b/examples/kitchen-sink/src/serverSetup.ts index f27f6af50b..6921719966 100644 --- a/examples/kitchen-sink/src/serverSetup.ts +++ b/examples/kitchen-sink/src/serverSetup.ts @@ -49,7 +49,7 @@ export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { // Example of adding an extra domain to CORS. middlewareConfig.set( "cors", - cors({ origin: [config.frontendUrl, "http://127.0.0.1:3000"] }), + cors({ origin: [...config.allowedCORSOrigins, "http://127.0.0.1:3000"] }), ); return middlewareConfig; }; diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index 05b2ace230..e3a4275b83 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -7,6 +7,7 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guides/migrate-from-0-18-to-0-19) for step-by-step documentation on how to upgrade. - Wasp now requires your project's `package.json` to contain `"workspaces": [".wasp/build/*", ".wasp/out/*"]`. ([#3159](https://github.com/wasp-lang/wasp/pull/3159)) +- The type of `config.allowedCORSOrigins` (from `wasp/server`) was changed from `string | string[]` to `(string | RegExp)[]`. ### 🎉 New Features @@ -15,12 +16,15 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guide - You can now specify a custom volume mount path for the development database with the `--db-volume-mount-path` option in `wasp start db`. ([#3362](https://github.com/wasp-lang/wasp/pull/3362)) - You can now specify which PostgreSQL image to use in `wasp deploy railway` with the `--db-image` argument. ([#3184](https://github.com/wasp-lang/wasp/pull/3184)) - You can now specify which PostgreSQL image to use in `wasp deploy fly` with the `--db-image` argument. ([#3187](https://github.com/wasp-lang/wasp/pull/3187)) +- It's now simpler to extend our default CORS rules. ([#3339](https://github.com/wasp-lang/wasp/pull/3339)) ### 🐞 Bug fixes - Fixed a type error in the generated server app when `process.env.NODE_ENV` is also declared by another dependency in the project. ([#3189](https://github.com/wasp-lang/wasp/pull/3189)) - Fixed an incompatibility between `wasp deploy fly` and Fly CLI v0.3.214. ([#3372](https://github.com/wasp-lang/wasp/pull/3372)) - Raised the internal requirement of Tailwind CSS from `^3.2.7` to `^3.4.17` to ensure compatibility with ESM config files. ([#3380](https://github.com/wasp-lang/wasp/issues/3380)) +- Fixed an error with routing when serving the client from a subpath in a domain. ([#3322](https://github.com/wasp-lang/wasp/pull/3322)) +- Fixed an error with CORS when serving the client from a subpath in a domain. ([#3339](https://github.com/wasp-lang/wasp/pull/3339)) ### 🔧 Small improvements diff --git a/waspc/data/Generator/templates/sdk/wasp/server/config.ts b/waspc/data/Generator/templates/sdk/wasp/server/config.ts index 5a7ea9bbea..90a64bd7ff 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/config.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/config.ts @@ -1,6 +1,6 @@ {{={= =}=}} import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -11,7 +11,7 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; {=# isAuthEnabled =} auth: { jwtSecret: string; @@ -19,12 +19,12 @@ type Config = { {=/ isAuthEnabled =} } -const frontendUrl = stripTrailingSlash(env["{= clientUrlEnvVarName =}"]) -const serverUrl = stripTrailingSlash(env["{= serverUrlEnvVarName =}"]) +const frontendUrl = stripTrailingSlash(env['{= clientUrlEnvVarName =}']) +const serverUrl = stripTrailingSlash(env['{= serverUrlEnvVarName =}']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/data/Generator/templates/sdk/wasp/universal/url.ts b/waspc/data/Generator/templates/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/data/Generator/templates/sdk/wasp/universal/url.ts +++ b/waspc/data/Generator/templates/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums index a6b90848c1..9c78e303a8 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums @@ -781,7 +781,7 @@ "file", "../out/sdk/wasp/server/config.ts" ], - "2b97e6356358223ff1eb41a4a37b7d725d76b9ad02e4130f9a1e076eebba1adc" + "9d9b57bd3ad748fe7f63c6487772c1c3386873dc28001565a8cb315b75b1df54" ], [ [ @@ -1411,7 +1411,7 @@ "file", "../out/sdk/wasp/src/serverSetup.ts" ], - "5f0772d8722b7ed2f1cc06e914dbaece26e581755db63532dede188fa1d8f709" + "cea1f0eba8624e7b8bc6b6dd1e91058ab9ddece8c2a577356ecd5444c9c2f183" ], [ [ @@ -1453,7 +1453,7 @@ "file", "../out/sdk/wasp/universal/url.ts" ], - "58ff4ad8ffc79264d7215461571d909f3c5fb177ff32c67058d1da9cd4115d6e" + "f5ad6be8ea444a098e54ceb2f6645e3c9f484c234e9e9ce568a7fa39ed168ccf" ], [ [ diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts index 6ad69be4fe..caa73000d7 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts @@ -1,5 +1,5 @@ import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -10,18 +10,18 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; auth: { jwtSecret: string; } } -const frontendUrl = stripTrailingSlash(env["WASP_WEB_CLIENT_URL"]) -const serverUrl = stripTrailingSlash(env["WASP_SERVER_URL"]) +const frontendUrl = stripTrailingSlash(env['WASP_WEB_CLIENT_URL']) +const serverUrl = stripTrailingSlash(env['WASP_SERVER_URL']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/src/serverSetup.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/src/serverSetup.ts index f27f6af50b..6921719966 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/src/serverSetup.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/src/serverSetup.ts @@ -49,7 +49,7 @@ export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { // Example of adding an extra domain to CORS. middlewareConfig.set( "cors", - cors({ origin: [config.frontendUrl, "http://127.0.0.1:3000"] }), + cors({ origin: [...config.allowedCORSOrigins, "http://127.0.0.1:3000"] }), ); return middlewareConfig; }; diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/src/serverSetup.ts b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/src/serverSetup.ts index f27f6af50b..6921719966 100644 --- a/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/src/serverSetup.ts +++ b/waspc/e2e-tests/snapshots/kitchen-sink-golden/wasp-app/src/serverSetup.ts @@ -49,7 +49,7 @@ export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { // Example of adding an extra domain to CORS. middlewareConfig.set( "cors", - cors({ origin: [config.frontendUrl, "http://127.0.0.1:3000"] }), + cors({ origin: [...config.allowedCORSOrigins, "http://127.0.0.1:3000"] }), ); return middlewareConfig; }; diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/.waspchecksums b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/.waspchecksums index beb851ceaf..5f3b37e8a6 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/.waspchecksums +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/.waspchecksums @@ -256,7 +256,7 @@ "file", "../out/sdk/wasp/server/config.ts" ], - "307d8db970e747fe9f07a75c0122b3d2415fb876ee47ec437e0f806baa2d5191" + "0aa2b45db4b3667f245458cc2ec765374d40e6ad7894a3cd024aa95bd9cb08f8" ], [ [ @@ -410,7 +410,7 @@ "file", "../out/sdk/wasp/universal/url.ts" ], - "58ff4ad8ffc79264d7215461571d909f3c5fb177ff32c67058d1da9cd4115d6e" + "f5ad6be8ea444a098e54ceb2f6645e3c9f484c234e9e9ce568a7fa39ed168ccf" ], [ [ diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/server/config.ts b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/server/config.ts index 688604e71a..ee5d1125df 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/server/config.ts +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/server/config.ts @@ -1,5 +1,5 @@ import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -10,15 +10,15 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; } -const frontendUrl = stripTrailingSlash(env["WASP_WEB_CLIENT_URL"]) -const serverUrl = stripTrailingSlash(env["WASP_SERVER_URL"]) +const frontendUrl = stripTrailingSlash(env['WASP_WEB_CLIENT_URL']) +const serverUrl = stripTrailingSlash(env['WASP_SERVER_URL']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/universal/url.ts b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/universal/url.ts +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/build/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts index 688604e71a..ee5d1125df 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts @@ -1,5 +1,5 @@ import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -10,15 +10,15 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; } -const frontendUrl = stripTrailingSlash(env["WASP_WEB_CLIENT_URL"]) -const serverUrl = stripTrailingSlash(env["WASP_SERVER_URL"]) +const frontendUrl = stripTrailingSlash(env['WASP_WEB_CLIENT_URL']) +const serverUrl = stripTrailingSlash(env['WASP_SERVER_URL']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts +++ b/waspc/e2e-tests/snapshots/wasp-build-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/.waspchecksums b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/.waspchecksums index dbf5437e83..97b875e8de 100644 --- a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/.waspchecksums +++ b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/.waspchecksums @@ -256,7 +256,7 @@ "file", "../out/sdk/wasp/server/config.ts" ], - "307d8db970e747fe9f07a75c0122b3d2415fb876ee47ec437e0f806baa2d5191" + "0aa2b45db4b3667f245458cc2ec765374d40e6ad7894a3cd024aa95bd9cb08f8" ], [ [ @@ -410,7 +410,7 @@ "file", "../out/sdk/wasp/universal/url.ts" ], - "58ff4ad8ffc79264d7215461571d909f3c5fb177ff32c67058d1da9cd4115d6e" + "f5ad6be8ea444a098e54ceb2f6645e3c9f484c234e9e9ce568a7fa39ed168ccf" ], [ [ diff --git a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts index 688604e71a..ee5d1125df 100644 --- a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts +++ b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts @@ -1,5 +1,5 @@ import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -10,15 +10,15 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; } -const frontendUrl = stripTrailingSlash(env["WASP_WEB_CLIENT_URL"]) -const serverUrl = stripTrailingSlash(env["WASP_SERVER_URL"]) +const frontendUrl = stripTrailingSlash(env['WASP_WEB_CLIENT_URL']) +const serverUrl = stripTrailingSlash(env['WASP_SERVER_URL']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts +++ b/waspc/e2e-tests/snapshots/wasp-compile-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/.waspchecksums b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/.waspchecksums index 8db849fa72..56bb99d5cf 100644 --- a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/.waspchecksums +++ b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/.waspchecksums @@ -263,7 +263,7 @@ "file", "../out/sdk/wasp/server/config.ts" ], - "307d8db970e747fe9f07a75c0122b3d2415fb876ee47ec437e0f806baa2d5191" + "0aa2b45db4b3667f245458cc2ec765374d40e6ad7894a3cd024aa95bd9cb08f8" ], [ [ @@ -417,7 +417,7 @@ "file", "../out/sdk/wasp/universal/url.ts" ], - "58ff4ad8ffc79264d7215461571d909f3c5fb177ff32c67058d1da9cd4115d6e" + "f5ad6be8ea444a098e54ceb2f6645e3c9f484c234e9e9ce568a7fa39ed168ccf" ], [ [ diff --git a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts index 688604e71a..ee5d1125df 100644 --- a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts +++ b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/server/config.ts @@ -1,5 +1,5 @@ import { env } from './env.js' -import { stripTrailingSlash } from '../universal/url.js' +import { stripTrailingSlash, getOrigin } from '../universal/url.js' type NodeEnv = typeof env.NODE_ENV @@ -10,15 +10,15 @@ type Config = { databaseUrl: string; frontendUrl: string; serverUrl: string; - allowedCORSOrigins: string | string[]; + allowedCORSOrigins: (string | RegExp)[]; } -const frontendUrl = stripTrailingSlash(env["WASP_WEB_CLIENT_URL"]) -const serverUrl = stripTrailingSlash(env["WASP_SERVER_URL"]) +const frontendUrl = stripTrailingSlash(env['WASP_WEB_CLIENT_URL']) +const serverUrl = stripTrailingSlash(env['WASP_SERVER_URL']) -const allowedCORSOriginsPerEnv: Record = { - development: '*', - production: [frontendUrl] +const allowedCORSOriginsPerEnv: Record = { + development: [/.*/], + production: [getOrigin(frontendUrl)] } const allowedCORSOrigins = allowedCORSOriginsPerEnv[env.NODE_ENV] diff --git a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts index 126678dac9..73693c6b85 100644 --- a/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts +++ b/waspc/e2e-tests/snapshots/wasp-migrate-golden/wasp-app/.wasp/out/sdk/wasp/universal/url.ts @@ -3,3 +3,7 @@ export function stripTrailingSlash(url: undefined): undefined export function stripTrailingSlash(url?: string): string | undefined { return url?.replace(/\/$/, ""); } + +export function getOrigin(url: string) { + return new URL(url).origin; +} diff --git a/web/docs/advanced/middleware-config.md b/web/docs/advanced/middleware-config.md index 8a43e152cd..85c54638ae 100644 --- a/web/docs/advanced/middleware-config.md +++ b/web/docs/advanced/middleware-config.md @@ -98,7 +98,7 @@ If you would like to modify the middleware for _all_ operations and APIs, you ca export const serverMiddlewareFn = (middlewareConfig) => { // Example of adding extra domains to CORS. - middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] })) + middlewareConfig.set('cors', cors({ origin: [...config.allowedCORSOrigins, 'https://example1.com', 'https://example2.com'] })) return middlewareConfig } ``` @@ -120,8 +120,8 @@ If you would like to modify the middleware for _all_ operations and APIs, you ca import { config, type MiddlewareConfigFn } from 'wasp/server' export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { - // Example of adding an extra domains to CORS. - middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] })) + // Example of adding extra domains to CORS. + middlewareConfig.set('cors', cors({ origin: [...config.allowedCORSOrigins, 'https://example1.com', 'https://example2.com'] })) return middlewareConfig } ``` diff --git a/web/docs/migration-guides/migrate-from-0-18-to-0-19.md b/web/docs/migration-guides/migrate-from-0-18-to-0-19.md index 81199ca35b..cf3456be96 100644 --- a/web/docs/migration-guides/migrate-from-0-18-to-0-19.md +++ b/web/docs/migration-guides/migrate-from-0-18-to-0-19.md @@ -8,6 +8,10 @@ title: Migration from 0.18.X to 0.19.X Wasp now enables npm workspaces for managing the generated app. This change makes our dependency system more reliable, and better prepares us for future features. It also makes installs faster overall and reduces the size of each project on your disk. This is largely transparent, and you shouldn't notice any difference in how you develop your app. +### The type of `config.allowedCORSOrigins` has changed + +The type of `config.allowedCORSOrigins` (imported from `wasp/server`) was changed from `string | string[]` to `(string | RegExp)[]`, which is always an array. Now, it's simpler to extend our default CORS rules if you just want to add extra domains to the list. + ## How to migrate? To migrate your Wasp app from 0.18.X to 0.19.X, follow these steps: @@ -44,6 +48,11 @@ rm package-lock.json wasp ts-setup # ONLY if you are using the Wasp TS Config ``` -### 3. Enjoy your updated Wasp app +### 3. Fix type errors caused by `config.allowedCORSOrigins` +Search your codebase for the string `allowedCORSOrigins` and fix any potential type errors around its usage. You can follow [our middleware guide](../advanced/middleware-config.md) for the recommended way to extend the CORS configuration. + +If the search returns no results, it means you aren't using this feature and there's nothing to fix. + +### 4. Enjoy your updated Wasp app That's it! From 07b1e93189b8b4f364bbdfe240cc8a1f952c3807 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Wed, 19 Nov 2025 15:33:06 +0100 Subject: [PATCH 44/92] Update lockfiles --- examples/ask-the-documents/package-lock.json | 2 +- examples/kitchen-sink/package-lock.json | 2 +- examples/waspello/package-lock.json | 2 +- .../package-lock.json | 2 +- mage/package-lock.json | 1130 +++++++++++++++-- 5 files changed, 1019 insertions(+), 119 deletions(-) diff --git a/examples/ask-the-documents/package-lock.json b/examples/ask-the-documents/package-lock.json index 058ccaa4d6..19ddfaefe4 100644 --- a/examples/ask-the-documents/package-lock.json +++ b/examples/ask-the-documents/package-lock.json @@ -108,7 +108,7 @@ "react-hook-form": "^7.45.4", "react-router-dom": "^6.26.2", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, diff --git a/examples/kitchen-sink/package-lock.json b/examples/kitchen-sink/package-lock.json index cf691d3ad1..3602fa9bd8 100644 --- a/examples/kitchen-sink/package-lock.json +++ b/examples/kitchen-sink/package-lock.json @@ -117,7 +117,7 @@ "socket.io": "^4.6.1", "socket.io-client": "^4.6.1", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, diff --git a/examples/waspello/package-lock.json b/examples/waspello/package-lock.json index 15ca219f6c..daa019aac2 100644 --- a/examples/waspello/package-lock.json +++ b/examples/waspello/package-lock.json @@ -123,7 +123,7 @@ "react-hook-form": "^7.45.4", "react-router-dom": "^6.26.2", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, diff --git a/examples/websockets-realtime-voting/package-lock.json b/examples/websockets-realtime-voting/package-lock.json index 74f0707296..ab3d900218 100644 --- a/examples/websockets-realtime-voting/package-lock.json +++ b/examples/websockets-realtime-voting/package-lock.json @@ -107,7 +107,7 @@ "socket.io": "^4.6.1", "socket.io-client": "^4.6.1", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, diff --git a/mage/package-lock.json b/mage/package-lock.json index 5095073bd8..787b0479df 100644 --- a/mage/package-lock.json +++ b/mage/package-lock.json @@ -40,6 +40,55 @@ "vite": "^7.0.6" } }, + ".wasp/build/server": { + "name": "@wasp.sh/generated-server-build", + "version": "0.0.0", + "dependencies": { + "cookie-parser": "~1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.0.2", + "express": "~5.1.0", + "helmet": "^6.0.0", + "morgan": "~1.10.0", + "superjson": "^2.2.1" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.0", + "@tsconfig/node22": "latest", + "@types/cors": "^2.8.5", + "@types/express": "^5.0.0", + "@types/express-serve-static-core": "^5.0.0", + "@types/node": "^22.0.0", + "nodemon": "^2.0.19", + "rollup": "^4.9.6", + "rollup-plugin-esbuild": "^6.1.1", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + ".wasp/build/web-app": { + "name": "@wasp.sh/generated-webapp-build", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "~4.41.0", + "axios": "^1.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@tsconfig/vite-react": "^7.0.0", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, ".wasp/out/sdk/wasp": { "version": "1.0.0", "license": "ISC", @@ -66,7 +115,7 @@ "react-hook-form": "^7.45.4", "react-router-dom": "^6.26.2", "superjson": "^2.2.1", - "tailwindcss": "^3.2.7", + "tailwindcss": "^3.4.17", "vitest": "^1.2.1", "zod": "^3.23.8" }, @@ -75,13 +124,13 @@ "@types/express-serve-static-core": "^5.0.0" }, "peerDependencies": { - "@tanstack/react-query": "^4.39.1" + "@tanstack/react-query": "~4.41.0" } }, ".wasp/out/sdk/wasp/node_modules/@emnapi/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", - "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "license": "MIT", "optional": true, "dependencies": { @@ -90,9 +139,9 @@ } }, ".wasp/out/sdk/wasp/node_modules/@emnapi/runtime": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", - "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "license": "MIT", "optional": true, "dependencies": { @@ -355,6 +404,55 @@ "tslib": "^2.4.0" } }, + ".wasp/out/server": { + "name": "@wasp.sh/generated-server-dev", + "version": "0.0.0", + "dependencies": { + "cookie-parser": "~1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.0.2", + "express": "~5.1.0", + "helmet": "^6.0.0", + "morgan": "~1.10.0", + "superjson": "^2.2.1" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.0", + "@tsconfig/node22": "latest", + "@types/cors": "^2.8.5", + "@types/express": "^5.0.0", + "@types/express-serve-static-core": "^5.0.0", + "@types/node": "^22.0.0", + "nodemon": "^2.0.19", + "rollup": "^4.9.6", + "rollup-plugin-esbuild": "^6.1.1", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + ".wasp/out/web-app": { + "name": "@wasp.sh/generated-webapp-dev", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "~4.41.0", + "axios": "^1.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@tsconfig/vite-react": "^7.0.0", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", @@ -373,148 +471,245 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "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==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=4" + "node": ">=6.0.0" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { @@ -528,6 +723,54 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "0.45.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", @@ -1137,6 +1380,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1225,9 +1479,9 @@ } }, "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", - "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "license": "MIT", "optional": true, "dependencies": { @@ -1236,9 +1490,9 @@ } }, "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/runtime": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", - "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "license": "MIT", "optional": true, "dependencies": { @@ -1842,6 +2096,81 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.50.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", @@ -2125,7 +2454,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.41.0.tgz", "integrity": "sha512-193R4Jp9hjvlij6LryxrB5Mpbffd2L9PeWh3KlIy/hJV4SkBOfiQZ+jc5qAZLDCrdbkA5FjGj+UoDYw6TcNnyA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -2136,7 +2464,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.41.0.tgz", "integrity": "sha512-4/euCZAv8zeaB5P/nQiySzB0JHM3tiraU9KjSvSlJAX7oIE9uPDZlHCkDg/bHYNXewzvsg0FtOMq0VUq8XMMOQ==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "4.41.0", "use-sync-external-store": "^1.2.0" @@ -2266,6 +2593,20 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node22": { + "version": "22.0.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.5.tgz", + "integrity": "sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/vite-react": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/vite-react/-/vite-react-7.0.2.tgz", + "integrity": "sha512-lEj4y5SPRcH+bjw0tyuxrEnPqQUwfQzBKgd1YamD9xyet9zLwh2gwy5F8w/Nxg5DjdgYVjjKo5aLJUf0BTDz4w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", @@ -2281,6 +2622,51 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2308,6 +2694,16 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", @@ -2438,12 +2834,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/prop-types": { @@ -2483,6 +2879,13 @@ "@types/react": "*" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -2728,6 +3131,27 @@ "internmap": "2.0.3" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@vitest/expect": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", @@ -2872,6 +3296,22 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/@wasp.sh/generated-server-build": { + "resolved": ".wasp/build/server", + "link": true + }, + "node_modules/@wasp.sh/generated-server-dev": { + "resolved": ".wasp/out/server", + "link": true + }, + "node_modules/@wasp.sh/generated-webapp-build": { + "resolved": ".wasp/build/web-app", + "link": true + }, + "node_modules/@wasp.sh/generated-webapp-dev": { + "resolved": ".wasp/out/web-app", + "link": true + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3209,6 +3649,24 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3629,6 +4087,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -3650,6 +4115,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -3659,6 +4131,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -3682,6 +4173,19 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -3847,9 +4351,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3910,6 +4414,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4019,6 +4533,18 @@ "node": ">=12" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4110,6 +4636,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4641,6 +5174,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4705,6 +5248,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4825,6 +5381,15 @@ "integrity": "sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==", "license": "MIT" }, + "node_modules/helmet": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.2.0.tgz", + "integrity": "sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -4917,6 +5482,13 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -5172,6 +5744,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -5446,6 +6025,32 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonc-parser": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", @@ -6224,6 +6829,49 @@ "ufo": "^1.3.2" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -6401,6 +7049,109 @@ "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6528,6 +7279,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7148,6 +7908,13 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7290,6 +8057,16 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "6.28.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.2.tgz", @@ -7445,6 +8222,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -7507,6 +8294,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-esbuild": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.2.1.tgz", + "integrity": "sha512-jTNOMGoMRhs0JuueJrJqbW8tOwxumaWYq+V5i+PD+8ecSCVkuX27tGW7BXqDgoULQ55rO7IdNxPcnsWtshz3AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "get-tsconfig": "^4.10.0", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "esbuild": ">=0.18.0", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -7639,6 +8446,16 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -7863,6 +8680,29 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -8289,6 +9129,16 @@ "node": ">=6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -8399,10 +9249,17 @@ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==" }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/universalify": { @@ -8422,6 +9279,43 @@ "node": ">= 0.8" } }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -8465,7 +9359,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -9839,6 +10732,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", From c4030f3213a82e727f2b3c3c958c50e449d10421 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Wed, 19 Nov 2025 16:15:35 +0100 Subject: [PATCH 45/92] Version docs to 0.19 --- .../version-0.19.0/_TypescriptServerNote.md | 12 + .../version-0.19.0/_WaspStartNote.md | 7 + .../advanced/accessing-app-config.md | 62 + .../version-0.19.0/advanced/apis.md | 450 ++++++ .../advanced/email/_dummy-provider-note.md | 4 + .../version-0.19.0/advanced/email/email.md | 249 +++ .../version-0.19.0/advanced/jobs.md | 465 ++++++ .../version-0.19.0/advanced/links.md | 220 +++ .../advanced/middleware-config.md | 274 ++++ .../version-0.19.0/advanced/web-sockets.md | 334 ++++ .../version-0.19.0/auth/AuthMethodsGrid.tsx | 48 + .../version-0.19.0/auth/Pills.css | 21 + .../version-0.19.0/auth/Pills.jsx | 111 ++ .../auth/_accessing-user-data-note.md | 5 + .../auth/_multiple-identities-warning.md | 6 + .../auth/_read-more-about-auth-entities.md | 3 + .../version-0.19.0/auth/_user-fields.md | 9 + .../auth/_user-signup-fields-explainer.md | 15 + .../auth/advanced/custom-auth-actions.md | 261 ++++ .../version-0.19.0/auth/auth-hooks.md | 1002 ++++++++++++ .../version-0.19.0/auth/email.md | 895 +++++++++++ .../auth/email/create-your-own-ui.md | 529 +++++++ .../auth/entities/_discord-data.md | 6 + .../auth/entities/_email-data.md | 15 + .../auth/entities/_github-data.md | 6 + .../auth/entities/_google-data.md | 6 + .../auth/entities/_keycloak-data.md | 6 + .../auth/entities/_slack-data.md | 6 + .../auth/entities/_username-data.md | 6 + .../version-0.19.0/auth/entities/entities.md | 746 +++++++++ .../version-0.19.0/auth/overview.md | 749 +++++++++ .../auth/social-auth/SocialAuthGrid.tsx | 40 + .../auth/social-auth/_api-reference-intro.md | 10 + .../auth/social-auth/_default-behaviour.md | 3 + .../auth/social-auth/_getuserfields-type.md | 3 + .../social-auth/_override-example-intro.md | 10 + .../auth/social-auth/_override-intro.md | 10 + .../auth/social-auth/_using-auth-note.md | 3 + .../social-auth/_wasp-file-structure-note.md | 12 + .../auth/social-auth/create-your-own-ui.md | 34 + .../auth/social-auth/discord.md | 328 ++++ .../version-0.19.0/auth/social-auth/github.md | 342 ++++ .../version-0.19.0/auth/social-auth/google.md | 385 +++++ .../auth/social-auth/keycloak.md | 331 ++++ .../auth/social-auth/overview.md | 173 +++ .../version-0.19.0/auth/social-auth/slack.md | 394 +++++ web/versioned_docs/version-0.19.0/auth/ui.md | 589 +++++++ .../version-0.19.0/auth/username-and-pass.md | 381 +++++ .../username-and-pass/create-your-own-ui.md | 260 ++++ web/versioned_docs/version-0.19.0/contact.md | 7 + .../version-0.19.0/contributing.md | 19 + .../version-0.19.0/data-model/crud.md | 731 +++++++++ .../version-0.19.0/data-model/databases.md | 542 +++++++ .../version-0.19.0/data-model/entities.md | 168 ++ .../data-model/operations/_superjson-note.md | 16 + .../data-model/operations/actions.md | 897 +++++++++++ .../data-model/operations/overview.md | 13 + .../data-model/operations/queries.md | 718 +++++++++ .../version-0.19.0/data-model/prisma-file.md | 301 ++++ .../version-0.19.0/deployment/ci-cd.md | 140 ++ .../version-0.19.0/deployment/database.md | 76 + .../DeploymentOptionsGrid.tsx | 28 + .../deployment-methods/DeploymentTag.tsx | 17 + .../deployment-methods/GuideLink.css | 37 + .../deployment-methods/GuideLink.tsx | 21 + .../_addExternalAuthEnvVarsReminder.md | 3 + .../_building-the-web-client.md | 17 + .../deployment/deployment-methods/overview.md | 55 + .../deployment/deployment-methods/paas.md | 678 ++++++++ .../deployment-methods/self-hosted.md | 126 ++ .../wasp-deploy/WaspDeployProvidersGrid.tsx | 21 + .../wasp-deploy/_launch-command-env-vars.md | 6 + .../_railway-custom-postgres-option.md | 23 + .../deployment-methods/wasp-deploy/fly.md | 337 ++++ .../wasp-deploy/overview.md | 22 + .../deployment-methods/wasp-deploy/railway.md | 253 +++ .../version-0.19.0/deployment/env-vars.md | 53 + .../version-0.19.0/deployment/extras.md | 83 + .../version-0.19.0/deployment/intro.md | 46 + .../deployment/local-testing.md | 134 ++ .../version-0.19.0/general/cli.md | 210 +++ .../version-0.19.0/general/language.md | 91 ++ .../version-0.19.0/general/typescript.md | 155 ++ .../version-0.19.0/general/wasp-ts-config.md | 250 +++ .../introduction/editor-setup.md | 29 + .../introduction/introduction.md | 213 +++ .../introduction/quick-start.md | 153 ++ .../migrate-from-0-11-to-0-12.md | 1378 +++++++++++++++++ .../migrate-from-0-12-to-0-13.md | 132 ++ .../migrate-from-0-13-to-0-14.md | 639 ++++++++ .../migrate-from-0-14-to-0-15.md | 298 ++++ .../migrate-from-0-15-to-0-16.md | 98 ++ .../migrate-from-0-16-to-0-17.md | 295 ++++ .../migrate-from-0-17-to-0-18.md | 217 +++ .../migrate-from-0-18-to-0-19.md | 58 + .../version-0.19.0/project/EnvVarsTable.css | 28 + .../version-0.19.0/project/EnvVarsTable.tsx | 61 + .../project/SecretGeneratorBlock.module.css | 41 + .../project/SecretGeneratorBlock.tsx | 96 ++ .../version-0.19.0/project/_baseDirEnvNote.md | 6 + .../project/_clientEnvVarsNote.md | 4 + .../version-0.19.0/project/client-config.md | 446 ++++++ .../version-0.19.0/project/css-frameworks.md | 97 ++ .../project/custom-vite-config.md | 142 ++ .../version-0.19.0/project/customizing-app.md | 133 ++ .../version-0.19.0/project/dependencies.md | 30 + .../version-0.19.0/project/env-vars.md | 520 +++++++ .../version-0.19.0/project/server-config.md | 234 +++ .../project/starter-templates.md | 89 ++ .../version-0.19.0/project/static-assets.md | 66 + .../version-0.19.0/project/testing.md | 375 +++++ .../version-0.19.0/telemetry.md | 60 + .../version-0.19.0/tutorial/01-create.md | 86 + .../tutorial/02-project-structure.md | 146 ++ .../version-0.19.0/tutorial/03-pages.md | 141 ++ .../version-0.19.0/tutorial/04-entities.md | 50 + .../version-0.19.0/tutorial/05-queries.md | 194 +++ .../version-0.19.0/tutorial/06-actions.md | 270 ++++ .../version-0.19.0/tutorial/07-auth.md | 402 +++++ .../tutorial/TutorialAction.tsx | 146 ++ .../patches/03-pages__prepare-project.patch | 162 ++ .../patches/04-entities__prisma-task.patch | 14 + .../patches/05-queries__main-page-tasks.patch | 42 + .../05-queries__query-get-tasks-impl.patch | 14 + .../patches/05-queries__query-get-tasks.patch | 18 + .../06-actions__action-create-task-impl.patch | 19 + .../06-actions__action-create-task.patch | 13 + .../06-actions__action-update-task-impl.patch | 29 + .../06-actions__action-update-task.patch | 13 + ...ons__main-page-create-task-impl-form.patch | 36 + ...ions__main-page-create-task-use-form.patch | 12 + .../06-actions__main-page-update-task.patch | 44 + .../patches/07-auth__action-add-auth.patch | 49 + .../patches/07-auth__login-page-initial.patch | 20 + .../patches/07-auth__main-page-add-auth.patch | 19 + .../07-auth__main-page-add-logout.patch | 19 + .../07-auth__prisma-connect-task-user.patch | 24 + .../patches/07-auth__prisma-user.patch | 12 + .../patches/07-auth__query-add-auth.patch | 18 + .../07-auth__signup-page-initial.patch | 20 + .../07-auth__wasp-file-auth-required.patch | 12 + .../07-auth__wasp-file-auth-routes.patch | 18 + .../patches/07-auth__wasp-file-auth.patch | 23 + web/versioned_docs/version-0.19.0/vision.md | 31 + .../wasp-ai/creating-new-app.md | 45 + .../wasp-ai/developing-existing-app.md | 9 + .../version-0.19.0/wasp-ai/wasp-ai-1.png | Bin 0 -> 314644 bytes .../version-0.19.0/wasp-ai/wasp-ai-2.png | Bin 0 -> 304752 bytes .../version-0.19.0/writingguide.md | 172 ++ .../version-0.19.0-sidebars.json | 231 +++ web/versions.json | 12 +- 151 files changed, 24652 insertions(+), 1 deletion(-) create mode 100644 web/versioned_docs/version-0.19.0/_TypescriptServerNote.md create mode 100644 web/versioned_docs/version-0.19.0/_WaspStartNote.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/accessing-app-config.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/apis.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/email/_dummy-provider-note.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/email/email.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/jobs.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/links.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/middleware-config.md create mode 100644 web/versioned_docs/version-0.19.0/advanced/web-sockets.md create mode 100644 web/versioned_docs/version-0.19.0/auth/AuthMethodsGrid.tsx create mode 100644 web/versioned_docs/version-0.19.0/auth/Pills.css create mode 100644 web/versioned_docs/version-0.19.0/auth/Pills.jsx create mode 100644 web/versioned_docs/version-0.19.0/auth/_accessing-user-data-note.md create mode 100644 web/versioned_docs/version-0.19.0/auth/_multiple-identities-warning.md create mode 100644 web/versioned_docs/version-0.19.0/auth/_read-more-about-auth-entities.md create mode 100644 web/versioned_docs/version-0.19.0/auth/_user-fields.md create mode 100644 web/versioned_docs/version-0.19.0/auth/_user-signup-fields-explainer.md create mode 100644 web/versioned_docs/version-0.19.0/auth/advanced/custom-auth-actions.md create mode 100644 web/versioned_docs/version-0.19.0/auth/auth-hooks.md create mode 100644 web/versioned_docs/version-0.19.0/auth/email.md create mode 100644 web/versioned_docs/version-0.19.0/auth/email/create-your-own-ui.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_discord-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_email-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_github-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_google-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_keycloak-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_slack-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/_username-data.md create mode 100644 web/versioned_docs/version-0.19.0/auth/entities/entities.md create mode 100644 web/versioned_docs/version-0.19.0/auth/overview.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/SocialAuthGrid.tsx create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_api-reference-intro.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_default-behaviour.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_getuserfields-type.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_override-example-intro.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_override-intro.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_using-auth-note.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/_wasp-file-structure-note.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/create-your-own-ui.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/discord.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/github.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/google.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/keycloak.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/overview.md create mode 100644 web/versioned_docs/version-0.19.0/auth/social-auth/slack.md create mode 100644 web/versioned_docs/version-0.19.0/auth/ui.md create mode 100644 web/versioned_docs/version-0.19.0/auth/username-and-pass.md create mode 100644 web/versioned_docs/version-0.19.0/auth/username-and-pass/create-your-own-ui.md create mode 100644 web/versioned_docs/version-0.19.0/contact.md create mode 100644 web/versioned_docs/version-0.19.0/contributing.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/crud.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/databases.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/entities.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/operations/_superjson-note.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/operations/actions.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/operations/overview.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/operations/queries.md create mode 100644 web/versioned_docs/version-0.19.0/data-model/prisma-file.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/ci-cd.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/database.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/DeploymentOptionsGrid.tsx create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/DeploymentTag.tsx create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/GuideLink.css create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/GuideLink.tsx create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/_addExternalAuthEnvVarsReminder.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/_building-the-web-client.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/overview.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/paas.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/self-hosted.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/WaspDeployProvidersGrid.tsx create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/_launch-command-env-vars.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/_railway-custom-postgres-option.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/fly.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/overview.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/deployment-methods/wasp-deploy/railway.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/env-vars.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/extras.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/intro.md create mode 100644 web/versioned_docs/version-0.19.0/deployment/local-testing.md create mode 100644 web/versioned_docs/version-0.19.0/general/cli.md create mode 100644 web/versioned_docs/version-0.19.0/general/language.md create mode 100644 web/versioned_docs/version-0.19.0/general/typescript.md create mode 100644 web/versioned_docs/version-0.19.0/general/wasp-ts-config.md create mode 100644 web/versioned_docs/version-0.19.0/introduction/editor-setup.md create mode 100644 web/versioned_docs/version-0.19.0/introduction/introduction.md create mode 100644 web/versioned_docs/version-0.19.0/introduction/quick-start.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-11-to-0-12.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-12-to-0-13.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-13-to-0-14.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-14-to-0-15.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-15-to-0-16.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-16-to-0-17.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-17-to-0-18.md create mode 100644 web/versioned_docs/version-0.19.0/migration-guides/migrate-from-0-18-to-0-19.md create mode 100644 web/versioned_docs/version-0.19.0/project/EnvVarsTable.css create mode 100644 web/versioned_docs/version-0.19.0/project/EnvVarsTable.tsx create mode 100644 web/versioned_docs/version-0.19.0/project/SecretGeneratorBlock.module.css create mode 100644 web/versioned_docs/version-0.19.0/project/SecretGeneratorBlock.tsx create mode 100644 web/versioned_docs/version-0.19.0/project/_baseDirEnvNote.md create mode 100644 web/versioned_docs/version-0.19.0/project/_clientEnvVarsNote.md create mode 100644 web/versioned_docs/version-0.19.0/project/client-config.md create mode 100644 web/versioned_docs/version-0.19.0/project/css-frameworks.md create mode 100644 web/versioned_docs/version-0.19.0/project/custom-vite-config.md create mode 100644 web/versioned_docs/version-0.19.0/project/customizing-app.md create mode 100644 web/versioned_docs/version-0.19.0/project/dependencies.md create mode 100644 web/versioned_docs/version-0.19.0/project/env-vars.md create mode 100644 web/versioned_docs/version-0.19.0/project/server-config.md create mode 100644 web/versioned_docs/version-0.19.0/project/starter-templates.md create mode 100644 web/versioned_docs/version-0.19.0/project/static-assets.md create mode 100644 web/versioned_docs/version-0.19.0/project/testing.md create mode 100644 web/versioned_docs/version-0.19.0/telemetry.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/01-create.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/02-project-structure.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/03-pages.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/04-entities.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/05-queries.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/06-actions.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/07-auth.md create mode 100644 web/versioned_docs/version-0.19.0/tutorial/TutorialAction.tsx create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/03-pages__prepare-project.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/04-entities__prisma-task.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/05-queries__main-page-tasks.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/05-queries__query-get-tasks-impl.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/05-queries__query-get-tasks.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__action-create-task-impl.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__action-create-task.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__action-update-task-impl.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__action-update-task.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__main-page-create-task-impl-form.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__main-page-create-task-use-form.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/06-actions__main-page-update-task.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__action-add-auth.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__login-page-initial.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__main-page-add-auth.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__main-page-add-logout.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__prisma-connect-task-user.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__prisma-user.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__query-add-auth.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__signup-page-initial.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__wasp-file-auth-required.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__wasp-file-auth-routes.patch create mode 100644 web/versioned_docs/version-0.19.0/tutorial/patches/07-auth__wasp-file-auth.patch create mode 100644 web/versioned_docs/version-0.19.0/vision.md create mode 100644 web/versioned_docs/version-0.19.0/wasp-ai/creating-new-app.md create mode 100644 web/versioned_docs/version-0.19.0/wasp-ai/developing-existing-app.md create mode 100644 web/versioned_docs/version-0.19.0/wasp-ai/wasp-ai-1.png create mode 100644 web/versioned_docs/version-0.19.0/wasp-ai/wasp-ai-2.png create mode 100644 web/versioned_docs/version-0.19.0/writingguide.md create mode 100644 web/versioned_sidebars/version-0.19.0-sidebars.json diff --git a/web/versioned_docs/version-0.19.0/_TypescriptServerNote.md b/web/versioned_docs/version-0.19.0/_TypescriptServerNote.md new file mode 100644 index 0000000000..52dedfe365 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/_TypescriptServerNote.md @@ -0,0 +1,12 @@ +:::caution LSP Problems + +If you are using TypeScript, your editor may sometimes report type and import errors even while `wasp start` is running. + +This happens when the TypeScript Language Server gets out of sync with the current code. +If you're using VS Code, you can manually restart the language server by opening the command palette and selecting _"TypeScript: Restart TS Server."_ +Open the command pallete with: + +- `Ctrl` + `Shift` + `P` if you're on Windows or Linux. +- `Cmd` + `Shift` + `P` if you're on a Mac. + +::: diff --git a/web/versioned_docs/version-0.19.0/_WaspStartNote.md b/web/versioned_docs/version-0.19.0/_WaspStartNote.md new file mode 100644 index 0000000000..90fb67a959 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/_WaspStartNote.md @@ -0,0 +1,7 @@ +:::tip Keep Wasp start running + +`wasp start` automatically picks up the changes you make, regenerates the code, and restarts the app. So keep it running in the background. + +It also improves your experience by tracking the working directory and ensuring the generated code/types are up to date with your changes. + +::: diff --git a/web/versioned_docs/version-0.19.0/advanced/accessing-app-config.md b/web/versioned_docs/version-0.19.0/advanced/accessing-app-config.md new file mode 100644 index 0000000000..5e3d77f769 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/accessing-app-config.md @@ -0,0 +1,62 @@ +--- +title: Accessing the configuration +--- + +Whenever you start a Wasp app, you are starting two processes. + +- **The client process** - A React app that implements your app's frontend. + + During development, this is a dev server with hot reloading. In production, + it's a simple process that serves pre-built static files with environment variables + embedded during the build (details depend on [how you deploy it](../deployment/intro.md)). + +- **The server process** - An Express server that implements your app's backend. + + During development, this is an Express server controlled by a + [`nodemon`](https://www.npmjs.com/package/nodemon) process that takes care of + hot reloading and restarts. In production, it's a regular Express server run + using Node. + +Check [the introduction](/introduction/introduction.md) for a more in-depth explanation of Wasp's runtime architecture. + +You can configure both processes through environment variables. See [the deployment instructions](../project/env-vars.md) for a full list of supported variables. + +Wasp gives you runtime access to the processes' configurations through **configuration objects**. + +## Server configuration object + +The server configuration object contains these fields: + +- `frontendUrl: String` - Set it with env var `WASP_WEB_CLIENT_URL`. + + The URL of your client (the app's frontend).
+ Wasp automatically sets it during development when you run `wasp start`.
+ In production, you should set it to your client's URL as the server sees it + (i.e., with the DNS and proxies considered). + +You can access it like this: + +```js +import { config } from 'wasp/server' + +console.log(config.frontendUrl) +``` + +## Client configuration object + +The client configuration object contains these fields: + +- `apiUrl: String` - Set it with env var `REACT_APP_API_URL` + + The URL of your server (the app's backend).
+ Wasp automatically sets it during development when you run `wasp start`.
+ In production, it should contain the value of your server's URL as the user's browser + sees it (i.e., with the DNS and proxies considered). + +You can access it like this: + +```js +import { config } from 'wasp/client' + +console.log(config.apiUrl) +``` diff --git a/web/versioned_docs/version-0.19.0/advanced/apis.md b/web/versioned_docs/version-0.19.0/advanced/apis.md new file mode 100644 index 0000000000..88f478f977 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/apis.md @@ -0,0 +1,450 @@ +--- +title: Custom HTTP API Endpoints +--- + +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' +import { Required } from '@site/src/components/Tag' + +In Wasp, the default client-server interaction mechanism is through [Operations](../data-model/operations/overview). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`. Best of all, they should look and feel very familiar. + +## How to Create an API + +APIs are used to tie a JS function to a certain endpoint e.g. `POST /something/special`. They are distinct from Operations and have no client-side helpers (like `useQuery`). + +To create a Wasp API, you must: + +1. Declare the API in Wasp using the `api` declaration +2. Define the API's NodeJS implementation + +After completing these two steps, you'll be able to call the API from the client code (via our `Axios` wrapper), or from the outside world. + +### Declaring the API in Wasp + +First, we need to declare the API in the Wasp file and you can easily do this with the `api` declaration: + +```wasp title="main.wasp" +// ... + +api fooBar { // APIs and their implementations don't need to (but can) have the same name. + fn: import { fooBar } from "@src/apis", + httpRoute: (GET, "/foo/bar") +} +``` + +Read more about the supported fields in the [API Reference](#api-reference). + +### Defining the API's NodeJS Implementation + + + :::note + To make sure the Wasp compiler generates the types for APIs for use in the NodeJS implementation, you should add your `api` declarations to your `.wasp` file first _and_ keep the `wasp start` command running. + ::: + + +After you defined the API, it should be implemented as a NodeJS function that takes three arguments: + +1. `req`: Express Request object +2. `res`: Express Response object +3. `context`: An additional context object **injected into the API by Wasp**. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the [section about using entities in APIs](#using-entities-in-apis). + +```ts title="src/apis.ts" auto-js +import type { FooBar } from "wasp/server/api"; + +export const fooBar: FooBar = (req, res, context) => { + res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware. + res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` }); +}; +``` + + + :::note + The `FooBar` type is generated by Wasp based on the `api` declaration above. + ::: + + #### Providing Extra Type Information + + We'll see how we can provide extra type information to an API function. + + Let's say you wanted to create some `GET` route that would take an email address as a param, and provide them the answer to "Life, the Universe and Everything." 😀 What would this look like in TypeScript? + + Define the API in Wasp: + + ```wasp title="main.wasp" + api fooBar { + fn: import { fooBar } from "@src/apis", + entities: [Task], + httpRoute: (GET, "/foo/bar/:email") + } + ``` + + We can use the `FooBar` type to which we'll provide the generic **params** and **response** types, which then gives us full type safety in the implementation. + + ```ts title="src/apis.ts" + import { FooBar } from "wasp/server/api"; + + export const fooBar: FooBar< + { email: string }, // params + { answer: number } // response + > = (req, res, _context) => { + console.log(req.params.email); + res.json({ answer: 42 }); + }; + ``` + + +## Using the API + +### Using the API externally + +To use the API externally, you simply call the endpoint using the method and path you used. + +For example, if your app is running at `https://example.com` then from the above you could issue a `GET` to `https://example/com/foo/callback` (in your browser, Postman, `curl`, another web service, etc.). + +### Using the API from the Client + +To use the API from your client, including with auth support, you can import the Axios wrapper from `wasp/client/api` and invoke a call. For example: + +```tsx title="src/pages/SomePage.tsx" auto-js with-hole +import React, { useEffect } from "react"; +import { api } from "wasp/client/api"; + +async function fetchCustomRoute() { + const res = await api.get("/foo/bar"); + console.log(res.data); +} + +export const Foo = () => { + useEffect(() => { + fetchCustomRoute(); + }, []); + + return <>{$HOLE$}; +}; +``` + +#### Making Sure CORS Works + +APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled. + +You can do this by defining custom middleware for your APIs in the Wasp file. + +For example, an `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path: + +```wasp title="main.wasp" +apiNamespace fooBar { + middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis", + path: "/foo" +} +``` + +And then in the implementation file (returning the default config): + +```ts title="src/apis.ts" auto-js +import type { MiddlewareConfigFn } from "wasp/server"; +export const apiMiddleware: MiddlewareConfigFn = (config) => { + return config; +}; +``` + +We are returning the default middleware which enables CORS for all APIs under the `/foo` path. + +For more information about middleware configuration, please see: [Middleware Configuration](../advanced/middleware-config) + +## Using Entities in APIs + +In many cases, resources used in APIs will be [Entities](../data-model/entities.md). +To use an Entity in your API, add it to the `api` declaration in Wasp: + +```wasp {3} title="main.wasp" +api fooBar { + fn: import { fooBar } from "@src/apis", + entities: [Task], + httpRoute: (GET, "/foo/bar") +} +``` + +Wasp will inject the specified Entity into the APIs `context` argument, giving you access to the Entity's Prisma API: + +```ts title="src/apis.ts" auto-js +import type { FooBar } from "wasp/server/api"; + +export const fooBar: FooBar = async (req, res, context) => { + res.json({ count: await context.entities.Task.count() }); +}; +``` + +The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud). + +## Streaming Responses + +You can use streaming responses to send data to the client in chunks as it becomes available. This is useful for: + +- **LLM responses** - Stream AI-generated content as it's produced +- **Long-running processes** - Show progress updates in real-time +- **Large datasets** - Send data incrementally to avoid timeouts + +### Creating a Streaming API + +To create a streaming API, write a function that uses Express response methods like `res.write()` and `res.end()`: + +```wasp title="main.wasp" +api streamingText { + httpRoute: (POST, "/api/streaming-example"), + fn: import { getStreamingText } from "@src/streaming", +} +``` + + +Don't forget to set up the CORS middleware. See the [section explaning CORS](#making-sure-cors-works) for details. + + +```ts title="src/streaming.ts" auto-js +import OpenAI from "openai"; +import type { StreamingText } from "wasp/server/api"; + +const client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export const getStreamingText: StreamingText< + never, + string, + { message: string } +> = async (req, res) => { + const { message } = req.body; + + // Set appropriate headers for streaming. + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.setHeader("Transfer-Encoding", "chunked"); + + const stream = await client.responses.create({ + model: "gpt-5", + input: `Funny response to "${message}"`, + stream: true, + }); + + for await (const chunk of stream) { + if (chunk.type === "response.output_text.delta") { + // Write each chunk to the response as it arrives. + res.write(chunk.delta); + } + } + + // End the response. + res.end(); +}; +``` + +### Consuming Streaming Responses + +There are two ways you can consume streaming responses on the client side: using the Fetch API or using Axios. + +We recommend using the Fetch API becuase it supports streaming natively. You'll need to handle auth manually by adding the `Authorization` header. + +Axios doesn't natively support streaming responses and you have use the `onDownloadProgress` callback to simulate it. +Wasp internally uses Axios and exposes an Axios wrapper via `wasp/client/api` which handles auth automatically. + +#### Using the Fetch API + +Here's an example showing how to consume streaming responses using the Fetch API: + +```tsx title="src/StreamingPage.tsx" auto-js +import { useEffect, useState } from "react"; +import { config } from "wasp/client"; +import { getSessionId } from "wasp/client/api"; + +export function StreamingPage() { + const { response } = useTextStream("/api/streaming-example", { + message: "Best Office episode?", + }); + + return ( +
+

Streaming Example

+
{response}
+
+ ); +} + +function useTextStream(path: string, payload: { message: string }) { + const [response, setResponse] = useState(""); + + useEffect(() => { + const controller = new AbortController(); + + fetchStream( + path, + payload, + (chunk) => { + setResponse((prev) => prev + chunk); + }, + controller.signal + ); + + return () => { + controller.abort(); + }; + }, [path]); + + return { response }; +} + +async function fetchStream( + path: string, + payload: { message: string }, + onData: (data: string) => void, + signal: AbortSignal +) { + const sessionId = getSessionId(); + + try { + const response = await fetch(config.apiUrl + path, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(sessionId && { Authorization: `Bearer ${sessionId}` }), + }, + body: JSON.stringify(payload), + signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (response.body === null) { + throw new Error("Stream body is null"); + } + + const stream = response.body.pipeThrough(new TextDecoderStream()); + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + onData(value); + } + } catch (error: unknown) { + if (error instanceof Error) { + if (error.name === "AbortError") { + // Fetch was aborted, no need to log an error + return; + } + console.error("Fetch error:", error.message); + } else { + throw error; + } + } +} +``` + +#### Using Axios + +Here's an example showing how to consume streaming responses using the Axios wrapper from `wasp/client/api`: + +```tsx title="src/AxiosStreamingPage.tsx" auto-js +import { useEffect, useState } from "react"; +import { api } from "wasp/client/api"; + +export function StreamingPage() { + const { response } = useAxiosTextStream("/api/streaming-example", { + message: "Best Office episode?", + }); + + return ( +
+

Axios Streaming

+
{response}
+
+ ); +} + +function useAxiosTextStream(path: string, payload: { message: string }) { + const [response, setResponse] = useState(""); + + useEffect(() => { + const controller = new AbortController(); + + fetchAxiosStream( + path, + payload, + (data) => { + setResponse(data); + }, + controller.signal + ); + + return () => { + controller.abort(); + }; + }, [path]); + + return { response }; +} + +async function fetchAxiosStream( + path: string, + payload: { message: string }, + onData: (data: string) => void, + signal: AbortSignal +) { + try { + return await api.post(path, payload, { + responseType: "stream", + signal, + onDownloadProgress: (progressEvent) => { + const xhr = progressEvent.event.target; + onData(xhr.responseText); + }, + }); + } catch (error: unknown) { + if (error instanceof Error) { + if (error.name === "CanceledError") { + // Request was cancelled, no action needed + } else { + console.error("Fetch error:", error); + } + } else { + throw error; + } + } +} +``` + +## API Reference + +```wasp title="main.wasp" +api fooBar { + fn: import { fooBar } from "@src/apis", + httpRoute: (GET, "/foo/bar"), + entities: [Task], + auth: true, + middlewareConfigFn: import { apiMiddleware } from "@src/apis" +} +``` + +The `api` declaration has the following fields: + +- `fn: ExtImport` + + The import statement of the APIs NodeJs implementation. + +- `httpRoute: (HttpMethod, string)` + + The HTTP (method, path) pair, where the method can be one of: + + - `ALL`, `GET`, `POST`, `PUT` or `DELETE` + - and path is an Express path `string`. + +- `entities: [Entity]` + + A list of entities you wish to use inside your API. You can read more about it [here](#using-entities-in-apis). + +- `auth: bool` + + If auth is enabled, this will default to `true` and provide a `context.user` object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to `false`. + +- `middlewareConfigFn: ExtImport` + + The import statement to an Express middleware config function for this API. See more in [middleware section](../advanced/middleware-config) of the docs. diff --git a/web/versioned_docs/version-0.19.0/advanced/email/_dummy-provider-note.md b/web/versioned_docs/version-0.19.0/advanced/email/_dummy-provider-note.md new file mode 100644 index 0000000000..b08bf7ba88 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/email/_dummy-provider-note.md @@ -0,0 +1,4 @@ +:::note Dummy Provider is not for production use + +The `Dummy` provider is not for production use. It is only meant to be used during development. If you try building your app with the `Dummy` provider, the build will fail. +::: diff --git a/web/versioned_docs/version-0.19.0/advanced/email/email.md b/web/versioned_docs/version-0.19.0/advanced/email/email.md new file mode 100644 index 0000000000..f4b01a9594 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/email/email.md @@ -0,0 +1,249 @@ +--- +title: Sending Emails +--- + +import { Required } from '@site/src/components/Tag' +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' +import DummyProviderNote from './_dummy-provider-note.md' + +# Sending Emails + +With Wasp's email-sending feature, you can easily integrate email functionality into your web application. + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + +Choose from one of the providers: + +- `Dummy` (development only), +- `Mailgun`, +- `SendGrid` +- or the good old `SMTP`. + +Optionally, define the `defaultFrom` field, so you don't need to provide it whenever sending an email. + +## Sending Emails + +Before jumping into details about setting up various providers, let's see how easy it is to send emails. + +You import the `emailSender` that is provided by the `wasp/server/email` module and call the `send` method on it. + +```ts title="src/actions/sendEmail.ts" auto-js +import { emailSender } from "wasp/server/email"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + +Read more about the `send` method in the [API Reference](#javascript-api). + +The `send` method returns an object with the status of the sent email. It varies depending on the provider you use. + +## Providers + +We'll go over all of the available providers in the next section. For some of them, you'll need to set up some env variables. You can do that in the `.env.server` file. + +### Using the Dummy Provider {#dummy} + + + +To speed up development, Wasp offers a `Dummy` email sender that `console.log`s the emails in the console. Since it doesn't send emails for real, it doesn't require any setup. + +Set the provider to `Dummy` in your `main.wasp` file. + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Dummy, + } +} +``` + +### Using the SMTP Provider {#smtp} + +First, set the provider to `SMTP` in your `main.wasp` file. + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SMTP, + } +} +``` + +Then, add the following env variables to your `.env.server` file. + +```properties title=".env.server" +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_PORT= +``` + +Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well. + +:::caution SMTP ports might be blocked +Some hosting providers (for example, **Railway** on its free tier, or **Hetzner**) block outbound SMTP ports to prevent spam. +If you run into issues, check their documentation for a solution, or consider using a dedicated provider integration like [Mailgun](#mailgun) or [SendGrid](#sendgrid) instead of plain SMTP. +::: + +### Using the Mailgun Provider {#mailgun} + +Set the provider to `Mailgun` in the `main.wasp` file. + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: Mailgun, + } +} +``` + +Then, get the Mailgun API key and domain and add them to your `.env.server` file. + +#### Getting the API Key and Domain + +1. Go to [Mailgun](https://www.mailgun.com/) and create an account. +2. Go to [Domains](https://app.mailgun.com/mg/sending/new-domain) and create a new domain. +3. Copy the domain and add it to your `.env.server` file. +4. Create a new Sending API key under `Send > Sending > Domain settings` and find `Sending API keys`. +5. Copy the API key and add it to your `.env.server` file. + +```properties title=".env.server" +MAILGUN_API_KEY= +MAILGUN_DOMAIN= +``` + +#### Using the EU Region + +If your domain region is in the EU, you need to set the `MAILGUN_API_URL` variable in your `.env.server` file: + +```properties title=".env.server" +MAILGUN_API_URL=https://api.eu.mailgun.net +``` + +### Using the SendGrid Provider {#sendgrid} + +Set the provider field to `SendGrid` in your `main.wasp` file. + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: SendGrid, + } +} +``` + +Then, get the SendGrid API key and add it to your `.env.server` file. + +#### Getting the API Key + +1. Go to [SendGrid](https://sendgrid.com/) and create an account. +2. Go to [API Keys](https://app.sendgrid.com/settings/api_keys) and create a new API key. +3. Copy the API key and add it to your `.env.server` file. + +```properties title=".env.server" +SENDGRID_API_KEY= +``` + +## API Reference + +### `emailSender` dict + +```wasp title="main.wasp" +app Example { + ... + emailSender: { + provider: , + defaultFrom: { + name: "Example", + email: "hello@itsme.com" + }, + } +} +``` + +The `emailSender` dict has the following fields: + +- `provider: Provider` + + The provider you want to use. Choose from `Dummy`, `SMTP`, `Mailgun` or `SendGrid`. + + + +- `defaultFrom: dict` + + The default sender's details. If you set this field, you don't need to provide the `from` field when sending an email. + +### JavaScript API + +Using the `emailSender` in TypescriptJavaScript: + +```ts title="src/actions/sendEmail.ts" auto-js +import { emailSender } from "wasp/server/email"; + +// In some action handler... +const info = await emailSender.send({ + from: { + name: "John Doe", + email: "john@doe.com", + }, + to: "user@domain.com", + subject: "Saying hello", + text: "Hello world", + html: "Hello world", +}); +``` + +The `send` method accepts an object with the following fields: + +- `from: object` + + The sender's details. If you set up `defaultFrom` field in the `emailSender` dict in Wasp file, this field is optional. + + - `name: string` + + The name of the sender. + + - `email: string` + + The email address of the sender. + +- `to: string` + + The recipient's email address. + +- `subject: string` + + The subject of the email. + +- `text: string` + + The text version of the email. + +- `html: string` + + The HTML version of the email diff --git a/web/versioned_docs/version-0.19.0/advanced/jobs.md b/web/versioned_docs/version-0.19.0/advanced/jobs.md new file mode 100644 index 0000000000..45f00c6c76 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/jobs.md @@ -0,0 +1,465 @@ +--- +title: Recurring Jobs +--- + +import { Required } from '@site/src/components/Tag' +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers' + +In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth. + +What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background. + +Wasp supports background jobs that can help you with this: + +- Jobs persist between server restarts, +- Jobs can be retried if they fail, +- Jobs can be delayed until a future time, +- Jobs can have a recurring schedule. + +## Using Jobs + +### Job Definition and Usage + +Let's write an example Job that will print a message to the console and return a list of tasks from the database. + +1. Start by creating a Job declaration in your `.wasp` file: + + + + ```wasp title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar" + }, + entities: [Task], + } + ``` + + + + ```wasp title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar" + }, + entities: [Task], + } + ``` + + + +2. After declaring the Job, implement its worker function: + + + + ```js title="src/workers/bar.js" + export const foo = async ({ name }, context) => { + console.log(`Hello ${name}!`) + const tasks = await context.entities.Task.findMany({}) + return { tasks } + } + ``` + + + + ```ts title="src/workers/bar.ts" + import { type MySpecialJob } from 'wasp/server/jobs' + import { type Task } from 'wasp/entities' + + type Input = { name: string; } + type Output = { tasks: Task[]; } + + export const foo: MySpecialJob = async ({ name }, context) => { + console.log(`Hello ${name}!`) + const tasks = await context.entities.Task.findMany({}) + return { tasks } + } + ``` + + + +:::info The worker function +The worker function must be an `async` function. The function's return value represents the Job's result. + +The worker function accepts two arguments: + +- `args`: The data passed into the job when it's submitted. +- `context: { entities }`: The context object containing entities you put in the Job declaration. + ::: + + + `MySpecialJob` is a generic type Wasp generates to help you correctly type the Job's worker function, ensuring type information about the function's arguments and return value. Read more about type-safe jobs in the [Javascript API section](#javascript-api). + + +3. After successfully defining the job, you can submit work to be done in your [Operations](../data-model/operations/overview) or [setupFn](../project/server-config#setup-function) (or any other NodeJS code): + + + + ```js title="someAction.js" + import { mySpecialJob } from 'wasp/server/jobs' + + const submittedJob = await mySpecialJob.submit({ job: "Johnny" }) + + // Or, if you'd prefer it to execute in the future, just add a .delay(). + // It takes a number of seconds, Date, or ISO date string. + await mySpecialJob + .delay(10) + .submit({ name: "Johnny" }) + ``` + + + + ```ts title="someAction.ts" + import { mySpecialJob } from 'wasp/server/jobs' + + const submittedJob = await mySpecialJob.submit({ job: "Johnny" }) + + // Or, if you'd prefer it to execute in the future, just add a .delay(). + // It takes a number of seconds, Date, or ISO date string. + await mySpecialJob + .delay(10) + .submit({ name: "Johnny" }) + ``` + + + +And that's it. Your job will be executed by `PgBoss` as if you called `foo({ name: "Johnny" })`. + +In our example, `foo` takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function. + +### Recurring Jobs + +If you have work that needs to be done on some recurring basis, you can add a `schedule` to your job declaration: + + + + ```wasp {6-9} title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar" + }, + schedule: { + cron: "0 * * * *", + args: {=json { "job": "args" } json=} // optional + } + } + ``` + + + + ```wasp {6-9} title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar" + }, + schedule: { + cron: "0 * * * *", + args: {=json { "job": "args" } json=} // optional + } + } + ``` + + + +In this example, you _don't_ need to invoke anything in JavaScriptTypescript. You can imagine `foo({ job: "args" })` getting automatically scheduled and invoked for you every hour. + + + + + +## Job executors + +Wasp supports Jobs through the use of **job executors**. A job executor is responsible for handling the scheduling, monitoring, and execution of jobs. + +Currently, Wasp only has support for one job executor, `PgBoss`. + +### `PgBoss` {#pgboss} + +[`PgBoss`](https://github.com/timgit/pg-boss/tree/8.4.2) is a lightweight job queue built on top of PostgreSQL. It is suitable for low-volume production use cases and does not require any additional infrastructure or complex management. By using PostgreSQL (and [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/)) as its storage and synchronization mechanism, you get many benefits of a traditional job queue, on top of your existing Postgres database. + +#### Requirements + +`PgBoss` requires that your database provider is set to `"postgresql"` in your `schema.prisma` file. Read more about setting the provider [here](../data-model/databases.md#postgresql). + +#### Limitations + +`PgBoss` runs together with your web server, whenever it is up. This means that it is not a separate process or service, but rather a part of your web server's application. As such, it is not suitable for CPU-heavy workloads, as it shares the CPU with your web server's application logic. + +The `PgBoss` executor in Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads. This means that your server must be running whenever you want to process jobs. If you need to scale your job processing, you will need to run multiple instances of your web server, each with its own `PgBoss` instance. + +#### Customization {#pg_boss_new_options} + +If you need to customize the creation of the `PgBoss` instance, you can set an environment variable called `PG_BOSS_NEW_OPTIONS` to a stringified JSON object containing the initialization parameters. See the [pg-boss documentation](https://github.com/timgit/pg-boss/tree/8.4.2/docs#newoptions). + +Please note that setting `PG_BOSS_NEW_OPTIONS` environment variable overwrites all Wasp defaults, so you must include the `connectionString` parameter inside it as well. + +For example, to set the connection string and change the job archival and deletion settings, you can set the environment variable like this: + +```bash +# In an .env file +PG_BOSS_NEW_OPTIONS={"connectionString":"postgresql://user:password@server:5432/database","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5} + +# In the shell +PG_BOSS_NEW_OPTIONS='{"connectionString":"postgresql://user:password@server:5432/database","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5}' +``` + +You can read more about escaping JSON in environment variables in the [JSON Env Vars documentation](../project/env-vars.md#json-env-vars). + +#### Database setup + +:::tip You don't need to set up the database manually + +When using `PgBoss`, the database setup is automatically taken care of by the Wasp server, and doesn't need to be reflected in your schemas or migrations. The following information is given for your reference, and is explained in more detail in [the `PgBoss` documentation](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md). + +::: + +All job data will be stored in a separate database schema called `pgboss`. It has some internal tracking tables, such as `job`, `archive`, and `schedule`. `PgBoss` tables have a `name` column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by `PgBoss`. + +#### Known issues + +- **Renaming scheduled jobs** + + The job name/identifier in your `.wasp` file is the same name that will be used in the `name` column of `pgboss` tables. If you change a name that had a `schedule` associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the `pgboss.schedule` table. + + For example, if you renamed a job from `emailReminder` to `sendEmailReminder`, you would need to remove the old scheduled job with the following SQL query: + + ```sql + BEGIN; + DELETE FROM pgboss.schedule WHERE name = 'emailReminder'; + COMMIT; + ``` + + **Important:** Only modify the database directly if you're comfortable with SQL operations. If you're unsure, consider keeping the old job name or restarting with a fresh database in development. + +#### Job data retention and cleanup + +By default, `PgBoss` keeps job data for 12 hours after completion or failure. After that, it moves the data to an archive table, where it is kept for 7 days before being deleted. If you want to change this behavior, you can configure the `PG_BOSS_NEW_OPTIONS` environment variable to set custom values for job archival ([`archivedCompletedAfterSeconds`/`archiveFailedAfterSeconds`](https://github.com/timgit/pg-boss/tree/8.4.2/docs#newoptions:~:text=v1%22%20or%20%22v4%22-,archiveCompletedAfterSeconds,-Specifies%20how%20long)) and removal ([`deleteAfterSeconds`/`deleteAfterMinutes`/etc](https://github.com/timgit/pg-boss/tree/8.4.2/docs#newoptions:~:text=the%20skew%20warnings.-,Archive%20options,-When%20jobs%20in)). + +```bash +PG_BOSS_NEW_OPTIONS={"connectionString":"...your postgress connection url...","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5} +``` + +## API Reference + +### Declaring Jobs + + + + ```wasp title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar", + executorOptions: { + pgBoss: {=json { "retryLimit": 1 } json=} + } + }, + schedule: { + cron: "*/5 * * * *", + args: {=json { "foo": "bar" } json=}, + executorOptions: { + pgBoss: {=json { "retryLimit": 0 } json=} + } + }, + entities: [Task], + } + ``` + + + + ```wasp title="main.wasp" + job mySpecialJob { + executor: PgBoss, + perform: { + fn: import { foo } from "@src/workers/bar", + executorOptions: { + pgBoss: {=json { "retryLimit": 1 } json=} + } + }, + schedule: { + cron: "*/5 * * * *", + args: {=json { "foo": "bar" } json=}, + executorOptions: { + pgBoss: {=json { "retryLimit": 0 } json=} + } + }, + entities: [Task], + } + ``` + + + +The Job declaration has the following fields: + +- `executor: JobExecutor` + + The job executor to use for this job. Currently, the only supported executor is [`PgBoss`](#pgboss). + +- `perform: dict` + + - `fn: ExtImport` + + - An `async` function that performs the work. Since Wasp executes Jobs on the server, the import path must lead to a NodeJS file. + - It receives the following arguments: + - `args: Input`: The data passed to the job when it's submitted. + - `context: { entities: Entities }`: The context object containing any declared entities. + + Here's an example of a `perform.fn` function: + + + + ```js title="src/workers/bar.js" + export const foo = async ({ name }, context) => { + console.log(`Hello ${name}!`) + const tasks = await context.entities.Task.findMany({}) + return { tasks } + } + ``` + + + + ```ts title="src/workers/bar.ts" + import { type MySpecialJob } from 'wasp/server/jobs' + + type Input = { name: string; } + type Output = { tasks: Task[]; } + + export const foo: MySpecialJob = async ({ name }, context) => { + console.log(`Hello ${name}!`) + const tasks = await context.entities.Task.findMany({}) + return { tasks } + } + ``` + + Read more about type-safe jobs in the [Javascript API section](#javascript-api). + + + + - `executorOptions: dict` + + Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with `submit()` or in a `schedule`. + + - `pgBoss: JSON` + + See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options). + +- `schedule: dict` + + - `cron: string` + + A 5-placeholder format cron expression string. See rationale for minute-level precision [here](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#scheduling). + + _If you need help building cron expressions, Check out_ [Crontab guru](https://crontab.guru/#0_*_*_*_*). + + - `args: JSON` + + The arguments to pass to the `perform.fn` function when invoked. + + - `executorOptions: dict` + + Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The `perform.executorOptions` are the default options, and `schedule.executorOptions` can override/extend those. + + - `pgBoss: JSON` + + See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options). + +- `entities: [Entity]` + + A list of entities you wish to use inside your Job (similar to [Queries and Actions](../data-model/operations/queries#using-entities-in-queries)). + +### JavaScript API + +- Importing a Job: + + + + ```js title="someAction.js" + import { mySpecialJob } from 'wasp/server/jobs' + ``` + + + + ```ts title="someAction.ts" + import { mySpecialJob, type MySpecialJob } from 'wasp/server/jobs' + ``` + + :::info Type-safe jobs + Wasp generates a generic type for each Job declaration, which you can use to type your `perform.fn` function. The type is named after the job declaration, and is available in the `wasp/server/jobs` module. In the example above, the type is `MySpecialJob`. + + The type takes two type arguments: + + - `Input`: The type of the `args` argument of the `perform.fn` function. + - `Output`: The type of the return value of the `perform.fn` function. + ::: + + + +- `submit(jobArgs, executorOptions)` + + - `jobArgs: Input` + - `executorOptions: object` + + Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options. + + + + ```js title="someAction.js" + const submittedJob = await mySpecialJob.submit({ job: "args" }) + ``` + + + + ```js title="someAction.ts" + const submittedJob = await mySpecialJob.submit({ job: "args" }) + ``` + + + +- `delay(startAfter)` + + - `startAfter: int | string | Date` + + Delaying the invocation of the job handler. The delay can be one of: + + - Integer: number of seconds to delay. \[Default 0] + - String: ISO date string to run at. + - Date: Date to run at. + + + + ```js title="someAction.js" + const submittedJob = await mySpecialJob + .delay(10) + .submit({ job: "args" }, { "retryLimit": 2 }) + ``` + + + + ```ts title="someAction.ts" + const submittedJob = await mySpecialJob + .delay(10) + .submit({ job: "args" }, { "retryLimit": 2 }) + ``` + + + +#### Tracking + +The return value of `submit()` is an instance of `SubmittedJob`, which has the following fields: + +- `jobId`: The ID for the job in that executor. +- `jobName`: The name of the job you used in your `.wasp` file. +- `executorName`: The Symbol of the name of the job executor. + +There are also some namespaced, job executor-specific objects. + +- For pg-boss, you may access: `pgBoss` + - `details()`: pg-boss specific job detail information. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#getjobbyidid) + - `cancel()`: attempts to cancel a job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#cancelid) + - `resume()`: attempts to resume a canceled job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#resumeid) diff --git a/web/versioned_docs/version-0.19.0/advanced/links.md b/web/versioned_docs/version-0.19.0/advanced/links.md new file mode 100644 index 0000000000..4919fb2e04 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/links.md @@ -0,0 +1,220 @@ +--- +title: Type-Safe Links +--- + +import { Required } from '@site/src/components/Tag' + +If you are using Typescript, you can use Wasp's custom `Link` component to create type-safe links to other pages on your site. + +## Using the `Link` Component + +After you defined a route: + +```wasp title="main.wasp" +route TaskRoute { path: "/task/:id", to: TaskPage } +page TaskPage { ... } +``` + +You can get the benefits of type-safe links by using the `Link` component from `wasp/client/router`: + +```jsx title="TaskList.tsx" +import { Link } from 'wasp/client/router' + +export const TaskList = () => { + // ... + + return ( +
+ {tasks.map((task) => ( + + {/* 👆 All the params must be correctly passed in */} + {task.description} + + ))} +
+ ) +} +``` + +### Catch-all Routes + +If a route path ends with a `/*` pattern (also known as [splat](https://reactrouter.com/en/main/route/route#splats)), you can use the `Link` component like this: + +```wasp title="main.wasp" +route CatchAllRoute { path: "/pages/*", to: CatchAllPage } +page CatchAllPage { ... } +``` + +```jsx title="TaskList.tsx" + + About + +``` + +This will result in a link like this: `/pages/about`. + +### Optional Static Segments + +If a route contains optional static segments, you'll need to specify one of the possible paths: + +```wasp title="main.wasp" +route OptionalRoute { path: "/task/:id/details?", to: OptionalPage } +page OptionalPage { ... } +``` + +```jsx title="TaskList.tsx" +/* You can include ... */ + + Task 1 + + +/* ... or exclude the optional segment */ + + Task 1 + +``` + +### Using Search Query & Hash + +You can also pass `search` and `hash` props to the `Link` component: + +```tsx title="TaskList.tsx" + + {task.description} + +``` + +This will result in a link like this: `/task/1?sortBy=date#comments`. Check out the [API Reference](#link-component) for more details. + +## The `routes` Object + +You can also get all the pages in your app with the `routes` object: + +```jsx title="TaskList.tsx" +import { routes } from 'wasp/client/router' + +const linkToTask = routes.TaskRoute.build({ params: { id: 1 } }) +``` + +This will result in a link like this: `/task/1`. + +### Optional Static Segments + +If a route contains optional static segments, you'll need to specify one of the possible paths: + +```wasp title="main.wasp" +route OptionalRoute { path: "/task/:id/details?", to: OptionalPage } +page OptionalPage { ... } +``` + +```tsx title="TaskList.tsx" +const linkToOptional = routes.OptionalRoute.build({ + path: '/task/:id/details', // or '/task/:id' + params: { id: 1 }, +}) +``` + +You can also pass `search` and `hash` props to the `build` function. Check out the [API Reference](#routes-object) for more details. + +## API Reference + +### `Link` Component + +The `Link` component accepts the following props: + +- `to` + + - A valid Wasp Route path from your `main.wasp` file. + + In the case of optional static segments, you must provide one of the possible paths which include or exclude the optional segment. For example, if the path is `/task/:id/details?`, you must provide either `/task/:id/details` or `/task/:id`. + +- `params: { [name: string]: string | number }` (if the path contains params) + + - An object with keys and values for each param in the path. + - For example, if the path is `/task/:id`, then the `params` prop must be `{ id: 1 }`. Wasp supports required and optional params. + +- `search: string[][] | Record | string | URLSearchParams` + + - Any valid input for `URLSearchParams` constructor. + - For example, the object `{ sortBy: 'date' }` becomes `?sortBy=date`. + +- `hash: string` + +- all other props that the `react-router-dom`'s [Link](https://reactrouter.com/en/6.26.1/components/link) component accepts + +### `routes` Object + +The `routes` object contains a function for each route in your app. + +```ts title="router.tsx" +export const routes = { + // RootRoute has a path like "/" + RootRoute: { + build: (options?: { + search?: string[][] | Record | string | URLSearchParams + hash?: string + }) => // ... + }, + + // DetailRoute has a path like "/task/:id/:userId?" + DetailRoute: { + build: ( + options: { + params: { id: ParamValue; userId?: ParamValue; }, + search?: string[][] | Record | string | URLSearchParams + hash?: string + } + ) => // ... + }, + + // OptionalRoute has a path like "/task/:id/details?" + OptionalRoute: { + build: ( + options: { + path: '/task/:id/details' | '/task/:id', + params: { id: ParamValue }, + search?: string[][] | Record | string | URLSearchParams + hash?: string + } + ) => // ... + }, + + // CatchAllRoute has a path like "/pages/*" + CatchAllRoute: { + build: ( + options: { + params: { '*': ParamValue }, + search?: string[][] | Record | string | URLSearchParams + hash?: string + } + ) => // ... + }, +} +``` + +The `params` object is required if the route contains params. The `search` and `hash` parameters are optional. + +You can use the `routes` object like this: + +```tsx +import { routes } from 'wasp/client/router' + +const linkToRoot = routes.RootRoute.build() +const linkToTask = routes.DetailRoute.build({ params: { id: 1 } }) +const linkToOptional = routes.DetailRoute.build({ + path: '/task/:id/details', + params: { id: 1 }, +}) +const linkToCatchAll = routes.CatchAllRoute.build({ + params: { '*': 'about' }, +}) +``` diff --git a/web/versioned_docs/version-0.19.0/advanced/middleware-config.md b/web/versioned_docs/version-0.19.0/advanced/middleware-config.md new file mode 100644 index 0000000000..85c54638ae --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/middleware-config.md @@ -0,0 +1,274 @@ +--- +title: Configuring Middleware +--- + +import { ShowForTs } from '@site/src/components/TsJsHelpers'; + +Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-`api`/path basis. + +## Default Global Middleware 🌍 + +Wasp's Express server has the following middleware by default: + +- [Helmet](https://helmetjs.github.io/): Helmet helps you secure your Express apps by setting various HTTP headers. _It's not a silver bullet, but it's a good start._ +- [CORS](https://github.com/expressjs/cors#readme): CORS is a package for providing a middleware that can be used to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options. + + :::note + CORS middleware is required for the frontend to communicate with the backend. + ::: +- [Morgan](https://github.com/expressjs/morgan#readme): HTTP request logger middleware. +- [express.json](https://expressjs.com/en/api.html#express.json) (which uses [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)): parses incoming request bodies in a middleware before your handlers, making the result available under the `req.body` property. + + :::note + JSON middleware is required for [Operations](../data-model/operations/overview) to function properly. + ::: +- [express.urlencoded](https://expressjs.com/en/api.html#express.urlencoded) (which uses [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions)): returns middleware that only parses urlencoded bodies and only looks at requests where the `Content-Type` header matches the type option. +- [cookieParser](https://github.com/expressjs/cookie-parser#readme): parses Cookie header and populates `req.cookies` with an object keyed by the cookie names. + +## Customization + +You have three places where you can customize middleware: + +1. [global](#1-customize-global-middleware): here, any changes will apply by default _to all operations (`query` and `action`) and `api`._ This is helpful if you wanted to add support for multiple domains to CORS, for example. + +:::caution Modifying global middleware +Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options. +::: + +2. [per-api](#2-customize-api-specific-middleware): you can override middleware for a specific api route (e.g. `POST /webhook/callback`). This is helpful if you want to disable JSON parsing for some callback, for example. +3. [per-path](#3-customize-per-path-middleware): this is helpful if you need to customize middleware for all methods under a given path. + - It's helpful for things like "complex CORS requests" which may need to apply to both `OPTIONS` and `GET`, or to apply some middleware to a _set of `api` routes_. + +### Default Middleware Definitions + +Below is the actual definitions of default middleware which you can override. + + + + ```js + const defaultGlobalMiddleware = new Map([ + ['helmet', helmet()], + ['cors', cors({ origin: config.allowedCORSOrigins })], + ['logger', logger('dev')], + ['express.json', express.json()], + ['express.urlencoded', express.urlencoded()], + ['cookieParser', cookieParser()] + ]) + ``` + + + + ```ts + export type MiddlewareConfig = Map + + // Used in the examples below 👇 + export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig + + const defaultGlobalMiddleware: MiddlewareConfig = new Map([ + ['helmet', helmet()], + ['cors', cors({ origin: config.allowedCORSOrigins })], + ['logger', logger('dev')], + ['express.json', express.json()], + ['express.urlencoded', express.urlencoded()], + ['cookieParser', cookieParser()] + ]) + ``` + + + +## 1. Customize Global Middleware + +If you would like to modify the middleware for _all_ operations and APIs, you can do something like: + + + + ```wasp {6} title="main.wasp" + app todoApp { + // ... + + server: { + middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup" + }, + } + ``` + + ```ts title="src/serverSetup.js" + import cors from 'cors' + import { config } from 'wasp/server' + + export const serverMiddlewareFn = (middlewareConfig) => { + // Example of adding extra domains to CORS. + middlewareConfig.set('cors', cors({ origin: [...config.allowedCORSOrigins, 'https://example1.com', 'https://example2.com'] })) + return middlewareConfig + } + ``` + + + + ```wasp {6} title="main.wasp" + app todoApp { + // ... + + server: { + middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup" + }, + } + ``` + + ```ts title="src/serverSetup.ts" + import cors from 'cors' + import { config, type MiddlewareConfigFn } from 'wasp/server' + + export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { + // Example of adding extra domains to CORS. + middlewareConfig.set('cors', cors({ origin: [...config.allowedCORSOrigins, 'https://example1.com', 'https://example2.com'] })) + return middlewareConfig + } + ``` + + + +## 2. Customize `api`-specific Middleware + +If you would like to modify the middleware for a single API, you can do something like: + + + + ```wasp {5} title="main.wasp" + // ... + + api webhookCallback { + fn: import { webhookCallback } from "@src/apis", + middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis", + httpRoute: (POST, "/webhook/callback"), + auth: false + } + ``` + + ```ts title="src/apis.js" + import express from 'express' + + export const webhookCallback = (req, res, _context) => { + res.json({ msg: req.body.length }) + } + + export const webhookCallbackMiddlewareFn = (middlewareConfig) => { + console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw') + + middlewareConfig.delete('express.json') + middlewareConfig.set('express.raw', express.raw({ type: '*/*' })) + + return middlewareConfig + } + + ``` + + + + ```wasp {5} title="main.wasp" + // ... + + api webhookCallback { + fn: import { webhookCallback } from "@src/apis", + middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@src/apis", + httpRoute: (POST, "/webhook/callback"), + auth: false + } + ``` + + ```ts title="src/apis.ts" + import express from 'express' + import { type WebhookCallback } from 'wasp/server/api' + import { type MiddlewareConfigFn } from 'wasp/server' + + export const webhookCallback: WebhookCallback = (req, res, _context) => { + res.json({ msg: req.body.length }) + } + + export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { + console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw') + + middlewareConfig.delete('express.json') + middlewareConfig.set('express.raw', express.raw({ type: '*/*' })) + + return middlewareConfig + } + + ``` + + + +:::note +This gets installed on a per-method basis. Behind the scenes, this results in code like: + +```js +router.post('/webhook/callback', webhookCallbackMiddleware, ...) +``` + +::: + +## 3. Customize Per-Path Middleware + +If you would like to modify the middleware for all API routes under some common path, you can define a `middlewareConfigFn` on an `apiNamespace`: + + + + ```wasp {4} title="main.wasp" + // ... + + apiNamespace fooBar { + middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis", + path: "/foo/bar" + } + ``` + + ```ts title="src/apis.js" + export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => { + const customMiddleware = (_req, _res, next) => { + console.log('fooBarNamespaceMiddlewareFn: custom middleware') + next() + } + + middlewareConfig.set('custom.middleware', customMiddleware) + + return middlewareConfig + } + ``` + + + + ```wasp {4} title="main.wasp" + // ... + + apiNamespace fooBar { + middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@src/apis", + path: "/foo/bar" + } + ``` + + ```ts title="src/apis.ts" + import express from 'express' + import { type MiddlewareConfigFn } from 'wasp/server' + + export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { + const customMiddleware: express.RequestHandler = (_req, _res, next) => { + console.log('fooBarNamespaceMiddlewareFn: custom middleware') + next() + } + + middlewareConfig.set('custom.middleware', customMiddleware) + + return middlewareConfig + } + ``` + + + +:::note +This gets installed at the router level for the path. Behind the scenes, this results in something like: + +```js +router.use('/foo/bar', fooBarNamespaceMiddleware) +``` + +::: diff --git a/web/versioned_docs/version-0.19.0/advanced/web-sockets.md b/web/versioned_docs/version-0.19.0/advanced/web-sockets.md new file mode 100644 index 0000000000..7c51e2463a --- /dev/null +++ b/web/versioned_docs/version-0.19.0/advanced/web-sockets.md @@ -0,0 +1,334 @@ +--- +title: Web Sockets +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { ShowForTs } from '@site/src/components/TsJsHelpers'; +import { Required } from '@site/src/components/Tag'; + +Wasp provides a fully integrated WebSocket experience by utilizing [Socket.IO](https://socket.io/) on the client and server. + +We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful `useSocket` and `useSocketListener` abstractions for use in React components. + +To get started, you need to: + +1. Define your WebSocket logic on the server. +2. Enable WebSockets in your Wasp file, and connect it with your server logic. +3. Use WebSockets on the client, in React, via `useSocket` and `useSocketListener`. +4. Optionally, type the WebSocket events and payloads for full-stack type safety. + +Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file. + +## Turn On WebSockets in Your Wasp File + +We specify that we are using WebSockets by adding `webSocket` to our `app` and providing the required `fn`. You can optionally change the auto-connect behavior. + + + + ```wasp title="todoApp.wasp" + app todoApp { + // ... + + webSocket: { + fn: import { webSocketFn } from "@src/webSocket", + autoConnect: true, // optional, default: true + }, + } + ``` + + + + ```wasp title="todoApp.wasp" + app todoApp { + // ... + + webSocket: { + fn: import { webSocketFn } from "@src/webSocket", + autoConnect: true, // optional, default: true + }, + } + ``` + + + +## Defining the Events Handler + +Let's define the WebSockets server with all of the events and handler functions. + + + :::info Full-stack type safety + Check this out: we'll define the event types and payloads on the server, and they will be **automatically exposed on the client**. This helps you avoid mistakes when emitting events or handling them. + ::: + + +### `webSocketFn` Function + +On the server, you will get Socket.IO `io: Server` argument and `context` for your WebSocket function. The `context` object give you access to all of the entities from your Wasp app. + +You can use this `io` object to register callbacks for all the regular [Socket.IO events](https://socket.io/docs/v4/server-api/). Also, if a user is logged in, you will have a `socket.data.user` on the server. + +This is how we can define our `webSocketFn` function: + + + + ```ts title="src/webSocket.js" + import { v4 as uuidv4 } from 'uuid' + + export const webSocketFn = (io, context) => { + io.on('connection', (socket) => { + const username = socket.data.user?.getFirstProviderUserId() ?? 'Unknown' + console.log('a user connected: ', username) + + socket.on('chatMessage', async (msg) => { + console.log('message: ', msg) + io.emit('chatMessage', { id: uuidv4(), username, text: msg }) + // You can also use your entities here: + // await context.entities.SomeEntity.create({ someField: msg }) + }) + }) + } + ``` + + + + ```ts title="src/webSocket.ts" + import { v4 as uuidv4 } from 'uuid' + import { type WebSocketDefinition, type WaspSocketData } from 'wasp/server/webSocket' + + export const webSocketFn: WebSocketFn = (io, context) => { + io.on('connection', (socket) => { + const username = socket.data.user?.getFirstProviderUserId() ?? 'Unknown' + console.log('a user connected: ', username) + + socket.on('chatMessage', async (msg) => { + console.log('message: ', msg) + io.emit('chatMessage', { id: uuidv4(), username, text: msg }) + // You can also use your entities here: + // await context.entities.SomeEntity.create({ someField: msg }) + }) + }) + } + + // Typing our WebSocket function with the events and payloads + // allows us to get type safety on the client as well + + type WebSocketFn = WebSocketDefinition< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData + > + + interface ServerToClientEvents { + chatMessage: (msg: { id: string, username: string, text: string }) => void; + } + + interface ClientToServerEvents { + chatMessage: (msg: string) => void; + } + + interface InterServerEvents {} + + // Data that is attached to the socket. + // NOTE: Wasp automatically injects the JWT into the connection, + // and if present/valid, the server adds a user to the socket. + interface SocketData extends WaspSocketData {} + ``` + + + +## Using the WebSocket On The Client + + + :::info Full-stack type safety + All the hooks we use are typed with the events and payloads you defined on the server. VS Code will give you autocomplete for the events and payloads, and you will get type errors if you make a mistake. + ::: + + +### The `useSocket` Hook + +Client access to WebSockets is provided by the `useSocket` hook. It returns: + +- `socket: Socket` for sending and receiving events. +- `isConnected: boolean` for showing a display of the Socket.IO connection status. + - Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly `socket.connect()` or `socket.disconnect()`. + - If you set `autoConnect: false` in your Wasp file, then you should call these as needed. + +All components using `useSocket` share the same underlying `socket`. + +### The `useSocketListener` Hook + +Additionally, there is a `useSocketListener: (event, callback) => void` hook which is used for registering event handlers. It takes care of unregistering the handler on unmount. + + + + ```tsx title="src/ChatPage.jsx" + import React, { useState } from 'react' + import { + useSocket, + useSocketListener, + } from 'wasp/client/webSocket' + + export const ChatPage = () => { + const [messageText, setMessageText] = useState('') + const [messages, setMessages] = useState([]) + const { socket, isConnected } = useSocket() + + useSocketListener('chatMessage', logMessage) + + function logMessage(msg) { + setMessages((priorMessages) => [msg, ...priorMessages]) + } + + function handleSubmit(e) { + e.preventDefault() + socket.emit('chatMessage', messageText) + setMessageText('') + } + + const messageList = messages.map((msg) => ( +
  • + {msg.username}: {msg.text} +
  • + )) + const connectionIcon = isConnected ? '🟢' : '🔴' + + return ( + <> +

    Chat {connectionIcon}

    +
    +
    +
    +
    + setMessageText(e.target.value)} + /> +
    +
    + +
    +
    +
    +
      {messageList}
    +
    + + ) + } + ``` +
    + + + Wasp's **full-stack type safety** kicks in here: all the event types and payloads are automatically inferred from the server and are available on the client. + + You can additionally use the `ClientToServerPayload` and `ServerToClientPayload` helper types to get the payload type for a specific event. + + ```tsx title="src/ChatPage.tsx" + import React, { useState } from 'react' + import { + useSocket, + useSocketListener, + ServerToClientPayload, + } from 'wasp/client/webSocket' + + export const ChatPage = () => { + const [messageText, setMessageText] = useState< + // We are using a helper type to get the payload type for the "chatMessage" event. + ClientToServerPayload<'chatMessage'> + >('') + const [messages, setMessages] = useState< + ServerToClientPayload<'chatMessage'>[] + >([]) + // The "socket" instance is typed with the types you defined on the server. + const { socket, isConnected } = useSocket() + + // This is a type-safe event handler: "chatMessage" event and its payload type + // are defined on the server. + useSocketListener('chatMessage', logMessage) + + function logMessage(msg: ServerToClientPayload<'chatMessage'>) { + setMessages((priorMessages) => [msg, ...priorMessages]) + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + // This is a type-safe event emitter: "chatMessage" event and its payload type + // are defined on the server. + socket.emit('chatMessage', messageText) + setMessageText('') + } + + const messageList = messages.map((msg) => ( +
  • + {msg.username}: {msg.text} +
  • + )) + const connectionIcon = isConnected ? '🟢' : '🔴' + + return ( + <> +

    Chat {connectionIcon}

    +
    +
    +
    +
    + setMessageText(e.target.value)} + /> +
    +
    + +
    +
    +
    +
      {messageList}
    +
    + + ) + } + ``` +
    +
    + +## API Reference + + + + ```wasp title="todoApp.wasp" + app todoApp { + // ... + + webSocket: { + fn: import { webSocketFn } from "@src/webSocket", + autoConnect: true, // optional, default: true + }, + } + ``` + + + + ```wasp title="todoApp.wasp" + app todoApp { + // ... + + webSocket: { + fn: import { webSocketFn } from "@src/webSocket", + autoConnect: true, // optional, default: true + }, + } + ``` + + + +The `webSocket` dict has the following fields: + +- `fn: WebSocketFn` + + The function that defines the WebSocket events and handlers. + +- `autoConnect: bool` + + Whether to automatically connect to the WebSocket server. Default: `true`. diff --git a/web/versioned_docs/version-0.19.0/auth/AuthMethodsGrid.tsx b/web/versioned_docs/version-0.19.0/auth/AuthMethodsGrid.tsx new file mode 100644 index 0000000000..0436d5fb6d --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/AuthMethodsGrid.tsx @@ -0,0 +1,48 @@ +import { LinkGrid } from "@site/src/components/LinkGrid"; + +const authMethods = [ + { + title: "Email", + description: "Email verification, password reset, etc.", + linkTo: "./email", + }, + { + title: "Username & Password", + description: "The simplest way to get started", + linkTo: "./username-and-pass", + }, + { + title: "Google", + description: "Users sign in with their Google account", + linkTo: "./social-auth/google", + }, + { + title: "Github", + description: "Users sign in with their Github account", + linkTo: "./social-auth/github", + }, + { + title: "Keycloak", + description: "Users sign in with their Keycloak account", + linkTo: "./social-auth/keycloak", + }, + { + title: "Slack", + description: "Users sign in with their Slack account", + linkTo: "./social-auth/slack", + }, + { + title: "Discord", + description: "Users sign in with their Discord account", + linkTo: "./social-auth/discord", + }, +]; + +export function AuthMethodsGrid() { + return ( + + ); +} diff --git a/web/versioned_docs/version-0.19.0/auth/Pills.css b/web/versioned_docs/version-0.19.0/auth/Pills.css new file mode 100644 index 0000000000..50fe42528f --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/Pills.css @@ -0,0 +1,21 @@ +:root { + --auth-pills-color: #333; + --auth-pills-email: #e0f2fe; + --auth-pills-slack: #8fb57d; + --auth-pills-discord: #d3d6f2; + --auth-pills-github: #f1f5f9; + --auth-pills-google: #ecfccb; + --auth-pills-keycloak: #d0ebf5; + --auth-pills-username-and-pass: #fce7f3; +} + +:root[data-theme="dark"] { + --auth-pills-color: #fff; + --auth-pills-email: #0c4a6e; + --auth-pills-slack: #2f7041; + --auth-pills-discord: #2f3670; + --auth-pills-github: #334155; + --auth-pills-google: #365314; + --auth-pills-keycloak: #2d5866; + --auth-pills-username-and-pass: #831843; +} diff --git a/web/versioned_docs/version-0.19.0/auth/Pills.jsx b/web/versioned_docs/version-0.19.0/auth/Pills.jsx new file mode 100644 index 0000000000..8112a1acc5 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/Pills.jsx @@ -0,0 +1,111 @@ +import Link from "@docusaurus/Link"; +import "./Pills.css"; + +export function Pill({ children, linkToPage, style = {} }) { + return ( + + {children} + + ); +} + +export function EmailPill() { + return ( + + Email + + ); +} + +export function UsernameAndPasswordPill() { + return ( + + Username & Password + + ); +} + +export function SlackPill() { + return ( + + Slack + + ); +} + +export function DiscordPill() { + return ( + + Discord + + ); +} + +export function GithubPill() { + return ( + + Github + + ); +} + +export function GooglePill() { + return ( + + Google + + ); +} + +export function KeycloakPill() { + return ( + + Keycloak + + ); +} diff --git a/web/versioned_docs/version-0.19.0/auth/_accessing-user-data-note.md b/web/versioned_docs/version-0.19.0/auth/_accessing-user-data-note.md new file mode 100644 index 0000000000..8cf64ce9c0 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/_accessing-user-data-note.md @@ -0,0 +1,5 @@ + + Read more about accessing the user data in the [Accessing User Data](/auth/entities/entities.md#accessing-the-auth-fields) section of the docs. + + + diff --git a/web/versioned_docs/version-0.19.0/auth/_multiple-identities-warning.md b/web/versioned_docs/version-0.19.0/auth/_multiple-identities-warning.md new file mode 100644 index 0000000000..ea8f5d8251 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/_multiple-identities-warning.md @@ -0,0 +1,6 @@ +:::caution Using multiple auth identities for a single user + +Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the [account merging feature](https://github.com/wasp-lang/wasp/issues/954). + +Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account. +::: diff --git a/web/versioned_docs/version-0.19.0/auth/_read-more-about-auth-entities.md b/web/versioned_docs/version-0.19.0/auth/_read-more-about-auth-entities.md new file mode 100644 index 0000000000..5cb6fd93e9 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/_read-more-about-auth-entities.md @@ -0,0 +1,3 @@ + + You can read more about how the `User` is connected to the rest of the auth system and how you can access the user data in the [Accessing User Data](./entities) section of the docs. + diff --git a/web/versioned_docs/version-0.19.0/auth/_user-fields.md b/web/versioned_docs/version-0.19.0/auth/_user-fields.md new file mode 100644 index 0000000000..d0bd5b464c --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/_user-fields.md @@ -0,0 +1,9 @@ +import { Required } from '@site/src/components/Tag'; + +The user entity needs to have the following fields: + +- `id` + + It can be of any type, but it needs to be marked with `@id` + +You can add any other fields you want to the user entity. Make sure to also define them in the `userSignupFields` field if they need to be set during the sign-up process. diff --git a/web/versioned_docs/version-0.19.0/auth/_user-signup-fields-explainer.md b/web/versioned_docs/version-0.19.0/auth/_user-signup-fields-explainer.md new file mode 100644 index 0000000000..288e8e55ee --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/_user-signup-fields-explainer.md @@ -0,0 +1,15 @@ +`userSignupFields` defines all the extra fields that need to be set on the `User` during the sign-up process. For example, if you have `address` and `phone` fields on your `User` entity, you can set them by defining the `userSignupFields` like this: + +```ts title="src/auth.ts" auto-js +import { defineUserSignupFields } from 'wasp/server/auth' + +export const userSignupFields = defineUserSignupFields({ + address: (data) => { + if (!data.address) { + throw new Error('Address is required') + } + return data.address + }, + phone: (data) => data.phone, +}) +``` diff --git a/web/versioned_docs/version-0.19.0/auth/advanced/custom-auth-actions.md b/web/versioned_docs/version-0.19.0/auth/advanced/custom-auth-actions.md new file mode 100644 index 0000000000..8002a3a6a9 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/advanced/custom-auth-actions.md @@ -0,0 +1,261 @@ +# Custom sign-up actions + +If you need to deeply hook into the sign-up process, you can create your own sign-up action and customize the code to, for example, add extra validation, store more data, or otherwise call custom code at registration time. + +:::danger + +Custom sign-up actions are complex, and we don't recommend creating a custom sign-up action unless you have a good reason to do so. +They also require you to be careful, as any small mistake will compromise the security of your app. + +Before using custom actions, check if our support for [custom auth UI](../overview.md#custom-auth-ui) and for [auth hooks](../auth-hooks.md) could fit well with you requirements. + +::: + +You are not able to use Wasp UI with custom sign-up actions, so you're expected to implemented your own UI and call the custom actions you create from it. + +## Example code + +Below you will find a starting point for creating your own actions. The given implementation is similar to what Wasp does under the hood, and it is up to you to customize it. + +### Email + +```wasp title="main.wasp" +app myApp { + // ... + auth: { + // ... + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + }, +} + +// ... + +action customSignup { + fn: import { signup } from "@src/auth/signup", +} +``` + +```ts title="src/auth/hooks.ts" auto-js +import { HttpError } from 'wasp/server' + +// This disables Wasp's default sign-up action +export const onBeforeSignup = async () => { + throw new HttpError(403, 'This sign-up method is disabled') +} +``` + +```ts title="src/auth/signup.ts" auto-js +import type { CustomSignup } from "wasp/server/operations"; +import { HttpError } from "wasp/server"; +import { + createEmailVerificationLink, + createProviderId, + createUser, + ensurePasswordIsPresent, + ensureValidEmail, + ensureValidPassword, + findAuthIdentity, + getProviderData, + sanitizeAndSerializeProviderData, + sendEmailVerificationEmail, +} from "wasp/server/auth"; + +type CustomSignupInput = { + email: string; + password: string; +}; + +type CustomSignupOutput = { + success: boolean; + message: string; +}; + +export const signup: CustomSignup< + CustomSignupInput, + CustomSignupOutput +> = async (args, _context) => { + ensureValidEmail(args); + ensurePasswordIsPresent(args); + ensureValidPassword(args); + + try { + const providerId = createProviderId("email", args.email); + const existingAuthIdentity = await findAuthIdentity(providerId); + + let providerData; + + if (existingAuthIdentity) { + // User already exists, handle accordingly + + // For example, throw an error or return a message + throw new HttpError(400, "Email already exists."); + + // Or, another example, you can check if the user is already + // verified and re-send the verification email if not + providerData = getProviderData<"email">( + existingAuthIdentity.providerData, + ); + if (providerData.isEmailVerified) + throw new HttpError(400, "Email already verified."); + } + + if (!providerData) { + providerData = await sanitizeAndSerializeProviderData<"email">({ + // The provider will hash the password for us, so we don't need to do it here. + hashedPassword: args.password, + isEmailVerified: false, + emailVerificationSentAt: null, + passwordResetSentAt: null, + }); + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {}, + ); + } + + // Verification link links to a client route e.g. /email-verification + const verificationLink = await createEmailVerificationLink( + args.email, + "/email-verification", + ); + try { + await sendEmailVerificationEmail(args.email, { + from: { + name: "My App Postman", + email: "hello@itsme.com", + }, + to: args.email, + subject: "Verify your email", + text: `Click the link below to verify your email: ${verificationLink}`, + html: ` +

    Click the link below to verify your email

    + Verify email + `, + }); + } catch (e: unknown) { + console.error("Failed to send email verification email:", e); + throw new HttpError(500, "Failed to send email verification email."); + } + } catch (e: any) { + return { + success: false, + message: e.message, + }; + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: "User created successfully", + }; +}; +``` + +### Username and password + +```wasp title="main.wasp" +app myApp { + // ... + auth: { + // ... + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + }, +} + +// ... + +action customSignup { + fn: import { signup } from "@src/auth/signup", +} +``` + +```ts title="src/auth/hooks.ts" auto-js +import { HttpError } from 'wasp/server' + +// This disables Wasp's default sign-up action +export const onBeforeSignup = async () => { + throw new HttpError(403, 'This sign-up method is disabled') +} +``` + +```ts title="src/auth/signup.ts" auto-js +import type { CustomSignup } from "wasp/server/operations"; +import { + createProviderId, + createUser, + ensurePasswordIsPresent, + ensureValidPassword, + ensureValidUsername, + sanitizeAndSerializeProviderData, +} from "wasp/server/auth"; + +type CustomSignupInput = { + username: string; + password: string; +}; + +type CustomSignupOutput = { + success: boolean; + message: string; +}; + +export const signup: CustomSignup< + CustomSignupInput, + CustomSignupOutput +> = async (args, _context) => { + ensureValidUsername(args); + ensurePasswordIsPresent(args); + ensureValidPassword(args); + + try { + const providerId = createProviderId("username", args.username); + const providerData = await sanitizeAndSerializeProviderData<"username">({ + // The provider will hash the password for us, so we don't need to do it here. + hashedPassword: args.password, + }); + + await createUser(providerId, providerData, {}); + } catch (e: any) { + console.error("Error creating user:", e); + return { + success: false, + message: e.message, + }; + } + + return { + success: true, + message: "User created successfully", + }; +}; +``` + +## Validators API Reference + +We suggest using the built-in field validators for your authentication flow. You can import them from `wasp/server/auth`. These are the same validators that Wasp uses internally for the default authentication flow. + +#### Username + +- `ensureValidUsername(args)` + + Checks if the username is valid and throws an error if it's not. Read more about the validation rules [here](../overview.md#default-validations). + +#### Email + +- `ensureValidEmail(args)` + + Checks if the email is valid and throws an error if it's not. Read more about the validation rules [here](../overview.md#default-validations). + +#### Password + +- `ensurePasswordIsPresent(args)` + + Checks if the password is present and throws an error if it's not. + +- `ensureValidPassword(args)` + + Checks if the password is valid and throws an error if it's not. Read more about the validation rules [here](../overview.md#default-validations). diff --git a/web/versioned_docs/version-0.19.0/auth/auth-hooks.md b/web/versioned_docs/version-0.19.0/auth/auth-hooks.md new file mode 100644 index 0000000000..40f689eefa --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/auth-hooks.md @@ -0,0 +1,1002 @@ +--- +title: Auth Hooks +--- + +import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill, KeycloakPill, SlackPill, DiscordPill } from "./Pills"; +import { ImgWithCaption } from '@site/blog/components/ImgWithCaption' +import { ShowForTs } from '@site/src/components/TsJsHelpers' + +Auth hooks allow you to "hook into" the auth process at various stages and run your custom code. For example, if you want to forbid certain emails from signing up, or if you wish to send a welcome email to the user after they sign up, auth hooks are the way to go. + +## Supported hooks + +The following auth hooks are available in Wasp: + +- [`onBeforeSignup`](#executing-code-before-the-user-signs-up) +- [`onAfterSignup`](#executing-code-after-the-user-signs-up) +- [`onAfterEmailVerified`](#executing-code-after-a-user-verifies-their-email) +- [`onBeforeOAuthRedirect`](#executing-code-before-the-oauth-redirect) +- [`onBeforeLogin`](#executing-code-before-the-user-logs-in) +- [`onAfterLogin`](#executing-code-after-the-user-logs-in) + +We'll go through each of these hooks in detail. But first, let's see how the hooks fit into the auth flows: + + + + + + + \* When using the OAuth auth providers, the login hooks are both called before the session is created but the session is created quickly afterward, so it shouldn't make any difference in practice. + + +Users registering with [email](./email.md) must verify it before they can log in. This verification triggers the Email verification flow: + + + +Users signing in with [OAuth](./social-auth/overview.md) must authorize access before completing login. This authorization triggers the OAuth consent flow: + + + +## Using hooks + +To use auth hooks, you must first declare them in the Wasp file: + + + + ```wasp + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + auth: { + userEntity: User, + methods: { + ... + }, + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + + + ```wasp + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + auth: { + userEntity: User, + methods: { + ... + }, + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + + +If the hooks are defined as async functions, Wasp _awaits_ them. This means the auth process waits for the hooks to finish before continuing. + +Wasp ignores the hooks' return values. The only exception is the `onBeforeOAuthRedirect` hook, whose return value affects the OAuth redirect URL. + +We'll now go through each of the available hooks. + +### Executing code before the user signs up + +Wasp calls the `onBeforeSignup` hook before the user is created. + +The `onBeforeSignup` hook can be useful if you want to reject a user based on some criteria before they sign up. + +Works with + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + }, + } + ``` + + ```js title="src/auth/hooks.js" + import { HttpError } from 'wasp/server' + + export const onBeforeSignup = async ({ providerId, prisma, req }) => { + const count = await prisma.user.count() + console.log('number of users before', count) + console.log('provider name', providerId.providerName) + console.log('provider user ID', providerId.providerUserId) + + if (count > 100) { + throw new HttpError(403, 'Too many users') + } + + if ( + providerId.providerName === 'email' && + providerId.providerUserId === 'some@email.com' + ) { + throw new HttpError(403, 'This email is not allowed') + } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + }, + } + ``` + + ```ts title="src/auth/hooks.ts" + import { HttpError } from 'wasp/server' + import type { OnBeforeSignupHook } from 'wasp/server/auth' + + export const onBeforeSignup: OnBeforeSignupHook = async ({ + providerId, + prisma, + req, + }) => { + const count = await prisma.user.count() + console.log('number of users before', count) + console.log('provider name', providerId.providerName) + console.log('provider user ID', providerId.providerUserId) + + if (count > 100) { + throw new HttpError(403, 'Too many users') + } + + if ( + providerId.providerName === 'email' && + providerId.providerUserId === 'some@email.com' + ) { + throw new HttpError(403, 'This email is not allowed') + } + } + ``` + + + +Read more about the data the `onBeforeSignup` hook receives in the [API Reference](#the-onbeforesignup-hook). + +### Executing code after the user signs up + +Wasp calls the `onAfterSignup` hook after the user is created. + +The `onAfterSignup` hook can be useful if you want to send the user a welcome email or perform some other action after the user signs up like syncing the user with a third-party service. + +Since the `onAfterSignup` hook receives the OAuth tokens, you can use this hook to store the OAuth access token and/or [refresh token](#refreshing-the-oauth-access-token) in your database. + +Works with + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + }, + } + ``` + + ```js title="src/auth/hooks.js" + export const onAfterSignup = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + const count = await prisma.user.count() + console.log('number of users after', count) + console.log('user object', user) + + // If this is an OAuth signup, you have access to the OAuth tokens and the uniqueRequestId + if (oauth) { + console.log('accessToken', oauth.tokens.accessToken) + console.log('uniqueRequestId', oauth.uniqueRequestId) + + const id = oauth.uniqueRequestId + const data = someKindOfStore.get(id) + if (data) { + console.log('saved data for the ID', data) + } + someKindOfStore.delete(id) + } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + }, + } + ``` + + ```ts title="src/auth/hooks.ts" + import type { OnAfterSignupHook } from 'wasp/server/auth' + + export const onAfterSignup: OnAfterSignupHook = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + const count = await prisma.user.count() + console.log('number of users after', count) + console.log('user object', user) + + // If this is an OAuth signup, you have access to the OAuth tokens and the uniqueRequestId + if (oauth) { + console.log('accessToken', oauth.tokens.accessToken) + console.log('uniqueRequestId', oauth.uniqueRequestId) + + const id = oauth.uniqueRequestId + const data = someKindOfStore.get(id) + if (data) { + console.log('saved data for the ID', data) + } + someKindOfStore.delete(id) + } + } + ``` + + + +Read more about the data the `onAfterSignup` hook receives in the [API Reference](#the-onaftersignup-hook). + +### Executing code after a user verifies their email + +Wasp calls the `onAfterEmailVerified` hook exactly once, after the user verifies their email. + +The `onAfterEmailVerified` hook is useful for triggering actions in response to the verification event — such as sending a welcome email or syncing user data with a third-party service. + +The `onAfterEmailVerified` hook receives an `email` string and `user` object, this makes it easy to perform personalized actions upon email verification. + +Works with + + + + +```wasp title="main.wasp" +app myApp { + ... + auth: { + ... + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + }, +} +``` + +```js title="src/auth/hooks.js" +import { emailSender } from 'wasp/server/email' + +export const onAfterEmailVerified = async ({ email }) => { + const info = await emailSender.send({ + from: { + name: 'John Doe', + email: 'john@doe.com', + }, + to: email, + subject: 'Thank you for verifying your email!', + text: `Your email ${email} has been successfully verified!`, + }) + // ... +} +``` + + + + +```wasp title="main.wasp" +app myApp { + ... + auth: { + ... + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + }, +} +``` + +```ts title="src/auth/hooks.ts" +import type { OnAfterEmailVerifiedHook } from 'wasp/server/auth' +import { emailSender } from 'wasp/server/email' + +export const onAfterEmailVerified: OnAfterEmailVerifiedHook = async ({ + email, +}) => { + const info = await emailSender.send({ + from: { + name: 'John Doe', + email: 'john@doe.com', + }, + to: email, + subject: 'Thank you for verifying your email!', + text: `Your email ${email} has been successfully verified!`, + }) + // ... +} +``` + + + + +Read more about the data the `onAfterEmailVerified` hook receives in the [API Reference](#the-onafteremailverified-hook). + +### Executing code before the OAuth redirect + +Wasp calls the `onBeforeOAuthRedirect` hook after the OAuth redirect URL is generated but before redirecting the user. This hook can access the request object sent from the client at the start of the OAuth process. + +The `onBeforeOAuthRedirect` hook can be useful if you want to save some data (e.g. request query parameters) that you can use later in the OAuth flow. You can use the `uniqueRequestId` parameter to reference this data later in the `onAfterSignup` or `onAfterLogin` hooks. + +Works with + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + }, + } + ``` + + ```js title="src/auth/hooks.js" + export const onBeforeOAuthRedirect = async ({ url, oauth, prisma, req }) => { + console.log('query params before oAuth redirect', req.query) + + // Saving query params for later use in onAfterSignup or onAfterLogin hooks + const id = oauth.uniqueRequestId + someKindOfStore.set(id, req.query) + + return { url } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + }, + } + ``` + + ```ts title="src/auth/hooks.ts" + import type { OnBeforeOAuthRedirectHook } from 'wasp/server/auth' + + export const onBeforeOAuthRedirect: OnBeforeOAuthRedirectHook = async ({ + url, + oauth, + prisma, + req, + }) => { + console.log('query params before oAuth redirect', req.query) + + // Saving query params for later use in onAfterSignup or onAfterLogin hooks + const id = oauth.uniqueRequestId + someKindOfStore.set(id, req.query) + + return { url } + } + ``` + + + +This hook's return value must be an object that looks like this: `{ url: URL }`. Wasp uses the URL to redirect the user to the OAuth provider. + +Read more about the data the `onBeforeOAuthRedirect` hook receives in the [API Reference](#the-onbeforeoauthredirect-hook). + +### Executing code before the user logs in + +Wasp calls the `onBeforeLogin` hook before the user is logged in. + +The `onBeforeLogin` hook can be useful if you want to reject a user based on some criteria before they log in. + +Works with + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + }, + } + ``` + + ```js title="src/auth/hooks.js" + import { HttpError } from 'wasp/server' + + export const onBeforeLogin = async ({ providerId, user, prisma, req }) => { + if ( + providerId.providerName === 'email' && + providerId.providerUserId === 'some@email.com' + ) { + throw new HttpError(403, 'You cannot log in with this email') + } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + }, + } + ``` + + ```ts title="src/auth/hooks.ts" + import { HttpError } from 'wasp/server' + import type { OnBeforeLoginHook } from 'wasp/server/auth' + + export const onBeforeLogin: OnBeforeLoginHook = async ({ + providerId, + user, + prisma, + req, + }) => { + if ( + providerId.providerName === 'email' && + providerId.providerUserId === 'some@email.com' + ) { + throw new HttpError(403, 'You cannot log in with this email') + } + } + ``` + + + +Read more about the data the `onBeforeLogin` hook receives in the [API Reference](#the-onbeforelogin-hook). + +### Executing code after the user logs in + +Wasp calls the `onAfterLogin` hook after the user logs in. + +The `onAfterLogin` hook can be useful if you want to perform some action after the user logs in, like syncing the user with a third-party service. + +Since the `onAfterLogin` hook receives the OAuth tokens, you can use it to update the OAuth access token for the user in your database. You can also use it to [refresh the OAuth access token](#refreshing-the-oauth-access-token) if the provider supports it. + +Works with + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + ```js title="src/auth/hooks.js" + export const onAfterLogin = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + console.log('user object', user) + + // If this is an OAuth signup, you have access to the OAuth tokens and the uniqueRequestId + if (oauth) { + console.log('accessToken', oauth.tokens.accessToken) + console.log('uniqueRequestId', oauth.uniqueRequestId) + + const id = oauth.uniqueRequestId + const data = someKindOfStore.get(id) + if (data) { + console.log('saved data for the ID', data) + } + someKindOfStore.delete(id) + } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + ... + auth: { + ... + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + ```ts title="src/auth/hooks.ts" + import type { OnAfterLoginHook } from 'wasp/server/auth' + + export const onAfterLogin: OnAfterLoginHook = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + console.log('user object', user) + + // If this is an OAuth signup, you have access to the OAuth tokens and the uniqueRequestId + if (oauth) { + console.log('accessToken', oauth.tokens.accessToken) + console.log('uniqueRequestId', oauth.uniqueRequestId) + + const id = oauth.uniqueRequestId + const data = someKindOfStore.get(id) + if (data) { + console.log('saved data for the ID', data) + } + someKindOfStore.delete(id) + } + } + ``` + + + +Read more about the data the `onAfterLogin` hook receives in the [API Reference](#the-onafterlogin-hook). + +### Refreshing the OAuth access token + +Some OAuth providers support refreshing the access token when it expires. To refresh the access token, you need the OAuth **refresh token**. + +Wasp exposes the OAuth refresh token in the `onAfterSignup` and `onAfterLogin` hooks. You can store the refresh token in your database and use it to refresh the access token when it expires. + +Import the provider object with the OAuth client from the `wasp/server/auth` module. For example, to refresh the Google OAuth access token, import the `google` object from the `wasp/server/auth` module. You use the `refreshAccessToken` method of the OAuth client to refresh the access token. + +Here's an example of how you can refresh the access token for Google OAuth: + + + + ```js title="src/auth/hooks.js" + import { google } from 'wasp/server/auth' + + export const onAfterLogin = async ({ oauth }) => { + if (oauth.provider === 'google' && oauth.tokens.refreshToken !== null) { + const newTokens = await google.oAuthClient.refreshAccessToken( + oauth.tokens.refreshToken + ) + log('new tokens', newTokens) + } + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnAfterLoginHook } from 'wasp/server/auth' + import { google } from 'wasp/server/auth' + + export const onAfterLogin: OnAfterLoginHook = async ({ oauth }) => { + if (oauth.provider === 'google' && oauth.tokens.refreshToken !== null) { + const newTokens = await google.oAuthClient.refreshAccessToken( + oauth.tokens.refreshToken + ) + log('new tokens', newTokens) + } + } + ``` + + + +Google exposes the `accessTokenExpiresAt` field in the `oauth.tokens` object. You can use this field to determine when the access token expires. + +If you want to refresh the token periodically, use a [Wasp Job](../advanced/jobs.md). + +## API Reference + + + + ```wasp + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + auth: { + userEntity: User, + methods: { + ... + }, + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + + + ```wasp + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + auth: { + userEntity: User, + methods: { + ... + }, + onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks", + onAfterSignup: import { onAfterSignup } from "@src/auth/hooks", + onAfterEmailVerified: import { onAfterEmailVerified } from "@src/auth/hooks", + onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks", + onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks", + onAfterLogin: import { onAfterLogin } from "@src/auth/hooks", + }, + } + ``` + + + +### Common hook input + +The following properties are available in all auth hooks: + +- `prisma: PrismaClient` + + The Prisma client instance which you can use to query your database. + +- `req: Request` + + The [Express request object](https://expressjs.com/en/api.html#req) from which you can access the request headers, cookies, etc. + +### The `onBeforeSignup` hook + + + + ```js title="src/auth/hooks.js" + export const onBeforeSignup = async ({ providerId, prisma, req }) => { + // Hook code goes here + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnBeforeSignupHook } from 'wasp/server/auth' + + export const onBeforeSignup: OnBeforeSignupHook = async ({ + providerId, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + +The hook receives an object as **input** with the following properties: + +- [`providerId: ProviderId`](#providerid-fields) + +- Plus the [common hook input](#common-hook-input) + +Wasp ignores this hook's **return value**. + +### The `onAfterSignup` hook + + + + ```js title="src/auth/hooks.js" + export const onAfterSignup = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnAfterSignupHook } from 'wasp/server/auth' + + export const onAfterSignup: OnAfterSignupHook = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + +The hook receives an object as **input** with the following properties: + +- [`providerId: ProviderId`](#providerid-fields) + +- `user: User` + + The user object that was created. + +- [`oauth?: OAuthFields`](#oauth-fields) + +- Plus the [common hook input](#common-hook-input) + +Wasp ignores this hook's **return value**. + +### The `onAfterEmailVerified` hook + + + + +```js title="src/auth/hooks.js" +export const onAfterEmailVerified = async ({ email, user, prisma, req }) => { + // Hook code goes here +} +``` + + + + +```ts title="src/auth/hooks.ts" +import type { OnAfterEmailVerifiedHook } from 'wasp/server/auth' + +export const onAfterEmailVerified: OnAfterEmailVerifiedHook = async ({ + email, + user, + prisma, + req, +}) => { + // Hook code goes here +} +``` + + + + +The hook receives an object as **input** with the following properties: + +- `email: string` + + The user's veriried email address. + +- `user: User` + + The user who completed email verification. + +- Plus the [common hook input](#common-hook-input) + +Wasp ignores this hook's **return value**. + +### The `onBeforeOAuthRedirect` hook + + + + ```js title="src/auth/hooks.js" + export const onBeforeOAuthRedirect = async ({ url, oauth, prisma, req }) => { + // Hook code goes here + + return { url } + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnBeforeOAuthRedirectHook } from 'wasp/server/auth' + + export const onBeforeOAuthRedirect: OnBeforeOAuthRedirectHook = async ({ + url, + oauth, + prisma, + req, + }) => { + // Hook code goes here + + return { url } + } + ``` + + + +The hook receives an object as **input** with the following properties: + +- `url: URL` + + Wasp uses the URL for the OAuth redirect. + +- `oauth: { uniqueRequestId: string }` + + The `oauth` object has the following fields: + + - `uniqueRequestId: string` + + The unique request ID for the OAuth flow (you might know it as the `state` parameter in OAuth.) + + You can use the unique request ID to save data (e.g. request query params) that you can later use in the `onAfterSignup` or `onAfterLogin` hooks. + +- Plus the [common hook input](#common-hook-input) + +This hook's return value must be an object that looks like this: `{ url: URL }`. Wasp uses the URL to redirect the user to the OAuth provider. + +### The `onBeforeLogin` hook + + + + ```js title="src/auth/hooks.js" + export const onBeforeLogin = async ({ providerId, prisma, req }) => { + // Hook code goes here + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnBeforeLoginHook } from 'wasp/server/auth' + + export const onBeforeLogin: OnBeforeLoginHook = async ({ + providerId, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + +The hook receives an object as **input** with the following properties: + +- [`providerId: ProviderId`](#providerid-fields) + +- `user: User` + + The user that is trying to log in. + +- Plus the [common hook input](#common-hook-input) + +Wasp ignores this hook's **return value**. + +### The `onAfterLogin` hook + + + + ```js title="src/auth/hooks.js" + export const onAfterLogin = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + + ```ts title="src/auth/hooks.ts" + import type { OnAfterLoginHook } from 'wasp/server/auth' + + export const onAfterLogin: OnAfterLoginHook = async ({ + providerId, + user, + oauth, + prisma, + req, + }) => { + // Hook code goes here + } + ``` + + + +The hook receives an object as **input** with the following properties: + +- [`providerId: ProviderId`](#providerid-fields) + +- `user: User` + + The logged-in user's object. + +- [`oauth?: OAuthFields`](#oauth-fields) + +- Plus the [common hook input](#common-hook-input) + +Wasp ignores this hook's **return value**. + +### ProviderId fields + +The `providerId` object represents the user for the current authentication method. Wasp passes it to the `onBeforeSignup`, `onAfterSignup`, `onBeforeLogin`, and `onAfterLogin` hooks. + +It has the following fields: + +- `providerName: string` + + The provider's name (e.g. `'email'`, `'google'`, `'github`) + +- `providerUserId: string` + + The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID) + +### OAuth fields + +Wasp passes the `oauth` object to the `onAfterSignup` and `onAfterLogin` hooks only when the user is authenticated with [Social Auth](./social-auth/overview.md). + +It has the following fields: + +- `providerName: string` + + The name of the OAuth provider the user authenticated with (e.g. `'google'`, `'github'`). + +- `tokens: Tokens` + + You can use the OAuth tokens to make requests to the provider's API on the user's behalf. + + Depending on the OAuth provider, the `tokens` object might have different fields. For example, Google has the fields `accessToken`, `refreshToken`, `idToken`, and `accessTokenExpiresAt`. + + + To access the provider-specific fields, you must first narrow down the `oauth.tokens` object type to the specific OAuth provider type. + + ```ts + if (oauth && oauth.providerName === 'google') { + console.log(oauth.tokens.accessToken) + // ^ Google specific tokens are available here + console.log(oauth.tokens.refreshToken) + console.log(oauth.tokens.idToken) + console.log(oauth.tokens.accessTokenExpiresAt) + } + ``` + + +- `uniqueRequestId: string` + + The unique request ID for the OAuth flow (you might know it as the `state` parameter in OAuth.) + + You can use the unique request ID to get the data that was saved in the `onBeforeOAuthRedirect` hook. diff --git a/web/versioned_docs/version-0.19.0/auth/email.md b/web/versioned_docs/version-0.19.0/auth/email.md new file mode 100644 index 0000000000..32fe729731 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/email.md @@ -0,0 +1,895 @@ +--- +title: Overview +title-llm: Email Auth Overview +--- + +import { Required } from '@site/src/components/Tag'; +import MultipleIdentitiesWarning from './\_multiple-identities-warning.md'; +import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md'; +import UserSignupFieldsExplainer from './\_user-signup-fields-explainer.md'; +import UserFields from './\_user-fields.md'; +import EmailData from './entities/\_email-data.md'; +import AccessingUserDataNote from './\_accessing-user-data-note.md'; + +Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows. + +![Auth UI](/img/authui/all_screens.gif) + + + +## Setting Up Email Authentication + +We'll need to take the following steps to set up email authentication: + +1. Enable email authentication in the Wasp file +2. Add the `User` entity +3. Add the auth routes and pages +4. Use Auth UI components in our pages +5. Set up the email sender + +Structure of the `main.wasp` file we will end up with: + +```wasp title="main.wasp" +// Configuring e-mail authentication +app myApp { + auth: { ... }, + emailSender: { ... } +} + +// Defining routes and pages +route SignupRoute { ... } +page SignupPage { ... } +// ... +``` + +### 1. Enable Email Authentication in `main.wasp` + +Let's start with adding the following to our `main.wasp` file: + + + + ```wasp title="main.wasp" + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + title: "My App", + auth: { + // 1. Specify the user entity (we'll define it next) + userEntity: User, + methods: { + // 2. Enable email authentication + email: { + // 3. Specify the email from field + fromField: { + name: "My App Postman", + email: "hello@itsme.com" + }, + // 4. Specify the email verification and password reset options (we'll talk about them later) + emailVerification: { + clientRoute: EmailVerificationRoute, + }, + passwordReset: { + clientRoute: PasswordResetRoute, + }, + }, + }, + onAuthFailedRedirectTo: "/login", + onAuthSucceededRedirectTo: "/" + }, + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + title: "My App", + auth: { + // 1. Specify the user entity (we'll define it next) + userEntity: User, + methods: { + // 2. Enable email authentication + email: { + // 3. Specify the email from field + fromField: { + name: "My App Postman", + email: "hello@itsme.com" + }, + // 4. Specify the email verification and password reset options (we'll talk about them later) + emailVerification: { + clientRoute: EmailVerificationRoute, + }, + passwordReset: { + clientRoute: PasswordResetRoute, + }, + }, + }, + onAuthFailedRedirectTo: "/login", + onAuthSucceededRedirectTo: "/" + }, + } + ``` + + + +Read more about the `email` auth method options [here](#fields-in-the-email-dict). + +### 2. Add the User Entity + +The `User` entity can be as simple as including only the `id` field: + + + + ```prisma title="schema.prisma" + // 5. Define the user entity + model User { + // highlight-next-line + id Int @id @default(autoincrement()) + // Add your own fields below + // ... + } + ``` + + + + ```prisma title="schema.prisma" + // 5. Define the user entity + model User { + // highlight-next-line + id Int @id @default(autoincrement()) + // Add your own fields below + // ... + } + ``` + + + + + +### 3. Add the Routes and Pages + +Next, we need to define the routes and pages for the authentication pages. + +Add the following to the `main.wasp` file: + + + + ```wasp title="main.wasp" + // ... + + route LoginRoute { path: "/login", to: LoginPage } + page LoginPage { + component: import { Login } from "@src/pages/auth" + } + + route SignupRoute { path: "/signup", to: SignupPage } + page SignupPage { + component: import { Signup } from "@src/pages/auth" + } + + route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } + page RequestPasswordResetPage { + component: import { RequestPasswordReset } from "@src/pages/auth", + } + + route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } + page PasswordResetPage { + component: import { PasswordReset } from "@src/pages/auth", + } + + route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } + page EmailVerificationPage { + component: import { EmailVerification } from "@src/pages/auth", + } + ``` + + + + ```wasp title="main.wasp" + // ... + + route LoginRoute { path: "/login", to: LoginPage } + page LoginPage { + component: import { Login } from "@src/pages/auth" + } + + route SignupRoute { path: "/signup", to: SignupPage } + page SignupPage { + component: import { Signup } from "@src/pages/auth" + } + + route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } + page RequestPasswordResetPage { + component: import { RequestPasswordReset } from "@src/pages/auth", + } + + route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } + page PasswordResetPage { + component: import { PasswordReset } from "@src/pages/auth", + } + + route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } + page EmailVerificationPage { + component: import { EmailVerification } from "@src/pages/auth", + } + ``` + + + +We'll define the React components for these pages in the `src/pages/auth.{jsx,tsx}` file below. + +### 4. Create the Client Pages + +:::info +We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../project/css-frameworks). +::: + +Let's create a `auth.{jsx,tsx}` file in the `src/pages` folder and add the following to it: + + + + ```tsx title="src/pages/auth.jsx" + import { + LoginForm, + SignupForm, + VerifyEmailForm, + ForgotPasswordForm, + ResetPasswordForm, + } from 'wasp/client/auth' + import { Link } from 'react-router-dom' + + export function Login() { + return ( + + +
    + + Don't have an account yet? go to signup. + +
    + + Forgot your password? reset it. + +
    + ) + } + + export function Signup() { + return ( + + +
    + + I already have an account (go to login). + +
    + ) + } + + export function EmailVerification() { + return ( + + +
    + + If everything is okay, go to login + +
    + ) + } + + export function RequestPasswordReset() { + return ( + + + + ) + } + + export function PasswordReset() { + return ( + + +
    + + If everything is okay, go to login + +
    + ) + } + + // A layout component to center the content + export function Layout({ children }) { + return ( +
    +
    +
    +
    {children}
    +
    +
    +
    + ) + } + ``` +
    + + + ```tsx title="src/pages/auth.tsx" + import { + LoginForm, + SignupForm, + VerifyEmailForm, + ForgotPasswordForm, + ResetPasswordForm, + } from 'wasp/client/auth' + import { Link } from 'react-router-dom' + + export function Login() { + return ( + + +
    + + Don't have an account yet? go to signup. + +
    + + Forgot your password? reset it. + +
    + ) + } + + export function Signup() { + return ( + + +
    + + I already have an account (go to login). + +
    + ) + } + + export function EmailVerification() { + return ( + + +
    + + If everything is okay, go to login + +
    + ) + } + + export function RequestPasswordReset() { + return ( + + + + ) + } + + export function PasswordReset() { + return ( + + +
    + + If everything is okay, go to login + +
    + ) + } + + // A layout component to center the content + export function Layout({ children }: { children: React.ReactNode }) { + return ( +
    +
    +
    +
    {children}
    +
    +
    +
    + ) + } + ``` +
    +
    + +We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](../auth/ui). + +### 5. Set up an Email Sender + +To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box. + +We'll use the `Dummy` provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the [supported email providers](../advanced/email#providers). + +To set up the `Dummy` provider to send emails, add the following to the `main.wasp` file: + + + + ```wasp title="main.wasp" + app myApp { + // ... + // 7. Set up the email sender + emailSender: { + provider: Dummy, + } + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + // ... + // 7. Set up the email sender + emailSender: { + provider: Dummy, + } + } + ``` + + + +### Conclusion + +That's it! We have set up email authentication in our app. 🎉 + +Running `wasp db migrate-dev` and then `wasp start` should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the [auth overview](../auth/overview). + +## Login and Signup Flows + +### Login + +![Auth UI](/img/authui/login.png) + +### Signup + +![Auth UI](/img/authui/signup.png) + +Some of the behavior you get out of the box: + +1. Rate limiting + +We are limiting the rate of sign-up requests to **1 request per minute** per email address. This is done to prevent spamming. + +2. Preventing user email leaks + +If somebody tries to signup with an email that already exists and it's verified, we _pretend_ that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address. + +3. Allowing registration for unverified emails + +If a user tries to register with an existing but **unverified** email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address. + +4. Password validation + +Read more about the default password validation rules and how to override them in [auth overview docs](../auth/overview). + +## Email Verification Flow + +:::info Automatic email verification in development + +In development mode, you can skip the email verification step by setting the `SKIP_EMAIL_VERIFICATION_IN_DEV` environment variable to `true` in your `.env.server` file: + +```env title=".env.server" +SKIP_EMAIL_VERIFICATION_IN_DEV=true +``` + +This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app. +::: + +By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address. + +Our setup looks like this: + + + + ```wasp title="main.wasp" + // ... + + emailVerification: { + clientRoute: EmailVerificationRoute, + } + ``` + + + + ```wasp title="main.wasp" + // ... + + emailVerification: { + clientRoute: EmailVerificationRoute, + } + ``` + + + +When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `EmailVerificationRoute` route we defined in the `main.wasp` file. + +The content of the e-mail can be customized, read more about it [here](#emailverification-emailverificationconfig-). + +### Email Verification Page + +We defined our email verification page in the `auth.{jsx,tsx}` file. + +![Auth UI](/img/authui/email_verification.png) + +## Password Reset Flow + +Users can request a password and then they'll receive an e-mail with a link to reset their password. + +Some of the behavior you get out of the box: + +1. Rate limiting + +We are limiting the rate of sign-up requests to **1 request per minute** per email address. This is done to prevent spamming. + +2. Preventing user email leaks + +If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information. + +Our setup in `main.wasp` looks like this: + + + + ```wasp title="main.wasp" + // ... + + passwordReset: { + clientRoute: PasswordResetRoute, + } + ``` + + + + ```wasp title="main.wasp" + // ... + + passwordReset: { + clientRoute: PasswordResetRoute, + } + ``` + + + +### Request Password Reset Page + +Users request their password to be reset by going to the `/request-password-reset` route. We defined our request password reset page in the `auth.{jsx,tsx}` file. + +![Request password reset page](/img/authui/forgot_password_after.png) + +### Password Reset Page + +When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `PasswordResetRoute` route we defined in the `main.wasp` file. + +![Request password reset page](/img/authui/reset_password_after.png) + +Users can enter their new password there. + +The content of the e-mail can be customized, read more about it [here](#passwordreset-passwordresetconfig-). + +#### Password + +- `ensurePasswordIsPresent(args)` + + Checks if the password is present and throws an error if it's not. + +- `ensureValidPassword(args)` + + Checks if the password is valid and throws an error if it's not. Read more about the validation rules [here](../auth/overview#default-validations). + +## Using Auth + +To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [auth overview docs](../auth/overview). + +When you receive the `user` object [on the client or the server](./overview.md#accessing-the-logged-in-user), you'll be able to access the user's email and other information like this: + + + + + +## API Reference + +Let's go over the options we can specify when using email authentication. + +### `userEntity` fields + + + + ```wasp title="main.wasp" + app myApp { + title: "My app", + // ... + + auth: { + userEntity: User, + methods: { + email: { + // We'll explain these options below + }, + }, + onAuthFailedRedirectTo: "/someRoute" + }, + // ... + } + ``` + + ```prisma title="schema.prisma" + model User { + id Int @id @default(autoincrement()) + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + title: "My app", + // ... + + auth: { + userEntity: User, + methods: { + email: { + // We'll explain these options below + }, + }, + onAuthFailedRedirectTo: "/someRoute" + }, + // ... + } + ``` + + ```prisma title="schema.prisma" + model User { + id Int @id @default(autoincrement()) + } + ``` + + + + + +### Fields in the `email` dict + + + + ```wasp title="main.wasp" + app myApp { + title: "My app", + // ... + + auth: { + userEntity: User, + methods: { + email: { + userSignupFields: import { userSignupFields } from "@src/auth", + fromField: { + name: "My App", + email: "hello@itsme.com" + }, + emailVerification: { + clientRoute: EmailVerificationRoute, + getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", + }, + passwordReset: { + clientRoute: PasswordResetRoute, + getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", + }, + }, + }, + onAuthFailedRedirectTo: "/someRoute" + }, + // ... + } + ``` + + + + ```wasp title="main.wasp" + app myApp { + title: "My app", + // ... + + auth: { + userEntity: User, + methods: { + email: { + userSignupFields: import { userSignupFields } from "@src/auth", + fromField: { + name: "My App", + email: "hello@itsme.com" + }, + emailVerification: { + clientRoute: EmailVerificationRoute, + getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", + }, + passwordReset: { + clientRoute: PasswordResetRoute, + getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", + }, + }, + }, + onAuthFailedRedirectTo: "/someRoute" + }, + // ... + } + ``` + + + +#### `userSignupFields: ExtImport` + + + +Read more about the `userSignupFields` function [here](./overview#1-defining-extra-fields). + +#### `fromField: EmailFromField` + +`fromField` is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app. + +It has the following fields: + +- `name`: name of the sender +- `email`: e-mail address of the sender + +#### `emailVerification: EmailVerificationConfig` + +`emailVerification` is a dict that specifies the details of the e-mail verification process. + +It has the following fields: + +- `clientRoute: Route`: a route that is used for the user to verify their e-mail address. + + Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our `verifyEmail` action for that. + + + + ```js title="src/pages/EmailVerificationPage.jsx" + import { verifyEmail } from 'wasp/client/auth' + ... + await verifyEmail({ token }); + ``` + + + + ```ts title="src/pages/EmailVerificationPage.tsx" + import { verifyEmail } from 'wasp/client/auth' + ... + await verifyEmail({ token }); + ``` + + + + :::note + We used Auth UI above to avoid doing this work of sending the token to the server manually. + ::: + +- `getEmailContentFn: ExtImport`: a function that returns the content of the e-mail that is sent to the user. + + Defining `getEmailContentFn` can be done by defining a file in the `src` directory. + + + + ```ts title="src/email.js" + export const getVerificationEmailContent = ({ verificationLink }) => ({ + subject: 'Verify your email', + text: `Click the link below to verify your email: ${verificationLink}`, + html: ` +

    Click the link below to verify your email

    + Verify email + `, + }) + ``` +
    + + + ```ts title="src/email.ts" + import { GetVerificationEmailContentFn } from 'wasp/server/auth' + + export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ + verificationLink, + }) => ({ + subject: 'Verify your email', + text: `Click the link below to verify your email: ${verificationLink}`, + html: ` +

    Click the link below to verify your email

    + Verify email + `, + }) + ``` +
    +
    + + This is the default content of the e-mail, you can customize it to your liking. + +#### `passwordReset: PasswordResetConfig` + +`passwordReset` is a dict that specifies the password reset process. + +It has the following fields: + +- `clientRoute: Route`: a route that is used for the user to reset their password. + + Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our `requestPasswordReset` and `resetPassword` actions to do that. + + + + ```js title="src/pages/ForgotPasswordPage.jsx" + import { requestPasswordReset } from 'wasp/client/auth' + ... + await requestPasswordReset({ email }); + ``` + + ```js title="src/pages/PasswordResetPage.jsx" + import { resetPassword } from 'wasp/client/auth' + ... + await resetPassword({ password, token }) + ``` + + + + ```ts title="src/pages/ForgotPasswordPage.tsx" + import { requestPasswordReset } from 'wasp/client/auth' + ... + await requestPasswordReset({ email }); + ``` + + ```ts title="src/pages/PasswordResetPage.tsx" + import { resetPassword } from 'wasp/client/auth' + ... + await resetPassword({ password, token }) + ``` + + + + :::note + We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually. + ::: + +- `getEmailContentFn: ExtImport`: a function that returns the content of the e-mail that is sent to the user. + + Defining `getEmailContentFn` is done by defining a function that looks like this: + + + + ```ts title="src/email.js" + export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({ + subject: 'Password reset', + text: `Click the link below to reset your password: ${passwordResetLink}`, + html: ` +

    Click the link below to reset your password

    + Reset password + `, + }) + ``` +
    + + + ```ts title="src/email.ts" + import { GetPasswordResetEmailContentFn } from 'wasp/server/auth' + + export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({ + passwordResetLink, + }) => ({ + subject: 'Password reset', + text: `Click the link below to reset your password: ${passwordResetLink}`, + html: ` +

    Click the link below to reset your password

    + Reset password + `, + }) + ``` +
    +
    + + This is the default content of the e-mail, you can customize it to your liking. diff --git a/web/versioned_docs/version-0.19.0/auth/email/create-your-own-ui.md b/web/versioned_docs/version-0.19.0/auth/email/create-your-own-ui.md new file mode 100644 index 0000000000..adcb26f378 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/email/create-your-own-ui.md @@ -0,0 +1,529 @@ +--- +title: Create your own UI +title-llm: Create your own UI for Email Auth +--- + +import { Required } from '@site/src/components/Tag' + +When using the email auth provider, users log in with their email address and a password. On signup, Wasp validates the data and sends a verification email. The user account is not active until the user clicks the link in the verification email. Also, the user can reset their password through a similar flow. + +:::tip +Read more about the default email and password validation rules in the [auth overview docs](../overview.md#default-validations). +::: + +Even though Wasp offers premade [Auth UI](../ui.md) for your authentication flows, there are times when you might want more customization, so we also give you the option to create your own UI and call Wasp's auth actions from your own code, similar to how Auth UI does it under the hood. + +## Example code + +Below you can find a starting point for making your own UI in the client code. This example has all the necessary components to handle login, signup, email verification, and the password reset flow. You can customize any of its look and behaviour, just make sure to call the functions imported from `wasp/client/auth`. + + + + ```jsx title="src/pages/auth.jsx" + import { + login, + requestPasswordReset, + resetPassword, + signup, + verifyEmail, + } from 'wasp/client/auth' + + import { useState } from 'react' + import { useNavigate } from 'react-router-dom' + + // This will be shown when the user wants to log in + export function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const navigate = useNavigate() + + async function handleSubmit(event) { + event.preventDefault() + setError(null) + try { + await login({ email, password }) + navigate('/') + } catch (error) { + setError(error) + } + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + setPassword(e.target.value)} + placeholder="Password" + /> + +
    + ) + } + + // This will be shown when the user wants to sign up + export function Signup() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [needsConfirmation, setNeedsConfirmation] = useState(false) + + async function handleSubmit(event) { + event.preventDefault() + setError(null) + try { + await signup({ email, password }) + setNeedsConfirmation(true) + } catch (error) { + console.error('Error during signup:', error) + setError(error) + } + } + + if (needsConfirmation) { + return ( +

    + Check your email for the confirmation link. If you don't see it, check + spam/junk folder. +

    + ) + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + setPassword(e.target.value)} + placeholder="Password" + /> + +
    + ) + } + + // This will be shown has clicked on the link in their + // email to verify their email address + export function EmailVerification() { + const [error, setError] = useState(null) + const navigate = useNavigate() + + async function handleClick() { + setError(null) + try { + // The token is passed as a query parameter + const token = new URLSearchParams(window.location.search).get('token') + if (!token) throw new Error('Token not found in URL') + await verifyEmail({ token }) + navigate('/') + } catch (error) { + console.error('Error during email verification:', error) + setError(error) + } + } + + return ( + <> + {error &&

    Error: {error.message}

    } + + + + ) + } + + // This will be shown when the user wants to reset their password + export function RequestPasswordReset() { + const [email, setEmail] = useState('') + const [error, setError] = useState(null) + const [needsConfirmation, setNeedsConfirmation] = useState(false) + + async function handleSubmit(event) { + event.preventDefault() + setError(null) + try { + await requestPasswordReset({ email }) + setNeedsConfirmation(true) + } catch (error) { + console.error('Error during requesting reset:', error) + setError(error) + } + } + + if (needsConfirmation) { + return ( +

    + Check your email for the confirmation link. If you don't see it, check + spam/junk folder. +

    + ) + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + + +
    + ) + } + + // This will be shown when the user clicks on the link in their + // email to reset their password + export function PasswordReset() { + const [error, setError] = useState(null) + const [newPassword, setNewPassword] = useState('') + const navigate = useNavigate() + + async function handleSubmit(event) { + event.preventDefault() + setError(null) + try { + // The token is passed as a query parameter + const token = new URLSearchParams(window.location.search).get('token') + if (!token) throw new Error('Token not found in URL') + await resetPassword({ token, password: newPassword }) + navigate('/') + } catch (error) { + console.error('Error during password reset:', error) + setError(error) + } + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setNewPassword(e.target.value)} + placeholder="New password" + /> + + +
    + ) + } + ``` +
    + + + ```tsx title="src/pages/auth.tsx" + import { + login, + requestPasswordReset, + resetPassword, + signup, + verifyEmail, + } from 'wasp/client/auth' + + import { useState } from 'react' + import { useNavigate } from 'react-router-dom' + + // This will be shown when the user wants to log in + export function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const navigate = useNavigate() + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + setError(null) + try { + await login({ email, password }) + navigate('/') + } catch (error: unknown) { + setError(error as Error) + } + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + setPassword(e.target.value)} + placeholder="Password" + /> + +
    + ) + } + + // This will be shown when the user wants to sign up + export function Signup() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [needsConfirmation, setNeedsConfirmation] = useState(false) + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + setError(null) + try { + await signup({ email, password }) + setNeedsConfirmation(true) + } catch (error: unknown) { + console.error('Error during signup:', error) + setError(error as Error) + } + } + + if (needsConfirmation) { + return ( +

    + Check your email for the confirmation link. If you don't see it, check + spam/junk folder. +

    + ) + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + setPassword(e.target.value)} + placeholder="Password" + /> + +
    + ) + } + + // This will be shown has clicked on the link in their + // email to verify their email address + export function EmailVerification() { + const [error, setError] = useState(null) + const navigate = useNavigate() + + async function handleClick() { + setError(null) + try { + // The token is passed as a query parameter + const token = new URLSearchParams(window.location.search).get('token') + if (!token) throw new Error('Token not found in URL') + await verifyEmail({ token }) + navigate('/') + } catch (error: unknown) { + console.error('Error during email verification:', error) + setError(error as Error) + } + } + + return ( + <> + {error &&

    Error: {error.message}

    } + + + + ) + } + + // This will be shown when the user wants to reset their password + export function RequestPasswordReset() { + const [email, setEmail] = useState('') + const [error, setError] = useState(null) + const [needsConfirmation, setNeedsConfirmation] = useState(false) + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + setError(null) + try { + await requestPasswordReset({ email }) + setNeedsConfirmation(true) + } catch (error: unknown) { + console.error('Error during requesting reset:', error) + setError(error as Error) + } + } + + if (needsConfirmation) { + return ( +

    + Check your email for the confirmation link. If you don't see it, check + spam/junk folder. +

    + ) + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setEmail(e.target.value)} + placeholder="Email" + /> + + +
    + ) + } + + // This will be shown when the user clicks on the link in their + // email to reset their password + export function PasswordReset() { + const [error, setError] = useState(null) + const [newPassword, setNewPassword] = useState('') + const navigate = useNavigate() + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + setError(null) + try { + // The token is passed as a query parameter + const token = new URLSearchParams(window.location.search).get('token') + if (!token) throw new Error('Token not found in URL') + await resetPassword({ token, password: newPassword }) + navigate('/') + } catch (error: unknown) { + console.error('Error during password reset:', error) + setError(error as Error) + } + } + + return ( +
    + {error &&

    Error: {error.message}

    } + + setNewPassword(e.target.value)} + placeholder="New password" + /> + + +
    + ) + } + ``` +
    +
    + +## API Reference + +You can import the following functions from `wasp/client/auth`: + +### `login()` + +An action for logging in the user. Make sure to do a redirect on success (e.g. to the main page of the app). + +It takes one argument: + +- `data: object` + + It has the following fields: + + - `email: string` + + - `password: string` + +### `signup()` + +An action for signing up the user and starting the email verification. The user will not be logged in after this, as they still need +to verify their email. + +It takes one argument: + +- `data: object` + + It has the following fields: + + - `email: string` + + - `password: string` + +:::info +By default, Wasp will only save the `email` and `password` fields. If you want to add extra fields to your signup process, read about [defining extra signup fields](../overview.md#customizing-the-signup-process). +::: + +### `verifyEmail()` + +An action for marking the email as valid and the user account as active. Make sure to do a redirect on success (e.g. to the login page). + +It takes one argument: + +- `data: object` + + It has the following fields: + + - `token: string` + + The token that was created when signing up. It will be set as a URL Query Parameter named `token`. + +### `requestPasswordReset()` + +An action for asking for a password reset email. This doesn't immediately reset their password, just sends the email. + +It takes one argument: + +- `data: object` + + It has the following fields: + + - `email: string` + +### `resetPassword()` + +An action for confirming a password reset and providing the new password. Make sure to do a redirect on success (e.g. to the login page). + +It takes one argument: + +- `data: object` + + It has the following fields: + + - `token: string` + + The token that was created when requesting the password reset. It will be set as a URL Query Parameter named `token`. + + - `password: string` + + The new password for the user. diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_discord-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_discord-data.md new file mode 100644 index 0000000000..4e6f728c43 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_discord-data.md @@ -0,0 +1,6 @@ +```ts +const discordIdentity = user.identities.discord + +// Discord User ID for example "80351110224678912" +discordIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_email-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_email-data.md new file mode 100644 index 0000000000..72cf89fda9 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_email-data.md @@ -0,0 +1,15 @@ +```ts +const emailIdentity = user.identities.email + +// Email address the user used to sign up, e.g. "fluffyllama@app.com". +emailIdentity.id + +// `true` if the user has verified their email address. +emailIdentity.isEmailVerified + +// Datetime when the email verification email was sent. +emailIdentity.emailVerificationSentAt + +// Datetime when the last password reset email was sent. +emailIdentity.passwordResetSentAt +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_github-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_github-data.md new file mode 100644 index 0000000000..2eed5149f6 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_github-data.md @@ -0,0 +1,6 @@ +```ts +const githubIdentity = user.identities.github + +// GitHub User ID for example "12345678" +githubIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_google-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_google-data.md new file mode 100644 index 0000000000..1372d7b7d3 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_google-data.md @@ -0,0 +1,6 @@ +```ts +const googleIdentity = user.identities.google + +// Google User ID for example "123456789012345678901" +googleIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_keycloak-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_keycloak-data.md new file mode 100644 index 0000000000..962e168646 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_keycloak-data.md @@ -0,0 +1,6 @@ +```ts +const keycloakIdentity = user.identities.keycloak + +// Keycloak User ID for example "12345678-1234-1234-1234-123456789012" +keycloakIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_slack-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_slack-data.md new file mode 100644 index 0000000000..26d6bb5d4e --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_slack-data.md @@ -0,0 +1,6 @@ +```ts +const slackIdentity = user.identities.slack + +// Discord User ID for example "80351110224678912" +slackIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/_username-data.md b/web/versioned_docs/version-0.19.0/auth/entities/_username-data.md new file mode 100644 index 0000000000..9e291d2193 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/_username-data.md @@ -0,0 +1,6 @@ +```ts +const usernameIdentity = user.identities.username + +// Username that the user used to sign up, e.g. "fluffyllama" +usernameIdentity.id +``` diff --git a/web/versioned_docs/version-0.19.0/auth/entities/entities.md b/web/versioned_docs/version-0.19.0/auth/entities/entities.md new file mode 100644 index 0000000000..f85e9c337a --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/entities/entities.md @@ -0,0 +1,746 @@ +--- +title: Accessing User Data +--- + +import { ImgWithCaption } from '@site/blog/components/ImgWithCaption' +import { Internal } from '@site/src/components/Tag' +import MultipleIdentitiesWarning from '../\_multiple-identities-warning.md'; +import UsernameData from './\_username-data.md'; +import EmailData from './\_email-data.md'; +import GoogleData from './\_google-data.md'; +import GithubData from './\_github-data.md'; +import KeycloakData from './\_keycloak-data.md'; +import DiscordData from './\_discord-data.md'; + +First, we'll check out the most practical info: **how to access the user's data in your app**. + +Then, we'll dive into the details of the **auth entities** that Wasp creates behind the scenes to store the user's data. For auth each method, Wasp needs to store different information about the user. For example, username for [Username & password](./username-and-pass) auth, email verification status for [Email](./email) auth, and so on. + +We'll also show you how you can use these entities to create a custom signup action. + +## Accessing the Auth Fields + +When you receive the `user` object [on the client or the server](../overview.md#accessing-the-logged-in-user), it will contain all the user fields you defined in the `User` entity in the `schema.prisma` file. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the `username` or the email verification status. In Wasp, this data is called the `AuthUser` object. + +### `AuthUser` Object Fields + +All the `User` fields you defined will be present at the top level of the `AuthUser` object. The auth-related fields will be on the `identities` object. For each auth method you enable, there will be a separate data object in the `identities` object. + +The `AuthUser` object will change depending on which auth method you have enabled in the Wasp file. For example, if you enabled the email auth and Google auth, it would look something like this: + + + + If the user has only the Google identity, the `AuthUser` object will look like this: + + ```ts + const user = { + // User data + id: 'cluqs9qyh00007cn73apj4hp7', + address: 'Some address', + + // Auth methods specific data + identities: { + email: null, + google: { + id: '1117XXXX1301972049448', + }, + }, + } + ``` + + + + If the user has only the email identity, the `AuthUser` object will look like this: + + ```ts + const user = { + // User data + id: 'cluqsex9500017cn7i2hwsg17', + address: 'Some address', + + // Auth methods specific data + identities: { + email: { + id: 'user@app.com', + isEmailVerified: true, + emailVerificationSentAt: '2024-04-08T10:06:02.204Z', + passwordResetSentAt: null, + }, + google: null, + }, + } + ``` + + + +In the examples above, you can see the `identities` object contains the `email` and `google` objects. The `email` object contains the email-related data and the `google` object contains the Google-related data. + +:::info Make sure to check if the data exists + +Before accessing some auth method's data, you'll need to check if that data exists for the user and then access it: + +```ts +if (user.identities.google !== null) { + const userId = user.identities.google.id + // ... +} +``` + +You need to do this because if a user didn't sign up with some auth method, the data for that auth method will be `null`. +::: + +Let's look at the data for each of the available auth methods: + +- [Username & password](../username-and-pass.md) data + + + +- [Email](../email.md) data + + + +- [Google](../social-auth/google.md) data + + + +- [GitHub](../social-auth/github.md) data + + + +- [Keycloak](../social-auth/keycloak.md) data + + + +- [Discord](../social-auth/discord.md) data + + + +If you support multiple auth methods, you'll need to find which identity exists for the user and then access its data: + +```ts +if (user.identities.email !== null) { + const email = user.identities.email.id + // ... +} else if (user.identities.google !== null) { + const googleId = user.identities.google.id + // ... +} +``` + +### `getFirstProviderUserId` Helper + +The `getFirstProviderUserId` method returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID. + +This can be useful if you support multiple authentication methods and you need _any_ ID that identifies the user in your app. + + + + ```jsx title="src/MainPage.jsx" + const MainPage = ({ user }) => { + const userId = user.getFirstProviderUserId() + // ... + } + ``` + + ```js title="src/tasks.js" + export const createTask = async (args, context) => { + const userId = context.user.getFirstProviderUserId() + // ... + } + ``` + + + + ```tsx title="src/MainPage.tsx" + import { type AuthUser } from 'wasp/auth' + + const MainPage = ({ user }: { user: AuthUser }) => { + const userId = user.getFirstProviderUserId() + // ... + } + ``` + + ```ts title="src/tasks.ts" + export const createTask: CreateTask<...> = async (args, context) => { + const userId = context.user.getFirstProviderUserId() + // ... + } + ``` + + + + + \* Multiple identities per user will be possible in the future and then the `getFirstProviderUserId` method will return the ID of the first identity that it finds without any guarantees about which one it will be. + + +## Including the User with Other Entities + +Sometimes, you might want to include the user's data when fetching other entities. For example, you might want to include the user's data with the tasks they have created. + +We'll mention the `auth` and the `identities` relations which we will explain in more detail later in the [Entities Explained](#entities-explained) section. + +:::caution Be careful about sensitive data + +You'll need to include the `auth` and the `identities` relations to get the full auth data about the user. However, you should keep in mind that the `providerData` field in the `identities` can contain sensitive data like the user's hashed password (in case of email or username auth), so you will likely want to exclude it if you are returning those values to the client. + +::: + +You can include the full user's data with other entities using the `include` option in the Prisma queries: + + + + ```js title="src/tasks.js" + export const getAllTasks = async (args, context) => { + return context.entities.Task.findMany({ + orderBy: { id: 'desc' }, + select: { + id: true, + title: true, + // highlight-next-line + user: { + include: { + // highlight-next-line + auth: { + include: { + // highlight-next-line + identities: { + // Including only the `providerName` and `providerUserId` fields + select: { + providerName: true, + providerUserId: true, + }, + }, + }, + }, + }, + }, + }, + }) + } + ``` + + + + ```ts title="src/tasks.ts" + export const getAllTasks = (async (args, context) => { + return context.entities.Task.findMany({ + orderBy: { id: 'desc' }, + select: { + id: true, + title: true, + // highlight-next-line + user: { + include: { + // highlight-next-line + auth: { + include: { + // highlight-next-line + identities: { + // Including only the `providerName` and `providerUserId` fields + select: { + providerName: true, + providerUserId: true, + }, + }, + }, + }, + }, + }, + }, + }) + }) satisfies tasks.GetAllQuery<{}, {}> + ``` + + + +If you have some **piece of the auth data that you want to access frequently** (for example the `username`), it's best to store it at the top level of the `User` entity. + +For example, save the `username` or `email` as a property on the `User` and you'll be able to access it without including the `auth` and `identities` fields. We show an example in the [Defining Extra Fields on the User Entity](../overview.md#1-defining-extra-fields) section of the docs. + +### Getting Auth Data from the User Object + +When you have the `user` object with the `auth` and `identities` fields, it can be a bit tedious to obtain the auth data (like username or Google ID) from it: + + + + ```jsx title="src/MainPage.jsx" + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {task.user.auth?.identities[0].providerUserId} +
    + ))} +
    + ) + } + ``` +
    + + + ```tsx title="src/MainPage.tsx" + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {task.user.auth?.identities[0].providerUserId} +
    + ))} +
    + ) + } + ``` +
    +
    + +Wasp offers a few helper methods to access the user's auth data when you retrieve the `user` like this. They are `getUsername`, `getEmail` and `getFirstProviderUserId`. They can be used both on the client and the server. + +#### `getUsername` + +It accepts the `user` object and if the user signed up with the [Username & password](./username-and-pass) auth method, it returns the username or `null` otherwise. The `user` object needs to have the `auth` and the `identities` relations included. + + + + ```jsx title="src/MainPage.jsx" + import { getUsername } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getUsername(task.user)} +
    + ))} +
    + ) + } + ``` +
    + + + ```tsx title="src/MainPage.tsx" + import { getUsername } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getUsername(task.user)} +
    + ))} +
    + ) + } + ``` +
    +
    + +#### `getEmail` + +It accepts the `user` object and if the user signed up with the [Email](./email) auth method, it returns the email or `null` otherwise. The `user` object needs to have the `auth` and the `identities` relations included. + + + + ```jsx title="src/MainPage.jsx" + import { getEmail } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getEmail(task.user)} +
    + ))} +
    + ) + } + ``` +
    + + + ```tsx title="src/MainPage.tsx" + import { getEmail } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getEmail(task.user)} +
    + ))} +
    + ) + } + ``` +
    +
    + +#### `getFirstProviderUserId` + +It returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID. The `user` object needs to have the `auth` and the `identities` relations included. + + + + ```jsx title="src/MainPage.jsx" + import { getFirstProviderUserId } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getFirstProviderUserId(task.user)} +
    + ))} +
    + ) + } + ``` +
    + + + ```tsx title="src/MainPage.tsx" + import { getFirstProviderUserId } from 'wasp/auth' + + function MainPage() { + // ... + return ( +
    + {tasks.map((task) => ( +
    + {task.title} by {getFirstProviderUserId(task.user)} +
    + ))} +
    + ) + } + ``` +
    +
    + +## Entities Explained + +To store user's auth information, Wasp does a few things behind the scenes. Wasp takes your `schema.prisma` file and combines it with additional entities to create the final `schema.prisma` file that is used in your app. + +In this section, we will explain which entities are created and how they are connected. + +### User Entity + +When you want to add authentication to your app, you need to specify the `userEntity` field. + +For example, you might set it to `User`: + +```wasp title="main.wasp" +app myApp { + wasp: { + version: "{latestWaspVersion}" + }, + title: "My App", + auth: { + // highlight-next-line + userEntity: User, + // ... + }, +} +``` + +And define the `User` in the `schema.prisma` file: + +```prisma title="schema.prisma" +model User { + id Int @id @default(autoincrement()) + // Any other fields you want to store about the user +} +``` + +The `User` entity is a "business logic user" which represents a user of your app. + +You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address. + +You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created. + +You **own** the user entity and you can modify it as you wish. You can add new fields to it, remove fields from it, or change the type of the fields. You can also add new relations to it or remove existing relations from it. + + + +On the other hand, the `Auth`, `AuthIdentity` and `Session` entities are created behind the scenes and are used to store the user's login credentials. You as the developer don't need to care about this entity most of the time. Wasp **owns** these entities. + +In the case you want to create a custom signup action, you will need to use the `Auth` and `AuthIdentity` entities directly. + +### Example App Model + +Let's imagine we created a simple tasks management app: + +- The app has email and Google-based auth. +- Users can create tasks and see the tasks that they have created. + +Let's look at how would that look in the database: + + + +If we take a look at an example user in the database, we can see: + +- The business logic user, `User` is connected to multiple `Task` entities. + - In this example, "Example User" has two tasks. +- The `User` is connected to exactly one `Auth` entity. +- Each `Auth` entity can have multiple `AuthIdentity` entities. + - In this example, the `Auth` entity has two `AuthIdentity` entities: one for the email-based auth and one for the Google-based auth. +- Each `Auth` entity can have multiple `Session` entities. + - In this example, the `Auth` entity has one `Session` entity. + + + +### `Auth` Entity + +Wasp's internal `Auth` entity is used to connect the business logic user, `User` with the user's login credentials. + +```prisma +model Auth { + id String @id @default(uuid()) + userId Int? @unique + // Wasp injects this relation on the User entity as well + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + identities AuthIdentity[] + sessions Session[] +} +``` + +The `Auth` fields: + +- `id` is a unique identifier of the `Auth` entity. +- `userId` is a foreign key to the `User` entity. + - It is used to connect the `Auth` entity with the business logic user. +- `user` is a relation to the `User` entity. + - This relation is injected on the `User` entity as well. +- `identities` is a relation to the `AuthIdentity` entity. +- `sessions` is a relation to the `Session` entity. + +### `AuthIdentity` Entity + +The `AuthIdentity` entity is used to store the user's login credentials for various authentication methods. + +```prisma +model AuthIdentity { + providerName String + providerUserId String + providerData String @default("{}") + authId String + auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade) + + @@id([providerName, providerUserId]) +} +``` + +The `AuthIdentity` fields: + +- `providerName` is the name of the authentication provider. + - For example, `email` or `google`. +- `providerUserId` is the user's ID in the authentication provider. + - For example, the user's email or Google ID. +- `providerData` is a JSON string that contains additional data about the user from the authentication provider. + - For example, for password based auth, this field contains the user's hashed password. + - This field is a `String` and not a `Json` type because [Prisma doesn't support the `Json` type for SQLite](https://github.com/prisma/prisma/issues/3786). +- `authId` is a foreign key to the `Auth` entity. + - It is used to connect the `AuthIdentity` entity with the `Auth` entity. +- `auth` is a relation to the `Auth` entity. + +### `Session` Entity + +The `Session` entity is used to store the user's session information. It is used to keep the user logged in between page refreshes. + +```prisma +model Session { + id String @id @unique + expiresAt DateTime + userId String + auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@index([userId]) +} +``` + +The `Session` fields: + +- `id` is a unique identifier of the `Session` entity. +- `expiresAt` is the date when the session expires. +- `userId` is a foreign key to the `Auth` entity. + - It is used to connect the `Session` entity with the `Auth` entity. +- `auth` is a relation to the `Auth` entity. + +## Custom Signup Action + +Let's take a look at how you can use the `Auth` and `AuthIdentity` entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service. + +:::info Custom Signup Examples + +In the Advanced section you can see an example for [Email](../advanced/custom-auth-actions.md#email) or [Username and password](../advanced/custom-auth-actions.md#username-and-password) authentication. + +::: + +Below is a simplified version of a custom signup action which you probably wouldn't use in your app but it shows you how you can use the `Auth` and `AuthIdentity` entities to create a custom signup action. + + + + ```wasp title="main.wasp" + // ... + + action customSignup { + fn: import { signup } from "@src/auth/signup", + entities: [User] + } + ``` + + ```js title="src/auth/signup.js" + import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, + } from 'wasp/server/auth' + + export const signup = async (args, { entities: { User } }) => { + try { + // Provider ID is a combination of the provider name and the provider user ID + // And it is used to uniquely identify the user in your app + const providerId = createProviderId('username', args.username) + // sanitizeAndSerializeProviderData hashes the password and returns a JSON string + const providerData = await sanitizeAndSerializeProviderData({ + hashedPassword: args.password, + }) + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {} + ) + + // This is equivalent to: + // await User.create({ + // data: { + // auth: { + // create: { + // identities: { + // create: { + // providerName: 'username', + // providerUserId: args.username + // providerData, + // }, + // }, + // } + // }, + // } + // }) + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } + } + ``` + + + + ```wasp title="main.wasp" + // ... + + action customSignup { + fn: import { signup } from "@src/auth/signup", + entities: [User] + } + ``` + + ```ts title="src/auth/signup.ts" + import { + createProviderId, + sanitizeAndSerializeProviderData, + createUser, + } from 'wasp/server/auth' + import type { CustomSignup } from 'wasp/server/operations' + + type CustomSignupInput = { + username: string + password: string + } + type CustomSignupOutput = { + success: boolean + message: string + } + + export const signup: CustomSignup< + CustomSignupInput, + CustomSignupOutput + > = async (args, { entities: { User } }) => { + try { + // Provider ID is a combination of the provider name and the provider user ID + // And it is used to uniquely identify the user in your app + const providerId = createProviderId('username', args.username) + // sanitizeAndSerializeProviderData hashes the password and returns a JSON string + const providerData = await sanitizeAndSerializeProviderData<'username'>({ + hashedPassword: args.password, + }) + + await createUser( + providerId, + providerData, + // Any additional data you want to store on the User entity + {} + ) + + // This is equivalent to: + // await User.create({ + // data: { + // auth: { + // create: { + // identities: { + // create: { + // providerName: 'username', + // providerUserId: args.username + // providerData, + // }, + // }, + // } + // }, + // } + // }) + } catch (e) { + return { + success: false, + message: e.message, + } + } + + // Your custom code after sign-up. + // ... + + return { + success: true, + message: 'User created successfully', + } + } + ``` + + + +You can use whichever method suits your needs better: either the `createUser` function or Prisma's `User.create` method. The `createUser` function is a bit more convenient to use because it hides some of the complexity. On the other hand, the `User.create` method gives you more control over the data that is stored in the `Auth` and `AuthIdentity` entities. diff --git a/web/versioned_docs/version-0.19.0/auth/overview.md b/web/versioned_docs/version-0.19.0/auth/overview.md new file mode 100644 index 0000000000..1312b66714 --- /dev/null +++ b/web/versioned_docs/version-0.19.0/auth/overview.md @@ -0,0 +1,749 @@ +--- +title: Overview +title-llm: Authentication Overview +--- + +import { AuthMethodsGrid } from "./AuthMethodsGrid"; +import { LinkGrid } from "@site/src/components/LinkGrid"; +import { Required } from '@site/src/components/Tag'; +import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md'; + +Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box. + +Here's a 1-minute tour of how full-stack auth works in Wasp: + +
    +