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 16 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
21 changes: 21 additions & 0 deletions waspc/src/Wasp/Generator/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module Wasp.Generator.Common
nodeVersionRange,
npmVersionRange,
prismaVersion,
npmCmd,
buildNpmCmdWithArgs,
)
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 +41,21 @@ 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"

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" -> ("cmd.exe", [unwords $ "/c" : npmCmd : args])
_ -> (npmCmd, args)
27 changes: 18 additions & 9 deletions waspc/src/Wasp/Generator/DbGenerator/Jobs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ 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, 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)

-- `--no-install` is the magic that causes this command to fail if npx cannot find it locally
Expand Down Expand Up @@ -44,13 +44,22 @@ migrateDev projectDir maybeMigrationName = do
-- 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.
-- NOTE: This won't work on Windows, unless they have `script` command installed via cygwin, in which case
-- it will work since it same as on Linux then (Posix).
-- But maybe on Windows it doesn't even need `script`? We haven't tested it yet.
case System.Info.os of
"darwin" -> osxScriptArgs
_ -> posixScriptArgs
where
osxScriptArgs =
-- 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.
posixScriptArgs =
-- NOTE(martin): On Linux, command that `script` should execute is treated as one argument.
-- This should also work on Windows, if `script` command is installed via Cygwin.
["-feqc", unwords npxPrismaMigrateCmd, "/dev/null"]

let job = runNodeCommandAsJob serverDir "script" scriptArgs J.Db
let job = runNodeDependentCommandAsJob J.Db serverDir ("script", scriptArgs)

retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything

Expand All @@ -61,7 +70,7 @@ runStudio projectDir = do
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir

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

retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything

Expand All @@ -71,7 +80,7 @@ generatePrismaClient projectDir = do
let schemaFile = projectDir </> dbSchemaFileInProjectRootDir

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

retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors

Expand All @@ -80,7 +89,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
49 changes: 22 additions & 27 deletions waspc/src/Wasp/Generator/Job/Process.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Wasp.Generator.Job.Process
( runProcessAsJob,
runNodeCommandAsJob,
runNodeDependentCommandAsJob,
parseNodeVersion,
)
where
Expand All @@ -13,7 +13,6 @@ 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 StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import System.Exit (ExitCode (..))
Expand All @@ -26,6 +25,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 +42,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 +58,20 @@ 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 =
writeChan chan $
J.JobMessage
{ -- Since this is output of a command that was supposed to be shown in the terminal,
-- it is our safest bet to assume it is using locale encoding (default encoding on the machine),
-- instead of assuming it is utf8 (like we do for text files).
-- Take a look at https://serokell.io/blog/haskell-with-utf8 for detailed reasoning.
J._data = J.JobOutput (E.decodeLocaleEncoding bs) jobOutputType,
J._jobType = jobType
}

-- 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 +87,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"]
50 changes: 50 additions & 0 deletions waspc/src/Wasp/Util/Encoding.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Wasp.Util.Encoding
( decodeLocaleEncoding,
decodeEncoding,
)
where

import Control.Monad.State (evalStateT)
import Data.ByteString (ByteString)
import qualified Data.Encoding as E
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, decodeUtf8With)
import GHC.IO.Encoding (TextEncoding, textEncodingName)
import System.Directory.Internal.Prelude (fromMaybe)
import System.IO (latin1, localeEncoding, utf8)

-- | Decodes given byte string while assuming it is using locale encoding (which is machine's default encoding).
decodeLocaleEncoding :: ByteString -> Text
decodeLocaleEncoding = decodeEncoding localeEncoding

-- | Decodes given byte string while assuming it is using provided text encoding.
decodeEncoding :: TextEncoding -> ByteString -> Text
decodeEncoding enc = fromMaybe unsupportedEncoderError decoder
where
decoder = getTextLibDecoder enc <> getEncodingLibDecoder enc
unsupportedEncoderError = error $ "Wasp doesn't know how to decode " ++ textEncodingName enc ++ " encoding."

-- | Check if there is a decoder for given encoding in the @text@ package, and if so, returns it.
getTextLibDecoder :: TextEncoding -> Maybe (ByteString -> Text)
getTextLibDecoder enc
| textEncodingName enc == textEncodingName latin1 = Just decodeLatin1
| textEncodingName enc == textEncodingName utf8 = Just decodeUtf8Lenient
| otherwise = Nothing
where
decodeUtf8Lenient :: ByteString -> Text
decodeUtf8Lenient =
let onErrorUseReplacementChar _ _ = Just '\xfffd' -- This will replace any invalid characters with \xfffd.
in decodeUtf8With onErrorUseReplacementChar

-- | Check if there is a decoder for given encoding in the @encoding@ package, and if so, returns it.
-- @encoding@ package brings support for many more encodings to Haskell.
getEncodingLibDecoder :: TextEncoding -> Maybe (ByteString -> Text)
getEncodingLibDecoder enc = makeDecoder <$> E.encodingFromStringExplicit (textEncodingName enc)
where
makeDecoder :: E.DynEncoding -> ByteString -> Text
makeDecoder dynEnc =
\bs -> case E.decode dynEnc `evalStateT` bs of
Right decoded -> T.pack decoded
Left (e :: E.DecodingException) ->
error $ "Decoding " ++ textEncodingName localeEncoding ++ " bytestring failed: " ++ show e
2 changes: 2 additions & 0 deletions waspc/waspc.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ library
, fsnotify ^>= 0.3.0
, http-conduit ^>= 2.3.8
, uuid ^>= 1.3.15
, encoding ^>= 0.8.6
-- 'array' is used by code generated by Alex for src/Analyzer/Parser/Lexer.x
, array ^>= 0.5.4
other-modules: Paths_waspc
Expand Down Expand Up @@ -235,6 +236,7 @@ library
Wasp.SemanticVersion
Wasp.Util
Wasp.Util.Control.Monad
Wasp.Util.Encoding
Wasp.Util.Fib
Wasp.Util.IO
Wasp.Util.Terminal
Expand Down