diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index a66b07cbc8..ac1deb772a 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -1,11 +1,15 @@ # Changelog -## Next +## [WIP] 0.16.0 ### ⚠️ Breaking Changes - Renamed and split `deserializeAndSanitizeProviderData` to `getProviderData` and `getProviderDataWithPassword` so it's more explicit if the resulting data will contain the hashed password or not. +### 🎉 New Features and improvements + +- Added support for third-party starter templates via `wasp new -t github:/[/dir/to/template]`. + ### 🔧 Small improvements - Enabled strict null checks for the Wasp SDK which means that some of the return types are more precise now e.g. you'll need to check if some value is `null` before using it. diff --git a/waspc/cli/exe/Main.hs b/waspc/cli/exe/Main.hs index c68e30ebda..055eb1f68b 100644 --- a/waspc/cli/exe/Main.hs +++ b/waspc/cli/exe/Main.hs @@ -161,8 +161,7 @@ printUsage = title " GENERAL", 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", + " -t|--template ", "", cmd " new:ai []", " Uses AI to create a new Wasp project just based on the app name and the description.", diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index 6dd78fd804..c4637f6d39 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -15,14 +15,15 @@ import Wasp.Cli.Command.CreateNewProject.ProjectDescription ( NewProjectDescription (..), obtainNewProjectDescription, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates - ( DirBasedTemplateMetadata (_path), - StarterTemplate (..), - getStarterTemplates, - getTemplateStartingInstructions, - ) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates (getFeaturedStarterTemplates) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo (createProjectOnDiskFromGhRepoTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate + ( StarterTemplate (..), + WaspAppAiGenerator (WaspAI), + getTemplateName, + getTemplateStartingInstructions, + ) import Wasp.Cli.Command.Message (cliSendMessageC) import qualified Wasp.Message as Msg import qualified Wasp.Util.Terminal as Term @@ -31,9 +32,8 @@ import qualified Wasp.Util.Terminal as Term createNewProject :: Arguments -> Command () createNewProject args = do newProjectArgs <- parseNewProjectArgs args & either Common.throwProjectCreationError return - let starterTemplates = getStarterTemplates - - newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplates + let featuredStarterTemplates = getFeaturedStarterTemplates + newProjectDescription <- obtainNewProjectDescription newProjectArgs featuredStarterTemplates createProjectOnDisk newProjectDescription liftIO $ printGettingStartedInstructionsForProject newProjectDescription @@ -46,20 +46,21 @@ createProjectOnDisk _template = template, _absWaspProjectDir = absWaspProjectDir } = do - cliSendMessageC $ Msg.Start $ "Creating your project from the \"" ++ show template ++ "\" template..." + cliSendMessageC $ Msg.Start $ "Creating your project from the \"" ++ getTemplateName template ++ "\" template..." case template of - GhRepoStarterTemplate ghRepoRef metadata -> - createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef $ _path metadata - LocalStarterTemplate metadata -> - liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata - AiGeneratedStarterTemplate -> - AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName + GhRepoStarterTemplate ghRepoRef tmplDirPath _ -> + createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef tmplDirPath + LocalStarterTemplate tmplDirPath _ -> + liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName tmplDirPath + AiGeneratedStarterTemplate waspAppAiGenerator _ -> + case waspAppAiGenerator of + WaspAI -> AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName -- | This function assumes that the project dir was created inside the current working directory. printGettingStartedInstructionsForProject :: NewProjectDescription -> IO () printGettingStartedInstructionsForProject projectDescription = do let projectDirName = init . SP.toFilePath . SP.basename $ _absWaspProjectDir projectDescription - let instructions = getTemplateStartingInstructions projectDirName $ _template projectDescription + let instructions = getTemplateStartingInstructions (_template projectDescription) projectDirName putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectDirName ++ " directory!" putStrLn "" putStrLn instructions diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs index 45a3ae5b03..80268b0488 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs @@ -38,7 +38,7 @@ import Wasp.Cli.Command.CreateNewProject.ProjectDescription obtainAvailableProjectDirPath, parseWaspProjectNameIntoAppName, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Skeleton (readWaspProjectSkeletonFiles) import qualified Wasp.Cli.Interactive as Interactive import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.Util as U diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs index e29effe3f3..b7a346c0ec 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ArgumentsParser.hs @@ -10,7 +10,7 @@ import Wasp.Cli.Command.Call (Arguments) data NewProjectArgs = NewProjectArgs { _projectName :: Maybe String, - _templateName :: Maybe String + _templateId :: Maybe String } parseNewProjectArgs :: Arguments -> Either String NewProjectArgs @@ -33,7 +33,7 @@ parseNewProjectArgs newArgs = parserResultToEither $ execParserPure defaultPrefs Opt.strOption $ Opt.long "template" <> Opt.short 't' - <> Opt.metavar "TEMPLATE_NAME" + <> Opt.metavar "TEMPLATE" <> Opt.help "Template to use for the new project" parserResultToEither :: Opt.ParserResult NewProjectArgs -> Either String NewProjectArgs diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs index 5446040773..c9978a965e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs @@ -1,6 +1,5 @@ module Wasp.Cli.Command.CreateNewProject.Common ( throwProjectCreationError, - throwInvalidTemplateNameUsedError, defaultWaspVersionBounds, ) where @@ -13,11 +12,5 @@ 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 index ead1f2a732..2181be1d7f 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -9,9 +9,9 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription where import Control.Monad.IO.Class (liftIO) -import Data.Function ((&)) import Data.List (intercalate) import Data.List.NonEmpty (fromList) +import Data.Maybe (fromJust) import Path.IO (doesDirExist) import StrongPath (Abs, Dir, Path') import StrongPath.Path (toPathAbsDir) @@ -19,15 +19,15 @@ 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, + ( throwProjectCreationError, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates - ( StarterTemplate, - defaultStarterTemplate, - findTemplateByString, +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates (defaultStarterTemplate) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (StarterTemplate) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId + ( getStarterTemplateByIdOrThrow, ) import Wasp.Cli.FileSystem (getAbsPathToDirInCwd) +import Wasp.Cli.Interactive (IsOption (..)) import qualified Wasp.Cli.Interactive as Interactive import Wasp.Project (WaspProjectDir) import Wasp.Util (indent, kebabToCamelCase, whenM) @@ -62,32 +62,48 @@ instance Show NewProjectAppName where wasp new - Project name is required. - - Template name is required, we ask the user to choose from available templates. + - Template name is required, we ask the user to choose from featured templates. -} obtainNewProjectDescription :: NewProjectArgs -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _templateName = templateNameArg} starterTemplates = +obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _templateId = templateIdArg} starterTemplates = case projectNameArg of - Just projectName -> obtainNewProjectDescriptionFromCliArgs projectName templateNameArg starterTemplates - Nothing -> obtainNewProjectDescriptionInteractively templateNameArg starterTemplates + Just projectName -> obtainNewProjectDescriptionFromCliArgs projectName templateIdArg starterTemplates + Nothing -> obtainNewProjectDescriptionInteractively templateIdArg starterTemplates obtainNewProjectDescriptionFromCliArgs :: String -> Maybe String -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescriptionFromCliArgs projectName templateNameArg availableTemplates = +obtainNewProjectDescriptionFromCliArgs projectName templateIdArg featuredTemplates = obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName - templateNameArg - availableTemplates + templateIdArg + featuredTemplates (return defaultStarterTemplate) +data StarterTemplateMenuChoice = FeaturedStarterTemplate !StarterTemplate | CommunityTemplate + +instance IsOption StarterTemplateMenuChoice where + showOption (FeaturedStarterTemplate tmpl) = showOption tmpl + showOption CommunityTemplate = "community template" + showOptionDescription (FeaturedStarterTemplate tmpl) = showOptionDescription tmpl + showOptionDescription CommunityTemplate = Just "Check our list of community-made templates at https://wasp-lang.dev/docs/project/starter-templates#community-templates" + obtainNewProjectDescriptionInteractively :: Maybe String -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescriptionInteractively templateNameArg availableTemplates = do +obtainNewProjectDescriptionInteractively templateIdArg featuredTemplates = do projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)" obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName - templateNameArg - availableTemplates - (liftIO askForTemplateName) + templateIdArg + featuredTemplates + askForTemplate where - askForTemplateName = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates + askForTemplate = + liftIO + ( Interactive.askToChoose + "Choose a starter template" + (fromList $ (FeaturedStarterTemplate <$> featuredTemplates) ++ [CommunityTemplate]) + ) + >>= \case + FeaturedStarterTemplate tmpl -> pure tmpl + CommunityTemplate -> throwProjectCreationError $ "Project creation aborted. " <> fromJust (showOptionDescription CommunityTemplate) <> " and follow the instructions of a specific template." -- Common logic obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: @@ -96,15 +112,14 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: [StarterTemplate] -> Command StarterTemplate -> Command NewProjectDescription -obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateNameArg availableTemplates obtainTemplateWhenNoArg = do +obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg featuredTemplates getStarterTemplateWhenNoArg = do absWaspProjectDir <- obtainAvailableProjectDirPath projectName - selectedTemplate <- maybe obtainTemplateWhenNoArg findTemplateOrThrow templateNameArg + selectedTemplate <- + maybe + getStarterTemplateWhenNoArg + (either throwProjectCreationError pure . getStarterTemplateByIdOrThrow featuredTemplates) + templateIdArg mkNewProjectDescription projectName absWaspProjectDir selectedTemplate - where - findTemplateOrThrow :: String -> Command StarterTemplate - findTemplateOrThrow templateName = - findTemplateByString availableTemplates templateName - & maybe throwInvalidTemplateNameUsedError return obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir)) obtainAvailableProjectDirPath projectName = do diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs deleted file mode 100644 index 0c44950e11..0000000000 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs +++ /dev/null @@ -1,238 +0,0 @@ -{-# LANGUAGE TupleSections #-} - -module Wasp.Cli.Command.CreateNewProject.StarterTemplates - ( getStarterTemplates, - StarterTemplate (..), - DirBasedTemplateMetadata (..), - findTemplateByString, - defaultStarterTemplate, - readWaspProjectSkeletonFiles, - getTemplateStartingInstructions, - ) -where - -import Data.Foldable (find) -import Data.Text (Text) -import StrongPath (Dir', File', Path, Path', Rel, Rel', System, reldir, ()) -import qualified System.FilePath as FP -import qualified Wasp.Cli.GithubRepo as GhRepo -import qualified Wasp.Cli.Interactive as Interactive -import qualified Wasp.Data as Data -import Wasp.Project.Common (WaspProjectDir) -import Wasp.Util.IO (listDirectoryDeep, readFileStrict) -import qualified Wasp.Util.Terminal as Term - -data StarterTemplate - = -- | Template from a Github repo. - GhRepoStarterTemplate !GhRepo.GithubRepoRef !DirBasedTemplateMetadata - | -- | Template from a disk, that comes bundled with wasp CLI. - LocalStarterTemplate !DirBasedTemplateMetadata - | -- | Template that will be dynamically generated by Wasp AI based on user's input. - AiGeneratedStarterTemplate - -data DirBasedTemplateMetadata = DirBasedTemplateMetadata - { _name :: !String, - _path :: !(Path' Rel' Dir'), -- Path to a directory containing template files. - _description :: !String, - _buildStartingInstructions :: !StartingInstructionsBuilder - } - -instance Show StarterTemplate where - show (GhRepoStarterTemplate _ metadata) = _name metadata - show (LocalStarterTemplate metadata) = _name metadata - show AiGeneratedStarterTemplate = "ai-generated" - -instance Interactive.IsOption StarterTemplate where - showOption = show - - showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata - showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata - showOptionDescription AiGeneratedStarterTemplate = - Just "🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)" - -type StartingInstructionsBuilder = String -> String - -{- HLINT ignore getTemplateStartingInstructions "Redundant $" -} - --- | Returns instructions for running the newly created (from the template) Wasp project. --- Instructions assume that user is positioned right next to the just created project directory, --- whose name is provided via projectDirName. -getTemplateStartingInstructions :: String -> StarterTemplate -> String -getTemplateStartingInstructions projectDirName = \case - GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata projectDirName - LocalStarterTemplate metadata -> _buildStartingInstructions metadata projectDirName - AiGeneratedStarterTemplate -> - unlines - [ styleText $ "To run your new app, do:", - styleCode $ " cd " <> projectDirName, - styleCode $ " wasp db migrate-dev", - styleCode $ " wasp start" - ] - -getStarterTemplates :: [StarterTemplate] -getStarterTemplates = - [ defaultStarterTemplate, - todoTsStarterTemplate, - openSaasStarterTemplate, - embeddingsStarterTemplate, - AiGeneratedStarterTemplate - ] - -defaultStarterTemplate :: StarterTemplate -defaultStarterTemplate = basicStarterTemplate - -{- HLINT ignore basicStarterTemplate "Redundant $" -} - -basicStarterTemplate :: StarterTemplate -basicStarterTemplate = - LocalStarterTemplate $ - DirBasedTemplateMetadata - { _path = [reldir|basic|], - _name = "basic", - _description = "Simple starter template with a single page.", - _buildStartingInstructions = \projectDirName -> - unlines - [ styleText $ "To run your new app, do:", - styleCode $ " cd " <> projectDirName, - styleCode $ " wasp start" - ] - } - -{- HLINT ignore openSaasStarterTemplate "Redundant $" -} - -openSaasStarterTemplate :: StarterTemplate -openSaasStarterTemplate = - simpleGhRepoTemplate - ("open-saas", [reldir|template|]) - ( "saas", - "Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more." - <> " Check out https://opensaas.sh/ for more details." - ) - ( \projectDirName -> - unlines - [ styleText $ "To run your new app, follow the instructions below:", - styleText $ "", - styleText $ " 1. Position into app's root directory:", - styleCode $ " cd " <> projectDirName FP. "app", - styleText $ "", - styleText $ " 2. Run the development database (and leave it running):", - styleCode $ " wasp db start", - styleText $ "", - styleText $ " 3. Open new terminal window (or tab) in that same dir and continue in it.", - styleText $ "", - styleText $ " 4. Apply initial database migrations:", - styleCode $ " wasp db migrate-dev", - styleText $ "", - styleText $ " 5. Create initial dot env file from the template:", - styleCode $ " cp .env.server.example .env.server", - styleText $ "", - styleText $ " 6. Last step: run the app!", - styleCode $ " wasp start", - styleText $ "", - styleText $ "Check the README for additional guidance and the link to docs!" - ] - ) - -{- HLINT ignore todoTsStarterTemplate "Redundant $" -} - -todoTsStarterTemplate :: StarterTemplate -todoTsStarterTemplate = - simpleGhRepoTemplate - ("starters", [reldir|todo-ts|]) - ( "todo-ts", - "Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety." - ) - ( \projectDirName -> - unlines - [ styleText $ "To run your new app, do:", - styleCode $ " cd " ++ projectDirName, - styleCode $ " wasp db migrate-dev", - styleCode $ " wasp start", - styleText $ "", - styleText $ "Check the README for additional guidance!" - ] - ) - -{- Functions for styling instructions. Their names are on purpose of same length, for nicer code formatting. -} - -styleCode :: String -> String -styleCode = Term.applyStyles [Term.Bold] - -styleText :: String -> String -styleText = id - -{- -} - -{- HLINT ignore embeddingsStarterTemplate "Redundant $" -} - -embeddingsStarterTemplate :: StarterTemplate -embeddingsStarterTemplate = - simpleGhRepoTemplate - ("starters", [reldir|embeddings|]) - ( "embeddings", - "Comes with code for generating vector embeddings and performing vector similarity search." - ) - ( \projectDirName -> - unlines - [ styleText $ "To run your new app, follow the instructions below:", - styleText $ "", - styleText $ " 1. Position into app's root directory:", - styleCode $ " cd " <> projectDirName, - styleText $ "", - styleText $ " 2. Create initial dot env file from the template and fill in your API keys:", - styleCode $ " cp .env.server.example .env.server", - styleText $ " Fill in your API keys!", - styleText $ "", - styleText $ " 3. Run the development database (and leave it running):", - styleCode $ " wasp db start", - styleText $ "", - styleText $ " 4. Open new terminal window (or tab) in that same dir and continue in it.", - styleText $ "", - styleText $ " 5. Apply initial database migrations:", - styleCode $ " wasp db migrate-dev", - styleText $ "", - styleText $ " 6. Run wasp seed script that will generate embeddings from the text files in src/shared/docs:", - styleCode $ " wasp db seed", - styleText $ "", - styleText $ " 7. Last step: run the app!", - styleCode $ " wasp start", - styleText $ "", - styleText $ "Check the README for more detailed instructions and additional guidance!" - ] - ) - -simpleGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> StartingInstructionsBuilder -> StarterTemplate -simpleGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDescription) buildStartingInstructions = - GhRepoStarterTemplate - ( GhRepo.GithubRepoRef - { GhRepo._repoOwner = waspGhOrgName, - GhRepo._repoName = repoName, - GhRepo._repoReferenceName = waspVersionTemplateGitTag - } - ) - ( DirBasedTemplateMetadata - { _name = tmplDisplayName, - _description = tmplDescription, - _path = tmplPathInRepo, - _buildStartingInstructions = buildStartingInstructions - } - ) - -waspGhOrgName :: String -waspGhOrgName = "wasp-lang" - --- NOTE: As version of Wasp CLI changes, so we should update this tag name here, --- and also create it on gh repos of templates. --- By tagging templates for each version of Wasp CLI, we ensure that each release of --- Wasp CLI uses correct version of templates, that work with it. -waspVersionTemplateGitTag :: String -waspVersionTemplateGitTag = "wasp-v0.15-template" - -findTemplateByString :: [StarterTemplate] -> String -> Maybe StarterTemplate -findTemplateByString templates query = find ((== query) . show) templates - -readWaspProjectSkeletonFiles :: IO [(Path System (Rel WaspProjectDir) File', Text)] -readWaspProjectSkeletonFiles = do - skeletonFilesDir <- ( [reldir|Cli/templates/skeleton|]) <$> Data.getAbsDataDirPath - skeletonFilePaths <- listDirectoryDeep skeletonFilesDir - mapM (\path -> (path,) <$> readFileStrict (skeletonFilesDir path)) skeletonFilePaths diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Common.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Common.hs new file mode 100644 index 0000000000..6d33953e5d --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Common.hs @@ -0,0 +1,23 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common + ( waspVersionTemplateGitTag, + styleCode, + styleText, + ) +where + +import qualified Wasp.Util.Terminal as Term + +-- NOTE: As version of Wasp CLI changes, so we should update this tag name here, +-- and also create it on gh repos of templates. +-- By tagging templates for each version of Wasp CLI, we ensure that each release of +-- Wasp CLI uses correct version of templates, that work with it. +waspVersionTemplateGitTag :: String +waspVersionTemplateGitTag = "wasp-v0.15-template" + +-- * Functions for styling the template instructions. Their names are on purpose of same length, for nicer code formatting. + +styleCode :: String -> String +styleCode = Term.applyStyles [Term.Bold] + +styleText :: String -> String +styleText = id diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs new file mode 100644 index 0000000000..7f1b5c8ff7 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs @@ -0,0 +1,164 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates + ( getFeaturedStarterTemplates, + defaultStarterTemplate, + ) +where + +import StrongPath (Dir', Path', Rel', reldir) +import qualified System.FilePath as FP +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common (styleCode, styleText, waspVersionTemplateGitTag) +import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate as ST +import qualified Wasp.Cli.GithubRepo as GhRepo + +getFeaturedStarterTemplates :: [ST.StarterTemplate] +getFeaturedStarterTemplates = + [ defaultStarterTemplate, + todoTsStarterTemplate, + openSaasStarterTemplate, + embeddingsStarterTemplate, + waspAiGeneratedStarterTemplate + ] + +defaultStarterTemplate :: ST.StarterTemplate +defaultStarterTemplate = basicStarterTemplate + +{- HLINT ignore basicStarterTemplate "Redundant $" -} +basicStarterTemplate :: ST.StarterTemplate +basicStarterTemplate = + ST.LocalStarterTemplate [reldir|basic|] $ + ST.TemplateMetadata + { ST._tmplName = "basic", + ST._tmplDescription = "Simple starter template with a single page.", + ST._tmplBuildStartingInstructions = \projectDirName -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " <> projectDirName, + styleCode $ " wasp start" + ] + } + +{- HLINT ignore openSaasStarterTemplate "Redundant $" -} +openSaasStarterTemplate :: ST.StarterTemplate +openSaasStarterTemplate = + makeSimpleWaspGhRepoTemplate + ("open-saas", [reldir|template|]) + ( "saas", + "Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more." + <> " Check out https://opensaas.sh/ for more details." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, follow the instructions below:", + styleText $ "", + styleText $ " 1. Position into app's root directory:", + styleCode $ " cd " <> projectDirName FP. "app", + styleText $ "", + styleText $ " 2. Run the development database (and leave it running):", + styleCode $ " wasp db start", + styleText $ "", + styleText $ " 3. Open new terminal window (or tab) in that same dir and continue in it.", + styleText $ "", + styleText $ " 4. Apply initial database migrations:", + styleCode $ " wasp db migrate-dev", + styleText $ "", + styleText $ " 5. Create initial dot env file from the template:", + styleCode $ " cp .env.server.example .env.server", + styleText $ "", + styleText $ " 6. Last step: run the app!", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for additional guidance and the link to docs!" + ] + ) + +{- HLINT ignore todoTsStarterTemplate "Redundant $" -} +todoTsStarterTemplate :: ST.StarterTemplate +todoTsStarterTemplate = + makeSimpleWaspGhRepoTemplate + ("starters", [reldir|todo-ts|]) + ( "todo-ts", + "Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " ++ projectDirName, + styleCode $ " wasp db migrate-dev", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for additional guidance!" + ] + ) + +{- HLINT ignore embeddingsStarterTemplate "Redundant $" -} +embeddingsStarterTemplate :: ST.StarterTemplate +embeddingsStarterTemplate = + makeSimpleWaspGhRepoTemplate + ("starters", [reldir|embeddings|]) + ( "embeddings", + "Comes with code for generating vector embeddings and performing vector similarity search." + ) + ( \projectDirName -> + unlines + [ styleText $ "To run your new app, follow the instructions below:", + styleText $ "", + styleText $ " 1. Position into app's root directory:", + styleCode $ " cd " <> projectDirName, + styleText $ "", + styleText $ " 2. Create initial dot env file from the template and fill in your API keys:", + styleCode $ " cp .env.server.example .env.server", + styleText $ " Fill in your API keys!", + styleText $ "", + styleText $ " 3. Run the development database (and leave it running):", + styleCode $ " wasp db start", + styleText $ "", + styleText $ " 4. Open new terminal window (or tab) in that same dir and continue in it.", + styleText $ "", + styleText $ " 5. Apply initial database migrations:", + styleCode $ " wasp db migrate-dev", + styleText $ "", + styleText $ " 6. Run wasp seed script that will generate embeddings from the text files in src/shared/docs:", + styleCode $ " wasp db seed", + styleText $ "", + styleText $ " 7. Last step: run the app!", + styleCode $ " wasp start", + styleText $ "", + styleText $ "Check the README for more detailed instructions and additional guidance!" + ] + ) + +{- HLINT ignore aiGeneratedStarterTemplate "Redundant $" -} +waspAiGeneratedStarterTemplate :: ST.StarterTemplate +waspAiGeneratedStarterTemplate = + ST.AiGeneratedStarterTemplate ST.WaspAI $ + ST.TemplateMetadata + { _tmplName = "ai-generated", + _tmplDescription = "🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)", + _tmplBuildStartingInstructions = \projectDirName -> + unlines + [ styleText $ "To run your new app, do:", + styleCode $ " cd " <> projectDirName, + styleCode $ " wasp db migrate-dev", + styleCode $ " wasp start" + ] + } + +makeSimpleWaspGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> ST.StartingInstructionsBuilder -> ST.StarterTemplate +makeSimpleWaspGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDescription) buildStartingInstructions = + ST.GhRepoStarterTemplate + ( GhRepo.GithubRepoRef + { GhRepo._repoOwner = waspGhOrgName, + GhRepo._repoName = repoName, + GhRepo._repoReferenceName = waspVersionTemplateGitTag + } + ) + tmplPathInRepo + ( ST.TemplateMetadata + { _tmplName = tmplDisplayName, + _tmplDescription = tmplDescription, + _tmplBuildStartingInstructions = buildStartingInstructions + } + ) + +waspGhOrgName :: String +waspGhOrgName = "wasp-lang" diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs new file mode 100644 index 0000000000..c50341b597 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TupleSections #-} + +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Skeleton + ( readWaspProjectSkeletonFiles, + ) +where + +import Data.Functor ((<&>)) +import Data.Text (Text) +import StrongPath (File', Path, Rel, System, reldir, ()) +import qualified Wasp.Data as Data +import Wasp.Project.Common (WaspProjectDir) +import Wasp.Util.IO (listDirectoryDeep, readFileStrict) + +readWaspProjectSkeletonFiles :: IO [(Path System (Rel WaspProjectDir) File', Text)] +readWaspProjectSkeletonFiles = do + skeletonFilesDir <- Data.getAbsDataDirPath <&> ( [reldir|Cli/templates/skeleton|]) + skeletonFilePaths <- listDirectoryDeep skeletonFilesDir + mapM (\path -> (path,) <$> readFileStrict (skeletonFilesDir path)) skeletonFilePaths diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs new file mode 100644 index 0000000000..4aa97efba2 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -0,0 +1,61 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate + ( StarterTemplate (..), + TemplateMetadata (..), + StartingInstructionsBuilder, + WaspAppAiGenerator (..), + getTemplateStartingInstructions, + getTemplateName, + ) +where + +import StrongPath (Dir', Path', Rel') +import qualified Wasp.Cli.GithubRepo as GhRepo +import qualified Wasp.Cli.Interactive as Interactive + +data StarterTemplate + = -- | Template from a Github repo. + GhRepoStarterTemplate !GhRepo.GithubRepoRef !TemplateDirPath !TemplateMetadata + | -- | Template from a disk, that comes bundled with wasp CLI. + LocalStarterTemplate !TemplateDirPath !TemplateMetadata + | -- | Template that will be dynamically generated by AI based on user's input. + AiGeneratedStarterTemplate !WaspAppAiGenerator !TemplateMetadata + +-- | Path to a directory containing template files. +type TemplateDirPath = Path' Rel' Dir' + +data TemplateMetadata = TemplateMetadata + { _tmplName :: !String, + _tmplDescription :: !String, + _tmplBuildStartingInstructions :: !StartingInstructionsBuilder + } + +-- | AI generators that can generate Wasp apps. +data WaspAppAiGenerator = WaspAI + +instance Interactive.IsOption StarterTemplate where + showOption = getTemplateName + showOptionDescription = Just . getTemplateDescription + +-- | Function that returns instructions for running the newly created (from the template) Wasp project. +-- Instructions assume that user is positioned right next to the just created project directory, +-- whose name is provided via projectDirName. +type StartingInstructionsBuilder = ProjectDirName -> StartingInstructions + +type ProjectDirName = String + +type StartingInstructions = String + +getTemplateMetadata :: StarterTemplate -> TemplateMetadata +getTemplateMetadata = \case + GhRepoStarterTemplate _ _ metadata -> metadata + LocalStarterTemplate _ metadata -> metadata + AiGeneratedStarterTemplate _ metadata -> metadata + +getTemplateName :: StarterTemplate -> String +getTemplateName = _tmplName . getTemplateMetadata + +getTemplateDescription :: StarterTemplate -> String +getTemplateDescription = _tmplDescription . getTemplateMetadata + +getTemplateStartingInstructions :: StarterTemplate -> StartingInstructionsBuilder +getTemplateStartingInstructions = _tmplBuildStartingInstructions . getTemplateMetadata diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs new file mode 100644 index 0000000000..be1954d4b9 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -0,0 +1,115 @@ +{-# OPTIONS_GHC -Wno-unused-top-binds #-} + +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId + ( getStarterTemplateByIdOrThrow, + findTemplateByName, + ) +where + +import Control.Arrow (left) +import Control.Monad.Except (MonadError, throwError) +import Data.Foldable (find) +import Data.Function ((&)) +import Data.List (intercalate, isPrefixOf) +import Data.Maybe (fromMaybe) +import StrongPath (Dir', Path', Rel', parseRelDir, reldir) +import qualified System.FilePath.Posix as FP.Posix +import qualified Text.Parsec as P +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common (styleText, waspVersionTemplateGitTag) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (getTemplateName) +import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate as ST +import qualified Wasp.Cli.GithubRepo as GhRepo + +-- | A way to uniquely reference a specific Wasp starter template. +-- It is how users specify which starter template they want to use. +data StarterTemplateId + = -- | References one of the featured templates (templates that we know about here in Wasp CLI) by its name. + FeaturedTemplateName !String + | -- | References any template that is available as a github repo. + -- Most useful for referencing third-party repos that we don't know about, since featured ones + -- we can more simply reference by name. + GhRepoTemplateUri !GhRepo.GithubRepoOwner !GhRepo.GithubRepoName !(Maybe (Path' Rel' Dir')) + +-- | This type allows us to reason about types of StarterTemplateIds without having their runtime values. +data StarterTemplateIdType + = IdTypeFeaturedTemplateName + | IdTypeGhRepoTemplateUri + deriving (Enum, Bounded) + +-- | This function serves its purpose just by being defined, even if not used anywhere, because it +-- ensures (via compiler warning) that we don't forget do update StarterTemplateIdType accordingly +-- when we change StarterTemplateId (create, delete or modify data constructor). +getStarterTemplateIdType :: StarterTemplateId -> StarterTemplateIdType +getStarterTemplateIdType = \case + FeaturedTemplateName {} -> IdTypeFeaturedTemplateName + GhRepoTemplateUri {} -> IdTypeGhRepoTemplateUri + +getStarterTemplateIdTypeDescription :: StarterTemplateIdType -> String +getStarterTemplateIdTypeDescription = \case + IdTypeFeaturedTemplateName -> "a featured template name (e.g. \"saas\")" + IdTypeGhRepoTemplateUri -> "a github URI (github:/[/dir/to/template])" + +-- | Given a template id (as string), it will obtain the information on the template that this id references. +-- It will throw if the id is invalid (can't be parsed, or information on the template can't be obtain based on it). +getStarterTemplateByIdOrThrow :: (MonadError String m) => [ST.StarterTemplate] -> String -> m ST.StarterTemplate +getStarterTemplateByIdOrThrow featuredTemplates templateIdString = + (parseStarterTemplateId templateIdString & either throwTemplateIdParsingError pure) >>= \case + FeaturedTemplateName templateName -> + findTemplateByName featuredTemplates templateName + & maybe (throwInvalidTemplateNameUsedError templateName) pure + GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> + return $ + ST.GhRepoStarterTemplate + (GhRepo.GithubRepoRef repoOwner repoName waspVersionTemplateGitTag) + (maybeTmplDirPath & fromMaybe [reldir|.|]) + ( ST.TemplateMetadata + { ST._tmplName = repoName, + ST._tmplDescription = "Template from Github repo " <> repoOwner <> "/" <> repoName, + ST._tmplBuildStartingInstructions = \_ -> + unlines + [ styleText $ "Check https://github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." + ] + } + ) + where + throwTemplateIdParsingError errorMsg = + throwError $ + "Failed to parse template id: " <> errorMsg <> "\n" <> expectedInputMessage + + throwInvalidTemplateNameUsedError templateName = + throwError $ + "There is no featured template with name " <> wrapInQuotes templateName <> ".\n" <> expectedInputMessage + + expectedInputMessage = + "Expected " <> intercalate " or " (getStarterTemplateIdTypeDescription <$> [minBound .. maxBound]) <> "." + <> ("\nValid featured template names are " <> intercalate ", " (wrapInQuotes . getTemplateName <$> featuredTemplates) <> ".") + + wrapInQuotes str = "\"" <> str <> "\"" + +parseStarterTemplateId :: String -> Either String StarterTemplateId +parseStarterTemplateId = \case + templateId | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> parseGhRepoTemplateUri templateId & left show + templateId -> pure $ FeaturedTemplateName templateId + where + -- Parses following format: github:/[/] . + parseGhRepoTemplateUri :: String -> Either P.ParseError StarterTemplateId + parseGhRepoTemplateUri = P.parse parser "" + where + parser = do + _ <- P.string ghRepoTemplateIdPrefix + repoOwner <- P.many1 (P.noneOf [FP.Posix.pathSeparator]) + _ <- P.char FP.Posix.pathSeparator + repoName <- P.many1 (P.noneOf [FP.Posix.pathSeparator]) + maybeTmplDirStrongPath <- + P.optionMaybe (P.char FP.Posix.pathSeparator >> P.many1 P.anyChar) >>= \case + Nothing -> pure Nothing + -- Even though parseRelDir returns System Path, it is able to parse both Posix and System + -- separators, which enables us to use it here even though we expect Posix. + Just tmplDirFilePath -> either (fail . show) (pure . Just) $ parseRelDir tmplDirFilePath + return $ GhRepoTemplateUri repoOwner repoName maybeTmplDirStrongPath + + ghRepoTemplateIdPrefix :: String + ghRepoTemplateIdPrefix = "github:" + +findTemplateByName :: [ST.StarterTemplate] -> String -> Maybe ST.StarterTemplate +findTemplateByName templates templateName = find ((== templateName) . getTemplateName) templates diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index e7fdd2317e..d9c146c28e 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -6,7 +6,7 @@ cabal-version: 2.4 -- Consider using hpack, or maybe even hpack-dhall. name: waspc -version: 0.15.2 +version: 0.16.0 description: Please see the README on GitHub at homepage: https://github.com/wasp-lang/wasp/waspc#readme bug-reports: https://github.com/wasp-lang/wasp/issues @@ -511,6 +511,7 @@ library cli-lib , fsnotify , http-conduit , optparse-applicative ^>=0.17.0.0 + , parsec ^>= 3.1.14 , path , path-io , pretty-simple ^>= 4.1.2.0 @@ -545,9 +546,13 @@ library cli-lib Wasp.Cli.Command.CreateNewProject.ArgumentsParser Wasp.Cli.Command.CreateNewProject.Common Wasp.Cli.Command.CreateNewProject.ProjectDescription - Wasp.Cli.Command.CreateNewProject.StarterTemplates + Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common + Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local + Wasp.Cli.Command.CreateNewProject.StarterTemplates.Skeleton + Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate + Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId Wasp.Cli.Command.CreateNewProject.StarterTemplates.Templating Wasp.Cli.Command.Db Wasp.Cli.Command.Db.Migrate diff --git a/web/docs/project/starter-templates.md b/web/docs/project/starter-templates.md index 58145769ee..60f02b75a1 100644 --- a/web/docs/project/starter-templates.md +++ b/web/docs/project/starter-templates.md @@ -2,9 +2,9 @@ title: Starter Templates --- -We created a few starter templates to help you get started with Wasp. Check out the list [below](#available-templates). +When creating a new Wasp app, you can choose one of the starter templates to help you get started with your app faster! -## Using a Template +## Picking a Template Run `wasp new` to run the interactive mode for creating a new Wasp project. @@ -35,9 +35,11 @@ To run your new app, do: wasp db start ``` -## Available Templates +Alternatively, to programatically create a new project from a specific template, you can run `wasp new MyFirstProject -t `. -When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp. +## Official Templates + +Official templates are maintained by the Wasp team and are normally updated with every major release of Wasp. ### OpenSaaS.sh template @@ -82,8 +84,23 @@ wasp new -t todo-ts ### AI Generated Starter 🤖 Using the same tech as used on https://usemage.ai/, Wasp generates your custom starter template based on your -project description. It will automatically generate your data model, auth, queries, actions and React pages. +project description. It will automatically generate your data model, auth, queries, actions and React pages. _You will need to provide your own OpenAI API key to be able to use this template._ -**Features:** Generated using OpenAI's GPT models, Auth (username/password), Queries, Actions, Pages, Full-stack Type Safety \ No newline at end of file +**Features:** Generated using OpenAI's GPT models, Auth (username/password), Queries, Actions, Pages, Full-stack Type Safety + + +## Community templates #{community-templates} + +Below follows a list of Wasp starter templates (those that we know about) created by our awesome community! + +:::note +Wasp Team doesn't guarantee the quality or safety of the community provided templates, it is up to you to check and verify their code. +::: + +- [**Roke**](https://github.com/wardbox/roke) (https://roke.dev): A Wasp starter with sensible defaults. + Preconfigured shadcn/ui components, motion animation presets, helper scripts for common tasks, ... + +If you would like your template to be added to the list of community templates, or update your existing template listing, please make a PR with changes to this page. +