Skip to content

Create wasp build start command #2796

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

Open
wants to merge 90 commits into
base: main
Choose a base branch
from

Conversation

cprecioso
Copy link
Member

@cprecioso cprecioso commented May 26, 2025

Resolves #1883

Implements a new wasp build start command.
It will take the output from wasp build, build the client, the server, and run them both.

The logic is consistent with wasp-app-runner, except we don't run the SMTP server or the DB.

Nothing is excessively surprising, except maybe the creation of a JobExcept type to make working with the jobs easier. This might later grow into a full orchestration utility if we want.

@cprecioso cprecioso self-assigned this May 26, 2025
Copy link

cloudflare-workers-and-pages bot commented Jun 3, 2025

Deploying wasp-docs-on-main with  Cloudflare Pages  Cloudflare Pages

Latest commit: c09269c
Status: ✅  Deploy successful!
Preview URL: https://dfa20b17.wasp-docs-on-main.pages.dev
Branch Preview URL: https://1883-implement-wasp-build-st.wasp-docs-on-main.pages.dev

View logs

@cprecioso cprecioso force-pushed the 1883-implement-wasp-build-start-command-that-runs-output-of-wasp-build branch from f269680 to 01ac717 Compare June 4, 2025 16:20
@cprecioso cprecioso changed the base branch from main to cprecioso/push-yppoqwmksmyl June 5, 2025 07:15
@Martinsos Martinsos self-requested a review June 5, 2025 08:32
@Martinsos
Copy link
Member

Did a round of pair reviewing with Carlos on this one, will review it again once it is out of draft stage.

Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

I left some suggestions

makeEnvironmentVariableParser prefix =
Opt.strOption $
Opt.long (prefix <> "-env")
<> Opt.short (head prefix)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: maybe we drop the short option since it might cause some conflicts (now s and c are taken, who knows if in the future we want to add new long options e.g. secret-sauce which would result in another s option jk but you get my point)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I know, but if we want to add multiple envs, wasp build start --server-env JWT_SECRET=my-secret --server-env GOOGLE_CLIENT_ID=my-client-id --server-env GOOGLE_CLIENT_SECRET=my-client-secret etc is going to get old very quickly...

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, I'm not against short hand properties but in the dynamic way you generate them from the first char, I felt like there is some danger in stepping on toes :D Maybe the answer is to be explicit and have longOptionName and shortOptionName as input?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahh got it, makes sense!

parseDotEnvFile :: Path' Abs (File ()) -> IO [EnvVar]
parseDotEnvFile envFile =
Dotenv.parseFile (fromAbsFile envFile)
parseDotEnvFilePath :: FilePath -> IO [EnvVar]
Copy link
Contributor

Choose a reason for hiding this comment

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

We are losing type safety here e.g. FilePath is much less defined than Path' Abs (File ()). Can we avoid doing this and parse the user provided file path? For example, we assume it will be a relative file path which we can then combine with the Wasp project dir into an absolute file path?

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 spent some time fighting with SP but I don't think I was able to express what I wanted. The args can both be given as an absolute path, or as a relative path to cwd. How can I parse that with SP?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say you can parse the input and get an absolute path always:

  • if it's already absolute, great, parsing succeeded (parseAbsFile)
  • if it's not, construct an absolute path by combining the provided path and the Wasp project

This way, we validated input and we have strong guarantees about the paths we are dealing with. Do you want to give it a go?


extraEnvParamsFromConfig =
Config.serverEnvironmentVariables config
>>= \envVar -> ["--env", envVar]
Copy link
Contributor

Choose a reason for hiding this comment

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

We are not parsing the user provided server env variables here. We are doing much more with the client env vars e.g. parsing them into EnvVar and then nubing them. I'd like us to do the same here so we can be sure that we are not passing malformed input to the docker command.

We coule maybeparse the env vars at the beginning into EnvVar format which we can then show envVar here. That would make sense, because we don't need a list of strings in our config, but a list of EnvVar values.

Copy link
Member Author

Choose a reason for hiding this comment

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

We are doing much more with the client env vars

Yep, because we need to construct the env for the client manually. but here in docker we can just pass both as arguments, so I opted to skip the complex processing (esp. since Docker will just do it again).

Especially, converting to EnvVar ((String, String)), seems a bit pointless as I'm going to immediately convert them back to NAME=VALUE (because that's how docker expects it); plus if I do that I don't allow the passthrough syntax from Docker (-e JUST_THE_NAME).

Copy link
Contributor

Choose a reason for hiding this comment

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

Especially, converting to EnvVar ((String, String)), seems a bit pointless as I'm going to immediately convert them back to NAME=VALUE

The fact it passes from String -> EnvVar means that it is in a valid format and we can safely continue using the values. I think it's worthwhile doing even if we convert it right away.

plus if I do that I don't allow the passthrough syntax from Docker

Uu, this was unexpected, to me it feels like a bug because or command is not defined like that: https://github.com/wasp-lang/wasp/pull/2796/files#diff-4a6575b2b3839d30a9c37d2062e42b84eadb760bd7cef15b6ca264dfeb0f12ffR34

This is an extra argument for doing it - strings can be anything and we are letting Docker decide what the raw value means, instead of handling all the cases and making strongly typed programs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oki, will do

@infomiho infomiho self-requested a review July 17, 2025 16:43
Base automatically changed from push-uuqynktmlxnr to main July 17, 2025 20:46
Copy link
Member Author

@cprecioso cprecioso left a comment

Choose a reason for hiding this comment

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

@infomiho ready

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 put this in a new file because I don't know really where else to put it, it didn't seem fine to tuck it into any other.


fromFilePath :: FilePath -> Either String EnvVarFileArgument
fromFilePath str =
Path.parseSomeFile str
Copy link
Member Author

Choose a reason for hiding this comment

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

For checking if it is Abs or Rel, in the end I used the parseSomeFile fn from the Path package, seeing that StrongPath also has utilities to convert from it

Copy link
Member Author

Choose a reason for hiding this comment

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

Wdyt about this solution? Moved all the environment variable calculation to the config creationg, so we can just pass an [EnvVar] around, and not have 4 different but very similar properties.

And split the constructor into two parts, one initial one and another where we put the envs, as that part has to be IO.

Comment on lines +113 to +121
forceEnvVarsCommand :: [EnvVar] -> [EnvVar] -> Command [EnvVar]
forceEnvVarsCommand forced existing =
case forceEnvVars forced existing of
Left duplicateNames ->
throwError $
CommandError "Duplicate environment variables" $
("The following environment variables will be overwritten by Wasp and should be removed: " <>) $
intercalate ", " duplicateNames
Right combined -> return combined
Copy link
Member Author

Choose a reason for hiding this comment

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

If a user passes an environment variable that we override, they might get confused about why we're not using their value. So we explicitly tell them we're doing it and terminate.

image

Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

Left some comments, mostly focusing on Config because I felt we could maybe organise the code to be easier to follow along. Let me know what you think, I've given you what I came up with, don't feel like you need to use it as-is.


### 🎉 New Features

- New command: `wasp build start`. It allows you to run your built Wasp app as it would run in your production server. Useful for testing your production build locally, and making sure of passing the correct environment variables. [#2796](https://github.com/wasp-lang/wasp/pull/2796)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the command "making sure of passing the correct environment variables" or the user is doing that? I know it's the changelog but I'd maybe make it more clear what you mean.


makeBuildStartConfig :: AppSpec -> BuildStartArgs -> SP.Path' SP.Abs (SP.Dir WaspProjectDir) -> Command BuildStartConfig
makeBuildStartConfig appSpec args projectDir = do
let config = makeBuildStartConfigWithoutEnvVars appSpec projectDir
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm afraid this half completed object creation - it means with might accidentally use it somewhere at some point in the future.

I'd like to see us construct the full object from the start. Why can't we construct the object with all the variables inside?

return makeBuildStartConfig appSpec projectDir serverEnvVars clientEnvVars

Copy link
Contributor

Choose a reason for hiding this comment

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

I've played around with it and came up with this:

makeBuildStartConfig :: AppSpec -> BuildStartArgs -> SP.Path' SP.Abs (SP.Dir WaspProjectDir) -> Command BuildStartConfig
makeBuildStartConfig appSpec args projectDir = do
  let waspServerEnvVars =
        [ (Server.clientUrlEnvVarName, show clientPort),
          (Server.serverUrlEnvVarName, serverUrl')
        ]
  userServerEnvVars <- liftIO $ combineEnvVarsWithEnvFiles (Args.serverEnvironmentVariables args) (Args.serverEnvironmentFiles args)
  serverEnvVars' <- mergeEnvVarsCommand waspServerEnvVars userServerEnvVars

  let waspClientEnvVars = [(WebApp.serverUrlEnvVarName, serverUrl')]
  userClientEnvVars <- liftIO $ combineEnvVarsWithEnvFiles (Args.clientEnvironmentVariables args) (Args.clientEnvironmentFiles args)
  clientEnvVars' <- mergeEnvVarsCommand waspClientEnvVars userClientEnvVars

  return $
    BuildStartConfig
      { appUniqueId = appUniqueId',
        buildDir = buildDir',
        clientPortAndUrl = (clientPort, clientUrl),
        serverEnvVars = serverEnvVars',
        clientEnvVars = clientEnvVars',
        serverUrl = serverUrl'
      }
  where
    buildDir' = projectDir </> dotWaspDirInWaspProjectDir </> buildDirInDotWaspDir
    appUniqueId' = makeAppUniqueId projectDir appName
    (appName, _) = ASV.getApp appSpec

    -- This assumes that `getDefaultDevClientUrl` uses `defaultClientPort` internally.
    -- If that changes, we also need to change this.
    clientPort = defaultClientPort
    clientUrl = getDefaultDevClientUrl appSpec

    serverUrl' = defaultDevServerUrl

Comment on lines +80 to +83
-- This assumes that `getDefaultDevClientUrl` uses `defaultClientPort` internally.
-- If that changes, we also need to change this.
clientPort = defaultClientPort
clientUrl = getDefaultDevClientUrl appSpec
Copy link
Contributor

Choose a reason for hiding this comment

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

A rewrite I'd suggest is introducing a ClientUrl data type that hold both values. And we'd keep this in the `Wasp.Generator.WebAppGenerator.Common

I know it's out of scope for this PR but I feel like the tuple is not the right data structure, accessing the port with fst feels unreadable and error-prone to me. We maybe should address it in the future.

@@ -0,0 +1,15 @@
module Wasp.Cli.Util.Common
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels very generic Util.Common - can we do something better? We already have Wasp.Util.IO for file/directory operations. Maybe this can go under that module e.g. Wasp.Util.IO.WorkingDirectory

Comment on lines +26 to +33
envVarFromString :: String -> Either String EnvVar
envVarFromString var =
case break (== '=') var of
([], _) -> failure
(name, '=' : value) -> Right (name, value)
_ -> failure
where
failure = Left $ "Environment variable must be in the format NAME=VALUE: " ++ var
Copy link
Contributor

Choose a reason for hiding this comment

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

Unit tests for this would be good, just to cover the expected failure scenarios.

Comment on lines +48 to +57
data EnvVarFileArgument
= AbsoluteEnvVarFile (Path' Abs (File ()))
| RelativeEnvVarFile (Path' (Rel WorkingDir) (File ()))

readEnvVarFile :: EnvVarFileArgument -> IO [EnvVar]
readEnvVarFile arg = toStrongPath arg >>= parseDotEnvFile
where
toStrongPath :: EnvVarFileArgument -> IO (Path' Abs (File ()))
toStrongPath (AbsoluteEnvVarFile path) = return path
toStrongPath (RelativeEnvVarFile path) = (</> path) <$> getWorkingDir
Copy link
Contributor

Choose a reason for hiding this comment

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

Great change 👍


serverEnvVars' <-
readAndForceEnvVars
[ (Server.clientUrlEnvVarName, show $ fst $ clientPortAndUrl config),
Copy link
Contributor

Choose a reason for hiding this comment

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

clientUrlEnvVarName expects the URL, but we are giving it the port here

Comment on lines +66 to +74
forceEnvVars :: [EnvVar] -> [EnvVar] -> Either [String] [EnvVar]
forceEnvVars forced existing =
if null duplicateNames
then Right (forced <> existing)
else Left duplicateNames
where
duplicateNames = filter (`Set.member` forcedNames) existingNames
forcedNames = Set.fromList $ map fst forced
existingNames = map fst existing
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd maybe make the function more generic, it's used to merge env vars while keeping an eye on duplicates.

This is what I came up with while playing with it:

mergeEnvVars :: [EnvVar] -> [EnvVar] -> Either [String] [EnvVar]
mergeEnvVars envVars1 envVars2 =
  if null duplicateNames
    then Right (envVars1 <> envVars2)
    else Left duplicateNames
  where
    duplicateNames = filter (`Set.member` envVarNames1) envVarNames2
    envVarNames1 = Set.fromList $ map fst envVars1
    envVarNames2 = map fst envVars2

-- values we've hardcoded in the generator. In the future, we might want to make
-- these configurable via the Wasp app spec or command line arguments.

serverUrl :: BuildStartConfig -> String
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd also move this to the config because this requires you to have config to get the serverUrl in the makeBuildStartConfig.

Comment on lines +102 to +121
readAndForceEnvVars :: [EnvVar] -> [EnvVar] -> [EnvVarFileArgument] -> Command [EnvVar]
readAndForceEnvVars forced existing files = do
readVars <- liftIO $ readEnvVars existing files
forceEnvVarsCommand forced readVars

readEnvVars :: [EnvVar] -> [EnvVarFileArgument] -> IO [EnvVar]
readEnvVars pairs files = do
pairsFromFiles <- mapM readEnvVarFile files
let allEnvVars = pairs <> concat pairsFromFiles
return $ nubEnvVars allEnvVars

forceEnvVarsCommand :: [EnvVar] -> [EnvVar] -> Command [EnvVar]
forceEnvVarsCommand forced existing =
case forceEnvVars forced existing of
Left duplicateNames ->
throwError $
CommandError "Duplicate environment variables" $
("The following environment variables will be overwritten by Wasp and should be removed: " <>) $
intercalate ", " duplicateNames
Right combined -> return combined
Copy link
Contributor

Choose a reason for hiding this comment

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

Something felt off while I was reading this code, like IO and pure code were intertwined and some of the concerns weren't separated in the cleanest way, I've played around with it like this:

mergeEnvVarsCommand :: [EnvVar] -> [EnvVar] -> Command [EnvVar]
mergeEnvVarsCommand waspEnvVars userEnvVars = do
  case mergeEnvVars waspEnvVars userEnvVars of
    Left duplicateNames ->
      throwError $
        CommandError "Duplicate environment variables" $
          ("The following environment variables will be overwritten by Wasp and should be removed: " <>) $
            intercalate ", " duplicateNames
    Right combinedEnvVars -> return combinedEnvVars

combineEnvVarsWithEnvFiles :: [EnvVar] -> [EnvVarFileArgument] -> IO [EnvVar]
combineEnvVarsWithEnvFiles pairs files = do
  pairsFromFiles <- mapM readEnvVarFile files
  let allEnvVars = pairs <> concat pairsFromFiles
  return $ nubEnvVars allEnvVars

You'll notice that now the IO and non-IO stuff is separate and you'll need to invoke two functions in the main fn:

let waspClientEnvVars = [(WebApp.serverUrlEnvVarName, serverUrl')]
userClientEnvVars <- liftIO $ combineEnvVarsWithEnvFiles (Args.clientEnvironmentVariables args) (Args.clientEnvironmentFiles args)
clientEnvVars' <- mergeEnvVarsCommand waspClientEnvVars userClientEnvVars

This felt easier to follow along: we have Wasp vars and user vars. User vars are computed from vars and files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement wasp build start command that runs output of wasp build
4 participants