-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
base: main
Are you sure you want to change the base?
Create wasp build start
command
#2796
Conversation
…-runs-output-of-wasp-build
…-runs-output-of-wasp-build
Deploying wasp-docs-on-main with
|
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 |
…-runs-output-of-wasp-build
f269680
to
01ac717
Compare
Did a round of pair reviewing with Carlos on this one, will review it again once it is out of draft stage. |
…-command-that-runs-output-of-wasp-build
There was a problem hiding this 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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
waspc/src/Wasp/Env.hs
Outdated
parseDotEnvFile :: Path' Abs (File ()) -> IO [EnvVar] | ||
parseDotEnvFile envFile = | ||
Dotenv.parseFile (fromAbsFile envFile) | ||
parseDotEnvFilePath :: FilePath -> IO [EnvVar] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oki, will do
…-runs-output-of-wasp-build
…-runs-output-of-wasp-build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@infomiho ready
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…-runs-output-of-wasp-build
There was a problem hiding this 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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
-- This assumes that `getDefaultDevClientUrl` uses `defaultClientPort` internally. | ||
-- If that changes, we also need to change this. | ||
clientPort = defaultClientPort | ||
clientUrl = getDefaultDevClientUrl appSpec |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
.
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 |
There was a problem hiding this comment.
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.
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.