From 3f218fbbc5724f3ec9cf61c5c13e6d184dc3cde3 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Wed, 3 May 2023 12:57:17 +0200 Subject: [PATCH] Improve the scaffolding experience (#1140) --- waspc/ChangeLog.md | 4 + waspc/cli/exe/Main.hs | 7 +- waspc/cli/src/Wasp/Cli/Archive.hs | 46 ++++ waspc/cli/src/Wasp/Cli/Command/Call.hs | 4 +- .../src/Wasp/Cli/Command/CreateNewProject.hs | 227 ++++-------------- .../CreateNewProject/ArgumentsParser.hs | 44 ++++ .../Cli/Command/CreateNewProject/Common.hs | 15 ++ .../CreateNewProject/ProjectDescription.hs | 144 +++++++++++ .../CreateNewProject/StarterTemplates.hs | 41 ++++ .../StarterTemplates/Local.hs | 28 +++ .../StarterTemplates/Remote.hs | 31 +++ .../StarterTemplates/Remote/Github.hs | 11 + .../StarterTemplates/Templating.hs | 32 +++ waspc/cli/src/Wasp/Cli/Command/Start/Db.hs | 4 +- waspc/cli/src/Wasp/Cli/FileSystem.hs | 16 +- waspc/cli/src/Wasp/Cli/GithubRepo.hs | 114 +++++++++ waspc/cli/src/Wasp/Cli/Interactive.hs | 121 ++++++++++ .../Cli/templates/{new => basic}/.gitignore | 0 .../Cli/templates/{new => basic}/.wasproot | 0 waspc/data/Cli/templates/basic/main.wasp | 11 + .../templates/{new => basic}/src/.waspignore | 0 .../{new => basic}/src/client/Main.css | 0 .../{new => basic}/src/client/MainPage.jsx | 0 .../{new => basic}/src/client/tsconfig.json | 0 .../{new => basic}/src/client/vite-env.d.ts | 0 .../{new => basic}/src/client/waspLogo.png | Bin .../{new => basic}/src/server/tsconfig.json | 0 .../{new => basic}/src/shared/tsconfig.json | 0 waspc/src/Wasp/Generator/Common.hs | 16 -- waspc/src/Wasp/Generator/DockerGenerator.hs | 2 +- waspc/src/Wasp/Generator/Job/Process.hs | 61 +---- waspc/src/Wasp/Generator/Node/Version.hs | 79 ++++++ waspc/src/Wasp/Generator/ServerGenerator.hs | 7 +- waspc/src/Wasp/Generator/WebAppGenerator.hs | 4 +- waspc/waspc.cabal | 23 +- web/docs/cli.md | 110 ++++++--- web/docs/introduction/getting-started.md | 5 +- 37 files changed, 899 insertions(+), 308 deletions(-) create mode 100644 waspc/cli/src/Wasp/Cli/Archive.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs create mode 100644 waspc/cli/src/Wasp/Cli/GithubRepo.hs create mode 100644 waspc/cli/src/Wasp/Cli/Interactive.hs rename waspc/data/Cli/templates/{new => basic}/.gitignore (100%) rename waspc/data/Cli/templates/{new => basic}/.wasproot (100%) create mode 100644 waspc/data/Cli/templates/basic/main.wasp rename waspc/data/Cli/templates/{new => basic}/src/.waspignore (100%) rename waspc/data/Cli/templates/{new => basic}/src/client/Main.css (100%) rename waspc/data/Cli/templates/{new => basic}/src/client/MainPage.jsx (100%) rename waspc/data/Cli/templates/{new => basic}/src/client/tsconfig.json (100%) rename waspc/data/Cli/templates/{new => basic}/src/client/vite-env.d.ts (100%) rename waspc/data/Cli/templates/{new => basic}/src/client/waspLogo.png (100%) rename waspc/data/Cli/templates/{new => basic}/src/server/tsconfig.json (100%) rename waspc/data/Cli/templates/{new => basic}/src/shared/tsconfig.json (100%) create mode 100644 waspc/src/Wasp/Generator/Node/Version.hs diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index e348d469f..b139f2a4a 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -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 diff --git a/waspc/cli/exe/Main.hs b/waspc/cli/exe/Main.hs index aad28e0d8..3444eab1d 100644 --- a/waspc/cli/exe/Main.hs +++ b/waspc/cli/exe/Main.hs @@ -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 [args] Creates a new Wasp project.", + cmd " new [] [args] Creates a new Wasp project. Run it without arguments for interactive mode.", " OPTIONS:", " -t|--template ", " 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", "", diff --git a/waspc/cli/src/Wasp/Cli/Archive.hs b/waspc/cli/src/Wasp/Cli/Archive.hs new file mode 100644 index 000000000..5874b522f --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Archive.hs @@ -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 diff --git a/waspc/cli/src/Wasp/Cli/Command/Call.hs b/waspc/cli/src/Wasp/Cli/Command/Call.hs index 994dae1a9..a8acba874 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Call.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Call.hs @@ -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] diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index 43489f58e..28eba0ce7 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs new file mode 100644 index 000000000..e29effe3f --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs @@ -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" diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs new file mode 100644 index 000000000..a72493e7d --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs @@ -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) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs new file mode 100644 index 000000000..0ec4f6a0c --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -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 [-t ] + + - 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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs new file mode 100644 index 000000000..4e67a60cd --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs @@ -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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs new file mode 100644 index 000000000..0a4e86534 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Local.hs @@ -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) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs new file mode 100644 index 000000000..77e9fbdcd --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote.hs @@ -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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs new file mode 100644 index 000000000..3f3ea5808 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Remote/Github.hs @@ -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" + } diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs new file mode 100644 index 000000000..02eac1ba0 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs @@ -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) + ] diff --git a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs index 1f0b9b38e..061dba746 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Start/Db.hs @@ -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." ) diff --git a/waspc/cli/src/Wasp/Cli/FileSystem.hs b/waspc/cli/src/Wasp/Cli/FileSystem.hs index 2b5992962..ff1ff2947 100644 --- a/waspc/cli/src/Wasp/Cli/FileSystem.hs +++ b/waspc/cli/src/Wasp/Cli/FileSystem.hs @@ -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 diff --git a/waspc/cli/src/Wasp/Cli/GithubRepo.hs b/waspc/cli/src/Wasp/Cli/GithubRepo.hs new file mode 100644 index 000000000..952d96352 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/GithubRepo.hs @@ -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." diff --git a/waspc/cli/src/Wasp/Cli/Interactive.hs b/waspc/cli/src/Wasp/Cli/Interactive.hs new file mode 100644 index 000000000..5ffc9661f --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Interactive.hs @@ -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 diff --git a/waspc/data/Cli/templates/new/.gitignore b/waspc/data/Cli/templates/basic/.gitignore similarity index 100% rename from waspc/data/Cli/templates/new/.gitignore rename to waspc/data/Cli/templates/basic/.gitignore diff --git a/waspc/data/Cli/templates/new/.wasproot b/waspc/data/Cli/templates/basic/.wasproot similarity index 100% rename from waspc/data/Cli/templates/new/.wasproot rename to waspc/data/Cli/templates/basic/.wasproot diff --git a/waspc/data/Cli/templates/basic/main.wasp b/waspc/data/Cli/templates/basic/main.wasp new file mode 100644 index 000000000..25dadef3c --- /dev/null +++ b/waspc/data/Cli/templates/basic/main.wasp @@ -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" +} diff --git a/waspc/data/Cli/templates/new/src/.waspignore b/waspc/data/Cli/templates/basic/src/.waspignore similarity index 100% rename from waspc/data/Cli/templates/new/src/.waspignore rename to waspc/data/Cli/templates/basic/src/.waspignore diff --git a/waspc/data/Cli/templates/new/src/client/Main.css b/waspc/data/Cli/templates/basic/src/client/Main.css similarity index 100% rename from waspc/data/Cli/templates/new/src/client/Main.css rename to waspc/data/Cli/templates/basic/src/client/Main.css diff --git a/waspc/data/Cli/templates/new/src/client/MainPage.jsx b/waspc/data/Cli/templates/basic/src/client/MainPage.jsx similarity index 100% rename from waspc/data/Cli/templates/new/src/client/MainPage.jsx rename to waspc/data/Cli/templates/basic/src/client/MainPage.jsx diff --git a/waspc/data/Cli/templates/new/src/client/tsconfig.json b/waspc/data/Cli/templates/basic/src/client/tsconfig.json similarity index 100% rename from waspc/data/Cli/templates/new/src/client/tsconfig.json rename to waspc/data/Cli/templates/basic/src/client/tsconfig.json diff --git a/waspc/data/Cli/templates/new/src/client/vite-env.d.ts b/waspc/data/Cli/templates/basic/src/client/vite-env.d.ts similarity index 100% rename from waspc/data/Cli/templates/new/src/client/vite-env.d.ts rename to waspc/data/Cli/templates/basic/src/client/vite-env.d.ts diff --git a/waspc/data/Cli/templates/new/src/client/waspLogo.png b/waspc/data/Cli/templates/basic/src/client/waspLogo.png similarity index 100% rename from waspc/data/Cli/templates/new/src/client/waspLogo.png rename to waspc/data/Cli/templates/basic/src/client/waspLogo.png diff --git a/waspc/data/Cli/templates/new/src/server/tsconfig.json b/waspc/data/Cli/templates/basic/src/server/tsconfig.json similarity index 100% rename from waspc/data/Cli/templates/new/src/server/tsconfig.json rename to waspc/data/Cli/templates/basic/src/server/tsconfig.json diff --git a/waspc/data/Cli/templates/new/src/shared/tsconfig.json b/waspc/data/Cli/templates/basic/src/shared/tsconfig.json similarity index 100% rename from waspc/data/Cli/templates/new/src/shared/tsconfig.json rename to waspc/data/Cli/templates/basic/src/shared/tsconfig.json diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 4564fe7b7..7307b2cbc 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -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 diff --git a/waspc/src/Wasp/Generator/DockerGenerator.hs b/waspc/src/Wasp/Generator/DockerGenerator.hs index 0a66521bc..ae3b5e656 100644 --- a/waspc/src/Wasp/Generator/DockerGenerator.hs +++ b/waspc/src/Wasp/Generator/DockerGenerator.hs @@ -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) diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 1240bba58..41938a286 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -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." - ] diff --git a/waspc/src/Wasp/Generator/Node/Version.hs b/waspc/src/Wasp/Generator/Node/Version.hs new file mode 100644 index 000000000..5e1c62684 --- /dev/null +++ b/waspc/src/Wasp/Generator/Node/Version.hs @@ -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 + ] diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 9e21435cc..677d80d30 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -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") ] } diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 26c1ba01e..7b9565481 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -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 ] ) diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 8bce0f557..030b7175f 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -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 diff --git a/web/docs/cli.md b/web/docs/cli.md index 82186115a..87c6c3399 100644 --- a/web/docs/cli.md +++ b/web/docs/cli.md @@ -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 Creates new Wasp project. + new [] [args] Creates a new Wasp project. Run it without arguments for interactive mode. + OPTIONS: + -t|--template + 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 [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 ` 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 ` 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 ----------------------------------------------------------- + ``` diff --git a/web/docs/introduction/getting-started.md b/web/docs/introduction/getting-started.md index 06582326c..4f7ee0c83 100644 --- a/web/docs/introduction/getting-started.md +++ b/web/docs/introduction/getting-started.md @@ -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 wasp start # Serves the web app. ```