Improve the scaffolding experience (#1140)

This commit is contained in:
Mihovil Ilakovac 2023-05-03 12:57:17 +02:00 committed by GitHub
parent 2168282473
commit 3f218fbbc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 899 additions and 308 deletions

View File

@ -8,6 +8,10 @@ We now offer the ability to customize Express middleware:
- on a per-api basis
- on a per-path basis (groups of apis)
### Interactive new project creation
We now offer an interactive way to create a new project. You can run `wasp new` and follow the prompts to create a new project. This is the recommended way to create a new project. It will ask you for the project name and to choose one of the starter templates.
## v0.10.4
### Bug fixes

View File

@ -39,7 +39,7 @@ main :: IO ()
main = withUtf8 . (`E.catch` handleInternalErrors) $ do
args <- getArgs
let commandCall = case args of
("new" : projectName : newArgs) -> Command.Call.New projectName newArgs
("new" : newArgs) -> Command.Call.New newArgs
["start"] -> Command.Call.Start
["start", "db"] -> Command.Call.StartDb
["clean"] -> Command.Call.Clean
@ -63,7 +63,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
telemetryThread <- Async.async $ runCommand $ Telemetry.considerSendingData commandCall
case commandCall of
Command.Call.New projectName newArgs -> runCommand $ createNewProject projectName newArgs
Command.Call.New newArgs -> runCommand $ createNewProject newArgs
Command.Call.Start -> runCommand start
Command.Call.StartDb -> runCommand Command.Start.Db.start
Command.Call.Clean -> runCommand clean
@ -105,7 +105,7 @@ printUsage =
"",
title "COMMANDS",
title " GENERAL",
cmd " new <name> [args] Creates a new Wasp project.",
cmd " new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.",
" OPTIONS:",
" -t|--template <template-name>",
" Check out the templates list here: https://github.com/wasp-lang/starters",
@ -130,7 +130,6 @@ printUsage =
"",
title "EXAMPLES",
" wasp new MyApp",
" wasp new MyApp -t waspello",
" wasp start",
" wasp db migrate-dev",
"",

View File

@ -0,0 +1,46 @@
module Wasp.Cli.Archive where
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import Control.Exception (SomeException, try)
import qualified Data.ByteString.Lazy as BL
import Data.Functor ((<&>))
import Data.Maybe (fromJust)
import Network.HTTP.Conduit (simpleHttp)
import Path.IO (copyDirRecur)
import StrongPath (Abs, Dir, File, Path', Rel, (</>))
import qualified StrongPath as SP
import StrongPath.Path (toPathAbsDir)
import System.FilePath (takeFileName)
import Wasp.Cli.FileSystem (withTempDir)
fetchArchiveAndCopySubdirToDisk ::
String ->
Path' (Rel r) (Dir subdir) ->
Path' Abs (Dir d) ->
IO (Either String ())
fetchArchiveAndCopySubdirToDisk archiveDownloadUrl targetFolder destinationOnDisk = do
try
( withTempDir $ \tempDir -> do
let archiveName = takeFileName archiveDownloadUrl
archiveDownloadPath = tempDir </> (fromJust . SP.parseRelFile $ archiveName)
archiveUnpackPath = tempDir
targetFolderInArchivePath = archiveUnpackPath </> targetFolder
downloadFile archiveDownloadUrl archiveDownloadPath
unpackArchive archiveDownloadPath archiveUnpackPath
copyDirRecur (toPathAbsDir targetFolderInArchivePath) (toPathAbsDir destinationOnDisk)
)
<&> either showException Right
where
downloadFile :: String -> Path' Abs (File f) -> IO ()
downloadFile downloadUrl destinationPath =
simpleHttp downloadUrl >>= BL.writeFile (SP.fromAbsFile destinationPath)
unpackArchive :: Path' Abs (File f) -> Path' Abs (Dir d) -> IO ()
unpackArchive sourceFile destinationDir =
Tar.unpack (SP.fromAbsDir destinationDir) . Tar.read . GZip.decompress
=<< BL.readFile (SP.fromAbsFile sourceFile)
showException :: SomeException -> Either String ()
showException = Left . show

View File

@ -1,7 +1,7 @@
module Wasp.Cli.Command.Call where
data Call
= New ProjectName Arguments -- project name, new args
= New Arguments
| Start
| StartDb
| Clean
@ -22,6 +22,4 @@ data Call
| Test Arguments -- "client" | "server", then test cmd passthrough args
| Unknown Arguments -- all args
type ProjectName = String
type Arguments = [String]

View File

@ -3,185 +3,64 @@ module Wasp.Cli.Command.CreateNewProject
)
where
import Control.Monad (when)
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import Data.List (intercalate)
import Data.Maybe (isJust)
import qualified Data.Text as T
import Path.IO (copyDirRecur, doesDirExist)
import StrongPath (Abs, Dir, Path, Path', System, parseAbsDir, reldir, relfile, (</>))
import StrongPath.Path (toPathAbsDir)
import System.Directory (getCurrentDirectory)
import qualified System.FilePath as FP
import System.Process (callCommand)
import Text.Printf (printf)
import UnliftIO.Exception (SomeException, try)
import Wasp.Analyzer.Parser (isValidWaspIdentifier)
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Call (Arguments, ProjectName)
import Data.Function ((&))
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Call (Arguments)
import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (parseNewProjectArgs)
import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription
( NewProjectDescription (..),
obtainNewProjectDescription,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates
( StarterTemplateName (..),
getStarterTemplateNames,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote (createProjectOnDiskFromRemoteTemplate)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Data as Data
import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.Message as Msg
import Wasp.Project (WaspProjectDir)
import qualified Wasp.SemanticVersion as SV
import Wasp.Util (indent, kebabToCamelCase, whenM)
import qualified Wasp.Util.IO as IOUtil
import qualified Wasp.Util.Terminal as Term
import qualified Wasp.Version as WV
data ProjectInfo = ProjectInfo
{ _projectName :: String,
_appName :: String,
_templateName :: Maybe String
}
-- It receives all of the arguments that were passed to the `wasp new` command.
createNewProject :: Arguments -> Command ()
createNewProject args = do
newProjectArgs <- parseNewProjectArgs args & either throwProjectCreationError return
starterTemplateNames <- liftIO getStarterTemplateNames
createNewProject :: ProjectName -> Arguments -> Command ()
createNewProject projectName newArgs = do
projectInfo <- parseProjectInfo projectName newArgs
createWaspProjectDir projectInfo
liftIO $ printGettingStartedInstructions $ _projectName projectInfo
newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplateNames
createProjectOnDisk newProjectDescription
liftIO $ printGettingStartedInstructions $ _absWaspProjectDir newProjectDescription
where
printGettingStartedInstructions :: ProjectName -> IO ()
printGettingStartedInstructions projectFolder = do
putStrLn $ Term.applyStyles [Term.Green] ("Created new Wasp app in ./" ++ projectFolder ++ " directory!")
putStrLn "To run it, do:"
putStrLn ""
putStrLn $ Term.applyStyles [Term.Bold] (" cd " ++ projectFolder)
putStrLn $ Term.applyStyles [Term.Bold] " wasp start"
-- This function assumes that the project dir is created inside the current working directory when it
-- prints the instructions.
printGettingStartedInstructions :: Path' Abs (Dir WaspProjectDir) -> IO ()
printGettingStartedInstructions absProjectDir = do
let projectFolder = init . SP.toFilePath . SP.basename $ absProjectDir
{- ORMOLU_DISABLE -}
putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectFolder ++ " directory!"
putStrLn "To run it, do:"
putStrLn ""
putStrLn $ Term.applyStyles [Term.Bold] $ " cd " ++ projectFolder
putStrLn $ Term.applyStyles [Term.Bold] " wasp start"
{- ORMOLU_ENABLE -}
parseProjectInfo :: ProjectName -> Arguments -> Command ProjectInfo
parseProjectInfo projectName newArgs = case newArgs of
[] -> createProjectInfo projectName Nothing
[templateFlag, templateName] | templateFlag `elem` ["--template", "-t"] -> createProjectInfo projectName (Just templateName)
[templateFlag] | templateFlag `elem` ["--template", "-t"] -> throwProjectCreationError "You must provide a template name."
_anyOtherArgs -> throwProjectCreationError "Invalid arguments for 'wasp new' command."
createProjectInfo :: ProjectName -> Maybe String -> Command ProjectInfo
createProjectInfo name templateName
| isValidWaspIdentifier appName = return $ ProjectInfo {_projectName = name, _appName = appName, _templateName = templateName}
| otherwise =
throwProjectCreationError $
intercalate
"\n"
[ "The project's name is not in the valid format!",
indent 2 "- It can start with a letter or an underscore.",
indent 2 "- It can contain only letters, numbers, dashes, or underscores.",
indent 2 "- It can't be a Wasp keyword."
]
where
appName = kebabToCamelCase name
createWaspProjectDir :: ProjectInfo -> Command ()
createWaspProjectDir projectInfo@ProjectInfo {_templateName = template} = do
absWaspProjectDir <- getAbsoluteWaspProjectDir projectInfo
dirExists <- doesDirExist $ toPathAbsDir absWaspProjectDir
when dirExists $
throwProjectCreationError $
show absWaspProjectDir ++ " is an existing directory"
createProjectFromProjectInfo absWaspProjectDir
where
createProjectFromProjectInfo absWaspProjectDir = do
if isJust template
then createProjectFromTemplate absWaspProjectDir projectInfo
else liftIO $ do
initializeProjectFromSkeleton absWaspProjectDir
writeMainWaspFile absWaspProjectDir projectInfo
getAbsoluteWaspProjectDir :: ProjectInfo -> Command (Path System Abs (Dir WaspProjectDir))
getAbsoluteWaspProjectDir (ProjectInfo projectName _ _) = do
absCwd <- liftIO getCurrentDirectory
case parseAbsDir $ absCwd FP.</> projectName of
Right sp -> return sp
Left err ->
throwProjectCreationError $
"Failed to parse absolute path to wasp project dir: " ++ show err
-- Copies prepared files to the new project directory.
initializeProjectFromSkeleton :: Path' Abs (Dir WaspProjectDir) -> IO ()
initializeProjectFromSkeleton absWaspProjectDir = do
dataDir <- Data.getAbsDataDirPath
let absSkeletonDir = dataDir </> [reldir|Cli/templates/new|]
copyDirRecur (toPathAbsDir absSkeletonDir) (toPathAbsDir absWaspProjectDir)
writeMainWaspFile :: Path System Abs (Dir WaspProjectDir) -> ProjectInfo -> IO ()
writeMainWaspFile waspProjectDir (ProjectInfo projectName appName _) = IOUtil.writeFile absMainWaspFile mainWaspFileContent
where
absMainWaspFile = waspProjectDir </> [relfile|main.wasp|]
mainWaspFileContent =
unlines
[ "app %s {" `printf` appName,
" wasp: {",
" version: \"%s\"" `printf` waspVersionBounds,
" },",
" title: \"%s\"" `printf` projectName,
"}",
"",
"route RootRoute { path: \"/\", to: MainPage }",
"page MainPage {",
" component: import Main from \"@client/MainPage.jsx\"",
"}"
]
createProjectFromTemplate :: Path System Abs (Dir WaspProjectDir) -> ProjectInfo -> Command ()
createProjectFromTemplate absWaspProjectDir ProjectInfo {_appName = appName, _projectName = projectName, _templateName = maybeTemplateName} = do
cliSendMessageC $ Msg.Start "Creating project from template..."
templatePath <- getPathToRemoteTemplate maybeTemplateName
let projectDir = projectName
fetchTemplate templatePath projectDir
ensureTemplateWasFetched
replacePlaceholdersInWaspFile
where
getPathToRemoteTemplate :: Maybe String -> Command String
getPathToRemoteTemplate = \case
Just templateName -> return $ waspTemplatesRepo ++ "/" ++ templateName
Nothing -> throwProjectCreationError "Template name is not provided."
where
-- gh: prefix means Github repo
waspTemplatesRepo = "gh:wasp-lang/starters"
fetchTemplate :: String -> String -> Command ()
fetchTemplate templatePath projectDir = do
liftIO (try executeCmd) >>= \case
Left (e :: SomeException) -> throwProjectCreationError $ "Failed to create project from template: " ++ show e
Right _ -> return ()
where
executeCmd = callCommand $ unwords command
command = ["npx", "giget@latest", templatePath, projectDir]
-- gitget doesn't fail if the template dir doesn't exist in the repo, so we need to check if the directory exists.
ensureTemplateWasFetched :: Command ()
ensureTemplateWasFetched =
whenM (liftIO $ IOUtil.isDirectoryEmpty absWaspProjectDir) $
throwProjectCreationError "Are you sure that the template exists? 🤔 Check the list of templates here: https://github.com/wasp-lang/starters"
replacePlaceholdersInWaspFile :: Command ()
replacePlaceholdersInWaspFile = liftIO $ do
mainWaspFileContent <- IOUtil.readFileStrict absMainWaspFile
let replacedContent =
foldl
(\acc (placeholder, value) -> T.replace (T.pack placeholder) (T.pack value) acc)
mainWaspFileContent
replacements
IOUtil.writeFileFromText absMainWaspFile replacedContent
where
absMainWaspFile = absWaspProjectDir </> [relfile|main.wasp|]
replacements =
[ ("__waspAppName__", appName),
("__waspProjectName__", projectName),
("__waspVersion__", waspVersionBounds)
]
waspVersionBounds :: String
waspVersionBounds = show (SV.backwardsCompatibleWith WV.waspVersion)
throwProjectCreationError :: String -> Command a
throwProjectCreationError = throwError . CommandError "Project creation failed"
createProjectOnDisk :: NewProjectDescription -> Command ()
createProjectOnDisk
NewProjectDescription
{ _projectName = projectName,
_appName = appName,
_templateName = templateName,
_absWaspProjectDir = absWaspProjectDir
} = do
cliSendMessageC $ Msg.Start $ "Creating your project from the " ++ show templateName ++ " template..."
case templateName of
RemoteStarterTemplate remoteTemplateName ->
createProjectOnDiskFromRemoteTemplate absWaspProjectDir projectName appName remoteTemplateName
LocalStarterTemplate localTemplateName ->
liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName localTemplateName

View File

@ -0,0 +1,44 @@
module Wasp.Cli.Command.CreateNewProject.ArgumentsParser
( parseNewProjectArgs,
NewProjectArgs (..),
)
where
import Options.Applicative (defaultPrefs, execParserPure)
import qualified Options.Applicative as Opt
import Wasp.Cli.Command.Call (Arguments)
data NewProjectArgs = NewProjectArgs
{ _projectName :: Maybe String,
_templateName :: Maybe String
}
parseNewProjectArgs :: Arguments -> Either String NewProjectArgs
parseNewProjectArgs newArgs = parserResultToEither $ execParserPure defaultPrefs newProjectArgsParserInfo newArgs
where
newProjectArgsParserInfo :: Opt.ParserInfo NewProjectArgs
newProjectArgsParserInfo = Opt.info (newProjectArgsParser Opt.<**> Opt.helper) Opt.fullDesc
newProjectArgsParser :: Opt.Parser NewProjectArgs
newProjectArgsParser =
NewProjectArgs
<$> Opt.optional projectNameParser
<*> Opt.optional templateNameParser
projectNameParser :: Opt.Parser String
projectNameParser = Opt.strArgument $ Opt.metavar "PROJECT_NAME"
templateNameParser :: Opt.Parser String
templateNameParser =
Opt.strOption $
Opt.long "template"
<> Opt.short 't'
<> Opt.metavar "TEMPLATE_NAME"
<> Opt.help "Template to use for the new project"
parserResultToEither :: Opt.ParserResult NewProjectArgs -> Either String NewProjectArgs
parserResultToEither (Opt.Success success) = Right success
parserResultToEither (Opt.Failure failure) = Left $ show help
where
(help, _, _) = Opt.execFailure failure "wasp new"
parserResultToEither (Opt.CompletionInvoked _) = error "Completion invoked when parsing 'wasp new', but this should never happen"

View File

@ -0,0 +1,15 @@
module Wasp.Cli.Command.CreateNewProject.Common where
import Control.Monad.Except (throwError)
import Wasp.Cli.Command (Command, CommandError (..))
import qualified Wasp.SemanticVersion as SV
import qualified Wasp.Version as WV
throwProjectCreationError :: String -> Command a
throwProjectCreationError = throwError . CommandError "Project creation failed"
throwInvalidTemplateNameUsedError :: Command a
throwInvalidTemplateNameUsedError = throwProjectCreationError "Are you sure that the template exists? 🤔 Check the list of templates here: https://github.com/wasp-lang/starters"
defaultWaspVersionBounds :: String
defaultWaspVersionBounds = show (SV.backwardsCompatibleWith WV.waspVersion)

View File

@ -0,0 +1,144 @@
module Wasp.Cli.Command.CreateNewProject.ProjectDescription
( obtainNewProjectDescription,
NewProjectDescription (..),
NewProjectName (..),
NewProjectAppName (..),
)
where
import Control.Monad.IO.Class (liftIO)
import Data.Function ((&))
import Data.List (intercalate)
import Data.List.NonEmpty (fromList)
import Path.IO (doesDirExist)
import StrongPath (Abs, Dir, Path')
import StrongPath.Path (toPathAbsDir)
import Wasp.Analyzer.Parser (isValidWaspIdentifier)
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (NewProjectArgs (..))
import Wasp.Cli.Command.CreateNewProject.Common
( throwInvalidTemplateNameUsedError,
throwProjectCreationError,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates
( StarterTemplateName,
defaultStarterTemplateName,
findTemplateNameByString,
)
import Wasp.Cli.FileSystem (getAbsPathToDirInCwd)
import qualified Wasp.Cli.Interactive as Interactive
import Wasp.Project (WaspProjectDir)
import Wasp.Util (indent, kebabToCamelCase, whenM)
data NewProjectDescription = NewProjectDescription
{ _projectName :: NewProjectName,
_appName :: NewProjectAppName,
_templateName :: StarterTemplateName,
_absWaspProjectDir :: Path' Abs (Dir WaspProjectDir)
}
data NewProjectName = NewProjectName String
instance Show NewProjectName where
show (NewProjectName name) = name
data NewProjectAppName = NewProjectAppName String
instance Show NewProjectAppName where
show (NewProjectAppName name) = name
{-
There are two ways of getting the project description:
1. From CLI arguments
wasp new <project-name> [-t <template-name>]
- Project name is required.
- Template name is optional, if not provided, we use the default template.
2. Interactively
wasp new
- Project name is required.
- Template name is required, we ask the user to choose from available templates.
-}
obtainNewProjectDescription :: NewProjectArgs -> [StarterTemplateName] -> Command NewProjectDescription
obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _templateName = templateNameArg} starterTemplateNames =
case projectNameArg of
Just projectName -> obtainNewProjectDescriptionFromCliArgs projectName templateNameArg starterTemplateNames
Nothing -> obtainNewProjectDescriptionInteractively templateNameArg starterTemplateNames
obtainNewProjectDescriptionFromCliArgs :: String -> Maybe String -> [StarterTemplateName] -> Command NewProjectDescription
obtainNewProjectDescriptionFromCliArgs projectName templateNameArg availableTemplates =
obtainNewProjectDescriptionFromProjectNameAndTemplateArg
projectName
templateNameArg
availableTemplates
(return defaultStarterTemplateName)
obtainNewProjectDescriptionInteractively :: Maybe String -> [StarterTemplateName] -> Command NewProjectDescription
obtainNewProjectDescriptionInteractively templateNameArg availableTemplates = do
projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)"
obtainNewProjectDescriptionFromProjectNameAndTemplateArg
projectName
templateNameArg
availableTemplates
(liftIO askForTemplateName)
where
askForTemplateName = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates
-- Common logic
obtainNewProjectDescriptionFromProjectNameAndTemplateArg ::
String ->
Maybe String ->
[StarterTemplateName] ->
Command StarterTemplateName ->
Command NewProjectDescription
obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateNameArg availableTemplates obtainTemplateWhenNoArg = do
absWaspProjectDir <- obtainAvailableProjectDirPath projectName
selectedTemplate <- maybe obtainTemplateWhenNoArg findTemplateNameOrThrow templateNameArg
mkNewProjectDescription projectName absWaspProjectDir selectedTemplate
where
findTemplateNameOrThrow :: String -> Command StarterTemplateName
findTemplateNameOrThrow templateName =
findTemplateNameByString availableTemplates templateName
& maybe throwInvalidTemplateNameUsedError return
obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir))
obtainAvailableProjectDirPath projectName = do
absWaspProjectDir <- getAbsPathToNewProjectDirInCwd projectName
ensureProjectDirDoesNotExist projectName absWaspProjectDir
return absWaspProjectDir
where
getAbsPathToNewProjectDirInCwd :: String -> Command (Path' Abs (Dir WaspProjectDir))
getAbsPathToNewProjectDirInCwd projectDirName = do
liftIO (getAbsPathToDirInCwd projectDirName) >>= either throwError return
where
throwError err = throwProjectCreationError $ "Failed to get absolute path to Wasp project dir: " ++ show err
ensureProjectDirDoesNotExist :: String -> Path' Abs (Dir WaspProjectDir) -> Command ()
ensureProjectDirDoesNotExist projectDirName absWaspProjectDir = do
whenM (doesDirExist $ toPathAbsDir absWaspProjectDir) $
throwProjectCreationError $
"Directory \"" ++ projectDirName ++ "\" is not empty."
mkNewProjectDescription :: String -> Path' Abs (Dir WaspProjectDir) -> StarterTemplateName -> Command NewProjectDescription
mkNewProjectDescription projectName absWaspProjectDir templateName
| isValidWaspIdentifier appName =
return $
NewProjectDescription
{ _projectName = NewProjectName projectName,
_appName = NewProjectAppName appName,
_templateName = templateName,
_absWaspProjectDir = absWaspProjectDir
}
| otherwise =
throwProjectCreationError $
intercalate
"\n"
[ "The project's name is not in the valid format!",
indent 2 "- It can start with a letter or an underscore.",
indent 2 "- It can contain only letters, numbers, dashes, or underscores.",
indent 2 "- It can't be a Wasp keyword."
]
where
appName = kebabToCamelCase projectName

View File

@ -0,0 +1,41 @@
module Wasp.Cli.Command.CreateNewProject.StarterTemplates
( getStarterTemplateNames,
StarterTemplateName (..),
findTemplateNameByString,
defaultStarterTemplateName,
)
where
import Data.Either (fromRight)
import Data.Foldable (find)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github (starterTemplateGithubRepo)
import qualified Wasp.Cli.GithubRepo as GR
data StarterTemplateName = RemoteStarterTemplate String | LocalStarterTemplate String
deriving (Eq)
instance Show StarterTemplateName where
show (RemoteStarterTemplate templateName) = templateName
show (LocalStarterTemplate templateName) = templateName
getStarterTemplateNames :: IO [StarterTemplateName]
getStarterTemplateNames = do
remoteTemplates <- fromRight [] <$> fetchRemoteStarterTemplateNames
return $ localTemplates ++ remoteTemplates
fetchRemoteStarterTemplateNames :: IO (Either String [StarterTemplateName])
fetchRemoteStarterTemplateNames = do
fmap extractTemplateNames <$> GR.fetchRepoRootFolderContents starterTemplateGithubRepo
where
extractTemplateNames :: GR.RepoFolderContents -> [StarterTemplateName]
-- Each folder in the repo is a template.
extractTemplateNames = map (RemoteStarterTemplate . GR._name) . filter ((== GR.Folder) . GR._type)
localTemplates :: [StarterTemplateName]
localTemplates = [defaultStarterTemplateName]
defaultStarterTemplateName :: StarterTemplateName
defaultStarterTemplateName = LocalStarterTemplate "basic"
findTemplateNameByString :: [StarterTemplateName] -> String -> Maybe StarterTemplateName
findTemplateNameByString templateNames query = find ((== query) . show) templateNames

View File

@ -0,0 +1,28 @@
module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local
( createProjectOnDiskFromLocalTemplate,
)
where
import Data.Maybe (fromJust)
import Path.IO (copyDirRecur)
import StrongPath (Abs, Dir, Path', reldir, (</>))
import qualified StrongPath as SP
import StrongPath.Path (toPathAbsDir)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating (replaceTemplatePlaceholdersInWaspFile)
import qualified Wasp.Data as Data
import Wasp.Project (WaspProjectDir)
createProjectOnDiskFromLocalTemplate :: Path' Abs (Dir WaspProjectDir) -> NewProjectName -> NewProjectAppName -> String -> IO ()
createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName templateName = do
copyLocalTemplateToNewProjectDir templateName
replaceTemplatePlaceholdersInWaspFile appName projectName absWaspProjectDir
where
copyLocalTemplateToNewProjectDir :: String -> IO ()
copyLocalTemplateToNewProjectDir templateDir = do
dataDir <- Data.getAbsDataDirPath
let absLocalTemplateDir =
dataDir
</> [reldir|Cli/templates|]
</> (fromJust . SP.parseRelDir $ templateDir)
copyDirRecur (toPathAbsDir absLocalTemplateDir) (toPathAbsDir absWaspProjectDir)

View File

@ -0,0 +1,31 @@
module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote
( createProjectOnDiskFromRemoteTemplate,
)
where
import Control.Monad.IO.Class (liftIO)
import Data.Maybe (fromJust)
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github (starterTemplateGithubRepo)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating (replaceTemplatePlaceholdersInWaspFile)
import Wasp.Cli.GithubRepo (fetchFolderFromGithubRepoToDisk)
import Wasp.Project (WaspProjectDir)
createProjectOnDiskFromRemoteTemplate ::
Path' Abs (Dir WaspProjectDir) ->
NewProjectName ->
NewProjectAppName ->
String ->
Command ()
createProjectOnDiskFromRemoteTemplate absWaspProjectDir projectName appName templateName = do
fetchGithubTemplateToDisk absWaspProjectDir templateName >>= either throwProjectCreationError pure
liftIO $ replaceTemplatePlaceholdersInWaspFile appName projectName absWaspProjectDir
where
fetchGithubTemplateToDisk :: Path' Abs (Dir WaspProjectDir) -> String -> Command (Either String ())
fetchGithubTemplateToDisk projectDir templateFolderName = do
let templateFolderPath = fromJust . SP.parseRelDir $ templateFolderName
liftIO $ fetchFolderFromGithubRepoToDisk starterTemplateGithubRepo templateFolderPath projectDir

View File

@ -0,0 +1,11 @@
module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github where
import Wasp.Cli.GithubRepo (GithubRepoRef (..))
starterTemplateGithubRepo :: GithubRepoRef
starterTemplateGithubRepo =
GithubRepoRef
{ _repoOwner = "wasp-lang",
_repoName = "starters",
_repoReferenceName = "main"
}

View File

@ -0,0 +1,32 @@
module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating where
import Data.List (foldl')
import Data.Text (Text)
import qualified Data.Text as T
import StrongPath (Abs, Dir, File, Path', relfile, (</>))
import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName)
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Util.IO as IOUtil
-- Template file for wasp file has placeholders in it that we want to replace
-- in the .wasp file we have written to the disk.
replaceTemplatePlaceholdersInWaspFile :: NewProjectAppName -> NewProjectName -> Path' Abs (Dir WaspProjectDir) -> IO ()
replaceTemplatePlaceholdersInWaspFile appName projectName projectDir =
updateFileContent absMainWaspFile $ replacePlaceholders waspFileReplacements
where
updateFileContent :: Path' Abs (File f) -> (Text -> Text) -> IO ()
updateFileContent absFilePath updateFn =
IOUtil.readFileStrict absFilePath >>= IOUtil.writeFileFromText absFilePath . updateFn
replacePlaceholders :: [(String, String)] -> Text -> Text
replacePlaceholders replacements content = foldl' replacePlaceholder content replacements
where
replacePlaceholder content' (placeholder, value) = T.replace (T.pack placeholder) (T.pack value) content'
absMainWaspFile = projectDir </> [relfile|main.wasp|]
waspFileReplacements =
[ ("__waspAppName__", show appName),
("__waspProjectName__", show projectName),
("__waspVersion__", defaultWaspVersionBounds)
]

View File

@ -60,7 +60,9 @@ throwIfCustomDbAlreadyInUse spec = do
dbUrl <- liftIO $ lookupEnv databaseUrlEnvVarName
when (isJust dbUrl) $
throwCustomDbAlreadyInUseError
( "Wasp has detected existing " <> databaseUrlEnvVarName <> " var in your environment.\n"
( "Wasp has detected existing "
<> databaseUrlEnvVarName
<> " var in your environment.\n"
<> "To have Wasp run the dev database for you, make sure you remove that env var first."
)

View File

@ -4,16 +4,20 @@ module Wasp.Cli.FileSystem
getHomeDir,
waspInstallationDirInHomeDir,
waspExecutableInHomeDir,
getAbsPathToDirInCwd,
withTempDir,
UserCacheDir,
WaspCacheDir,
)
where
import Control.Monad.Catch (MonadThrow)
import Data.Maybe (fromJust)
import StrongPath (Abs, Dir, Dir', File', Path', Rel, reldir, relfile, (</>))
import qualified StrongPath as SP
import System.Directory
import qualified System.Directory as SD
import qualified System.FilePath as FP
import System.IO.Temp (withSystemTempDirectory)
data UserHomeDir
@ -22,7 +26,7 @@ data UserCacheDir
data WaspCacheDir
getHomeDir :: IO (Path' Abs (Dir UserHomeDir))
getHomeDir = fromJust . SP.parseAbsDir <$> getHomeDirectory
getHomeDir = fromJust . SP.parseAbsDir <$> SD.getHomeDirectory
getWaspCacheDir :: Path' Abs (Dir UserCacheDir) -> Path' Abs (Dir WaspCacheDir)
getWaspCacheDir userCacheDirPath = userCacheDirPath </> [reldir|wasp|]
@ -30,6 +34,9 @@ getWaspCacheDir userCacheDirPath = userCacheDirPath </> [reldir|wasp|]
getUserCacheDir :: IO (Path' Abs (Dir UserCacheDir))
getUserCacheDir = SD.getXdgDirectory SD.XdgCache "" >>= SP.parseAbsDir
withTempDir :: (Path' Abs (Dir r) -> IO a) -> IO a
withTempDir action = withSystemTempDirectory ".wasp" (action . fromJust . SP.parseAbsDir)
-- NOTE: these paths are based on the installer script and if you change them there
-- you need to change them here as well (and vice versa).
-- Task to improve this: https://github.com/wasp-lang/wasp/issues/980
@ -38,3 +45,8 @@ 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 dirName = do
absCwd <- SD.getCurrentDirectory
return $ SP.parseAbsDir $ absCwd FP.</> dirName

View File

@ -0,0 +1,114 @@
{-# LANGUAGE OverloadedStrings #-}
module Wasp.Cli.GithubRepo where
import Control.Exception (try)
import Data.Aeson
( FromJSON,
parseJSON,
withObject,
(.:),
)
import Data.Functor ((<&>))
import Data.List (intercalate)
import Data.Maybe (fromJust, maybeToList)
import qualified Network.HTTP.Simple as HTTP
import StrongPath (Abs, Dir, Path', Rel, (</>))
import qualified StrongPath as SP
import Wasp.Cli.Archive (fetchArchiveAndCopySubdirToDisk)
data GithubRepoRef = GithubRepoRef
{ _repoOwner :: GithubRepoOwner,
_repoName :: GithubRepoName,
-- Which point in repo history to download (a branch or commit hash).
_repoReferenceName :: GithubRepoReferenceName
}
deriving (Show, Eq)
type GithubRepoOwner = String
type GithubRepoName = String
type GithubRepoReferenceName = String
fetchFolderFromGithubRepoToDisk ::
GithubRepoRef ->
Path' (Rel repoRoot) (Dir folderInRepo) ->
Path' Abs (Dir destinationDir) ->
IO (Either String ())
fetchFolderFromGithubRepoToDisk githubRepoRef folderInRepoRoot destinationOnDisk = do
let downloadUrl = getGithubRepoArchiveDownloadURL githubRepoRef
folderInArchiveRoot = mapFolderPathInRepoToFolderPathInGithubArchive githubRepoRef folderInRepoRoot
fetchArchiveAndCopySubdirToDisk downloadUrl folderInArchiveRoot destinationOnDisk
where
getGithubRepoArchiveDownloadURL :: GithubRepoRef -> String
getGithubRepoArchiveDownloadURL
GithubRepoRef
{ _repoName = repoName,
_repoOwner = repoOwner,
_repoReferenceName = repoReferenceName
} = intercalate "/" ["https://github.com", repoOwner, repoName, "archive", downloadArchiveName]
where
downloadArchiveName = repoReferenceName ++ ".tar.gz"
mapFolderPathInRepoToFolderPathInGithubArchive ::
forall archiveInnerDir targetDir archiveRoot.
GithubRepoRef ->
Path' (Rel archiveInnerDir) (Dir targetDir) ->
Path' (Rel archiveRoot) (Dir targetDir)
mapFolderPathInRepoToFolderPathInGithubArchive
GithubRepoRef
{ _repoName = repoName,
_repoReferenceName = repoReferenceName
}
targetFolderPath = githubRepoArchiveRootFolderName </> targetFolderPath
where
-- Github repo tars have a root folder that is named after the repo
-- name and the reference (branch or tag).
githubRepoArchiveRootFolderName :: Path' (Rel archiveRoot) (Dir archiveInnerDir)
githubRepoArchiveRootFolderName = fromJust . SP.parseRelDir $ repoName ++ "-" ++ repoReferenceName
fetchRepoRootFolderContents :: GithubRepoRef -> IO (Either String RepoFolderContents)
fetchRepoRootFolderContents githubRepo = fetchRepoFolderContents githubRepo Nothing
fetchRepoFolderContents :: GithubRepoRef -> Maybe String -> IO (Either String RepoFolderContents)
fetchRepoFolderContents githubRepo pathToFolderInRepo = do
try (HTTP.httpJSONEither ghRepoInfoRequest) <&> \case
Right response -> either (Left . show) Right $ HTTP.getResponseBody response
Left (e :: HTTP.HttpException) -> Left $ show e
where
ghRepoInfoRequest =
-- Github returns 403 if we don't specify user-agent.
HTTP.addRequestHeader "User-Agent" "wasp-lang/wasp" $ HTTP.parseRequest_ apiURL
apiURL = intercalate "/" $ ["https://api.github.com/repos", _repoOwner githubRepo, _repoName githubRepo, "contents"] ++ maybeToList pathToFolderInRepo
type RepoFolderContents = [RepoObject]
data RepoObject = RepoObject
{ _name :: String,
_type :: RepoObjectType,
_downloadUrl :: Maybe String
}
deriving (Show)
data RepoObjectType = Folder | File
deriving (Show, Eq)
instance FromJSON RepoObject where
parseJSON = withObject "RepoObject" $ \o -> do
name <- o .: "name"
type_ <- o .: "type"
downloadUrl <- o .: "download_url"
return
RepoObject
{ _name = name,
_type = parseType type_,
_downloadUrl = downloadUrl
}
where
parseType :: String -> RepoObjectType
parseType = \case
"dir" -> Folder
"file" -> File
_ -> error "Unable to parse repo object type."

View File

@ -0,0 +1,121 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
module Wasp.Cli.Interactive
( askForInput,
askToChoose,
askForRequiredInput,
Option,
)
where
import Control.Applicative ((<|>))
import Data.Foldable (find)
import Data.Function ((&))
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import System.IO (hFlush, stdout)
import Text.Read (readMaybe)
import qualified Wasp.Util.Terminal as Term
{-
Why are we doing this?
Using a list of Strings for options results in the Strings being wrapped in quotes
when printed.
What we want to avoid:
Choose an option:
- "one"
- "two"
What we want:
Choose an option:
- one
- two
We want to avoid this so users can type the name of the option when answering
without having to type the quotes as well.
We introduced the Option class to get different "show" behavior for Strings and other
types. Option delegates to the Show instance for all other types, but for Strings it
just returns the String itself.
-}
class Option o where
showOption :: o -> String
instance {-# OVERLAPPING #-} Option [Char] where
showOption = id
instance {-# OVERLAPPABLE #-} Show t => Option t where
showOption = show
askForRequiredInput :: String -> IO String
askForRequiredInput = repeatIfNull . askForInput
askToChoose :: forall o. Option o => String -> NonEmpty o -> IO o
askToChoose _ (singleOption :| []) = return singleOption
askToChoose question options = do
putStrLn $ Term.applyStyles [Term.Bold] question
putStrLn showIndexedOptions
answer <- prompt
getOptionMatchingAnswer answer & maybe printErrorAndAskAgain return
where
getOptionMatchingAnswer :: String -> Maybe o
getOptionMatchingAnswer "" = Just defaultOption
getOptionMatchingAnswer answer =
getOptionByIndex answer <|> getOptionByName answer
getOptionByIndex :: String -> Maybe o
getOptionByIndex idxStr =
case readMaybe idxStr of
Just idx | idx >= 1 && idx <= length options -> Just $ options NE.!! (idx - 1)
_invalidIndex -> Nothing
getOptionByName :: String -> Maybe o
getOptionByName name = find ((== name) . showOption) options
printErrorAndAskAgain :: IO o
printErrorAndAskAgain = do
putStrLn $ Term.applyStyles [Term.Red] "Invalid selection, write the name or the index of the option."
askToChoose question options
showIndexedOptions :: String
showIndexedOptions = intercalate "\n" $ showIndexedOption <$> zip [1 ..] (NE.toList options)
where
showIndexedOption (idx, option) =
showIndex idx <> " " <> showOption option <> (if isDefaultOption option then " (default)" else "")
showIndex i = Term.applyStyles [Term.Yellow] $ "[" ++ show (i :: Int) ++ "]"
defaultOption :: o
defaultOption = NE.head options
isDefaultOption :: o -> Bool
isDefaultOption option = showOption option == showOption defaultOption
askForInput :: String -> IO String
askForInput question = putStr (Term.applyStyles [Term.Bold] question) >> prompt
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
repeatUntil predicate errorMessage action = do
result <- action
if predicate result
then do
putStrLn $ Term.applyStyles [Term.Red] errorMessage
repeatUntil predicate errorMessage action
else return result
prompt :: IO String
prompt = do
putStrFlush $ Term.applyStyles [Term.Yellow] ""
T.unpack . T.strip . T.pack <$> getLine
-- Explicit flush ensures prompt messages are printed immediately on all systems.
putStrFlush :: String -> IO ()
putStrFlush msg = do
putStr msg
hFlush stdout

View File

@ -0,0 +1,11 @@
app __waspAppName__ {
wasp: {
version: "__waspVersion__"
},
title: "__waspProjectName__"
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@client/MainPage.jsx"
}

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -6,8 +6,6 @@ module Wasp.Generator.Common
WebAppRootDir,
AppComponentRootDir,
DbRootDir,
latestMajorNodeVersion,
nodeVersionRange,
prismaVersion,
makeJsonWithEntityData,
GeneratedSrcDir,
@ -50,20 +48,6 @@ data DbRootDir
instance AppComponentRootDir DbRootDir
-- | Latest concrete major node version supported by the nodeVersionRange, and
-- therefore by Wasp.
-- Here we assume that nodeVersionRange is using latestNodeLTSVersion as its basis.
-- TODO: instead of making assumptions, extract the latest major node version
-- directly from the nodeVersionRange.
latestMajorNodeVersion :: SV.Version
latestMajorNodeVersion = latestNodeLTSVersion
nodeVersionRange :: SV.Range
nodeVersionRange = SV.Range [SV.backwardsCompatibleWith latestNodeLTSVersion]
latestNodeLTSVersion :: SV.Version
latestNodeLTSVersion = SV.Version 18 12 0
prismaVersion :: SV.Version
prismaVersion = SV.Version 4 12 0

View File

@ -19,7 +19,6 @@ import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Generator.Common
( ProjectRootDir,
ServerRootDir,
latestMajorNodeVersion,
)
import Wasp.Generator.DbGenerator.Common
( PrismaDbSchema,
@ -29,6 +28,7 @@ import Wasp.Generator.DbGenerator.Common
import Wasp.Generator.FileDraft (FileDraft (..), createTemplateFileDraft)
import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD
import Wasp.Generator.Monad (Generator, GeneratorError, runGenerator)
import Wasp.Generator.Node.Version (latestMajorNodeVersion)
import Wasp.Generator.Templates (TemplatesDir, compileAndRenderTemplate)
import qualified Wasp.SemanticVersion as SV
import Wasp.Util (getEnvVarDefinition)

View File

@ -4,7 +4,6 @@ module Wasp.Generator.Job.Process
( runProcessAsJob,
runNodeCommandAsJob,
runNodeCommandAsJobWithExtraEnv,
parseNodeVersion,
)
where
@ -19,14 +18,11 @@ import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import System.Environment (getEnvironment)
import System.Exit (ExitCode (..))
import System.IO.Error (catchIOError, isDoesNotExistError)
import qualified System.Info
import qualified System.Process as P
import Text.Read (readMaybe)
import qualified Text.Regex.TDFA as R
import UnliftIO.Exception (bracket)
import qualified Wasp.Generator.Common as C
import qualified Wasp.Generator.Job as J
import qualified Wasp.Generator.Node.Version as NodeVersion
import qualified Wasp.SemanticVersion as SV
-- TODO:
@ -101,15 +97,15 @@ runNodeCommandAsJob = runNodeCommandAsJobWithExtraEnv []
runNodeCommandAsJobWithExtraEnv :: [(String, String)] -> Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job
runNodeCommandAsJobWithExtraEnv extraEnvVars fromDir command args jobType chan =
getNodeVersion >>= \case
NodeVersion.getNodeVersion >>= \case
Left errorMsg -> exitWithError (ExitFailure 1) (T.pack errorMsg)
Right nodeVersion ->
if SV.isVersionInRange nodeVersion C.nodeVersionRange
if SV.isVersionInRange nodeVersion NodeVersion.nodeVersionRange
then do
envVars <- getAllEnvVars
let nodeCommandProcess = (P.proc command args) {P.env = Just envVars, P.cwd = Just $ SP.fromAbsDir fromDir}
runProcessAsJob nodeCommandProcess jobType chan
else exitWithError (ExitFailure 1) (T.pack $ makeNodeVersionMismatchMessage nodeVersion)
else exitWithError (ExitFailure 1) (T.pack $ NodeVersion.makeNodeVersionMismatchMessage nodeVersion)
where
-- Haskell will use the first value for variable name it finds. Since env
-- vars in 'extraEnvVars' should override the the inherited env vars, we
@ -127,52 +123,3 @@ runNodeCommandAsJobWithExtraEnv extraEnvVars fromDir command args jobType chan =
J._jobType = jobType
}
return exitCode
getNodeVersion :: IO (Either String SV.Version)
getNodeVersion = do
(exitCode, stdout, stderr) <-
P.readProcessWithExitCode "node" ["--version"] ""
`catchIOError` ( \e ->
if isDoesNotExistError e
then return (ExitFailure 1, "", "Command 'node' not found.")
else ioError e
)
return $ case exitCode of
ExitFailure _ ->
Left
( "Running 'node --version' failed: " ++ stderr
++ " "
++ waspNodeRequirementMessage
)
ExitSuccess -> case parseNodeVersion stdout of
Nothing ->
Left
( "Wasp failed to parse node version."
++ " This is most likely a bug in Wasp, please file an issue."
)
Just version -> Right version
parseNodeVersion :: String -> Maybe SV.Version
parseNodeVersion nodeVersionStr =
case nodeVersionStr R.=~ ("v([^\\.]+).([^\\.]+).(.+)" :: String) of
((_, _, _, [majorStr, minorStr, patchStr]) :: (String, String, String, [String])) -> do
mjr <- readMaybe majorStr
mnr <- readMaybe minorStr
ptc <- readMaybe patchStr
return $ SV.Version mjr mnr ptc
_ -> Nothing
makeNodeVersionMismatchMessage :: SV.Version -> String
makeNodeVersionMismatchMessage nodeVersion =
unwords
[ "Your node version does not match Wasp's requirements.",
"You are running node " ++ show nodeVersion ++ ".",
waspNodeRequirementMessage
]
waspNodeRequirementMessage :: String
waspNodeRequirementMessage =
unwords
[ "Wasp requires node " ++ show C.nodeVersionRange ++ ".",
"Check Wasp docs for more details: https://wasp-lang.dev/docs#requirements."
]

View File

@ -0,0 +1,79 @@
module Wasp.Generator.Node.Version
( getNodeVersion,
nodeVersionRange,
latestMajorNodeVersion,
waspNodeRequirementMessage,
makeNodeVersionMismatchMessage,
)
where
import Data.Conduit.Process.Typed (ExitCode (..))
import System.IO.Error (catchIOError, isDoesNotExistError)
import qualified System.Process as P
import Text.Read (readMaybe)
import qualified Text.Regex.TDFA as R
import qualified Wasp.SemanticVersion as SV
getNodeVersion :: IO (Either String SV.Version)
getNodeVersion = do
(exitCode, stdout, stderr) <-
P.readProcessWithExitCode "node" ["--version"] ""
`catchIOError` ( \e ->
if isDoesNotExistError e
then return (ExitFailure 1, "", "Command 'node' not found.")
else ioError e
)
return $ case exitCode of
ExitFailure _ ->
Left
( "Running 'node --version' failed: "
++ stderr
++ " "
++ waspNodeRequirementMessage
)
ExitSuccess -> case parseNodeVersion stdout of
Nothing ->
Left
( "Wasp failed to parse node version."
++ " This is most likely a bug in Wasp, please file an issue."
)
Just version -> Right version
parseNodeVersion :: String -> Maybe SV.Version
parseNodeVersion nodeVersionStr =
case nodeVersionStr R.=~ ("v([^\\.]+).([^\\.]+).(.+)" :: String) of
((_, _, _, [majorStr, minorStr, patchStr]) :: (String, String, String, [String])) -> do
mjr <- readMaybe majorStr
mnr <- readMaybe minorStr
ptc <- readMaybe patchStr
return $ SV.Version mjr mnr ptc
_ -> Nothing
waspNodeRequirementMessage :: String
waspNodeRequirementMessage =
unwords
[ "Wasp requires node " ++ show nodeVersionRange ++ ".",
"Check Wasp docs for more details: https://wasp-lang.dev/docs/quick-start#requirements."
]
nodeVersionRange :: SV.Range
nodeVersionRange = SV.Range [SV.backwardsCompatibleWith latestNodeLTSVersion]
latestNodeLTSVersion :: SV.Version
latestNodeLTSVersion = SV.Version 18 12 0
-- | Latest concrete major node version supported by the nodeVersionRange, and
-- therefore by Wasp.
-- Here we assume that nodeVersionRange is using latestNodeLTSVersion as its basis.
-- TODO: instead of making assumptions, extract the latest major node version
-- directly from the nodeVersionRange.
latestMajorNodeVersion :: SV.Version
latestMajorNodeVersion = latestNodeLTSVersion
makeNodeVersionMismatchMessage :: SV.Version -> String
makeNodeVersionMismatchMessage nodeVersion =
unwords
[ "Your node version does not match Wasp's requirements.",
"You are running node " ++ show nodeVersion ++ ".",
waspNodeRequirementMessage
]

View File

@ -44,14 +44,13 @@ import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Env (envVarsToDotEnvContent)
import Wasp.Generator.Common
( ServerRootDir,
latestMajorNodeVersion,
makeJsonWithEntityData,
nodeVersionRange,
prismaVersion,
)
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.Node.Version as NodeVersion
import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.ServerGenerator.ApiRoutesG (genApis)
import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (depsRequiredByPassport)
@ -125,7 +124,7 @@ genPackageJson spec waspDependencies = do
object
[ "depsChunk" .= N.getDependenciesPackageJsonEntry combinedDependencies,
"devDepsChunk" .= N.getDevDependenciesPackageJsonEntry combinedDependencies,
"nodeVersionRange" .= show nodeVersionRange,
"nodeVersionRange" .= show NodeVersion.nodeVersionRange,
"startProductionScript"
.= ( (if hasEntities then "npm run db-migrate-prod && " else "")
++ "NODE_ENV=production npm run start"
@ -177,7 +176,7 @@ npmDepsForWasp spec =
("@types/express", "^4.17.13"),
("@types/express-serve-static-core", "^4.17.13"),
("@types/node", "^18.11.9"),
("@tsconfig/node" ++ show (major latestMajorNodeVersion), "^1.0.1")
("@tsconfig/node" ++ show (major NodeVersion.latestMajorNodeVersion), "^1.0.1")
]
}

View File

@ -29,13 +29,13 @@ import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Env (envVarsToDotEnvContent)
import Wasp.Generator.Common
( makeJsonWithEntityData,
nodeVersionRange,
prismaVersion,
)
import qualified Wasp.Generator.ConfigFile as G.CF
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
import Wasp.Generator.FileDraft
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.Node.Version as NodeVersion
import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.WebAppGenerator.AuthG (genAuth)
import qualified Wasp.Generator.WebAppGenerator.Common as C
@ -101,7 +101,7 @@ genPackageJson spec waspDependencies = do
[ "appName" .= (fst (getApp spec) :: String),
"depsChunk" .= N.getDependenciesPackageJsonEntry combinedDependencies,
"devDepsChunk" .= N.getDevDependenciesPackageJsonEntry combinedDependencies,
"nodeVersionRange" .= show nodeVersionRange
"nodeVersionRange" .= show NodeVersion.nodeVersionRange
]
)

View File

@ -49,9 +49,10 @@ data-files:
Cli/templates/**/*.jsx
Cli/templates/**/*.png
Cli/templates/**/*.ts
Cli/templates/new/.gitignore
Cli/templates/new/.wasproot
Cli/templates/new/src/.waspignore
Cli/templates/basic/.gitignore
Cli/templates/basic/.wasproot
Cli/templates/basic/src/.waspignore
Cli/templates/basic/main.wasp
data-dir: data/
source-repository head
@ -251,6 +252,7 @@ library
Wasp.Generator.AuthProviders.OAuth
Wasp.Generator.AuthProviders.Local
Wasp.Generator.AuthProviders.Email
Wasp.Generator.Node.Version
Wasp.Generator.ServerGenerator
Wasp.Generator.ServerGenerator.JsImport
Wasp.Generator.ServerGenerator.ApiRoutesG
@ -371,10 +373,15 @@ library cli-lib
, waspc
, waspls
, unliftio ^>= 0.2.20
, bytestring ^>= 0.10.12
, tar ^>=0.5.1.1
, zlib ^>=0.6.3.0
, temporary ^>=1.3
other-modules: Paths_waspc
exposed-modules:
Wasp.Cli.Command
Wasp.Cli.FileSystem
Wasp.Cli.Archive
Wasp.Cli.Command.BashCompletion
Wasp.Cli.Command.Build
Wasp.Cli.Command.Call
@ -383,6 +390,14 @@ library cli-lib
Wasp.Cli.Command.Common
Wasp.Cli.Command.Compile
Wasp.Cli.Command.CreateNewProject
Wasp.Cli.Command.CreateNewProject.Common
Wasp.Cli.Command.CreateNewProject.ProjectDescription
Wasp.Cli.Command.CreateNewProject.ArgumentsParser
Wasp.Cli.Command.CreateNewProject.StarterTemplates
Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local
Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote
Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github
Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating
Wasp.Cli.Command.Db
Wasp.Cli.Command.Db.Migrate
Wasp.Cli.Command.Db.Reset
@ -405,6 +420,8 @@ library cli-lib
Wasp.Cli.Terminal
Wasp.Cli.Command.Message
Wasp.Cli.Message
Wasp.Cli.Interactive
Wasp.Cli.GithubRepo
executable wasp-cli
import: common-all, common-exe

View File

@ -5,7 +5,7 @@ This document describes the Wasp CLI commands, arguments, and options.
## Overview
The `wasp` command can be called from command line once [installed](https://wasp-lang.dev/docs/#2-installation).
The `wasp` command can be called from command line once [installed](/docs/quick-start).
When called without arguments, it will display its command usage and help document:
```
@ -14,7 +14,11 @@ USAGE
COMMANDS
GENERAL
new <project-name> Creates new Wasp project.
new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
OPTIONS:
-t|--template <template-name>
Check out the templates list here: https://github.com/wasp-lang/starters
version Prints current version of CLI.
waspls Run Wasp Language Server. Add --help to get more info.
completion Prints help on bash completion.
@ -23,7 +27,8 @@ COMMANDS
start Runs Wasp app in development mode, watching for file changes.
start db Starts managed development database for you.
db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
clean Deletes all generated code and other cached artifacts. Wasp equivalent of 'have you tried closing and opening it again?'.
clean Deletes all generated code and other cached artifacts.
Wasp equivalent of 'have you tried closing and opening it again?'.
build Generates full web app code, ready for deployment. Use when deploying or ejecting.
deploy Deploys your Wasp app to cloud hosting providers.
telemetry Prints telemetry status.
@ -43,43 +48,39 @@ Newsletter: https://wasp-lang.dev/#signup
```
## Commands
### General
- `wasp new <project-name>` creates new Wasp project. A directory with the provided project-name will be created, containing boilerplate code.
### Creating a new project
- `wasp new` runs the interactive mode for creating a new Wasp project. It will ask you for the project name, and then for the template to use. It will use the template to generate the directory with the provided project-name.
```
$ wasp new
Enter the project name (e.g. my-project) ▸ MyFirstProject
Choose a starter template
[1] basic (default)
[2] saas
[3] todo-ts
▸ 1
🐝 --- Creating your project from the basic template... ---------------------------
Created new Wasp app in ./MyFirstProject directory!
To run it, do:
cd MyFirstProject
wasp start
```
- `wasp new <project-name>` creates new Wasp project from the default template skipping the interactive mode.
```
$ wasp new MyFirstProject
🐝 --- Creating your project from the basic template... ---------------------------
Created new Wasp app in ./MyFirstProject directory!
To run it, do:
cd MyFirstProject
wasp start
```
- `wasp version` prints current version of CLI.
```
$ wasp version
0.2.0.1
```
- `wasp uninstall` removes Wasp from your system.
```
$ wasp uninstall
🐝 --- Uninstalling Wasp ... ------------------------------------------------------
We will remove the following directories:
{home}/.local/share/wasp-lang/
{home}/.cache/wasp/
We will also remove the following files:
{home}/.local/bin/wasp
Are you sure you want to continue? [y/N]
y
✅ --- Uninstalled Wasp -----------------------------------------------------------
```
### Bash Completion
To setup Bash completion, execute `wasp completion` and follow the instructions.
### In project
- `wasp start` runs Wasp app in development mode. It opens a browser tab with your application running, and watches for any changes to .wasp or files in `src/` to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.
- `wasp start db` starts the database for you. This can be very handy, since you don't need to spin up your own database or provide its connection URL to the Wasp app!
@ -110,11 +111,44 @@ To setup Bash completion, execute `wasp completion` and follow the instructions.
- `wasp deps` prints the dependencies that Wasp uses in your project.
- `wasp info` prints basic information about current Wasp project.
#### Database
### Database
Wasp has a set of commands for working with the database. They all start with `db` and mostly call prisma commands in the background.
- `wasp db migrate-dev` ensures dev database corresponds to the current state of schema (entities): it generates a new migration if there are changes in the schema and it applies any pending migration to the database.
- Supports a `--name foo` option for providing a migration name, as well as `--create-only` for creating an empty migration but not applying it.
- `wasp db studio` opens the GUI for inspecting your database.
### Bash Completion
To setup Bash completion, execute `wasp completion` and follow the instructions.
### Other
- `wasp version` prints current version of CLI.
```
$ wasp version
0.2.0.1
```
- `wasp uninstall` removes Wasp from your system.
```
$ wasp uninstall
🐝 --- Uninstalling Wasp ... ------------------------------------------------------
We will remove the following directories:
{home}/.local/share/wasp-lang/
{home}/.cache/wasp/
We will also remove the following files:
{home}/.local/bin/wasp
Are you sure you want to continue? [y/N]
y
✅ --- Uninstalled Wasp -----------------------------------------------------------
```

View File

@ -22,10 +22,9 @@ curl -sSL https://get.wasp-lang.dev/installer.sh | sh
Then, create a new app by running:
```shell
wasp new MyNewApp # Creates a new web app named MyNewApp.
cd MyNewApp
wasp new # Enter the project and choose the template
cd <my-project-name>
wasp start # Serves the web app.
```