Adapt wasp clean and fix NPM dep installation (#1603)

---------

Co-authored-by: Martin Sosic <sosic.martin@gmail.com>
This commit is contained in:
Filip Sodić 2024-01-29 15:21:36 +01:00 committed by GitHub
parent df5a1358e6
commit 1456a89222
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 454 additions and 382 deletions

View File

@ -19,7 +19,6 @@ import Wasp.Cli.Command.CreateNewProject (createNewProject)
import qualified Wasp.Cli.Command.CreateNewProject.AI as Command.CreateNewProject.AI
import Wasp.Cli.Command.Db (runDbCommand)
import qualified Wasp.Cli.Command.Db.Migrate as Command.Db.Migrate
import qualified Wasp.Cli.Command.Db.Reset as Command.Db.Reset
import qualified Wasp.Cli.Command.Db.Seed as Command.Db.Seed
import qualified Wasp.Cli.Command.Db.Studio as Command.Db.Studio
import Wasp.Cli.Command.Deploy (deploy)
@ -158,7 +157,7 @@ printUsage =
cmd " start Runs Wasp app in development mode, watching for file changes.",
cmd " start db Starts managed development database for you.",
cmd " db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.",
cmd " clean Deletes all generated code and other cached artifacts.",
cmd " clean Deletes all generated code, all cached artifacts, and the node_modules dir.",
" Wasp equivalent of 'have you tried closing and opening it again?'.",
cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.",
cmd " deploy Deploys your Wasp app to cloud hosting providers.",
@ -200,7 +199,6 @@ dbCli :: [String] -> IO ()
dbCli args = case args of
["start"] -> runCommand Command.Start.Db.start
"migrate-dev" : optionalMigrateArgs -> runDbCommand $ Command.Db.Migrate.migrateDev optionalMigrateArgs
["reset"] -> runDbCommand Command.Db.Reset.reset
["seed"] -> runDbCommand $ Command.Db.Seed.seed Nothing
["seed", seedName] -> runDbCommand $ Command.Db.Seed.seed $ Just seedName
["studio"] -> runDbCommand Command.Db.Studio.studio

View File

@ -26,7 +26,26 @@ bashCompletion = do
["db", cmdPrefix] -> listMatchingCommands cmdPrefix dbSubCommands
_ -> liftIO . putStrLn $ ""
where
commands = ["new", "version", "waspls", "start", "db", "clean", "uninstall", "build", "telemetry", "deps", "info", "completion", "completion:generate"]
commands =
[ "new",
"new:ai",
"version",
"waspls",
"completion",
"completion:generate",
"uninstall",
"start",
"db",
"clean",
"build",
"deploy",
"telemetry",
"deps",
"dockerfile",
"info",
"test",
"studio"
]
dbSubCommands = ["migrate-dev", "studio"]
listMatchingCommands :: String -> [String] -> Command ()
listMatchingCommands cmdPrefix cmdList = listCommands $ filter (cmdPrefix `isPrefixOf`) cmdList

View File

@ -3,26 +3,18 @@ module Wasp.Cli.Command.Clean
)
where
import Control.Monad.IO.Class (liftIO)
import qualified StrongPath as SP
import System.Directory
( doesDirectoryExist,
removeDirectoryRecursive,
)
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Command.Common (deleteDirectoryIfExistsVerbosely)
import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require)
import qualified Wasp.Message as Msg
import Wasp.Project.Common (dotWaspDirInWaspProjectDir)
import Wasp.Project.Common (dotWaspDirInWaspProjectDir, nodeModulesDirInWaspProjectDir)
clean :: Command ()
clean = do
InWaspProject waspProjectDir <- require
let dotWaspDirFp = SP.toFilePath $ waspProjectDir SP.</> dotWaspDirInWaspProjectDir
cliSendMessageC $ Msg.Start "Deleting .wasp/ directory..."
doesDotWaspDirExist <- liftIO $ doesDirectoryExist dotWaspDirFp
if doesDotWaspDirExist
then do
liftIO $ removeDirectoryRecursive dotWaspDirFp
cliSendMessageC $ Msg.Success "Deleted .wasp/ directory."
else cliSendMessageC $ Msg.Success "Nothing to delete: .wasp directory does not exist."
let dotWaspDir = waspProjectDir SP.</> dotWaspDirInWaspProjectDir
let nodeModulesDir = waspProjectDir SP.</> nodeModulesDirInWaspProjectDir
deleteDirectoryIfExistsVerbosely dotWaspDir
deleteDirectoryIfExistsVerbosely nodeModulesDir

View File

@ -1,15 +1,21 @@
module Wasp.Cli.Command.Common
( readWaspCompileInfo,
throwIfExeIsNotAvailable,
deleteDirectoryIfExistsVerbosely,
)
where
import Control.Monad.Except
import qualified Control.Monad.Except as E
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import StrongPath.Operations
import System.Directory (findExecutable)
import System.Directory
( findExecutable,
)
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Message as Msg
import Wasp.Project (WaspProjectDir)
import qualified Wasp.Project.Common as Project.Common
import Wasp.Util (ifM)
@ -34,3 +40,16 @@ throwIfExeIsNotAvailable exeName explanationMsg = do
Nothing ->
E.throwError $
CommandError ("Couldn't find `" <> exeName <> "` executable") explanationMsg
deleteDirectoryIfExistsVerbosely :: Path' Abs (Dir d) -> Command ()
deleteDirectoryIfExistsVerbosely dir = do
cliSendMessageC $ Msg.Start $ "Deleting the " ++ dirName ++ " directory..."
dirExist <- liftIO $ IOUtil.doesDirectoryExist dir
if dirExist
then do
liftIO $ IOUtil.removeDirectory dir
cliSendMessageC $ Msg.Success $ "Deleted the " ++ dirName ++ " directory."
else do
cliSendMessageC $ Msg.Success $ "Nothing to delete: The " ++ dirName ++ " directory does not exist."
where
dirName = SP.toFilePath $ basename dir

View File

@ -19,33 +19,50 @@ import Wasp.Cli.Message (cliSendMessage)
import qualified Wasp.Generator.Common as Wasp.Generator
import qualified Wasp.Message as Msg
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
import Wasp.Project.Common (extClientCodeDirInWaspProjectDir, extServerCodeDirInWaspProjectDir, extSharedCodeDirInWaspProjectDir)
import Wasp.Project.Common (srcDirInWaspProjectDir)
-- TODO: Idea: Read .gitignore file, and ignore everything from it. This will then also cover the
-- .wasp dir, and users can easily add any custom stuff they want ignored. But, we also have to
-- be ready for the case when there is no .gitignore, that could be possible.
-- | Forever listens for any file changes in waspProjectDir, and if there is a change,
-- compiles Wasp source files in waspProjectDir and regenerates files in outDir.
-- It will defer recompilation until no new change was detected in the last second.
-- It also takes 'ongoingCompilationResultMVar' MVar, into which it stores the result
-- (warnings, errors) of the latest (re)compile whenever it happens. If there is already
-- something in the MVar, it will get overwritten.
-- | Forever listens for any file changes at the very top level of @waspProjectDir@, and also for
-- any changes at any depth in the @waspProjectDir@/src/ dir. If there is a change, compiles Wasp
-- source files in @waspProjectDir@ and regenerates files in @outDir@. It will defer recompilation
-- until no new change was detected in the last second. It also takes 'ongoingCompilationResultMVar'
-- MVar, into which it stores the result (warnings, errors) of the latest (re)compile whenever it
-- happens. If there is already something in the MVar, it will get overwritten.
watch ::
Path' Abs (Dir WaspProjectDir) ->
Path' Abs (Dir Wasp.Generator.ProjectRootDir) ->
MVar ([CompileWarning], [CompileError]) ->
IO ()
watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mgr -> do
currentTime <- getCurrentTime
chan <- newChan
_ <- FSN.watchDirChan mgr (SP.fromAbsDir waspProjectDir) eventFilter chan
let watchProjectSubdirTree path = FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> path) eventFilter chan
_ <- watchProjectSubdirTree extClientCodeDirInWaspProjectDir
_ <- watchProjectSubdirTree extServerCodeDirInWaspProjectDir
_ <- watchProjectSubdirTree extSharedCodeDirInWaspProjectDir
listenForEvents chan currentTime
_ <- watchFilesAtTopLevelOfWaspProjectDir mgr chan
_ <- watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan
listenForEvents chan =<< getCurrentTime
where
watchFilesAtTopLevelOfWaspProjectDir mgr chan =
FSN.watchDirChan mgr (SP.fromAbsDir waspProjectDir) eventFilter chan
where
eventFilter :: FSN.Event -> Bool
eventFilter event =
-- TODO: Might be valuable to also filter out files from .gitignore.
not (isEditorTmpFile filename)
&& filename /= "package-lock.json"
where
filename = FP.takeFileName $ FSN.eventPath event
watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan =
FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> srcDirInWaspProjectDir) eventFilter chan
where
eventFilter :: FSN.Event -> Bool
eventFilter event =
-- TODO: Might be valuable to also filter out files from .gitignore.
not (isEditorTmpFile filename)
where
filename = FP.takeFileName $ FSN.eventPath event
listenForEvents :: Chan FSN.Event -> UTCTime -> IO ()
listenForEvents chan lastCompileTime = do
event <- readChan chan
@ -114,12 +131,13 @@ watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mg
-- create next to the source code. Bad thing here is that users can't modify this,
-- so better approach would be probably to use information from .gitignore instead, or
-- maybe combining the two somehow.
eventFilter :: FSN.Event -> Bool
eventFilter event =
let filename = FP.takeFileName $ FSN.eventPath event
in not (null filename)
&& take 2 filename /= ".#" -- Ignore emacs lock files.
&& not (head filename == '#' && last filename == '#') -- Ignore emacs auto-save files.
&& last filename /= '~' -- Ignore emacs and vim backup files.
&& not (head filename == '.' && ".swp" `isSuffixOf` filename) -- Ignore vim swp files.
&& not (head filename == '.' && ".un~" `isSuffixOf` filename) -- Ignore vim undo files.
isEditorTmpFile :: String -> Bool
isEditorTmpFile "" = False
isEditorTmpFile filename =
or
[ take 2 filename == ".#", -- Emacs lock files.
head filename == '#' && last filename == '#', -- Emacs auto-save files.
last filename == '~', -- Emacs and vim backup files.
head filename == '.' && ".swp" `isSuffixOf` filename, -- Vim swp files.
head filename == '.' && ".un~" `isSuffixOf` filename -- Vim undo files.
]

View File

@ -4,15 +4,16 @@ module Wasp.Generator.NpmDependencies
( DependencyConflictError (..),
getDependenciesPackageJsonEntry,
getDevDependenciesPackageJsonEntry,
getUserNpmDepsForPackage,
combineNpmDepsForPackage,
NpmDepsForPackage (..),
NpmDepsForPackageError (..),
conflictErrorToMessage,
genNpmDepsForPackage,
NpmDepsForFullStack,
NpmDepsForFramework,
NpmDepsForWasp (..),
NpmDepsForUser (..),
buildNpmDepsForFullStack,
buildWaspFrameworkNpmDeps,
)
where
@ -27,16 +28,15 @@ import qualified Wasp.AppSpec.App.Dependency as D
import qualified Wasp.AppSpec.PackageJson as AS.PackageJson
import Wasp.Generator.Monad (Generator, GeneratorError (..), logAndThrowGeneratorError)
data NpmDepsForFullStack = NpmDepsForFullStack
data NpmDepsForFramework = NpmDepsForFramework
{ npmDepsForServer :: NpmDepsForPackage,
npmDepsForWebApp :: NpmDepsForPackage
}
deriving (Show, Eq, Generic)
instance ToJSON NpmDepsForFullStack where
toEncoding = genericToEncoding defaultOptions
instance ToJSON NpmDepsForFramework
instance FromJSON NpmDepsForFullStack
instance FromJSON NpmDepsForFramework
data NpmDepsForPackage = NpmDepsForPackage
{ dependencies :: [D.Dependency],
@ -44,6 +44,10 @@ data NpmDepsForPackage = NpmDepsForPackage
}
deriving (Show, Generic)
instance ToJSON NpmDepsForPackage
instance FromJSON NpmDepsForPackage
data NpmDepsForWasp = NpmDepsForWasp
{ waspDependencies :: [D.Dependency],
waspDevDependencies :: [D.Dependency]
@ -54,12 +58,11 @@ data NpmDepsForUser = NpmDepsForUser
{ userDependencies :: [D.Dependency],
userDevDependencies :: [D.Dependency]
}
deriving (Show)
deriving (Show, Eq, Generic)
instance ToJSON NpmDepsForPackage where
toEncoding = genericToEncoding defaultOptions
instance ToJSON NpmDepsForUser
instance FromJSON NpmDepsForPackage
instance FromJSON NpmDepsForUser
data NpmDepsForPackageError = NpmDepsForPackageError
{ dependenciesConflictErrors :: [DependencyConflictError],
@ -89,12 +92,12 @@ genNpmDepsForPackage spec npmDepsForWasp =
++ devDependenciesConflictErrors conflictErrorDeps
)
buildNpmDepsForFullStack :: AppSpec -> NpmDepsForWasp -> NpmDepsForWasp -> Either String NpmDepsForFullStack
buildNpmDepsForFullStack spec forServer forWebApp =
buildWaspFrameworkNpmDeps :: AppSpec -> NpmDepsForWasp -> NpmDepsForWasp -> Either String NpmDepsForFramework
buildWaspFrameworkNpmDeps spec forServer forWebApp =
case (combinedServerDeps, combinedWebAppDeps) of
(Right a, Right b) ->
Right
NpmDepsForFullStack
NpmDepsForFramework
{ npmDepsForServer = a,
npmDepsForWebApp = b
}
@ -135,6 +138,12 @@ sortedDependencies a = (sort $ dependencies a, sort $ devDependencies a)
-- to combine them together, returning (Right) a new NpmDepsForPackage
-- that combines them, and on error (Left), returns a NpmDepsForPackageError
-- which describes which dependencies are in conflict.
-- TODO: The comment above and function name are not exactly correct any more,
-- as user deps don't get combined with the wasp deps any more, instead user deps
-- are just checked against wasp deps to see if there are any conflicts, and then
-- wasp deps are more or less returned as they are (maybe with some changes? But certainly no user deps added).
-- This function deserves rewriting / rethinking. This should be addressed while solving
-- GH issue https://github.com/wasp-lang/wasp/issues/1644 .
combineNpmDepsForPackage :: NpmDepsForWasp -> NpmDepsForUser -> Either NpmDepsForPackageError NpmDepsForPackage
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser =
if null conflictErrors && null devConflictErrors

View File

@ -1,114 +1,136 @@
module Wasp.Generator.NpmInstall
( isNpmInstallNeeded,
installNpmDependenciesWithInstallRecord,
( installNpmDependenciesWithInstallRecord,
)
where
import Control.Concurrent (Chan, newChan, readChan, threadDelay, writeChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad (when)
import Control.Monad.Except (MonadError (throwError), runExceptT)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as B
import Data.Function ((&))
import Data.Functor ((<&>))
import qualified Data.Text as T
import StrongPath (Abs, Dir, File', Path', Rel, relfile, (</>))
import Debug.Pretty.Simple (pTrace)
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import System.Directory (doesFileExist, removeFile)
import System.Exit (ExitCode (..))
import UnliftIO (race)
import Wasp.AppSpec (AppSpec)
import Wasp.AppSpec (AppSpec (waspProjectDir))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Job (Job, JobMessage, JobType)
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO.PrefixedWriter (PrefixedWriter, printJobMessagePrefixed, runPrefixedWriter)
import Wasp.Generator.Monad (GeneratorError (..), GeneratorWarning (..))
import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.Monad (GeneratorError (..))
import Wasp.Generator.NpmInstall.Common (AllNpmDeps (..), getAllNpmDeps)
import Wasp.Generator.NpmInstall.InstalledNpmDepsLog (forgetInstalledNpmDepsLog, loadInstalledNpmDepsLog, saveInstalledNpmDepsLog)
import qualified Wasp.Generator.SdkGenerator as SdkGenerator
import Wasp.Generator.ServerGenerator as SG
import qualified Wasp.Generator.ServerGenerator.Setup as ServerSetup
import Wasp.Generator.WebAppGenerator as WG
import qualified Wasp.Generator.WebAppGenerator.Setup as WebAppSetup
import Wasp.Project.Common (WaspProjectDir)
-- | Figure out if npm install is needed.
--
-- Redundant npm installs can be avoided if the dependencies specified
-- by the user and wasp have not changed since the last time this ran.
--
-- Npm instal is needed only if the dependencies described in the user wasp file are
-- different from the dependencies that we just installed. To this end, this
-- code keeps track of the dependencies installed with a metadata file, which
-- it updates after each install.
--
-- NOTE: we assume that the dependencies in package.json are the same as the
-- ones we derive from the AppSpec. We derive them the same way but it does
-- involve different code paths.
-- This module could work in an completely different way, independently
-- from AppSpec at all. It could work by ensuring a `npm install` is
-- consistent with a metadata file derived from `package.json` during its
-- previous run. This would be more decoupled from the rest of the system.
-- Npm conflict handling could be ignored in that case, because it would work
-- from the record of what's in package.json.
isNpmInstallNeeded :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Either String (Maybe N.NpmDepsForFullStack))
isNpmInstallNeeded spec dstDir = do
let errorOrNpmDepsForFullStack = N.buildNpmDepsForFullStack spec (SG.npmDepsForWasp spec) (WG.npmDepsForWasp spec)
case errorOrNpmDepsForFullStack of
Left message -> return $ Left $ "determining npm deps to install failed: " ++ message
Right npmDepsForFullStack -> do
isInstallNeeded <- isNpmInstallDifferent npmDepsForFullStack dstDir
return $
Right $
if isInstallNeeded
then Just npmDepsForFullStack
else Nothing
-- Run npm install for desired AppSpec dependencies, recording what we installed
-- Installation may fail, in which the installation record is removed.
-- Runs `npm install` for:
-- 1. User's Wasp project (based on their package.json): user deps.
-- 2. Wasp's generated webapp project: wasp deps.
-- 3. Wasp's generated server project: wasp deps.
-- (1) runs first, (2) and (3) run concurrently after it.
-- It collects the output produced by these commands to pass them along to IO with a prefix.
installNpmDependenciesWithInstallRecord ::
N.NpmDepsForFullStack ->
Path' Abs (Dir WaspProjectDir) ->
AppSpec ->
Path' Abs (Dir ProjectRootDir) ->
IO ([GeneratorWarning], [GeneratorError])
installNpmDependenciesWithInstallRecord npmDepsForFullStack waspProjectDir dstDir = do
-- in case anything fails during installation that would leave node modules in
-- a broken state, we remove the file before we start npm install
fileExists <- doesFileExist dependenciesInstalledFp
when fileExists $ removeFile dependenciesInstalledFp
-- now actually do the installation
npmInstallResult <- installNpmDependencies waspProjectDir dstDir
case npmInstallResult of
Left npmInstallError -> do
return ([], [GenericGeneratorError $ "npm install failed: " ++ npmInstallError])
Right () -> do
-- on successful npm install, record what we installed
B.writeFile dependenciesInstalledFp (Aeson.encode npmDepsForFullStack)
return ([], [])
IO (Either GeneratorError ())
installNpmDependenciesWithInstallRecord spec dstDir = runExceptT $ do
messagesChan <- liftIO newChan
allNpmDeps <- getAllNpmDeps spec & onLeftThrowError
liftIO (areThereNpmDepsToInstall allNpmDeps dstDir) >>= \case
False -> pure ()
True -> do
-- In case anything fails during installation that would leave node modules in
-- a broken state, we remove the log of installed npm deps before we start npm install.
liftIO $ forgetInstalledNpmDepsLog dstDir
liftIO (installProjectNpmDependencies messagesChan (waspProjectDir spec))
>>= onLeftThrowError
liftIO (installWebAppAndServerNpmDependencies messagesChan dstDir)
>>= onLeftThrowError
liftIO $ saveInstalledNpmDepsLog allNpmDeps dstDir
pure ()
where
dependenciesInstalledFp = SP.fromAbsFile $ dstDir </> installedFullStackNpmDependenciesFileInProjectRootDir
onLeftThrowError =
either (\e -> throwError $ GenericGeneratorError $ "npm install failed: " ++ e) pure
-- Returns True only if the stored full stack dependencies are different from the
-- the full stack dependencies in the argument. If an installation record is missing
-- then it's always different.
isNpmInstallDifferent :: N.NpmDepsForFullStack -> Path' Abs (Dir ProjectRootDir) -> IO Bool
isNpmInstallDifferent appSpecFullStackNpmDependencies dstDir = do
installedFullStackNpmDependencies <- loadInstalledFullStackNpmDependencies dstDir
return $ Just appSpecFullStackNpmDependencies /= installedFullStackNpmDependencies
-- Installs npm dependencies from the user's package.json, by running `npm install` .
installProjectNpmDependencies ::
Chan JobMessage -> SP.Path SP.System Abs (Dir WaspProjectDir) -> IO (Either String ())
installProjectNpmDependencies messagesChan projectDir =
handleProjectInstallMessages messagesChan `concurrently` installProjectDepsJob
<&> snd
<&> \case
ExitFailure code -> Left $ "Project setup failed with exit code " ++ show code ++ "."
_success -> Right ()
where
installProjectDepsJob =
installNpmDependenciesAndReport (SdkGenerator.installNpmDependencies projectDir) messagesChan J.Wasp
handleProjectInstallMessages :: Chan J.JobMessage -> IO ()
handleProjectInstallMessages = runPrefixedWriter . processMessages
where
processMessages :: Chan J.JobMessage -> PrefixedWriter ()
processMessages chan = do
jobMsg <- liftIO $ readChan chan
case J._data jobMsg of
J.JobOutput {} -> printJobMessagePrefixed jobMsg >> processMessages chan
J.JobExit {} -> return ()
-- TODO: we probably want to put this in a `waspmeta` directory in the future
installedFullStackNpmDependenciesFileInProjectRootDir :: Path' (Rel ProjectRootDir) File'
installedFullStackNpmDependenciesFileInProjectRootDir = [relfile|installedFullStackNpmDependencies.json|]
-- Install npm dependencies for the Wasp's generated webapp and server projects.
installWebAppAndServerNpmDependencies ::
Chan JobMessage -> SP.Path SP.System Abs (Dir ProjectRootDir) -> IO (Either String ())
installWebAppAndServerNpmDependencies messagesChan dstDir =
handleSetupJobsMessages messagesChan
`concurrently` (installServerDepsJob `concurrently` installWebAppDepsJob)
<&> snd
<&> \case
(ExitSuccess, ExitSuccess) -> Right ()
exitCodes -> Left $ setupFailedMessage exitCodes
where
installServerDepsJob = installNpmDependenciesAndReport (ServerSetup.installNpmDependencies dstDir) messagesChan J.Server
installWebAppDepsJob = installNpmDependenciesAndReport (WebAppSetup.installNpmDependencies dstDir) messagesChan J.WebApp
-- Load the record of the dependencies we installed from disk.
loadInstalledFullStackNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe N.NpmDepsForFullStack)
loadInstalledFullStackNpmDependencies dstDir = do
let dependenciesInstalledFp = SP.fromAbsFile $ dstDir </> installedFullStackNpmDependenciesFileInProjectRootDir
fileExists <- doesFileExist dependenciesInstalledFp
if fileExists
then do
fileContents <- B.readFile dependenciesInstalledFp
return (Aeson.decode fileContents :: Maybe N.NpmDepsForFullStack)
else return Nothing
handleSetupJobsMessages = runPrefixedWriter . processMessages (False, False)
where
processMessages :: (Bool, Bool) -> Chan J.JobMessage -> PrefixedWriter ()
processMessages (True, True) _ = return ()
processMessages (isWebAppDone, isServerDone) chan = do
jobMsg <- liftIO $ readChan chan
case J._data jobMsg of
J.JobOutput {} ->
printJobMessagePrefixed jobMsg
>> processMessages (isWebAppDone, isServerDone) chan
J.JobExit {} -> case J._jobType jobMsg of
J.WebApp -> processMessages (True, isServerDone) chan
J.Server -> processMessages (isWebAppDone, True) chan
J.Db -> error "This should never happen. No Db job should be active."
J.Wasp -> error "This should never happen. No Wasp job should be active."
setupFailedMessage (serverExitCode, webAppExitCode) =
let serverErrorMessage = case serverExitCode of
ExitFailure code -> " Server setup failed with exit code " ++ show code ++ "."
_success -> ""
webAppErrorMessage = case webAppExitCode of
ExitFailure code -> " Web app setup failed with exit code " ++ show code ++ "."
_success -> ""
in "Setup failed!" ++ serverErrorMessage ++ webAppErrorMessage
installNpmDependenciesAndReport :: Job -> Chan JobMessage -> JobType -> IO ExitCode
installNpmDependenciesAndReport installJob chan jobType = do
writeChan chan $ J.JobMessage {J._data = J.JobOutput "Starting npm install\n" J.Stdout, J._jobType = jobType}
result <- installJob chan `race` reportInstallationProgress chan jobType
case result of
Left exitCode -> return exitCode
Right _ -> error "This should never happen, reporting installation progress should run forever."
reportInstallationProgress :: Chan JobMessage -> JobType -> IO ()
reportInstallationProgress chan jobType = reportPeriodically allPossibleMessages
@ -130,78 +152,21 @@ reportInstallationProgress chan jobType = reportPeriodically allPossibleMessages
"You've been waiting so patiently, just wait a little longer (for the installation to finish)..."
]
installNpmDependenciesAndReport :: Job -> Chan JobMessage -> JobType -> IO ExitCode
installNpmDependenciesAndReport installJob chan jobType = do
writeChan chan $ J.JobMessage {J._data = J.JobOutput "Starting npm install\n" J.Stdout, J._jobType = jobType}
result <- installJob chan `race` reportInstallationProgress chan jobType
case result of
Left exitCode -> return exitCode
Right _ -> error "This should never happen, reporting installation progress should run forever."
{- HLINT ignore installNpmDependencies "Redundant <$>" -}
-- Run the individual `npm install` commands for both server and webapp projects
-- It runs these concurrently, collects the output produced by these commands
-- to pass them along to IO with a prefix
installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> Path' Abs (Dir ProjectRootDir) -> IO (Either String ())
installNpmDependencies projectDir dstDir = do
messagesChan <- newChan
installProjectNpmDependencies messagesChan projectDir >>= \case
ExitFailure code -> return $ Left $ "Project setup failed with exit code " ++ show code ++ "."
_success -> do
installWebAppAndServerNpmDependencies messagesChan dstDir <&> \case
(ExitSuccess, ExitSuccess) -> Right ()
exitCodes -> Left $ setupFailedMessage exitCodes
where
setupFailedMessage (serverExitCode, webAppExitCode) =
let serverErrorMessage = case serverExitCode of
ExitFailure code -> " Server setup failed with exit code " ++ show code ++ "."
_success -> ""
webAppErrorMessage = case webAppExitCode of
ExitFailure code -> " Web app setup failed with exit code " ++ show code ++ "."
_success -> ""
in "Setup failed!" ++ serverErrorMessage ++ webAppErrorMessage
installProjectNpmDependencies ::
Chan JobMessage -> SP.Path SP.System Abs (Dir WaspProjectDir) -> IO ExitCode
installProjectNpmDependencies messagesChan projectDir =
snd <$> handleProjectInstallMessages messagesChan `concurrently` installProjectDepsJob
where
installProjectDepsJob =
installNpmDependenciesAndReport
(SdkGenerator.installNpmDependencies projectDir)
messagesChan
J.Wasp
handleProjectInstallMessages :: Chan J.JobMessage -> IO ()
handleProjectInstallMessages = runPrefixedWriter . processMessages
where
processMessages :: Chan J.JobMessage -> PrefixedWriter ()
processMessages chan = do
jobMsg <- liftIO $ readChan chan
case J._data jobMsg of
J.JobOutput {} -> printJobMessagePrefixed jobMsg >> processMessages chan
J.JobExit {} -> return ()
installWebAppAndServerNpmDependencies ::
Chan JobMessage -> SP.Path SP.System Abs (Dir ProjectRootDir) -> IO (ExitCode, ExitCode)
installWebAppAndServerNpmDependencies messagesChan dstDir =
snd <$> handleSetupJobsMessages messagesChan `concurrently` (installServerDepsJob `concurrently` installWebAppDepsJob)
where
installServerDepsJob = installNpmDependenciesAndReport (ServerSetup.installNpmDependencies dstDir) messagesChan J.Server
installWebAppDepsJob = installNpmDependenciesAndReport (WebAppSetup.installNpmDependencies dstDir) messagesChan J.WebApp
handleSetupJobsMessages = runPrefixedWriter . processMessages (False, False)
where
processMessages :: (Bool, Bool) -> Chan J.JobMessage -> PrefixedWriter ()
processMessages (True, True) _ = return ()
processMessages (isWebAppDone, isServerDone) chan = do
jobMsg <- liftIO $ readChan chan
case J._data jobMsg of
J.JobOutput {} ->
printJobMessagePrefixed jobMsg
>> processMessages (isWebAppDone, isServerDone) chan
J.JobExit {} -> case J._jobType jobMsg of
J.WebApp -> processMessages (True, isServerDone) chan
J.Server -> processMessages (isWebAppDone, True) chan
J.Db -> error "This should never happen. No Db job should be active."
J.Wasp -> error "This should never happen. No Wasp job should be active."
-- | Figure out if installation of npm deps is needed, be it for user npm deps (top level
-- package.json), for wasp framework npm deps (web app, server), or for wasp sdk npm deps.
--
-- To this end, this code keeps track of the dependencies installed with a metadata file, which
-- it updates after each install.
--
-- TODO(martin): Here, we do a single check for all the deps. This means we don't know if user deps
-- or wasp sdk deps or wasp framework deps need installing, and so the user of this function will
-- likely run `npm install` for all of them, which means 3 times (for user npm deps (+ wasp sdk
-- deps, those are all done with single npm install), for wasp webapp npm deps, for wasp server
-- npm deps). We could, relatively easily, since we already differentiate all these deps, return
-- exact info on which deps need installation, and therefore run only needed npm installs. We
-- could return such info by either returning a triple (Bool, Bool, Bool) for (user+sdk, webapp,
-- server) deps, or we could return a list of enum which says which deps to install.
areThereNpmDepsToInstall :: AllNpmDeps -> Path' Abs (Dir ProjectRootDir) -> IO Bool
areThereNpmDepsToInstall allNpmDeps dstDir = do
installedNpmDeps <- loadInstalledNpmDepsLog dstDir
return $ installedNpmDeps /= Just allNpmDeps

View File

@ -0,0 +1,42 @@
{-# LANGUAGE DeriveGeneric #-}
module Wasp.Generator.NpmInstall.Common
( AllNpmDeps (..),
getAllNpmDeps,
)
where
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.Generator.NpmDependencies as N
import qualified Wasp.Generator.SdkGenerator as SdkGenerator
import qualified Wasp.Generator.ServerGenerator as SG
import qualified Wasp.Generator.WebAppGenerator as WG
data AllNpmDeps = AllNpmDeps
{ _userNpmDeps :: !N.NpmDepsForUser, -- Deps coming from user's package.json .
_waspFrameworkNpmDeps :: !N.NpmDepsForFramework, -- Deps coming from Wasp's framework code (webapp, server) package.jsons.
_waspSdkNpmDeps :: !N.NpmDepsForPackage -- Deps coming from Wasp's SDK's package.json .
}
deriving (Eq, Show, Generic)
instance ToJSON AllNpmDeps
instance FromJSON AllNpmDeps
getAllNpmDeps :: AppSpec -> Either String AllNpmDeps
getAllNpmDeps spec =
let userNpmDeps = N.getUserNpmDepsForPackage spec
errorOrWaspFrameworkNpmDeps =
N.buildWaspFrameworkNpmDeps spec (SG.npmDepsForWasp spec) (WG.npmDepsForWasp spec)
waspSdkNpmDeps = SdkGenerator.npmDepsForSdk spec
in case errorOrWaspFrameworkNpmDeps of
Left message -> Left $ "determining npm deps to install failed: " ++ message
Right waspFrameworkNpmDeps ->
Right $
AllNpmDeps
{ _userNpmDeps = userNpmDeps,
_waspFrameworkNpmDeps = waspFrameworkNpmDeps,
_waspSdkNpmDeps = waspSdkNpmDeps
}

View File

@ -0,0 +1,42 @@
module Wasp.Generator.NpmInstall.InstalledNpmDepsLog
( loadInstalledNpmDepsLog,
saveInstalledNpmDepsLog,
forgetInstalledNpmDepsLog,
)
where
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as B
import StrongPath (Abs, Dir, File', Path', Rel, relfile, (</>))
import qualified StrongPath as SP
import System.Directory (doesFileExist)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.NpmInstall.Common (AllNpmDeps)
import Wasp.Util.IO (deleteFileIfExists)
-- Load the log of the npm dependencies we installed, from disk.
loadInstalledNpmDepsLog :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe AllNpmDeps)
loadInstalledNpmDepsLog dstDir = do
fileExists <- doesFileExist $ SP.fromAbsFile logFilePath
if fileExists
then do
fileContents <- B.readFile $ SP.fromAbsFile logFilePath
return (Aeson.decode fileContents :: Maybe AllNpmDeps)
else return Nothing
where
logFilePath = getInstalledNpmDepsLogFilePath dstDir
-- Save the record of the Wasp's (webapp + server) npm dependencies we installed, to disk.
saveInstalledNpmDepsLog :: AllNpmDeps -> Path' Abs (Dir ProjectRootDir) -> IO ()
saveInstalledNpmDepsLog deps dstDir =
B.writeFile (SP.fromAbsFile $ getInstalledNpmDepsLogFilePath dstDir) (Aeson.encode deps)
forgetInstalledNpmDepsLog :: Path' Abs (Dir ProjectRootDir) -> IO ()
forgetInstalledNpmDepsLog dstDir =
deleteFileIfExists $ getInstalledNpmDepsLogFilePath dstDir
getInstalledNpmDepsLogFilePath :: Path' Abs (Dir ProjectRootDir) -> Path' Abs File'
getInstalledNpmDepsLogFilePath dstDir = dstDir </> installedNpmDepsLogFileInProjectRootDir
installedNpmDepsLogFileInProjectRootDir :: Path' (Rel ProjectRootDir) File'
installedNpmDepsLogFileInProjectRootDir = [relfile|installedNpmDepsLog.json|]

View File

@ -43,39 +43,42 @@ genPackageJson spec =
[relfile|package.json|]
( Just $
object
[ "depsChunk" .= N.getDependenciesPackageJsonEntry npmDepsForSdk,
"devDepsChunk" .= N.getDevDependenciesPackageJsonEntry npmDepsForSdk
[ "depsChunk" .= N.getDependenciesPackageJsonEntry npmDeps,
"devDepsChunk" .= N.getDevDependenciesPackageJsonEntry npmDeps
]
)
where
npmDepsForSdk =
N.NpmDepsForPackage
{ N.dependencies =
AS.Dependency.fromList
[ ("@prisma/client", show prismaVersion),
("prisma", show prismaVersion),
("@tanstack/react-query", "^4.29.0"),
("axios", "^1.4.0"),
("express", "~4.18.1"),
("jsonwebtoken", "^8.5.1"),
("mitt", "3.0.0"),
("react", "^18.2.0"),
("react-router-dom", "^5.3.3"),
("react-hook-form", "^7.45.4"),
("secure-password", "^4.0.0"),
("superjson", "^1.12.2"),
("@types/express-serve-static-core", "^4.17.13")
]
++ depsRequiredForAuth spec
-- This must be installed in the SDK because it lists prisma/client as a dependency.
-- Installing it inside .wasp/out/server/node_modules would also
-- install prisma/client in the same folder, which would cause our
-- runtime to load the wrong (uninitialized prisma/client)
-- TODO(filip): Find a better way to handle duplicate
-- dependencies: https://github.com/wasp-lang/wasp/issues/1640
++ ServerAuthG.depsRequiredByAuth spec,
N.devDependencies = AS.Dependency.fromList []
}
npmDeps = npmDepsForSdk spec
npmDepsForSdk :: AppSpec -> N.NpmDepsForPackage
npmDepsForSdk spec =
N.NpmDepsForPackage
{ N.dependencies =
AS.Dependency.fromList
[ ("@prisma/client", show prismaVersion),
("prisma", show prismaVersion),
("@tanstack/react-query", "^4.29.0"),
("axios", "^1.4.0"),
("express", "~4.18.1"),
("jsonwebtoken", "^8.5.1"),
("mitt", "3.0.0"),
("react", "^18.2.0"),
("react-router-dom", "^5.3.3"),
("react-hook-form", "^7.45.4"),
("secure-password", "^4.0.0"),
("superjson", "^1.12.2"),
("@types/express-serve-static-core", "^4.17.13")
]
++ depsRequiredForAuth spec
-- This must be installed in the SDK because it lists prisma/client as a dependency.
-- Installing it inside .wasp/out/server/node_modules would also
-- install prisma/client in the same folder, which would cause our
-- runtime to load the wrong (uninitialized prisma/client)
-- TODO(filip): Find a better way to handle duplicate
-- dependencies: https://github.com/wasp-lang/wasp/issues/1640
++ ServerAuthG.depsRequiredByAuth spec,
N.devDependencies = AS.Dependency.fromList []
}
depsRequiredForAuth :: AppSpec -> [AS.Dependency.Dependency]
depsRequiredForAuth spec =

View File

@ -5,30 +5,20 @@ where
import Control.Monad (when)
import StrongPath (Abs, Dir, Path')
import Wasp.AppSpec (AppSpec (waspProjectDir))
import Wasp.AppSpec (AppSpec)
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Generator.DbGenerator as DbGenerator
import Wasp.Generator.Monad (GeneratorError (..), GeneratorWarning (..))
import Wasp.Generator.NpmInstall (installNpmDependenciesWithInstallRecord, isNpmInstallNeeded)
import Wasp.Generator.NpmInstall (installNpmDependenciesWithInstallRecord)
import qualified Wasp.Message as Msg
runSetup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError])
runSetup spec dstDir sendMessage = do
runNpmInstallIfNeeded spec dstDir sendMessage >>= \case
npmInstallResults@(_, []) -> (npmInstallResults <>) <$> setUpDatabase spec dstDir sendMessage
npmInstallResults -> return npmInstallResults
runNpmInstallIfNeeded :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError])
runNpmInstallIfNeeded spec dstDir sendMessage = do
isNpmInstallNeeded spec dstDir >>= \case
Left errorMessage -> return ([], [GenericGeneratorError errorMessage])
Right maybeFullStackDeps -> case maybeFullStackDeps of
Nothing -> return ([], [])
Just fullStackDeps -> do
(npmInstallWarnings, npmInstallErrors) <-
installNpmDependenciesWithInstallRecord fullStackDeps (waspProjectDir spec) dstDir
when (null npmInstallErrors) (sendMessage $ Msg.Success "Successfully completed npm install.")
return (npmInstallWarnings, npmInstallErrors)
installNpmDependenciesWithInstallRecord spec dstDir >>= \case
Right () -> do
sendMessage $ Msg.Success "Successfully completed npm install."
setUpDatabase spec dstDir sendMessage
Left e -> return ([], [e])
setUpDatabase :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> Msg.SendMessage -> IO ([GeneratorWarning], [GeneratorError])
setUpDatabase spec dstDir sendMessage = do

View File

@ -1,19 +1,22 @@
module Wasp.Project.Common
( findFileInWaspProjectDir,
CompileError,
CompileWarning,
WaspProjectDir,
packageJsonInWaspProjectDir,
( WaspProjectDir,
DotWaspDir,
NodeModulesDir,
findFileInWaspProjectDir,
dotWaspDirInWaspProjectDir,
dotWaspRootFileInWaspProjectDir,
dotWaspInfoFileInGeneratedCodeDir,
extServerCodeDirInWaspProjectDir,
extClientCodeDirInWaspProjectDir,
extSharedCodeDirInWaspProjectDir,
generatedCodeDirInDotWaspDir,
buildDirInDotWaspDir,
waspProjectDirFromProjectRootDir,
dotWaspRootFileInWaspProjectDir,
dotWaspInfoFileInGeneratedCodeDir,
srcDirInWaspProjectDir,
extServerCodeDirInWaspProjectDir,
extClientCodeDirInWaspProjectDir,
extSharedCodeDirInWaspProjectDir,
packageJsonInWaspProjectDir,
nodeModulesDirInWaspProjectDir,
CompileError,
CompileWarning,
)
where
@ -26,6 +29,8 @@ data WaspProjectDir -- Root dir of Wasp project, containing source files.
data DotWaspDir -- Here we put everything that wasp generates.
data NodeModulesDir
-- | NOTE: If you change the depth of this path, also update @waspProjectDirFromProjectRootDir@ below.
-- TODO: SHould this be renamed to include word "root"?
dotWaspDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir DotWaspDir)
@ -53,18 +58,24 @@ dotWaspRootFileInWaspProjectDir = [relfile|.wasproot|]
dotWaspInfoFileInGeneratedCodeDir :: Path' (Rel Wasp.Generator.Common.ProjectRootDir) File'
dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|]
srcDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir)
srcDirInWaspProjectDir = [reldir|src|]
extServerCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir)
extServerCodeDirInWaspProjectDir = [reldir|src|]
extServerCodeDirInWaspProjectDir = srcDirInWaspProjectDir
extClientCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir)
extClientCodeDirInWaspProjectDir = [reldir|src|]
extClientCodeDirInWaspProjectDir = srcDirInWaspProjectDir
extSharedCodeDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalCodeDir)
extSharedCodeDirInWaspProjectDir = [reldir|src|]
extSharedCodeDirInWaspProjectDir = srcDirInWaspProjectDir
packageJsonInWaspProjectDir :: Path' (Rel WaspProjectDir) File'
packageJsonInWaspProjectDir = [relfile|package.json|]
nodeModulesDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir NodeModulesDir)
nodeModulesDirInWaspProjectDir = [reldir|node_modules|]
findFileInWaspProjectDir ::
Path' Abs (Dir WaspProjectDir) ->
Path' (Rel WaspProjectDir) File' ->

View File

@ -6,10 +6,12 @@ module Wasp.Util.IO
deleteDirectoryIfExists,
deleteFileIfExists,
doesFileExist,
doesDirectoryExist,
readFile,
readFileStrict,
writeFile,
removeFile,
removeDirectory,
tryReadFile,
isDirectoryEmpty,
writeFileFromText,
@ -17,7 +19,7 @@ module Wasp.Util.IO
)
where
import Control.Monad (filterM, when)
import Control.Monad (filterM)
import Control.Monad.Extra (whenM)
import qualified Data.ByteString.Lazy as B
import Data.Text (Text)
@ -85,10 +87,8 @@ listDirectory absDirPath = do
-- with relative paths, define a new function (e.g., `readFileRel`).
deleteDirectoryIfExists :: Path' Abs (Dir d) -> IO ()
deleteDirectoryIfExists dirPath = do
let dirPathStr = SP.fromAbsDir dirPath
exists <- SD.doesDirectoryExist dirPathStr
when exists $ SD.removeDirectoryRecursive dirPathStr
deleteDirectoryIfExists dirPath =
whenM (doesDirectoryExist dirPath) (removeDirectory dirPath)
deleteFileIfExists :: Path' Abs (File f) -> IO ()
deleteFileIfExists filePath = whenM (doesFileExist filePath) $ removeFile filePath
@ -96,6 +96,9 @@ deleteFileIfExists filePath = whenM (doesFileExist filePath) $ removeFile filePa
doesFileExist :: Path' Abs (File f) -> IO Bool
doesFileExist = SD.doesFileExist . SP.fromAbsFile
doesDirectoryExist :: Path' Abs (Dir f) -> IO Bool
doesDirectoryExist = SD.doesDirectoryExist . SP.fromAbsDir
readFile :: Path' Abs (File f) -> IO String
readFile = P.readFile . SP.fromAbsFile
@ -114,6 +117,9 @@ writeFileFromText = T.IO.writeFile . SP.fromAbsFile
removeFile :: Path' Abs (File f) -> IO ()
removeFile = SD.removeFile . SP.fromAbsFile
removeDirectory :: Path' Abs (Dir d) -> IO ()
removeDirectory = SD.removeDirectoryRecursive . SP.fromAbsDir
tryReadFile :: FilePath -> IO (Maybe Text)
tryReadFile fp =
(Just <$> Text.IO.readFile fp)

View File

@ -199,7 +199,7 @@ spec_Evaluator = do
let typeDefs = TD.addDeclType @Special $ TD.empty
let source =
[ "special Test {",
" imps: [import { field } from \"@server/main.js\", import main from \"@server/main.js\"],",
" imps: [import { field } from \"@src/main.js\", import main from \"@src/main.js\"],",
" json: {=json { \"key\": 1 } json=}",
"}"
]

View File

@ -9,7 +9,6 @@ import Data.List (intercalate)
import Data.Maybe (fromJust)
import qualified StrongPath as SP
import Test.Tasty.Hspec
import qualified Wasp.AI.GenerateNewProject.Common as Auth
import Wasp.Analyzer
import Wasp.Analyzer.Parser (Ctx)
import qualified Wasp.Analyzer.TypeChecker as TC
@ -18,7 +17,6 @@ import qualified Wasp.AppSpec.App as App
import qualified Wasp.AppSpec.App.Auth as Auth
import qualified Wasp.AppSpec.App.Client as Client
import qualified Wasp.AppSpec.App.Db as Db
import qualified Wasp.AppSpec.App.Dependency as Dependency
import qualified Wasp.AppSpec.App.EmailSender as EmailSender
import qualified Wasp.AppSpec.App.Server as Server
import qualified Wasp.AppSpec.App.Wasp as Wasp
@ -51,25 +49,22 @@ spec_Analyzer = do
" userEntity: User,",
" methods: {",
" usernameAndPassword: {",
" userSignupFields: import { getUserFields } from \"@server/auth/signup.js\",",
" userSignupFields: import { getUserFields } from \"@src/auth/signup.js\",",
" }",
" },",
" onAuthFailedRedirectTo: \"/\",",
" },",
" dependencies: [",
" (\"redux\", \"^4.0.5\")",
" ],",
" server: {",
" setupFn: import { setupServer } from \"@server/bar.js\"",
" setupFn: import { setupServer } from \"@src/bar.js\"",
" },",
" client: {",
" rootComponent: import { App } from \"@client/App.jsx\",",
" setupFn: import { setupClient } from \"@client/baz.js\",",
" rootComponent: import { App } from \"@src/App.jsx\",",
" setupFn: import { setupClient } from \"@src/baz.js\",",
" baseDir: \"/\"",
" },",
" db: {",
" system: PostgreSQL,",
" seeds: [ import { devSeedSimple } from \"@server/dbSeeds.js\" ],",
" seeds: [ import { devSeedSimple } from \"@src/dbSeeds.js\" ],",
" prisma: {",
" clientPreviewFeatures: [\"extendedWhereUnique\"],",
" dbExtensions: [{ name: \"pg_trgm\", version: \"1.0.0\" }]",
@ -89,23 +84,23 @@ spec_Analyzer = do
"psl=}",
"",
"page HomePage {",
" component: import Home from \"@client/pages/Main\"",
" component: import Home from \"@src/pages/Main\"",
"}",
"",
"page ProfilePage {",
" component: import { profilePage } from \"@client/pages/Profile\",",
" component: import { profilePage } from \"@src/pages/Profile\",",
" authRequired: true",
"}",
"",
"route HomeRoute { path: \"/\", to: HomePage }",
"",
"query getUsers {",
" fn: import { getAllUsers } from \"@server/foo.js\",",
" fn: import { getAllUsers } from \"@src/foo.js\",",
" entities: [User]",
"}",
"",
"action updateUser {",
" fn: import { updateUser } from \"@server/foo.js\",",
" fn: import { updateUser } from \"@src/foo.js\",",
" entities: [User],",
" auth: true",
"}",
@ -113,7 +108,7 @@ spec_Analyzer = do
"job BackgroundJob {",
" executor: PgBoss,",
" perform: {",
" fn: import { backgroundJob } from \"@server/jobs/baz.js\",",
" fn: import { backgroundJob } from \"@src/jobs/baz.js\",",
" executorOptions: {",
" pgBoss: {=json { \"retryLimit\": 1 } json=}",
" }",
@ -155,10 +150,6 @@ spec_Analyzer = do
Auth.onAuthFailedRedirectTo = "/",
Auth.onAuthSucceededRedirectTo = Nothing
},
App.dependencies =
Just
[ Dependency.Dependency {Dependency.name = "redux", Dependency.version = "^4.0.5"}
],
App.server =
Just
Server.Server
@ -350,7 +341,7 @@ spec_Analyzer = do
let source =
unlines
[ "route HomeRoute { path: \"/\", to: HomePage }",
"page HomePage { component: import Home from \"@client/HomePage.js\" }"
"page HomePage { component: import Home from \"@src/HomePage.js\" }"
]
isRight (analyze source) `shouldBe` True
@ -360,22 +351,22 @@ spec_Analyzer = do
unlines
[ "app MyApp {",
" title: \"My app\",",
" dependencies: [",
" (\"bar\", 13),",
" (\"foo\", 14)",
" ]",
" db: {",
" seeds: [ (\"foo\", \"bar\") ],",
" }",
"}"
]
analyze source
`errorMessageShouldBe` ( ctx (4, 5) (4, 15),
`errorMessageShouldBe` ( ctx (4, 14) (4, 27),
intercalate
"\n"
[ "Type error:",
" Expected type: (string, string)",
" Actual type: (string, number)",
" Expected type: external import",
" Actual type: (string, string)",
"",
" -> For dictionary field 'dependencies':",
" -> In list"
" -> For dictionary field 'db':",
" -> For dictionary field 'seeds':",
" -> In list"
]
)
@ -384,22 +375,19 @@ spec_Analyzer = do
unlines
[ "app MyApp {",
" title: \"My app\",",
" dependencies: [",
" { name: \"bar\", version: 13 },",
" { name: \"foo\", version: \"1.2.3\" }",
" ]",
" db: {",
" seeds: [ 42, \"foo\" ],",
" }",
"}"
]
analyze source
`errorMessageShouldBe` ( ctx (5, 29) (5, 35),
`errorMessageShouldBe` ( ctx (4, 18) (4, 22),
intercalate
"\n"
[ "Type error:",
" Can't mix the following types:",
" - number",
" - string",
"",
" -> For dictionary field 'version'"
" - string"
]
)

View File

@ -2,6 +2,7 @@
module AppSpec.ValidTest where
import qualified Data.Map as M
import Data.Maybe (fromJust)
import Fixtures (systemSPRoot)
import qualified StrongPath as SP
@ -17,6 +18,7 @@ import qualified Wasp.AppSpec.Core.Decl as AS.Decl
import qualified Wasp.AppSpec.Core.Ref as AS.Core.Ref
import qualified Wasp.AppSpec.Entity as AS.Entity
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import qualified Wasp.AppSpec.PackageJson as AS.PJS
import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Route as AS.Route
import qualified Wasp.AppSpec.Valid as ASV
@ -344,7 +346,6 @@ spec_AppSpecValid = do
AS.App.server = Nothing,
AS.App.client = Nothing,
AS.App.auth = Nothing,
AS.App.dependencies = Nothing,
AS.App.head = Nothing,
AS.App.emailSender = Nothing,
AS.App.webSocket = Nothing
@ -359,6 +360,12 @@ spec_AppSpecValid = do
AS.externalClientFiles = [],
AS.externalServerFiles = [],
AS.externalSharedFiles = [],
AS.packageJson =
AS.PJS.PackageJson
{ AS.PJS.name = "testApp",
AS.PJS.dependencies = M.empty,
AS.PJS.devDependencies = M.empty
},
AS.isBuild = False,
AS.migrationsDir = Nothing,
AS.devEnvVarsClient = [],

View File

@ -45,7 +45,7 @@ spec_combineNpmDepsForPackage = do
devDependenciesConflictErrors = []
}
it "wasp deps completely overlap with user deps, no duplication" $ do
it "wasp deps completely overlap with user deps: all wasp deps are dropped" $ do
let npmDepsForWasp =
NpmDepsForWasp
{ waspDependencies = waspDeps,
@ -53,26 +53,18 @@ spec_combineNpmDepsForPackage = do
}
let npmDepsForUser =
NpmDepsForUser
{ userDependencies =
D.fromList
[ ("a", "1"),
("b", "2")
],
{ userDependencies = waspDeps,
userDevDependencies = []
}
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser
`shouldBe` Right
NpmDepsForPackage
{ dependencies =
D.fromList
[ ("a", "1"),
("b", "2")
],
{ dependencies = [],
devDependencies = []
}
it "user dependencies supplement wasp dependencies" $ do
it "user dependencies have no overlap with wasp deps: wasp deps remain the same" $ do
let npmDepsForWasp =
NpmDepsForWasp
{ waspDependencies = waspDeps,
@ -91,17 +83,11 @@ spec_combineNpmDepsForPackage = do
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser
`shouldBe` Right
NpmDepsForPackage
{ dependencies =
D.fromList
[ ("a", "1"),
("b", "2"),
("c", "3"),
("d", "4")
],
{ dependencies = waspDeps,
devDependencies = []
}
it "user dependencies partially overlap wasp dependencies, so only non-overlapping supplement" $ do
it "user dependencies partially overlap wasp dependencies, so intersection gets removed from wasp deps" $ do
let npmDepsForWasp =
NpmDepsForWasp
{ waspDependencies = waspDeps,
@ -120,12 +106,7 @@ spec_combineNpmDepsForPackage = do
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser
`shouldBe` Right
NpmDepsForPackage
{ dependencies =
D.fromList
[ ("a", "1"),
("b", "2"),
("d", "4")
],
{ dependencies = D.fromList [("b", "2")],
devDependencies = []
}
@ -183,7 +164,7 @@ spec_combineNpmDepsForPackage = do
devDependenciesConflictErrors = []
}
it "dev dependencies are also combined" $ do
it "both dev deps and normal deps are same for user and wasp: all wasp deps are removed" $ do
let npmDepsForWasp =
NpmDepsForWasp
{ waspDependencies = waspDeps,
@ -192,35 +173,17 @@ spec_combineNpmDepsForPackage = do
let npmDepsForUser =
NpmDepsForUser
{ userDependencies =
D.fromList
[ ("a", "1"),
("d", "4")
],
userDevDependencies =
D.fromList
[ ("alpha", "10"),
("gamma", "30")
]
{ userDependencies = waspDeps,
userDevDependencies = waspDevDeps
}
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser
`shouldBe` Right
NpmDepsForPackage
{ dependencies =
D.fromList
[ ("a", "1"),
("b", "2"),
("d", "4")
],
devDependencies =
D.fromList
[ ("alpha", "10"),
("beta", "20"),
("gamma", "30")
]
{ dependencies = [],
devDependencies = []
}
it "wasp dev dependency overlaps with user dependency, should remain devDependency" $ do
it "wasp dev dependency overlaps with user non-dev dependency: should have no effect" $ do
let npmDepsForWasp =
NpmDepsForWasp
{ waspDependencies = waspDeps,
@ -239,16 +202,8 @@ spec_combineNpmDepsForPackage = do
combineNpmDepsForPackage npmDepsForWasp npmDepsForUser
`shouldBe` Right
NpmDepsForPackage
{ dependencies =
D.fromList
[ ("a", "1"),
("b", "2")
],
devDependencies =
D.fromList
[ ("alpha", "10"),
("beta", "20")
]
{ dependencies = waspDeps,
devDependencies = waspDevDeps
}
it "conflictErrorToMessage" $ do

View File

@ -1,5 +1,6 @@
module Generator.WebAppGeneratorTest where
import qualified Data.Map as M
import Fixtures
import qualified StrongPath as SP
import System.FilePath ((</>))
@ -8,6 +9,7 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Wasp as AS.Wasp
import qualified Wasp.AppSpec.Core.Decl as AS.Decl
import qualified Wasp.AppSpec.PackageJson as AS.PJS
import Wasp.Generator.FileDraft
import qualified Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft as CMTextFD
import qualified Wasp.Generator.FileDraft.CopyDirFileDraft as CopyDirFD
@ -39,7 +41,6 @@ spec_WebAppGenerator = do
AS.App.server = Nothing,
AS.App.client = Nothing,
AS.App.auth = Nothing,
AS.App.dependencies = Nothing,
AS.App.head = Nothing,
AS.App.emailSender = Nothing,
AS.App.webSocket = Nothing
@ -49,6 +50,12 @@ spec_WebAppGenerator = do
AS.externalClientFiles = [],
AS.externalServerFiles = [],
AS.externalSharedFiles = [],
AS.packageJson =
AS.PJS.PackageJson
{ AS.PJS.name = "testApp",
AS.PJS.dependencies = M.empty,
AS.PJS.devDependencies = M.empty
},
AS.isBuild = False,
AS.migrationsDir = Nothing,
AS.devEnvVarsServer = [],

View File

@ -293,6 +293,8 @@ library
Wasp.Generator.Monad
Wasp.Generator.NpmDependencies
Wasp.Generator.NpmInstall
Wasp.Generator.NpmInstall.Common
Wasp.Generator.NpmInstall.InstalledNpmDepsLog
Wasp.Generator.SdkGenerator
Wasp.Generator.ServerGenerator
Wasp.Generator.ServerGenerator.JsImport
@ -499,7 +501,6 @@ library cli-lib
Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating
Wasp.Cli.Command.Db
Wasp.Cli.Command.Db.Migrate
Wasp.Cli.Command.Db.Reset
Wasp.Cli.Command.Db.Seed
Wasp.Cli.Command.Db.Studio
Wasp.Cli.Command.Deps