From e3ffedf0621688281ea298368266075995589cb9 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Wed, 4 Dec 2024 23:06:30 +0100 Subject: [PATCH 01/12] Added support for external / 3rd-party starter templates. --- .../src/Wasp/Cli/Command/CreateNewProject.hs | 3 +- .../CreateNewProject/ArgumentsParser.hs | 4 +- .../CreateNewProject/ProjectDescription.hs | 35 ++++---- .../CreateNewProject/StarterTemplates.hs | 88 +++++++++++++++++-- waspc/waspc.cabal | 1 + 5 files changed, 101 insertions(+), 30 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index 6dd78fd804..0fd9b080a3 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -31,8 +31,7 @@ import qualified Wasp.Util.Terminal as Term createNewProject :: Arguments -> Command () createNewProject args = do newProjectArgs <- parseNewProjectArgs args & either Common.throwProjectCreationError return - let starterTemplates = getStarterTemplates - + let starterTemplates = getStarterTemplates -- TODO: Why do we inject this here and not access it directly from where we need it? newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplates createProjectOnDisk newProjectDescription 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/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index ead1f2a732..c35f4ffde0 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -9,6 +9,7 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription where import Control.Monad.IO.Class (liftIO) +import qualified Data.Aeson.Encoding as P import Data.Function ((&)) import Data.List (intercalate) import Data.List.NonEmpty (fromList) @@ -25,9 +26,11 @@ import Wasp.Cli.Command.CreateNewProject.Common import Wasp.Cli.Command.CreateNewProject.StarterTemplates ( StarterTemplate, defaultStarterTemplate, - findTemplateByString, + findTemplateByName, + obtainTemplateByIdOrThrow, ) import Wasp.Cli.FileSystem (getAbsPathToDirInCwd) +import qualified Wasp.Cli.GithubRepo as GhRepo import qualified Wasp.Cli.Interactive as Interactive import Wasp.Project (WaspProjectDir) import Wasp.Util (indent, kebabToCamelCase, whenM) @@ -49,6 +52,8 @@ data NewProjectAppName = NewProjectAppName String instance Show NewProjectAppName where show (NewProjectAppName name) = name +-- TODO: Rename "available" to "embedded" templates? + {- There are two ways of getting the project description: 1. From CLI arguments @@ -65,29 +70,29 @@ instance Show NewProjectAppName where - Template name is required, we ask the user to choose from available 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 availableTemplates = obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName - templateNameArg + templateIdArg availableTemplates (return defaultStarterTemplate) obtainNewProjectDescriptionInteractively :: Maybe String -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescriptionInteractively templateNameArg availableTemplates = do +obtainNewProjectDescriptionInteractively templateIdArg availableTemplates = do projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)" obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName - templateNameArg + templateIdArg availableTemplates - (liftIO askForTemplateName) + (liftIO askForTemplate) where - askForTemplateName = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates + askForTemplate = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates -- Common logic obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: @@ -96,15 +101,11 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: [StarterTemplate] -> Command StarterTemplate -> Command NewProjectDescription -obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateNameArg availableTemplates obtainTemplateWhenNoArg = do +obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg availableTemplates obtainTemplateWhenNoArg = do absWaspProjectDir <- obtainAvailableProjectDirPath projectName - selectedTemplate <- maybe obtainTemplateWhenNoArg findTemplateOrThrow templateNameArg + -- TODO: So here I pass availableTemplates to obtainTemplateByIdOrThrow but I could just reference them directly from there? + selectedTemplate <- maybe obtainTemplateWhenNoArg (obtainTemplateByIdOrThrow availableTemplates) 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 index 0c44950e11..ebefefd45e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs @@ -4,17 +4,26 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates ( getStarterTemplates, StarterTemplate (..), DirBasedTemplateMetadata (..), - findTemplateByString, + findTemplateByName, defaultStarterTemplate, readWaspProjectSkeletonFiles, getTemplateStartingInstructions, + obtainTemplateByIdOrThrow, ) where +import Control.Arrow (left) import Data.Foldable (find) +import Data.Function ((&)) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) import Data.Text (Text) -import StrongPath (Dir', File', Path, Path', Rel, Rel', System, reldir, ()) +import StrongPath (Dir', File', Path, Path', Posix, Rel, Rel', System, parseRelDir, parseRelDirP, reldir, ()) import qualified System.FilePath as FP +import qualified System.FilePath.Posix as FP.Posix +import qualified Text.Parsec as P +import Wasp.Cli.Command (Command) +import Wasp.Cli.Command.CreateNewProject.Common (throwInvalidTemplateNameUsedError) import qualified Wasp.Cli.GithubRepo as GhRepo import qualified Wasp.Cli.Interactive as Interactive import qualified Wasp.Data as Data @@ -50,6 +59,7 @@ instance Interactive.IsOption StarterTemplate where showOptionDescription AiGeneratedStarterTemplate = Just "🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)" +-- TODO: This doesn't seem to be documented well, what is this first String? Maybe project path? Why is it not strong path then? type StartingInstructionsBuilder = String -> String {- HLINT ignore getTemplateStartingInstructions "Redundant $" -} @@ -102,7 +112,7 @@ basicStarterTemplate = openSaasStarterTemplate :: StarterTemplate openSaasStarterTemplate = - simpleGhRepoTemplate + simpleWaspGhRepoTemplate ("open-saas", [reldir|template|]) ( "saas", "Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more." @@ -137,7 +147,7 @@ openSaasStarterTemplate = todoTsStarterTemplate :: StarterTemplate todoTsStarterTemplate = - simpleGhRepoTemplate + simpleWaspGhRepoTemplate ("starters", [reldir|todo-ts|]) ( "todo-ts", "Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety." @@ -167,7 +177,7 @@ styleText = id embeddingsStarterTemplate :: StarterTemplate embeddingsStarterTemplate = - simpleGhRepoTemplate + simpleWaspGhRepoTemplate ("starters", [reldir|embeddings|]) ( "embeddings", "Comes with code for generating vector embeddings and performing vector similarity search." @@ -201,8 +211,8 @@ embeddingsStarterTemplate = ] ) -simpleGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> StartingInstructionsBuilder -> StarterTemplate -simpleGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDescription) buildStartingInstructions = +simpleWaspGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> StartingInstructionsBuilder -> StarterTemplate +simpleWaspGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDescription) buildStartingInstructions = GhRepoStarterTemplate ( GhRepo.GithubRepoRef { GhRepo._repoOwner = waspGhOrgName, @@ -228,11 +238,71 @@ waspGhOrgName = "wasp-lang" waspVersionTemplateGitTag :: String waspVersionTemplateGitTag = "wasp-v0.15-template" -findTemplateByString :: [StarterTemplate] -> String -> Maybe StarterTemplate -findTemplateByString templates query = find ((== query) . show) templates +-- TODO: I don't like that we are relying on Show here for search. +-- Either name should reflect that, or we shouldn't use Show but name directly or id or something. +findTemplateByName :: [StarterTemplate] -> String -> Maybe StarterTemplate +findTemplateByName templates templateName = find ((== templateName) . 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 + +-- TODO: tests, docs. + +data StarterTemplateId + = GhRepoTemplateUri !GhRepo.GithubRepoOwner !GhRepo.GithubRepoName !(Maybe (Path' Rel' Dir')) + | EmbeddedTemplateName !String + +parseTemplateId :: String -> Either String StarterTemplateId +parseTemplateId = \case + templateId + | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> + -- TODO: Do something with the parse error message or just let it through as I do now? + parseGhRepoTemplateUri templateId & left show + templateId -> + pure $ EmbeddedTemplateName 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]) + repoName <- P.many1 (P.noneOf [FP.Posix.pathSeparator]) + maybeTmplDirStrongPath <- + P.optionMaybe (P.char FP.Posix.pathSeparator >> P.many1 P.anyChar) >>= \case + Nothing -> pure Nothing + -- NOTE: 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:" + +obtainTemplateByIdOrThrow :: [StarterTemplate] -> String -> Command StarterTemplate +obtainTemplateByIdOrThrow availableTemplates templateId = + -- TODO: Refactor/rename throwInvalidTemplateNameUsedError? Yeah or probably just make another error function here, that is better suited for the situation (which is failed parsing of the template id. + (parseTemplateId templateId & either (const throwInvalidTemplateNameUsedError) pure) >>= \case + EmbeddedTemplateName templateName -> + findTemplateByName availableTemplates templateName + & maybe throwInvalidTemplateNameUsedError pure + GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> + return $ + GhRepoStarterTemplate + (GhRepo.GithubRepoRef repoOwner repoName waspVersionTemplateGitTag) + ( DirBasedTemplateMetadata + { _name = repoName, + _description = "Template from Github repo " <> repoOwner <> "/" <> repoName, + _path = maybeTmplDirPath & fromMaybe [reldir|.|], + _buildStartingInstructions = \projectDirName -> + unlines + -- TODO: Improve next line, repoName in projectDirName doesn't make much sense. + [ styleText $ "Created new project from template " <> repoName <> " in " <> projectDirName <> " !", + styleText $ "Check github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." + ] + } + ) diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index e7fdd2317e..3fbd438c2f 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -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 From 8dbc6129411213a2b355e6bd5b985f9090a6b9ef Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 11:16:12 +0100 Subject: [PATCH 02/12] Reorganized and split into smaller modules. --- .../src/Wasp/Cli/Command/CreateNewProject.hs | 14 +- .../Wasp/Cli/Command/CreateNewProject/AI.hs | 2 +- .../CreateNewProject/ProjectDescription.hs | 15 +- .../CreateNewProject/StarterTemplates.hs | 308 ------------------ .../StarterTemplates/Common.hs | 23 ++ .../FeaturedStarterTemplates.hs | 166 ++++++++++ .../StarterTemplates/Skeleton.hs | 18 + .../StarterTemplates/StarterTemplate.hs | 62 ++++ .../StarterTemplates/StarterTemplateId.hs | 83 +++++ waspc/waspc.cabal | 6 +- 10 files changed, 372 insertions(+), 325 deletions(-) delete mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Common.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index 0fd9b080a3..accc1329af 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -15,14 +15,14 @@ import Wasp.Cli.Command.CreateNewProject.ProjectDescription ( NewProjectDescription (..), obtainNewProjectDescription, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates +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 ( DirBasedTemplateMetadata (_path), StarterTemplate (..), - getStarterTemplates, getTemplateStartingInstructions, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo (createProjectOnDiskFromGhRepoTemplate) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate) import Wasp.Cli.Command.Message (cliSendMessageC) import qualified Wasp.Message as Msg import qualified Wasp.Util.Terminal as Term @@ -31,8 +31,8 @@ import qualified Wasp.Util.Terminal as Term createNewProject :: Arguments -> Command () createNewProject args = do newProjectArgs <- parseNewProjectArgs args & either Common.throwProjectCreationError return - let starterTemplates = getStarterTemplates -- TODO: Why do we inject this here and not access it directly from where we need it? - newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplates + let featuredStarterTemplates = getFeaturedStarterTemplates + newProjectDescription <- obtainNewProjectDescription newProjectArgs featuredStarterTemplates createProjectOnDisk newProjectDescription liftIO $ printGettingStartedInstructionsForProject newProjectDescription @@ -51,7 +51,7 @@ createProjectOnDisk createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef $ _path metadata LocalStarterTemplate metadata -> liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata - AiGeneratedStarterTemplate -> + AiGeneratedStarterTemplate _metadata -> AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName -- | This function assumes that the project dir was created inside the current working directory. 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/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index c35f4ffde0..154ab0883a 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -23,11 +23,11 @@ import Wasp.Cli.Command.CreateNewProject.Common ( throwInvalidTemplateNameUsedError, throwProjectCreationError, ) -import Wasp.Cli.Command.CreateNewProject.StarterTemplates - ( StarterTemplate, - defaultStarterTemplate, - findTemplateByName, - obtainTemplateByIdOrThrow, +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates (defaultStarterTemplate) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (StarterTemplate) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId + ( findTemplateByName, + getStarterTemplateByIdOrThrow, ) import Wasp.Cli.FileSystem (getAbsPathToDirInCwd) import qualified Wasp.Cli.GithubRepo as GhRepo @@ -101,10 +101,9 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: [StarterTemplate] -> Command StarterTemplate -> Command NewProjectDescription -obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg availableTemplates obtainTemplateWhenNoArg = do +obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg availableTemplates getStarterTemplateWhenNoArg = do absWaspProjectDir <- obtainAvailableProjectDirPath projectName - -- TODO: So here I pass availableTemplates to obtainTemplateByIdOrThrow but I could just reference them directly from there? - selectedTemplate <- maybe obtainTemplateWhenNoArg (obtainTemplateByIdOrThrow availableTemplates) templateIdArg + selectedTemplate <- maybe getStarterTemplateWhenNoArg (getStarterTemplateByIdOrThrow availableTemplates) templateIdArg mkNewProjectDescription projectName absWaspProjectDir selectedTemplate obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir)) 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 ebefefd45e..0000000000 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs +++ /dev/null @@ -1,308 +0,0 @@ -{-# LANGUAGE TupleSections #-} - -module Wasp.Cli.Command.CreateNewProject.StarterTemplates - ( getStarterTemplates, - StarterTemplate (..), - DirBasedTemplateMetadata (..), - findTemplateByName, - defaultStarterTemplate, - readWaspProjectSkeletonFiles, - getTemplateStartingInstructions, - obtainTemplateByIdOrThrow, - ) -where - -import Control.Arrow (left) -import Data.Foldable (find) -import Data.Function ((&)) -import Data.List (isPrefixOf) -import Data.Maybe (fromMaybe) -import Data.Text (Text) -import StrongPath (Dir', File', Path, Path', Posix, Rel, Rel', System, parseRelDir, parseRelDirP, reldir, ()) -import qualified System.FilePath as FP -import qualified System.FilePath.Posix as FP.Posix -import qualified Text.Parsec as P -import Wasp.Cli.Command (Command) -import Wasp.Cli.Command.CreateNewProject.Common (throwInvalidTemplateNameUsedError) -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)" - --- TODO: This doesn't seem to be documented well, what is this first String? Maybe project path? Why is it not strong path then? -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 = - simpleWaspGhRepoTemplate - ("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 = - simpleWaspGhRepoTemplate - ("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 = - simpleWaspGhRepoTemplate - ("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!" - ] - ) - -simpleWaspGhRepoTemplate :: (String, Path' Rel' Dir') -> (String, String) -> StartingInstructionsBuilder -> StarterTemplate -simpleWaspGhRepoTemplate (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" - --- TODO: I don't like that we are relying on Show here for search. --- Either name should reflect that, or we shouldn't use Show but name directly or id or something. -findTemplateByName :: [StarterTemplate] -> String -> Maybe StarterTemplate -findTemplateByName templates templateName = find ((== templateName) . 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 - --- TODO: tests, docs. - -data StarterTemplateId - = GhRepoTemplateUri !GhRepo.GithubRepoOwner !GhRepo.GithubRepoName !(Maybe (Path' Rel' Dir')) - | EmbeddedTemplateName !String - -parseTemplateId :: String -> Either String StarterTemplateId -parseTemplateId = \case - templateId - | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> - -- TODO: Do something with the parse error message or just let it through as I do now? - parseGhRepoTemplateUri templateId & left show - templateId -> - pure $ EmbeddedTemplateName 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]) - repoName <- P.many1 (P.noneOf [FP.Posix.pathSeparator]) - maybeTmplDirStrongPath <- - P.optionMaybe (P.char FP.Posix.pathSeparator >> P.many1 P.anyChar) >>= \case - Nothing -> pure Nothing - -- NOTE: 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:" - -obtainTemplateByIdOrThrow :: [StarterTemplate] -> String -> Command StarterTemplate -obtainTemplateByIdOrThrow availableTemplates templateId = - -- TODO: Refactor/rename throwInvalidTemplateNameUsedError? Yeah or probably just make another error function here, that is better suited for the situation (which is failed parsing of the template id. - (parseTemplateId templateId & either (const throwInvalidTemplateNameUsedError) pure) >>= \case - EmbeddedTemplateName templateName -> - findTemplateByName availableTemplates templateName - & maybe throwInvalidTemplateNameUsedError pure - GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> - return $ - GhRepoStarterTemplate - (GhRepo.GithubRepoRef repoOwner repoName waspVersionTemplateGitTag) - ( DirBasedTemplateMetadata - { _name = repoName, - _description = "Template from Github repo " <> repoOwner <> "/" <> repoName, - _path = maybeTmplDirPath & fromMaybe [reldir|.|], - _buildStartingInstructions = \projectDirName -> - unlines - -- TODO: Improve next line, repoName in projectDirName doesn't make much sense. - [ styleText $ "Created new project from template " <> repoName <> " in " <> projectDirName <> " !", - styleText $ "Check github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." - ] - } - ) 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..de9e6257a3 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs @@ -0,0 +1,166 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates + ( getFeaturedStarterTemplates, + defaultStarterTemplate, + aiGeneratedStarterTemplate, + ) +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, + aiGeneratedStarterTemplate + ] + +defaultStarterTemplate :: ST.StarterTemplate +defaultStarterTemplate = basicStarterTemplate + +{- HLINT ignore basicStarterTemplate "Redundant $" -} +basicStarterTemplate :: ST.StarterTemplate +basicStarterTemplate = + ST.LocalStarterTemplate $ + ST.DirBasedTemplateMetadata + { ST._path = [reldir|basic|], + ST._name = "basic", + ST._description = "Simple starter template with a single page.", + ST._buildStartingInstructions = \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 $" -} +aiGeneratedStarterTemplate :: ST.StarterTemplate +aiGeneratedStarterTemplate = + ST.AiGeneratedStarterTemplate $ + 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 + } + ) + ( ST.DirBasedTemplateMetadata + { _name = tmplDisplayName, + _description = tmplDescription, + _path = tmplPathInRepo, + _buildStartingInstructions = 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..a1a8f932e5 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE TupleSections #-} + +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Skeleton + ( readWaspProjectSkeletonFiles, + ) +where + +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 <- ( [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/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs new file mode 100644 index 0000000000..68c7ff6ae9 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -0,0 +1,62 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate + ( StarterTemplate (..), + DirBasedTemplateMetadata (..), + TemplateMetadata (..), + StartingInstructionsBuilder, + getTemplateStartingInstructions, + ) +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 !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. + -- TODO: Add notion of AI that will be used, as a field. For now just data WaspAppAiGenerator = WaspAI. + -- Then also update aiGeneratedStarterTemplate to be called waspAiGeneratedStarterTemplate and to use that value. + AiGeneratedStarterTemplate !TemplateMetadata + +data DirBasedTemplateMetadata = DirBasedTemplateMetadata + { _name :: !String, + _path :: !(Path' Rel' Dir'), -- Path to a directory containing template files. + _description :: !String, + _buildStartingInstructions :: !StartingInstructionsBuilder + } + +-- TODO: Have DirBasedTemplateMetadata use TemplateMetadata inside it. +-- Maybe even go step further and use typeclass instead? It should give me name, description, starting instructions. +-- class IsStarterTemplate +data TemplateMetadata = TemplateMetadata + { _tmplName :: !String, + _tmplDescription :: !String, + _tmplBuildStartingInstructions :: !StartingInstructionsBuilder + } + +instance Show StarterTemplate where + show (GhRepoStarterTemplate _ metadata) = _name metadata + show (LocalStarterTemplate metadata) = _name metadata + show (AiGeneratedStarterTemplate metadata) = _tmplName metadata + +instance Interactive.IsOption StarterTemplate where + showOption = show + + showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata + showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata + showOptionDescription (AiGeneratedStarterTemplate metadata) = Just $ _tmplDescription metadata + +-- TODO: This doesn't seem to be documented well, what is this first String? Maybe project path? Why is it not strong path then? +type StartingInstructionsBuilder = String -> String + +-- | 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 metadata -> _tmplBuildStartingInstructions metadata projectDirName 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..8377c0a9a7 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -0,0 +1,83 @@ +module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId + ( getStarterTemplateByIdOrThrow, + findTemplateByName, + ) +where + +import Control.Arrow (left) +import Data.Foldable (find) +import Data.Function ((&)) +import Data.List (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 (Command) +import Wasp.Cli.Command.CreateNewProject.Common (throwInvalidTemplateNameUsedError) +import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common (styleText, waspVersionTemplateGitTag) +import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate as ST +import qualified Wasp.Cli.GithubRepo as GhRepo + +data StarterTemplateId + = GhRepoTemplateUri !GhRepo.GithubRepoOwner !GhRepo.GithubRepoName !(Maybe (Path' Rel' Dir')) + | EmbeddedTemplateName !String + +-- TODO: Comment (explain it will try to find it in featured templates or construct it based on id). +getStarterTemplateByIdOrThrow :: [ST.StarterTemplate] -> String -> Command ST.StarterTemplate +getStarterTemplateByIdOrThrow availableTemplates templateId = + -- TODO: Refactor/rename throwInvalidTemplateNameUsedError? Yeah or probably just make another error function here, that is better suited for the situation (which is failed parsing of the template id. + (parseStarterTemplateId templateId & either (const throwInvalidTemplateNameUsedError) pure) >>= \case + EmbeddedTemplateName templateName -> + findTemplateByName availableTemplates templateName + & maybe throwInvalidTemplateNameUsedError pure + GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> + return $ + ST.GhRepoStarterTemplate + (GhRepo.GithubRepoRef repoOwner repoName waspVersionTemplateGitTag) + ( ST.DirBasedTemplateMetadata + { ST._name = repoName, + ST._description = "Template from Github repo " <> repoOwner <> "/" <> repoName, + ST._path = maybeTmplDirPath & fromMaybe [reldir|.|], + ST._buildStartingInstructions = \projectDirName -> + unlines + -- TODO: Improve next line, repoName in projectDirName doesn't make much sense. + [ styleText $ "Created new project from template " <> repoName <> " in " <> projectDirName <> " !", + styleText $ "Check github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." + ] + } + ) + +-- TODO: Write a comment here. +parseStarterTemplateId :: String -> Either String StarterTemplateId +parseStarterTemplateId = \case + templateId + | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> + -- TODO: Do something with the parse error message or just let it through as I do now? + parseGhRepoTemplateUri templateId & left show + templateId -> + pure $ EmbeddedTemplateName 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]) + repoName <- P.many1 (P.noneOf [FP.Posix.pathSeparator]) + maybeTmplDirStrongPath <- + P.optionMaybe (P.char FP.Posix.pathSeparator >> P.many1 P.anyChar) >>= \case + Nothing -> pure Nothing + -- NOTE: 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:" + +-- TODO: I don't like that we are relying on Show here for search. +-- Either name should reflect that, or we shouldn't use Show but name directly or id or something. +-- Yeah, I think we should go for implementing `getTemplateName` function. +findTemplateByName :: [ST.StarterTemplate] -> String -> Maybe ST.StarterTemplate +findTemplateByName templates templateName = find ((== templateName) . show) templates diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 3fbd438c2f..232da78414 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -546,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 From 7b61e2eb873dfa9fb6efb527cbe0ab351ee75b21 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 11:49:48 +0100 Subject: [PATCH 03/12] Took care of some TODOs. --- .../src/Wasp/Cli/Command/CreateNewProject.hs | 2 +- .../CreateNewProject/ProjectDescription.hs | 27 +++++++------------ .../StarterTemplates/StarterTemplate.hs | 21 ++++++++------- .../StarterTemplates/StarterTemplateId.hs | 4 +-- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index accc1329af..fe08dec849 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -58,7 +58,7 @@ createProjectOnDisk 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/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index 154ab0883a..a8c099349a 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -9,8 +9,6 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription where import Control.Monad.IO.Class (liftIO) -import qualified Data.Aeson.Encoding as P -import Data.Function ((&)) import Data.List (intercalate) import Data.List.NonEmpty (fromList) import Path.IO (doesDirExist) @@ -20,17 +18,14 @@ 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.FeaturedStarterTemplates (defaultStarterTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (StarterTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId - ( findTemplateByName, - getStarterTemplateByIdOrThrow, + ( getStarterTemplateByIdOrThrow, ) import Wasp.Cli.FileSystem (getAbsPathToDirInCwd) -import qualified Wasp.Cli.GithubRepo as GhRepo import qualified Wasp.Cli.Interactive as Interactive import Wasp.Project (WaspProjectDir) import Wasp.Util (indent, kebabToCamelCase, whenM) @@ -52,8 +47,6 @@ data NewProjectAppName = NewProjectAppName String instance Show NewProjectAppName where show (NewProjectAppName name) = name --- TODO: Rename "available" to "embedded" templates? - {- There are two ways of getting the project description: 1. From CLI arguments @@ -67,7 +60,7 @@ 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, _templateId = templateIdArg} starterTemplates = @@ -76,23 +69,23 @@ obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _temp Nothing -> obtainNewProjectDescriptionInteractively templateIdArg starterTemplates obtainNewProjectDescriptionFromCliArgs :: String -> Maybe String -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescriptionFromCliArgs projectName templateIdArg availableTemplates = +obtainNewProjectDescriptionFromCliArgs projectName templateIdArg featuredTemplates = obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg - availableTemplates + featuredTemplates (return defaultStarterTemplate) obtainNewProjectDescriptionInteractively :: Maybe String -> [StarterTemplate] -> Command NewProjectDescription -obtainNewProjectDescriptionInteractively templateIdArg availableTemplates = do +obtainNewProjectDescriptionInteractively templateIdArg featuredTemplates = do projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)" obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg - availableTemplates + featuredTemplates (liftIO askForTemplate) where - askForTemplate = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates + askForTemplate = Interactive.askToChoose "Choose a starter template" $ fromList featuredTemplates -- Common logic obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: @@ -101,9 +94,9 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: [StarterTemplate] -> Command StarterTemplate -> Command NewProjectDescription -obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg availableTemplates getStarterTemplateWhenNoArg = do +obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg featuredTemplates getStarterTemplateWhenNoArg = do absWaspProjectDir <- obtainAvailableProjectDirPath projectName - selectedTemplate <- maybe getStarterTemplateWhenNoArg (getStarterTemplateByIdOrThrow availableTemplates) templateIdArg + selectedTemplate <- maybe getStarterTemplateWhenNoArg (getStarterTemplateByIdOrThrow featuredTemplates) templateIdArg mkNewProjectDescription projectName absWaspProjectDir selectedTemplate obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir)) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs index 68c7ff6ae9..42a1948e22 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -49,14 +49,17 @@ instance Interactive.IsOption StarterTemplate where showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata showOptionDescription (AiGeneratedStarterTemplate metadata) = Just $ _tmplDescription metadata --- TODO: This doesn't seem to be documented well, what is this first String? Maybe project path? Why is it not strong path then? -type StartingInstructionsBuilder = String -> String - --- | Returns instructions for running the newly created (from the template) Wasp project. +-- | 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. -getTemplateStartingInstructions :: String -> StarterTemplate -> String -getTemplateStartingInstructions projectDirName = \case - GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata projectDirName - LocalStarterTemplate metadata -> _buildStartingInstructions metadata projectDirName - AiGeneratedStarterTemplate metadata -> _tmplBuildStartingInstructions metadata projectDirName +type StartingInstructionsBuilder = ProjectDirName -> StartingInstructions + +type ProjectDirName = String + +type StartingInstructions = String + +getTemplateStartingInstructions :: StarterTemplate -> StartingInstructionsBuilder +getTemplateStartingInstructions = \case + GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata + LocalStarterTemplate metadata -> _buildStartingInstructions metadata + AiGeneratedStarterTemplate metadata -> _tmplBuildStartingInstructions metadata diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index 8377c0a9a7..22df724f71 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -24,11 +24,11 @@ data StarterTemplateId -- TODO: Comment (explain it will try to find it in featured templates or construct it based on id). getStarterTemplateByIdOrThrow :: [ST.StarterTemplate] -> String -> Command ST.StarterTemplate -getStarterTemplateByIdOrThrow availableTemplates templateId = +getStarterTemplateByIdOrThrow featuredTemplates templateId = -- TODO: Refactor/rename throwInvalidTemplateNameUsedError? Yeah or probably just make another error function here, that is better suited for the situation (which is failed parsing of the template id. (parseStarterTemplateId templateId & either (const throwInvalidTemplateNameUsedError) pure) >>= \case EmbeddedTemplateName templateName -> - findTemplateByName availableTemplates templateName + findTemplateByName featuredTemplates templateName & maybe throwInvalidTemplateNameUsedError pure GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> return $ From aaeda4fea2dec58a99889849a7a44e8c665307b2 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 13:03:51 +0100 Subject: [PATCH 04/12] More TODOs solved. --- .../Cli/Command/CreateNewProject/Common.hs | 7 -- .../StarterTemplates/StarterTemplateId.hs | 67 +++++++++++++------ 2 files changed, 48 insertions(+), 26 deletions(-) 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/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index 22df724f71..750d6c6c69 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -7,29 +7,50 @@ where import Control.Arrow (left) import Data.Foldable (find) import Data.Function ((&)) -import Data.List (isPrefixOf) +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 (Command) -import Wasp.Cli.Command.CreateNewProject.Common (throwInvalidTemplateNameUsedError) +import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Common (styleText, waspVersionTemplateGitTag) 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. data StarterTemplateId - = GhRepoTemplateUri !GhRepo.GithubRepoOwner !GhRepo.GithubRepoName !(Maybe (Path' Rel' Dir')) - | EmbeddedTemplateName !String + = -- | 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')) --- TODO: Comment (explain it will try to find it in featured templates or construct it based on id). +data StarterTemplateIdType + = IdTypeFeaturedTemplateName + | IdTypeGhRepoTemplateUri + deriving (Enum, Bounded) + +-- TODO: Explain what is this type used for, and also ignore the warning about it not being used. +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:/[/some/dir])" + +-- | 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 :: [ST.StarterTemplate] -> String -> Command ST.StarterTemplate -getStarterTemplateByIdOrThrow featuredTemplates templateId = - -- TODO: Refactor/rename throwInvalidTemplateNameUsedError? Yeah or probably just make another error function here, that is better suited for the situation (which is failed parsing of the template id. - (parseStarterTemplateId templateId & either (const throwInvalidTemplateNameUsedError) pure) >>= \case - EmbeddedTemplateName templateName -> +getStarterTemplateByIdOrThrow featuredTemplates templateIdString = + (parseStarterTemplateId templateIdString & either throwTemplateIdParsingError pure) >>= \case + FeaturedTemplateName templateName -> findTemplateByName featuredTemplates templateName - & maybe throwInvalidTemplateNameUsedError pure + & maybe (throwInvalidTemplateNameUsedError templateName) pure GhRepoTemplateUri repoOwner repoName maybeTmplDirPath -> return $ ST.GhRepoStarterTemplate @@ -46,16 +67,24 @@ getStarterTemplateByIdOrThrow featuredTemplates templateId = ] } ) + where + throwTemplateIdParsingError errorMsg = + throwProjectCreationError $ + "Failed to parse template id: " <> errorMsg <> "\n" <> expectedInputMessage + + throwInvalidTemplateNameUsedError templateName = + throwProjectCreationError $ + "There is no featured template with name " <> templateName <> ".\n" <> expectedInputMessage + + -- TODO: Use getTemplateName here instead of `show`. + expectedInputMessage = + "Expected " <> intercalate " or " (getStarterTemplateIdTypeDescription <$> [minBound .. maxBound]) <> "." + <> (" Valid featured template names are " <> intercalate ", " (show <$> featuredTemplates) <> ".") --- TODO: Write a comment here. parseStarterTemplateId :: String -> Either String StarterTemplateId parseStarterTemplateId = \case - templateId - | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> - -- TODO: Do something with the parse error message or just let it through as I do now? - parseGhRepoTemplateUri templateId & left show - templateId -> - pure $ EmbeddedTemplateName templateId + templateId | ghRepoTemplateIdPrefix `isPrefixOf` templateId -> parseGhRepoTemplateUri templateId & left show + templateId -> pure $ FeaturedTemplateName templateId where -- Parses following format: github:/[/] . parseGhRepoTemplateUri :: String -> Either P.ParseError StarterTemplateId @@ -68,8 +97,8 @@ parseStarterTemplateId = \case maybeTmplDirStrongPath <- P.optionMaybe (P.char FP.Posix.pathSeparator >> P.many1 P.anyChar) >>= \case Nothing -> pure Nothing - -- NOTE: 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. + -- 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 From d0fc526790dacf3d3cee82b69ec638957d71115b Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 19:38:50 +0100 Subject: [PATCH 05/12] fix --- .../src/Wasp/Cli/Command/CreateNewProject.hs | 3 ++- .../StarterTemplates/StarterTemplate.hs | 14 ++++++++------ .../StarterTemplates/StarterTemplateId.hs | 17 ++++++++++------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index fe08dec849..a1a86216c7 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -21,6 +21,7 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOn import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate ( DirBasedTemplateMetadata (_path), StarterTemplate (..), + getTemplateName, getTemplateStartingInstructions, ) import Wasp.Cli.Command.Message (cliSendMessageC) @@ -45,7 +46,7 @@ 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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs index 42a1948e22..c580935c55 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -4,6 +4,7 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate TemplateMetadata (..), StartingInstructionsBuilder, getTemplateStartingInstructions, + getTemplateName, ) where @@ -37,13 +38,8 @@ data TemplateMetadata = TemplateMetadata _tmplBuildStartingInstructions :: !StartingInstructionsBuilder } -instance Show StarterTemplate where - show (GhRepoStarterTemplate _ metadata) = _name metadata - show (LocalStarterTemplate metadata) = _name metadata - show (AiGeneratedStarterTemplate metadata) = _tmplName metadata - instance Interactive.IsOption StarterTemplate where - showOption = show + showOption = getTemplateName showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata @@ -63,3 +59,9 @@ getTemplateStartingInstructions = \case GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata LocalStarterTemplate metadata -> _buildStartingInstructions metadata AiGeneratedStarterTemplate metadata -> _tmplBuildStartingInstructions metadata + +getTemplateName :: StarterTemplate -> String +getTemplateName = \case + GhRepoStarterTemplate _ metadata -> _name metadata + LocalStarterTemplate metadata -> _name metadata + AiGeneratedStarterTemplate metadata -> _tmplName metadata diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index 750d6c6c69..a17585d31e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-unused-top-binds #-} + module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId ( getStarterTemplateByIdOrThrow, findTemplateByName, @@ -15,6 +17,7 @@ import qualified Text.Parsec as P import Wasp.Cli.Command (Command) import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) 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 @@ -27,12 +30,15 @@ data StarterTemplateId -- 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) --- TODO: Explain what is this type used for, and also ignore the warning about it not being used. +-- | 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 @@ -45,6 +51,7 @@ getStarterTemplateIdTypeDescription = \case -- | 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). +-- TODO: Use MonadThrow? getStarterTemplateByIdOrThrow :: [ST.StarterTemplate] -> String -> Command ST.StarterTemplate getStarterTemplateByIdOrThrow featuredTemplates templateIdString = (parseStarterTemplateId templateIdString & either throwTemplateIdParsingError pure) >>= \case @@ -76,10 +83,9 @@ getStarterTemplateByIdOrThrow featuredTemplates templateIdString = throwProjectCreationError $ "There is no featured template with name " <> templateName <> ".\n" <> expectedInputMessage - -- TODO: Use getTemplateName here instead of `show`. expectedInputMessage = "Expected " <> intercalate " or " (getStarterTemplateIdTypeDescription <$> [minBound .. maxBound]) <> "." - <> (" Valid featured template names are " <> intercalate ", " (show <$> featuredTemplates) <> ".") + <> (" Valid featured template names are " <> intercalate ", " (getTemplateName <$> featuredTemplates) <> ".") parseStarterTemplateId :: String -> Either String StarterTemplateId parseStarterTemplateId = \case @@ -105,8 +111,5 @@ parseStarterTemplateId = \case ghRepoTemplateIdPrefix :: String ghRepoTemplateIdPrefix = "github:" --- TODO: I don't like that we are relying on Show here for search. --- Either name should reflect that, or we shouldn't use Show but name directly or id or something. --- Yeah, I think we should go for implementing `getTemplateName` function. findTemplateByName :: [ST.StarterTemplate] -> String -> Maybe ST.StarterTemplate -findTemplateByName templates templateName = find ((== templateName) . show) templates +findTemplateByName templates templateName = find ((== templateName) . getTemplateName) templates From 5fec0d08561494f03b6388217755983c810b1f82 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 20:14:10 +0100 Subject: [PATCH 06/12] fix --- .../CreateNewProject/ProjectDescription.hs | 6 +++++- .../StarterTemplates/StarterTemplateId.hs | 17 +++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index a8c099349a..8f6addfdc2 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -96,7 +96,11 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg :: Command NewProjectDescription obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg featuredTemplates getStarterTemplateWhenNoArg = do absWaspProjectDir <- obtainAvailableProjectDirPath projectName - selectedTemplate <- maybe getStarterTemplateWhenNoArg (getStarterTemplateByIdOrThrow featuredTemplates) templateIdArg + selectedTemplate <- + maybe + getStarterTemplateWhenNoArg + (either throwProjectCreationError pure . getStarterTemplateByIdOrThrow featuredTemplates) + templateIdArg mkNewProjectDescription projectName absWaspProjectDir selectedTemplate obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir)) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index a17585d31e..4ad6d5dd82 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -7,6 +7,7 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId where import Control.Arrow (left) +import Control.Monad.Except (MonadError, throwError) import Data.Foldable (find) import Data.Function ((&)) import Data.List (intercalate, isPrefixOf) @@ -14,14 +15,13 @@ 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 (Command) -import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) 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 @@ -51,8 +51,7 @@ getStarterTemplateIdTypeDescription = \case -- | 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). --- TODO: Use MonadThrow? -getStarterTemplateByIdOrThrow :: [ST.StarterTemplate] -> String -> Command ST.StarterTemplate +getStarterTemplateByIdOrThrow :: (MonadError String m) => [ST.StarterTemplate] -> String -> m ST.StarterTemplate getStarterTemplateByIdOrThrow featuredTemplates templateIdString = (parseStarterTemplateId templateIdString & either throwTemplateIdParsingError pure) >>= \case FeaturedTemplateName templateName -> @@ -66,21 +65,19 @@ getStarterTemplateByIdOrThrow featuredTemplates templateIdString = { ST._name = repoName, ST._description = "Template from Github repo " <> repoOwner <> "/" <> repoName, ST._path = maybeTmplDirPath & fromMaybe [reldir|.|], - ST._buildStartingInstructions = \projectDirName -> + ST._buildStartingInstructions = \_ -> unlines - -- TODO: Improve next line, repoName in projectDirName doesn't make much sense. - [ styleText $ "Created new project from template " <> repoName <> " in " <> projectDirName <> " !", - styleText $ "Check github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." + [ styleText $ "Check https://github.com/" <> repoOwner <> "/" <> repoName <> " for starting instructions." ] } ) where throwTemplateIdParsingError errorMsg = - throwProjectCreationError $ + throwError $ "Failed to parse template id: " <> errorMsg <> "\n" <> expectedInputMessage throwInvalidTemplateNameUsedError templateName = - throwProjectCreationError $ + throwError $ "There is no featured template with name " <> templateName <> ".\n" <> expectedInputMessage expectedInputMessage = From 9fc1ae0679ab31a749c445393a99b16fbd6a45e7 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 20:34:35 +0100 Subject: [PATCH 07/12] fix --- .../cli/src/Wasp/Cli/Command/CreateNewProject.hs | 6 ++++-- .../StarterTemplates/FeaturedStarterTemplates.hs | 9 ++++----- .../StarterTemplates/StarterTemplate.hs | 16 +++++++++------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index a1a86216c7..e513684e44 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -21,6 +21,7 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOn import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate ( DirBasedTemplateMetadata (_path), StarterTemplate (..), + WaspAppAiGenerator (WaspAI), getTemplateName, getTemplateStartingInstructions, ) @@ -52,8 +53,9 @@ createProjectOnDisk createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef $ _path metadata LocalStarterTemplate metadata -> liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata - AiGeneratedStarterTemplate _metadata -> - AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName + AiGeneratedStarterTemplate waspAppAiGenerator _metadata -> + 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 () diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs index de9e6257a3..521ca405f2 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs @@ -1,7 +1,6 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates ( getFeaturedStarterTemplates, defaultStarterTemplate, - aiGeneratedStarterTemplate, ) where @@ -17,7 +16,7 @@ getFeaturedStarterTemplates = todoTsStarterTemplate, openSaasStarterTemplate, embeddingsStarterTemplate, - aiGeneratedStarterTemplate + waspAiGeneratedStarterTemplate ] defaultStarterTemplate :: ST.StarterTemplate @@ -130,9 +129,9 @@ embeddingsStarterTemplate = ) {- HLINT ignore aiGeneratedStarterTemplate "Redundant $" -} -aiGeneratedStarterTemplate :: ST.StarterTemplate -aiGeneratedStarterTemplate = - ST.AiGeneratedStarterTemplate $ +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)", diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs index c580935c55..8b4e4e417d 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -3,6 +3,7 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate DirBasedTemplateMetadata (..), TemplateMetadata (..), StartingInstructionsBuilder, + WaspAppAiGenerator (..), getTemplateStartingInstructions, getTemplateName, ) @@ -17,10 +18,8 @@ data StarterTemplate 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. - -- TODO: Add notion of AI that will be used, as a field. For now just data WaspAppAiGenerator = WaspAI. - -- Then also update aiGeneratedStarterTemplate to be called waspAiGeneratedStarterTemplate and to use that value. - AiGeneratedStarterTemplate !TemplateMetadata + | -- | Template that will be dynamically generated by AI based on user's input. + AiGeneratedStarterTemplate !WaspAppAiGenerator !TemplateMetadata data DirBasedTemplateMetadata = DirBasedTemplateMetadata { _name :: !String, @@ -38,12 +37,15 @@ data TemplateMetadata = TemplateMetadata _tmplBuildStartingInstructions :: !StartingInstructionsBuilder } +-- | AI generators that can generate Wasp apps. +data WaspAppAiGenerator = WaspAI + instance Interactive.IsOption StarterTemplate where showOption = getTemplateName showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata - showOptionDescription (AiGeneratedStarterTemplate metadata) = Just $ _tmplDescription metadata + showOptionDescription (AiGeneratedStarterTemplate _ metadata) = Just $ _tmplDescription metadata -- | 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, @@ -58,10 +60,10 @@ getTemplateStartingInstructions :: StarterTemplate -> StartingInstructionsBuilde getTemplateStartingInstructions = \case GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata LocalStarterTemplate metadata -> _buildStartingInstructions metadata - AiGeneratedStarterTemplate metadata -> _tmplBuildStartingInstructions metadata + AiGeneratedStarterTemplate _ metadata -> _tmplBuildStartingInstructions metadata getTemplateName :: StarterTemplate -> String getTemplateName = \case GhRepoStarterTemplate _ metadata -> _name metadata LocalStarterTemplate metadata -> _name metadata - AiGeneratedStarterTemplate metadata -> _tmplName metadata + AiGeneratedStarterTemplate _ metadata -> _tmplName metadata From 5c8e7ad78ec51a88eb9f95d8e1382fca309586be Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 20:56:09 +0100 Subject: [PATCH 08/12] fix --- .../src/Wasp/Cli/Command/CreateNewProject.hs | 13 +++--- .../FeaturedStarterTemplates.hs | 21 +++++----- .../StarterTemplates/StarterTemplate.hs | 42 ++++++++----------- .../StarterTemplates/StarterTemplateId.hs | 10 ++--- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs index e513684e44..c4637f6d39 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs @@ -19,8 +19,7 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplat import Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo (createProjectOnDiskFromGhRepoTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate - ( DirBasedTemplateMetadata (_path), - StarterTemplate (..), + ( StarterTemplate (..), WaspAppAiGenerator (WaspAI), getTemplateName, getTemplateStartingInstructions, @@ -49,11 +48,11 @@ createProjectOnDisk } = do 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 waspAppAiGenerator _metadata -> + 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 diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs index 521ca405f2..7f1b5c8ff7 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/FeaturedStarterTemplates.hs @@ -25,12 +25,11 @@ defaultStarterTemplate = basicStarterTemplate {- HLINT ignore basicStarterTemplate "Redundant $" -} basicStarterTemplate :: ST.StarterTemplate basicStarterTemplate = - ST.LocalStarterTemplate $ - ST.DirBasedTemplateMetadata - { ST._path = [reldir|basic|], - ST._name = "basic", - ST._description = "Simple starter template with a single page.", - ST._buildStartingInstructions = \projectDirName -> + 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, @@ -153,11 +152,11 @@ makeSimpleWaspGhRepoTemplate (repoName, tmplPathInRepo) (tmplDisplayName, tmplDe GhRepo._repoReferenceName = waspVersionTemplateGitTag } ) - ( ST.DirBasedTemplateMetadata - { _name = tmplDisplayName, - _description = tmplDescription, - _path = tmplPathInRepo, - _buildStartingInstructions = buildStartingInstructions + tmplPathInRepo + ( ST.TemplateMetadata + { _tmplName = tmplDisplayName, + _tmplDescription = tmplDescription, + _tmplBuildStartingInstructions = buildStartingInstructions } ) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs index 8b4e4e417d..4aa97efba2 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplate.hs @@ -1,6 +1,5 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate ( StarterTemplate (..), - DirBasedTemplateMetadata (..), TemplateMetadata (..), StartingInstructionsBuilder, WaspAppAiGenerator (..), @@ -15,22 +14,15 @@ import qualified Wasp.Cli.Interactive as Interactive data StarterTemplate = -- | Template from a Github repo. - GhRepoStarterTemplate !GhRepo.GithubRepoRef !DirBasedTemplateMetadata + GhRepoStarterTemplate !GhRepo.GithubRepoRef !TemplateDirPath !TemplateMetadata | -- | Template from a disk, that comes bundled with wasp CLI. - LocalStarterTemplate !DirBasedTemplateMetadata + LocalStarterTemplate !TemplateDirPath !TemplateMetadata | -- | Template that will be dynamically generated by AI based on user's input. AiGeneratedStarterTemplate !WaspAppAiGenerator !TemplateMetadata -data DirBasedTemplateMetadata = DirBasedTemplateMetadata - { _name :: !String, - _path :: !(Path' Rel' Dir'), -- Path to a directory containing template files. - _description :: !String, - _buildStartingInstructions :: !StartingInstructionsBuilder - } +-- | Path to a directory containing template files. +type TemplateDirPath = Path' Rel' Dir' --- TODO: Have DirBasedTemplateMetadata use TemplateMetadata inside it. --- Maybe even go step further and use typeclass instead? It should give me name, description, starting instructions. --- class IsStarterTemplate data TemplateMetadata = TemplateMetadata { _tmplName :: !String, _tmplDescription :: !String, @@ -42,10 +34,7 @@ data WaspAppAiGenerator = WaspAI instance Interactive.IsOption StarterTemplate where showOption = getTemplateName - - showOptionDescription (GhRepoStarterTemplate _ metadata) = Just $ _description metadata - showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata - showOptionDescription (AiGeneratedStarterTemplate _ metadata) = Just $ _tmplDescription metadata + 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, @@ -56,14 +45,17 @@ type ProjectDirName = String type StartingInstructions = String -getTemplateStartingInstructions :: StarterTemplate -> StartingInstructionsBuilder -getTemplateStartingInstructions = \case - GhRepoStarterTemplate _ metadata -> _buildStartingInstructions metadata - LocalStarterTemplate metadata -> _buildStartingInstructions metadata - AiGeneratedStarterTemplate _ metadata -> _tmplBuildStartingInstructions metadata +getTemplateMetadata :: StarterTemplate -> TemplateMetadata +getTemplateMetadata = \case + GhRepoStarterTemplate _ _ metadata -> metadata + LocalStarterTemplate _ metadata -> metadata + AiGeneratedStarterTemplate _ metadata -> metadata getTemplateName :: StarterTemplate -> String -getTemplateName = \case - GhRepoStarterTemplate _ metadata -> _name metadata - LocalStarterTemplate metadata -> _name metadata - AiGeneratedStarterTemplate _ metadata -> _tmplName metadata +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 index 4ad6d5dd82..4a3d8f8183 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -61,11 +61,11 @@ getStarterTemplateByIdOrThrow featuredTemplates templateIdString = return $ ST.GhRepoStarterTemplate (GhRepo.GithubRepoRef repoOwner repoName waspVersionTemplateGitTag) - ( ST.DirBasedTemplateMetadata - { ST._name = repoName, - ST._description = "Template from Github repo " <> repoOwner <> "/" <> repoName, - ST._path = maybeTmplDirPath & fromMaybe [reldir|.|], - ST._buildStartingInstructions = \_ -> + (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." ] From 2d60856acd729ef63016b1d6f4f6375904efc1a7 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 5 Dec 2024 21:24:41 +0100 Subject: [PATCH 09/12] fix --- waspc/ChangeLog.md | 6 +++++- .../StarterTemplates/StarterTemplateId.hs | 12 +++++++++--- waspc/waspc.cabal | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) 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/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index 4a3d8f8183..ad6c0b707e 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -20,6 +20,9 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (getTe import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate as ST import qualified Wasp.Cli.GithubRepo as GhRepo +-- TODO: Update CLI instructions. +-- TODO: Add specifing of a third-party template under interactive choosing of template. + -- | A way to uniquely reference a specific Wasp starter template. -- It is how users specify which starter template they want to use. data StarterTemplateId @@ -47,7 +50,7 @@ getStarterTemplateIdType = \case getStarterTemplateIdTypeDescription :: StarterTemplateIdType -> String getStarterTemplateIdTypeDescription = \case IdTypeFeaturedTemplateName -> "a featured template name (e.g. \"saas\")" - IdTypeGhRepoTemplateUri -> "a github URI (github:/[/some/dir])" + 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). @@ -78,11 +81,13 @@ getStarterTemplateByIdOrThrow featuredTemplates templateIdString = throwInvalidTemplateNameUsedError templateName = throwError $ - "There is no featured template with name " <> templateName <> ".\n" <> expectedInputMessage + "There is no featured template with name " <> wrapInQuotes templateName <> ".\n" <> expectedInputMessage expectedInputMessage = "Expected " <> intercalate " or " (getStarterTemplateIdTypeDescription <$> [minBound .. maxBound]) <> "." - <> (" Valid featured template names are " <> intercalate ", " (getTemplateName <$> featuredTemplates) <> ".") + <> ("\nValid featured template names are " <> intercalate ", " (wrapInQuotes . getTemplateName <$> featuredTemplates) <> ".") + + wrapInQuotes str = "\"" <> str <> "\"" parseStarterTemplateId :: String -> Either String StarterTemplateId parseStarterTemplateId = \case @@ -96,6 +101,7 @@ parseStarterTemplateId = \case 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 diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 232da78414..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 From 35e71a8134974060e948cea2dd551d5048d26ad4 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 9 Dec 2024 00:04:46 +0100 Subject: [PATCH 10/12] Added docs an interactive option. --- waspc/cli/exe/Main.hs | 3 +- .../CreateNewProject/ProjectDescription.hs | 23 +++++++++++++-- .../StarterTemplates/StarterTemplateId.hs | 3 -- web/docs/project/starter-templates.md | 29 +++++++++++++++---- 4 files changed, 45 insertions(+), 13 deletions(-) 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/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index 8f6addfdc2..9cfb151019 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -8,9 +8,11 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription ) where +import Control.Monad.Error (MonadError (throwError)) import Control.Monad.IO.Class (liftIO) 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) @@ -26,6 +28,7 @@ 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) @@ -76,6 +79,14 @@ obtainNewProjectDescriptionFromCliArgs projectName templateIdArg featuredTemplat 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 templateIdArg featuredTemplates = do projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)" @@ -83,9 +94,17 @@ obtainNewProjectDescriptionInteractively templateIdArg featuredTemplates = do projectName templateIdArg featuredTemplates - (liftIO askForTemplate) + askForTemplate where - askForTemplate = Interactive.askToChoose "Choose a starter template" $ fromList featuredTemplates + 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 :: diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs index ad6c0b707e..be1954d4b9 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/StarterTemplateId.hs @@ -20,9 +20,6 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (getTe import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate as ST import qualified Wasp.Cli.GithubRepo as GhRepo --- TODO: Update CLI instructions. --- TODO: Add specifing of a third-party template under interactive choosing of template. - -- | A way to uniquely reference a specific Wasp starter template. -- It is how users specify which starter template they want to use. data StarterTemplateId 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. + From d2983773be23f09fdbacecb175e05a3b82f5e2bc Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 9 Dec 2024 00:32:51 +0100 Subject: [PATCH 11/12] fix --- .../Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs index 9cfb151019..2181be1d7f 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/ProjectDescription.hs @@ -8,7 +8,6 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription ) where -import Control.Monad.Error (MonadError (throwError)) import Control.Monad.IO.Class (liftIO) import Data.List (intercalate) import Data.List.NonEmpty (fromList) @@ -79,7 +78,7 @@ obtainNewProjectDescriptionFromCliArgs projectName templateIdArg featuredTemplat featuredTemplates (return defaultStarterTemplate) -data StarterTemplateMenuChoice = FeaturedStarterTemplate StarterTemplate | CommunityTemplate +data StarterTemplateMenuChoice = FeaturedStarterTemplate !StarterTemplate | CommunityTemplate instance IsOption StarterTemplateMenuChoice where showOption (FeaturedStarterTemplate tmpl) = showOption tmpl From 98a4d3d7d6e06c4a6200d18fce8bdd518bc6a02d Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 12 Dec 2024 10:11:52 +0100 Subject: [PATCH 12/12] fix --- .../Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs index a1a8f932e5..c50341b597 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Skeleton.hs @@ -5,6 +5,7 @@ module Wasp.Cli.Command.CreateNewProject.StarterTemplates.Skeleton ) where +import Data.Functor ((<&>)) import Data.Text (Text) import StrongPath (File', Path, Rel, System, reldir, ()) import qualified Wasp.Data as Data @@ -13,6 +14,6 @@ import Wasp.Util.IO (listDirectoryDeep, readFileStrict) readWaspProjectSkeletonFiles :: IO [(Path System (Rel WaspProjectDir) File', Text)] readWaspProjectSkeletonFiles = do - skeletonFilesDir <- ( [reldir|Cli/templates/skeleton|]) <$> Data.getAbsDataDirPath + skeletonFilesDir <- Data.getAbsDataDirPath <&> ( [reldir|Cli/templates/skeleton|]) skeletonFilePaths <- listDirectoryDeep skeletonFilesDir mapM (\path -> (path,) <$> readFileStrict (skeletonFilesDir path)) skeletonFilePaths