Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support in Wasp for external/community starter templates #2402

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion waspc/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Changelog

## Next
## [WIP] 0.16.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was wrong with Next? 😢 I kinda liked it, you don't have to think about what the next version will be

Copy link
Member Author

@Martinsos Martinsos Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next is cool! But in this case I knew what next version is going to be, so I thought why not write it down? But yeah I could have left Next you are right, wouldn't be an issue. I can also roll with Next in the future.


### ⚠️ 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:<repo_owner>/<repo_name>[/dir/to/template]`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: I should remove this if we decide to drop -t github: support.


### 🔧 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.
Expand Down
3 changes: 1 addition & 2 deletions waspc/cli/exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ printUsage =
title " GENERAL",
cmd " new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.",
" OPTIONS:",
" -t|--template <template-name>",
" Check out the templates list here: https://github.com/wasp-lang/starters",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was pointing to a repo that contained only one or two starters, so I instead removed this, and you instead get more info on valid template names if you make a mistake while using -t, which is more maintainable and less crowded here in the top level cli info.

" -t|--template <template-id>",
"",
cmd " new:ai <app-name> <app-description> [<config-json>]",
" Uses AI to create a new Wasp project just based on the app name and the description.",
Expand Down
35 changes: 18 additions & 17 deletions waspc/cli/src/Wasp/Cli/Command/CreateNewProject.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import Wasp.Cli.Command.CreateNewProject.ProjectDescription
( NewProjectDescription (..),
obtainNewProjectDescription,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates
( DirBasedTemplateMetadata (_path),
StarterTemplate (..),
getStarterTemplates,
getTemplateStartingInstructions,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates (getFeaturedStarterTemplates)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.GhRepo (createProjectOnDiskFromGhRepoTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate
( StarterTemplate (..),
WaspAppAiGenerator (WaspAI),
getTemplateName,
getTemplateStartingInstructions,
)
import Wasp.Cli.Command.Message (cliSendMessageC)
import qualified Wasp.Message as Msg
import qualified Wasp.Util.Terminal as Term
Expand All @@ -31,9 +32,8 @@ import qualified Wasp.Util.Terminal as Term
createNewProject :: Arguments -> Command ()
createNewProject args = do
newProjectArgs <- parseNewProjectArgs args & either Common.throwProjectCreationError return
let starterTemplates = getStarterTemplates

newProjectDescription <- obtainNewProjectDescription newProjectArgs starterTemplates
let featuredStarterTemplates = getFeaturedStarterTemplates
newProjectDescription <- obtainNewProjectDescription newProjectArgs featuredStarterTemplates

createProjectOnDisk newProjectDescription
liftIO $ printGettingStartedInstructionsForProject newProjectDescription
Expand All @@ -46,20 +46,21 @@ createProjectOnDisk
_template = template,
_absWaspProjectDir = absWaspProjectDir
} = do
cliSendMessageC $ Msg.Start $ "Creating your project from the \"" ++ show template ++ "\" template..."
cliSendMessageC $ Msg.Start $ "Creating your project from the \"" ++ getTemplateName template ++ "\" template..."
case template of
GhRepoStarterTemplate ghRepoRef metadata ->
createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef $ _path metadata
LocalStarterTemplate metadata ->
liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata
AiGeneratedStarterTemplate ->
AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName
GhRepoStarterTemplate ghRepoRef tmplDirPath _ ->
createProjectOnDiskFromGhRepoTemplate absWaspProjectDir projectName appName ghRepoRef tmplDirPath
LocalStarterTemplate tmplDirPath _ ->
liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName tmplDirPath
AiGeneratedStarterTemplate waspAppAiGenerator _ ->
case waspAppAiGenerator of
WaspAI -> AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName
Comment on lines +56 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we generalise this, do we expect different generators in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe :D! Not likely though. The reason why I generalized it is that it allowed me to treat this template the same way as other templates, most importantly I could put it into the "FeaturedStarterTemplates.hs" file in a nice way. As an "overengineering" it is really a tiny step, but it allows me that + I found it ok that it is explicit this will use Wasp AI, I don't think that makes code more complex.


-- | This function assumes that the project dir was created inside the current working directory.
printGettingStartedInstructionsForProject :: NewProjectDescription -> IO ()
printGettingStartedInstructionsForProject projectDescription = do
let projectDirName = init . SP.toFilePath . SP.basename $ _absWaspProjectDir projectDescription
let instructions = getTemplateStartingInstructions projectDirName $ _template projectDescription
let instructions = getTemplateStartingInstructions (_template projectDescription) projectDirName
putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectDirName ++ " directory!"
putStrLn ""
putStrLn instructions
2 changes: 1 addition & 1 deletion waspc/cli/src/Wasp/Cli/Command/CreateNewProject/AI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +33,7 @@ parseNewProjectArgs newArgs = parserResultToEither $ execParserPure defaultPrefs
Opt.strOption $
Opt.long "template"
<> Opt.short 't'
<> Opt.metavar "TEMPLATE_NAME"
<> Opt.metavar "TEMPLATE"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we maybe want to go with TEMPLATE_ID to keep it consistent with the record above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of didn't want to bother them with the details, I thought I will just put TEMPLATE and they will figure it out. TEMPLATE_ID sounded a bit low-level. I can put it thought if you think it is better DX / clearer.

<> Opt.help "Template to use for the new project"

parserResultToEither :: Opt.ParserResult NewProjectArgs -> Either String NewProjectArgs
Expand Down
7 changes: 0 additions & 7 deletions waspc/cli/src/Wasp/Cli/Command/CreateNewProject/Common.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module Wasp.Cli.Command.CreateNewProject.Common
( throwProjectCreationError,
throwInvalidTemplateNameUsedError,
defaultWaspVersionBounds,
)
where
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,25 @@ module Wasp.Cli.Command.CreateNewProject.ProjectDescription
where

import Control.Monad.IO.Class (liftIO)
import Data.Function ((&))
import Data.List (intercalate)
import Data.List.NonEmpty (fromList)
import Data.Maybe (fromJust)
import Path.IO (doesDirExist)
import StrongPath (Abs, Dir, Path')
import StrongPath.Path (toPathAbsDir)
import Wasp.Analyzer.Parser (isValidWaspIdentifier)
import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (NewProjectArgs (..))
import Wasp.Cli.Command.CreateNewProject.Common
( throwInvalidTemplateNameUsedError,
throwProjectCreationError,
( throwProjectCreationError,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates
( StarterTemplate,
defaultStarterTemplate,
findTemplateByString,
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.FeaturedStarterTemplates (defaultStarterTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplate (StarterTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.StarterTemplateId
( getStarterTemplateByIdOrThrow,
)
import Wasp.Cli.FileSystem (getAbsPathToDirInCwd)
import Wasp.Cli.Interactive (IsOption (..))
import qualified Wasp.Cli.Interactive as Interactive
import Wasp.Project (WaspProjectDir)
import Wasp.Util (indent, kebabToCamelCase, whenM)
Expand Down Expand Up @@ -62,32 +62,48 @@ instance Show NewProjectAppName where
wasp new

- Project name is required.
- Template name is required, we ask the user to choose from available templates.
- Template name is required, we ask the user to choose from featured templates.
-}
obtainNewProjectDescription :: NewProjectArgs -> [StarterTemplate] -> Command NewProjectDescription
obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _templateName = templateNameArg} starterTemplates =
obtainNewProjectDescription NewProjectArgs {_projectName = projectNameArg, _templateId = templateIdArg} starterTemplates =
case projectNameArg of
Just projectName -> obtainNewProjectDescriptionFromCliArgs projectName templateNameArg starterTemplates
Nothing -> obtainNewProjectDescriptionInteractively templateNameArg starterTemplates
Just projectName -> obtainNewProjectDescriptionFromCliArgs projectName templateIdArg starterTemplates
Nothing -> obtainNewProjectDescriptionInteractively templateIdArg starterTemplates

obtainNewProjectDescriptionFromCliArgs :: String -> Maybe String -> [StarterTemplate] -> Command NewProjectDescription
obtainNewProjectDescriptionFromCliArgs projectName templateNameArg availableTemplates =
obtainNewProjectDescriptionFromCliArgs projectName templateIdArg featuredTemplates =
obtainNewProjectDescriptionFromProjectNameAndTemplateArg
projectName
templateNameArg
availableTemplates
templateIdArg
featuredTemplates
(return defaultStarterTemplate)

data StarterTemplateMenuChoice = FeaturedStarterTemplate !StarterTemplate | CommunityTemplate

instance IsOption StarterTemplateMenuChoice where
showOption (FeaturedStarterTemplate tmpl) = showOption tmpl
showOption CommunityTemplate = "community template"
showOptionDescription (FeaturedStarterTemplate tmpl) = showOptionDescription tmpl
showOptionDescription CommunityTemplate = Just "Check our list of community-made templates at https://wasp-lang.dev/docs/project/starter-templates#community-templates"

obtainNewProjectDescriptionInteractively :: Maybe String -> [StarterTemplate] -> Command NewProjectDescription
obtainNewProjectDescriptionInteractively templateNameArg availableTemplates = do
obtainNewProjectDescriptionInteractively templateIdArg featuredTemplates = do
projectName <- liftIO $ Interactive.askForRequiredInput "Enter the project name (e.g. my-project)"
obtainNewProjectDescriptionFromProjectNameAndTemplateArg
projectName
templateNameArg
availableTemplates
(liftIO askForTemplateName)
templateIdArg
featuredTemplates
askForTemplate
where
askForTemplateName = Interactive.askToChoose "Choose a starter template" $ fromList availableTemplates
askForTemplate =
liftIO
( Interactive.askToChoose
"Choose a starter template"
(fromList $ (FeaturedStarterTemplate <$> featuredTemplates) ++ [CommunityTemplate])
)
>>= \case
FeaturedStarterTemplate tmpl -> pure tmpl
CommunityTemplate -> throwProjectCreationError $ "Project creation aborted. " <> fromJust (showOptionDescription CommunityTemplate) <> " and follow the instructions of a specific template."

-- Common logic
obtainNewProjectDescriptionFromProjectNameAndTemplateArg ::
Expand All @@ -96,15 +112,14 @@ obtainNewProjectDescriptionFromProjectNameAndTemplateArg ::
[StarterTemplate] ->
Command StarterTemplate ->
Command NewProjectDescription
obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateNameArg availableTemplates obtainTemplateWhenNoArg = do
obtainNewProjectDescriptionFromProjectNameAndTemplateArg projectName templateIdArg featuredTemplates getStarterTemplateWhenNoArg = do
absWaspProjectDir <- obtainAvailableProjectDirPath projectName
selectedTemplate <- maybe obtainTemplateWhenNoArg findTemplateOrThrow templateNameArg
selectedTemplate <-
maybe
getStarterTemplateWhenNoArg
(either throwProjectCreationError pure . getStarterTemplateByIdOrThrow featuredTemplates)
templateIdArg
mkNewProjectDescription projectName absWaspProjectDir selectedTemplate
where
findTemplateOrThrow :: String -> Command StarterTemplate
findTemplateOrThrow templateName =
findTemplateByString availableTemplates templateName
& maybe throwInvalidTemplateNameUsedError return

obtainAvailableProjectDirPath :: String -> Command (Path' Abs (Dir WaspProjectDir))
obtainAvailableProjectDirPath projectName = do
Expand Down
Loading
Loading