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

Add support for Windows NPM #658

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion waspc/cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs: $ncpus
test-show-details: direct

-- WARNING: Run cabal update if your local package index is older than this date.
index-state: 2022-03-22T14:16:26Z
index-state: 2022-07-05T07:45:23Z
38 changes: 38 additions & 0 deletions waspc/src/Wasp/Generator/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ module Wasp.Generator.Common
nodeVersionRange,
npmVersionRange,
prismaVersion,
npmCmd,
buildNpmCmdWithArgs,
npxCmd,
buildNpxCmdWithArgs,
)
where

import System.Info (os)
import qualified Wasp.SemanticVersion as SV

-- | Directory where the whole web app project (client, server, ...) is generated.
Expand Down Expand Up @@ -38,3 +43,36 @@ npmVersionRange =

prismaVersion :: SV.Version
prismaVersion = SV.Version 3 15 2

npmCmd :: String
npmCmd = case os of
-- Windows adds ".exe" to command, when calling it programmatically, if it doesn't
-- have an extension already, meaning that calling `npm` actually calls `npm.exe`.
-- However, there is no `npm.exe` on Windows, instead there is `npm` or `npm.cmd`, so we make sure here to call `npm.cmd`.
-- Extra info: https://stackoverflow.com/questions/43139364/createprocess-weird-behavior-with-files-without-extension .
"mingw32" -> "npm.cmd"
_ -> "npm"

npxCmd :: String
npxCmd = case os of
-- Read above, for "npm", why we need to handle Win in special way.
"mingw32" -> "npx.cmd"
_ -> "npx"

buildNpmCmdWithArgs :: [String] -> (String, [String])
buildNpmCmdWithArgs args = case os of
-- On Windows, due to how npm.cmd script is written, it happens that script
-- resolves some paths (work directory) incorrectly when called programmatically, sometimes.
-- Therefore, we call it via `cmd.exe`, which ensures this issue doesn't happen.
-- Extra info: https://stackoverflow.com/a/44820337 .
"mingw32" -> wrapCmdAndArgsInWinCmdExe (npmCmd, args)
_ -> (npmCmd, args)

buildNpxCmdWithArgs :: [String] -> (String, [String])
buildNpxCmdWithArgs args = case os of
-- Read above, for "npm", why we need to handle Win in special way.
"mingw32" -> wrapCmdAndArgsInWinCmdExe (npxCmd, args)
_ -> (npxCmd, args)

wrapCmdAndArgsInWinCmdExe :: (String, [String]) -> (String, [String])
wrapCmdAndArgsInWinCmdExe (cmd, args) = ("cmd.exe", [unwords $ "/c" : cmd : args])
54 changes: 33 additions & 21 deletions waspc/src/Wasp/Generator/DbGenerator/Jobs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ import StrongPath (Abs, Dir, Path', (</>))
import qualified StrongPath as SP
import System.Exit (ExitCode (..))
import qualified System.Info
import Wasp.Generator.Common (ProjectRootDir, prismaVersion)
import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs, buildNpxCmdWithArgs, prismaVersion)
import Wasp.Generator.DbGenerator.Common (dbSchemaFileInProjectRootDir)
import Wasp.Generator.Job (JobMessage, JobMessageData (JobExit, JobOutput))
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.Job.Process (runNodeDependentCommandAsJob)
import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir)

-- Args to be passed to `npx` in order to run prisma.
-- `--no-install` is the magic that causes this command to fail if npx cannot find it locally
-- (either in node_modules, or globally in npx). We do not want to allow npx to ask
-- a user without prisma to install the latest version.
-- We also pin the version to what we need so it won't accidentally find a different version globally
-- somewhere on the PATH.
npxPrismaCmd :: [String]
npxPrismaCmd = ["npx", "--no-install", "prisma@" ++ show prismaVersion]
npxPrismaArgs :: [String]
npxPrismaArgs = ["--no-install", "prisma@" ++ show prismaVersion]

migrateDev :: Path' Abs (Dir ProjectRootDir) -> Maybe String -> J.Job
migrateDev projectDir maybeMigrationName = do
Expand All @@ -33,24 +34,35 @@ migrateDev projectDir maybeMigrationName = do

let optionalMigrationArgs = maybe [] (\name -> ["--name", name]) maybeMigrationName

-- NOTE(matija): We are running this command from server's root dir since that is where
-- Prisma packages (cli and client) are currently installed.
-- NOTE(martin): For this to work on Mac, filepath in the list below must be as it is now - not wrapped in any quotes.
let npxPrismaMigrateArgs = npxPrismaArgs ++ ["migrate", "dev", "--schema", SP.toFilePath schemaFile] ++ optionalMigrationArgs
let npxPrismaMigrateCmdWithArgs = buildNpxCmdWithArgs npxPrismaMigrateArgs

-- NOTE(martin): `prisma migrate dev` refuses to execute when interactivity is needed if stdout is being piped,
-- because it assumes it is used in non-interactive environment. In our case we are piping both stdin and stdout
-- so we do have interactivity, but Prisma doesn't know that.
-- I opened an issue with Prisma https://github.com/prisma/prisma/issues/7113, but in the meantime
-- we are using `script` to trick Prisma into thinking it is running in TTY (interactively).
let osSpecificCmdAndArgs = case System.Info.os of
"darwin" -> osxCmdAndArgs
"mingw32" -> winCmdAndArgs
_ -> posixCmdAndArgs
where
osxCmdAndArgs =
-- NOTE(martin): On MacOS, command that `script` should execute is treated as multiple arguments.
("script", ["-Fq", "/dev/null"] ++ uncurry (:) npxPrismaMigrateCmdWithArgs)
posixCmdAndArgs =
-- NOTE(martin): On Linux, command that `script` should execute is treated as one argument.
("script", ["-feqc", unwords $ uncurry (:) npxPrismaMigrateCmdWithArgs, "/dev/null"])
winCmdAndArgs =
-- TODO: For Windows we don't do anything at the moment.
-- Does this work when interactivity is needed, or does Prisma block us same as on mac and linux?
-- If it does, find an alternative to `script` since it does not exist for Windows.
npxPrismaMigrateCmdWithArgs

-- NOTE(martin): For this to work on Mac, filepath in the list below must be as it is now - not wrapped in any quotes.
let npxPrismaMigrateCmd = npxPrismaCmd ++ ["migrate", "dev", "--schema", SP.toFilePath schemaFile] ++ optionalMigrationArgs
let scriptArgs =
if System.Info.os == "darwin"
then -- NOTE(martin): On MacOS, command that `script` should execute is treated as multiple arguments.
["-Fq", "/dev/null"] ++ npxPrismaMigrateCmd
else -- NOTE(martin): On Linux, command that `script` should execute is treated as one argument.
["-feqc", unwords npxPrismaMigrateCmd, "/dev/null"]

let job = runNodeCommandAsJob serverDir "script" scriptArgs J.Db
-- NOTE(matija): We are running this command from server's root dir since that is where
-- Prisma packages (cli and client) are currently installed.
let job = runNodeDependentCommandAsJob J.Db serverDir osSpecificCmdAndArgs

retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything

Expand All @@ -60,8 +72,8 @@ runStudio projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir

let npxPrismaStudioCmd = npxPrismaCmd ++ ["studio", "--schema", SP.toFilePath schemaFile]
let job = runNodeCommandAsJob serverDir (head npxPrismaStudioCmd) (tail npxPrismaStudioCmd) J.Db
let npxPrismaStudioCmdWithArgs = buildNpxCmdWithArgs $ npxPrismaArgs ++ ["studio", "--schema", SP.toFilePath schemaFile]
let job = runNodeDependentCommandAsJob J.Db serverDir npxPrismaStudioCmdWithArgs

retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything

Expand All @@ -70,8 +82,8 @@ generatePrismaClient projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir

let npxPrismaGenerateCmd = npxPrismaCmd ++ ["generate", "--schema", SP.toFilePath schemaFile]
let job = runNodeCommandAsJob serverDir (head npxPrismaGenerateCmd) (tail npxPrismaGenerateCmd) J.Db
let npxPrismaGenerateCmdWithArgs = buildNpxCmdWithArgs $ npxPrismaArgs ++ ["generate", "--schema", SP.toFilePath schemaFile]
let job = runNodeDependentCommandAsJob J.Db serverDir npxPrismaGenerateCmdWithArgs

retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors

Expand All @@ -80,7 +92,7 @@ generatePrismaClient projectDir = do
npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job
npmInstall projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
runNodeCommandAsJob serverDir "npm" ["install"] J.Db
runNodeDependentCommandAsJob J.Db serverDir $ buildNpmCmdWithArgs ["install"]

data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors

Expand Down
54 changes: 27 additions & 27 deletions waspc/src/Wasp/Generator/Job/Process.hs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-}

module Wasp.Generator.Job.Process
( runProcessAsJob,
runNodeCommandAsJob,
runNodeDependentCommandAsJob,
parseNodeVersion,
)
where

import Control.Concurrent (writeChan)
import Control.Concurrent.Async (Concurrently (..))
import Data.ByteString (ByteString)
import Data.Conduit (runConduit, (.|))
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Process as CP
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import GHC.IO.Encoding (initLocaleEncoding)
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import System.Exit (ExitCode (..))
Expand All @@ -26,6 +28,7 @@ import UnliftIO.Exception (bracket)
import qualified Wasp.Generator.Common as C
import qualified Wasp.Generator.Job as J
import qualified Wasp.SemanticVersion as SV
import qualified Wasp.Util.Encoding as E

-- TODO:
-- Switch from Data.Conduit.Process to Data.Conduit.Process.Typed.
Expand All @@ -42,29 +45,8 @@ runProcessAsJob process jobType chan =
runStreamingProcessAsJob
where
runStreamingProcessAsJob (CP.Inherited, stdoutStream, stderrStream, processHandle) = do
let forwardStdoutToChan =
runConduit $
stdoutStream
.| CL.mapM_
( \bs ->
writeChan chan $
J.JobMessage
{ J._data = J.JobOutput (decodeUtf8 bs) J.Stdout,
J._jobType = jobType
}
)

let forwardStderrToChan =
runConduit $
stderrStream
.| CL.mapM_
( \bs ->
writeChan chan $
J.JobMessage
{ J._data = J.JobOutput (decodeUtf8 bs) J.Stderr,
J._jobType = jobType
}
)
let forwardStdoutToChan = forwardStandardOutputStreamToChan stdoutStream J.Stdout
let forwardStderrToChan = forwardStandardOutputStreamToChan stderrStream J.Stderr

exitCode <-
runConcurrently $
Expand All @@ -79,6 +61,22 @@ runProcessAsJob process jobType chan =
}

return exitCode
where
-- @stream@ can be stdout stream or stderr stream.
forwardStandardOutputStreamToChan stream jobOutputType = runConduit $ stream .| CL.mapM_ forwardByteStringChunkToChan
where
forwardByteStringChunkToChan bs = do
writeChan chan $
J.JobMessage
{ -- NOTE: We decode while using locale encoding, since that is the best option when
-- dealing with ephemeral standard in/outputs. Here is a blog explaining this in more details:
-- https://serokell.io/blog/haskell-with-utf8 .
J._data = J.JobOutput (decodeLocaleEncoding bs) jobOutputType,
J._jobType = jobType
}

decodeLocaleEncoding :: ByteString -> T.Text
decodeLocaleEncoding = T.pack . E.decodeWithTELenient initLocaleEncoding

-- NOTE(shayne): On *nix, we use interruptProcessGroupOf instead of terminateProcess because many
-- processes we run will spawn child processes, which themselves may spawn child processes.
Expand All @@ -94,8 +92,10 @@ runProcessAsJob process jobType chan =
else P.interruptProcessGroupOf processHandle
return $ ExitFailure 1

runNodeCommandAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job
runNodeCommandAsJob fromDir command args jobType chan = do
-- | First checks if correct version of node is installed on the machine, then runs the given command
-- as a Job (since it assumes this command requires node to be installed).
runNodeDependentCommandAsJob :: J.JobType -> Path' Abs (Dir a) -> (String, [String]) -> J.Job
runNodeDependentCommandAsJob jobType fromDir (command, args) chan = do
errorOrNodeVersion <- getNodeVersion
case errorOrNodeVersion of
Left errorMsg -> exitWithError (ExitFailure 1) (T.pack errorMsg)
Expand Down
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import StrongPath
Posix,
Rel,
parseRelFile,
relFileToPosix,
reldir,
reldirP,
relfile,
Expand Down Expand Up @@ -62,7 +63,7 @@ genJob (jobName, job) =
-- `Aeson.Text.encodeToLazyText` on an Aeson.Object, or `show` on an AS.JSON.
"jobSchedule" .= Aeson.Text.encodeToLazyText (fromMaybe Aeson.Null maybeJobSchedule),
"jobPerformOptions" .= show (fromMaybe AS.JSON.emptyObject maybeJobPerformOptions),
"executorJobRelFP" .= toFilePath (executorJobTemplateInJobsDir (J.executor job))
"executorJobRelFP" .= toFilePath (fromJust $ relFileToPosix $ executorJobTemplateInJobsDir $ J.executor job)
]
)
where
Expand Down
6 changes: 3 additions & 3 deletions waspc/src/Wasp/Generator/ServerGenerator/Setup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module Wasp.Generator.ServerGenerator.Setup
where

import StrongPath (Abs, Dir, Path', (</>))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs)
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.Job.Process (runNodeDependentCommandAsJob)
import qualified Wasp.Generator.ServerGenerator.Common as Common

installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job
installNpmDependencies projectDir = do
let serverDir = projectDir </> Common.serverRootDirInProjectRootDir
runNodeCommandAsJob serverDir "npm" ["install"] J.Server
runNodeDependentCommandAsJob J.Server serverDir $ buildNpmCmdWithArgs ["install"]
6 changes: 3 additions & 3 deletions waspc/src/Wasp/Generator/ServerGenerator/Start.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module Wasp.Generator.ServerGenerator.Start
where

import StrongPath (Abs, Dir, Path', (</>))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs)
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.Job.Process (runNodeDependentCommandAsJob)
import qualified Wasp.Generator.ServerGenerator.Common as Common

startServer :: Path' Abs (Dir ProjectRootDir) -> J.Job
startServer projectDir = do
let serverDir = projectDir </> Common.serverRootDirInProjectRootDir
runNodeCommandAsJob serverDir "npm" ["start"] J.Server
runNodeDependentCommandAsJob J.Server serverDir $ buildNpmCmdWithArgs ["start"]
6 changes: 3 additions & 3 deletions waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module Wasp.Generator.WebAppGenerator.Setup
where

import StrongPath (Abs, Dir, Path', (</>))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs)
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.Job.Process (runNodeDependentCommandAsJob)
import qualified Wasp.Generator.WebAppGenerator.Common as Common

installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job
installNpmDependencies projectDir = do
let webAppDir = projectDir </> Common.webAppRootDirInProjectRootDir
runNodeCommandAsJob webAppDir "npm" ["install"] J.WebApp
runNodeDependentCommandAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["install"]
6 changes: 3 additions & 3 deletions waspc/src/Wasp/Generator/WebAppGenerator/Start.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module Wasp.Generator.WebAppGenerator.Start
where

import StrongPath (Abs, Dir, Path', (</>))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs)
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
import Wasp.Generator.Job.Process (runNodeDependentCommandAsJob)
import qualified Wasp.Generator.WebAppGenerator.Common as Common

startWebApp :: Path' Abs (Dir ProjectRootDir) -> J.Job
startWebApp projectDir = do
let webAppDir = projectDir </> Common.webAppRootDirInProjectRootDir
runNodeCommandAsJob webAppDir "npm" ["start"] J.WebApp
runNodeDependentCommandAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["start"]
Loading