From 6148029334018c29159100c465cd3d20a64b7d2f Mon Sep 17 00:00:00 2001 From: metamodern Date: Wed, 29 Jun 2022 21:17:59 +0300 Subject: [PATCH 01/20] add support for windows NPM --- waspc/src/Wasp/Generator/Common.hs | 13 +++++++++++++ waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 4 ++-- waspc/src/Wasp/Generator/Job/Process.hs | 3 ++- waspc/src/Wasp/Generator/ServerGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/ServerGenerator/Start.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Start.hs | 4 ++-- 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 0bfa3a797a..0e86c667c8 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -3,10 +3,14 @@ module Wasp.Generator.Common nodeVersionRange, npmVersionRange, prismaVersion, + oSSpecificNpm, + compileOsSpecificNodeCommand ) where import qualified Wasp.SemanticVersion as SV +import System.Info (os) +import Data.List -- | Directory where the whole web app project (client, server, ...) is generated. data ProjectRootDir @@ -38,3 +42,12 @@ npmVersionRange = prismaVersion :: SV.Version prismaVersion = SV.Version 3 15 2 + +oSSpecificNpm :: [Char] +oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" + +compileOsSpecificNodeCommand :: [Char] -> [[Char]] -> ([Char], [[Char]]) +compileOsSpecificNodeCommand command arguments = + if os /= "mingw32" + then (command, arguments) + else ("cmd.exe", [intercalate " " (["/c", command] ++ arguments)]) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 1908c006b8..07d5869fa4 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -11,7 +11,7 @@ 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, prismaVersion, oSSpecificNpm) import Wasp.Generator.DbGenerator.Common (dbSchemaFileInProjectRootDir) import Wasp.Generator.Job (JobMessage, JobMessageData (JobExit, JobOutput)) import qualified Wasp.Generator.Job as J @@ -80,7 +80,7 @@ generatePrismaClient projectDir = do npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job npmInstall projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir - runNodeCommandAsJob serverDir "npm" ["install"] J.Db + runNodeCommandAsJob serverDir oSSpecificNpm ["install"] J.Db data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 2663fff4a5..f4e73da7e2 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -102,7 +102,8 @@ runNodeCommandAsJob fromDir command args jobType chan = do Right nodeVersion -> if SV.isVersionInRange nodeVersion C.nodeVersionRange then do - let process = (P.proc command args) {P.cwd = Just $ SP.fromAbsDir fromDir} + let (specificCommand, specificArgs) = C.compileOsSpecificNodeCommand command args + let process = (P.proc specificCommand specificArgs) {P.cwd = Just $ SP.fromAbsDir fromDir} runProcessAsJob process jobType chan else exitWithError diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index 03194b729f..f4c61d5871 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -4,7 +4,7 @@ module Wasp.Generator.ServerGenerator.Setup where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common @@ -12,4 +12,4 @@ 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 + runNodeCommandAsJob serverDir oSSpecificNpm ["install"] J.Server diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index 5628213983..b45d6b1d15 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -4,7 +4,7 @@ module Wasp.Generator.ServerGenerator.Start where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common @@ -12,4 +12,4 @@ 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 + runNodeCommandAsJob serverDir oSSpecificNpm ["start"] J.Server diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index 4e82d4d399..ea1070c789 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -4,7 +4,7 @@ module Wasp.Generator.WebAppGenerator.Setup where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common @@ -12,4 +12,4 @@ 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 + runNodeCommandAsJob webAppDir oSSpecificNpm ["install"] J.WebApp diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 188560ee81..bf32ef5aba 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -4,7 +4,7 @@ module Wasp.Generator.WebAppGenerator.Start where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm, compileOsSpecificNodeCommand) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common @@ -12,4 +12,4 @@ 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 + runNodeCommandAsJob webAppDir oSSpecificNpm ["start"] J.WebApp From 8fb15a25b8962478cebca6f8a5045e32c400bd4b Mon Sep 17 00:00:00 2001 From: metamodern Date: Wed, 29 Jun 2022 21:42:08 +0300 Subject: [PATCH 02/20] lint: remove redundant import --- waspc/src/Wasp/Generator/WebAppGenerator/Start.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index bf32ef5aba..5ed297639b 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -4,7 +4,7 @@ module Wasp.Generator.WebAppGenerator.Start where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm, compileOsSpecificNodeCommand) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common From 18d5ccad1323f909973b3f2f3ea95dac328a3df0 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 30 Jun 2022 13:08:46 +0200 Subject: [PATCH 03/20] Fixed path in generated JS file that was not always Posix. --- waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs index 8bd1c49ecf..41b2b0ae7d 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/JobGenerator.hs @@ -19,6 +19,7 @@ import StrongPath Posix, Rel, parseRelFile, + relFileToPosix, reldir, reldirP, relfile, @@ -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 From 496d1b355bba731b1fd3cb5f2eae440ea4bb94ac Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 30 Jun 2022 19:29:15 +0200 Subject: [PATCH 04/20] Now we decode output of subprocesses with locale encoding instead of utf8. --- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 17 ++++-- waspc/src/Wasp/Generator/Job/Process.hs | 59 ++++++++++++-------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 07d5869fa4..2abc504ff8 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -11,7 +11,7 @@ import StrongPath (Abs, Dir, Path', ()) import qualified StrongPath as SP import System.Exit (ExitCode (..)) import qualified System.Info -import Wasp.Generator.Common (ProjectRootDir, prismaVersion, oSSpecificNpm) +import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm, prismaVersion) import Wasp.Generator.DbGenerator.Common (dbSchemaFileInProjectRootDir) import Wasp.Generator.Job (JobMessage, JobMessageData (JobExit, JobOutput)) import qualified Wasp.Generator.Job as J @@ -44,10 +44,19 @@ 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 diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index f4e73da7e2..892ea3e29d 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -9,14 +9,18 @@ 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 Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeUtf8) +import Data.Text.Encoding (decodeLatin1, decodeUtf8With) +import GHC.IO.Encoding (TextEncoding, textEncodingName) import StrongPath (Abs, Dir, Path') import qualified StrongPath as SP import System.Exit (ExitCode (..)) +import System.IO (latin1, localeEncoding, utf8) import System.IO.Error (catchIOError, isDoesNotExistError) import qualified System.Info import qualified System.Process as P @@ -42,29 +46,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 $ @@ -79,6 +62,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 (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. @@ -94,6 +91,20 @@ runProcessAsJob process jobType chan = else P.interruptProcessGroupOf processHandle return $ ExitFailure 1 +-- | Decodes given byte string while assuming it is using locale encoding (which is system'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 + | textEncodingName enc == textEncodingName latin1 = decodeLatin1 + -- This will replace any invalid characters with \xfffd. + | textEncodingName enc == textEncodingName utf8 = decodeUtf8With onErrorUseReplacementChar + | otherwise = error $ "Encoding " ++ textEncodingName localeEncoding ++ " is not supported." + where + onErrorUseReplacementChar _ _ = Just '\xfffd' + runNodeCommandAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job runNodeCommandAsJob fromDir command args jobType chan = do errorOrNodeVersion <- getNodeVersion From d9b3c4a7c24d57807b013e536426bf9190721c47 Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 16:35:32 +0300 Subject: [PATCH 05/20] fix: types to be Strings --- waspc/src/Wasp/Generator/Common.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 0e86c667c8..cc6077cc93 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -43,10 +43,10 @@ npmVersionRange = prismaVersion :: SV.Version prismaVersion = SV.Version 3 15 2 -oSSpecificNpm :: [Char] +oSSpecificNpm :: String oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" -compileOsSpecificNodeCommand :: [Char] -> [[Char]] -> ([Char], [[Char]]) +compileOsSpecificNodeCommand :: String -> [String] -> (String, [String]) compileOsSpecificNodeCommand command arguments = if os /= "mingw32" then (command, arguments) From 53f14df6adfce13a2895529adb5bac1e78bc9209 Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 16:36:40 +0300 Subject: [PATCH 06/20] lint: clear the namespace --- waspc/src/Wasp/Generator/Common.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index cc6077cc93..87ca62faeb 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -10,7 +10,7 @@ where import qualified Wasp.SemanticVersion as SV import System.Info (os) -import Data.List +import Data.List (intercalate) -- | Directory where the whole web app project (client, server, ...) is generated. data ProjectRootDir From bea2d076889c6e1de7540353533f12bf976ee104 Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 17:18:04 +0300 Subject: [PATCH 07/20] fix: bad naming --- waspc/src/Wasp/Generator/Common.hs | 6 +++--- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 10 +++++----- waspc/src/Wasp/Generator/Job/Process.hs | 8 ++++---- waspc/src/Wasp/Generator/ServerGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/ServerGenerator/Start.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Start.hs | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 87ca62faeb..95a905a985 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -4,7 +4,7 @@ module Wasp.Generator.Common npmVersionRange, prismaVersion, oSSpecificNpm, - compileOsSpecificNodeCommand + buildNpmCmdWithArgs ) where @@ -46,8 +46,8 @@ prismaVersion = SV.Version 3 15 2 oSSpecificNpm :: String oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" -compileOsSpecificNodeCommand :: String -> [String] -> (String, [String]) -compileOsSpecificNodeCommand command arguments = +buildNpmCmdWithArgs :: String -> [String] -> (String, [String]) +buildNpmCmdWithArgs command arguments = if os /= "mingw32" then (command, arguments) else ("cmd.exe", [intercalate " " (["/c", command] ++ arguments)]) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 2abc504ff8..b048464341 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -15,7 +15,7 @@ import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm, 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 (runCommandThatRequiresNodeAsJob) import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir) -- `--no-install` is the magic that causes this command to fail if npx cannot find it locally @@ -59,7 +59,7 @@ migrateDev projectDir maybeMigrationName = do -- 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 = runCommandThatRequiresNodeAsJob serverDir "script" scriptArgs J.Db retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -70,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 = runCommandThatRequiresNodeAsJob serverDir (head npxPrismaStudioCmd) (tail npxPrismaStudioCmd) J.Db retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -80,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 = runCommandThatRequiresNodeAsJob serverDir (head npxPrismaGenerateCmd) (tail npxPrismaGenerateCmd) J.Db retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors @@ -89,7 +89,7 @@ generatePrismaClient projectDir = do npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job npmInstall projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir - runNodeCommandAsJob serverDir oSSpecificNpm ["install"] J.Db + runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["install"] J.Db data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 892ea3e29d..799e99cd71 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -2,7 +2,7 @@ module Wasp.Generator.Job.Process ( runProcessAsJob, - runNodeCommandAsJob, + runCommandThatRequiresNodeAsJob, parseNodeVersion, ) where @@ -105,15 +105,15 @@ decodeEncoding enc where onErrorUseReplacementChar _ _ = Just '\xfffd' -runNodeCommandAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job -runNodeCommandAsJob fromDir command args jobType chan = do +runCommandThatRequiresNodeAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job +runCommandThatRequiresNodeAsJob fromDir command args jobType chan = do errorOrNodeVersion <- getNodeVersion case errorOrNodeVersion of Left errorMsg -> exitWithError (ExitFailure 1) (T.pack errorMsg) Right nodeVersion -> if SV.isVersionInRange nodeVersion C.nodeVersionRange then do - let (specificCommand, specificArgs) = C.compileOsSpecificNodeCommand command args + let (specificCommand, specificArgs) = C.buildNpmCmdWithArgs command args let process = (P.proc specificCommand specificArgs) {P.cwd = Just $ SP.fromAbsDir fromDir} runProcessAsJob process jobType chan else diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index f4c61d5871..b41dbceba8 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) +import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) 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 oSSpecificNpm ["install"] J.Server + runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["install"] J.Server diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index b45d6b1d15..b312d9de75 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) +import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) 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 oSSpecificNpm ["start"] J.Server + runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["start"] J.Server diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index ea1070c789..2f9c15f0a7 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) +import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) 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 oSSpecificNpm ["install"] J.WebApp + runCommandThatRequiresNodeAsJob webAppDir oSSpecificNpm ["install"] J.WebApp diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 5ed297639b..3694529024 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) +import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) 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 oSSpecificNpm ["start"] J.WebApp + runCommandThatRequiresNodeAsJob webAppDir oSSpecificNpm ["start"] J.WebApp From 14001b5abfc0eced22f9196c7bbdd61f3d3aa3d4 Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 18:00:18 +0300 Subject: [PATCH 08/20] add function comments --- waspc/src/Wasp/Generator/Common.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 95a905a985..efb4509dba 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -43,9 +43,13 @@ npmVersionRange = prismaVersion :: SV.Version prismaVersion = SV.Version 3 15 2 +-- | Adds .cmd extension to callable npm command if ran on Windows, since npm does note have .exe on Windows. +-- The reason explained here: https://stackoverflow.com/questions/43139364/createprocess-weird-behavior-with-files-without-extension oSSpecificNpm :: String oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" +-- | Changes an npm command to a cmd.exe command on Windows only. Calling npm from API causes troubles. +-- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 buildNpmCmdWithArgs :: String -> [String] -> (String, [String]) buildNpmCmdWithArgs command arguments = if os /= "mingw32" From 2ddeb9f7acca69ada7beae0ee86ec04678906bde Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 18:21:43 +0300 Subject: [PATCH 09/20] format: with ormulu --- waspc/src/Wasp/Generator/Common.hs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index efb4509dba..f9418ee111 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -4,13 +4,13 @@ module Wasp.Generator.Common npmVersionRange, prismaVersion, oSSpecificNpm, - buildNpmCmdWithArgs + buildNpmCmdWithArgs, ) where -import qualified Wasp.SemanticVersion as SV -import System.Info (os) import Data.List (intercalate) +import System.Info (os) +import qualified Wasp.SemanticVersion as SV -- | Directory where the whole web app project (client, server, ...) is generated. data ProjectRootDir @@ -49,9 +49,9 @@ oSSpecificNpm :: String oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" -- | Changes an npm command to a cmd.exe command on Windows only. Calling npm from API causes troubles. --- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 +-- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 buildNpmCmdWithArgs :: String -> [String] -> (String, [String]) -buildNpmCmdWithArgs command arguments = +buildNpmCmdWithArgs command arguments = if os /= "mingw32" - then (command, arguments) - else ("cmd.exe", [intercalate " " (["/c", command] ++ arguments)]) + then (command, arguments) + else ("cmd.exe", [intercalate " " (["/c", command] ++ arguments)]) From e9a4f245576c6632fe04722885b91f9345d53217 Mon Sep 17 00:00:00 2001 From: Bunyamin Shabanov <42899786+MetaMmodern@users.noreply.github.com> Date: Sat, 2 Jul 2022 19:38:41 +0300 Subject: [PATCH 10/20] Update waspc/src/Wasp/Generator/Common.hs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applied suggested change Co-authored-by: Martin Šošić --- waspc/src/Wasp/Generator/Common.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index f9418ee111..210ae7b3d0 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -43,10 +43,13 @@ npmVersionRange = prismaVersion :: SV.Version prismaVersion = SV.Version 3 15 2 --- | Adds .cmd extension to callable npm command if ran on Windows, since npm does note have .exe on Windows. --- The reason explained here: https://stackoverflow.com/questions/43139364/createprocess-weird-behavior-with-files-without-extension -oSSpecificNpm :: String -oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" +npmCmd :: String +npmCmd = + -- Windows add ".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 . + "npm" ++ if os /= "mingw32" then "" else ".cmd" -- | Changes an npm command to a cmd.exe command on Windows only. Calling npm from API causes troubles. -- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 From bdafb2830929c13c06ec19fc354c7c31a55f85ef Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 21:06:57 +0300 Subject: [PATCH 11/20] fix: change call signature to npm only --- waspc/src/Wasp/Generator/Common.hs | 8 ++++---- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 5 +++-- waspc/src/Wasp/Generator/Job/Process.hs | 3 +-- waspc/src/Wasp/Generator/ServerGenerator/Setup.hs | 5 +++-- waspc/src/Wasp/Generator/ServerGenerator/Start.hs | 5 +++-- waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs | 5 +++-- waspc/src/Wasp/Generator/WebAppGenerator/Start.hs | 5 +++-- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index f9418ee111..ffa382979c 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -50,8 +50,8 @@ oSSpecificNpm = "npm" ++ if os /= "mingw32" then "" else ".cmd" -- | Changes an npm command to a cmd.exe command on Windows only. Calling npm from API causes troubles. -- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 -buildNpmCmdWithArgs :: String -> [String] -> (String, [String]) -buildNpmCmdWithArgs command arguments = +buildNpmCmdWithArgs :: [String] -> (String, [String]) +buildNpmCmdWithArgs arguments = if os /= "mingw32" - then (command, arguments) - else ("cmd.exe", [intercalate " " (["/c", command] ++ arguments)]) + then (oSSpecificNpm, arguments) + else ("cmd.exe", [intercalate " " (["/c", oSSpecificNpm] ++ arguments)]) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index b048464341..18977000dc 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -11,7 +11,7 @@ import StrongPath (Abs, Dir, Path', ()) import qualified StrongPath as SP import System.Exit (ExitCode (..)) import qualified System.Info -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm, 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 @@ -89,7 +89,8 @@ generatePrismaClient projectDir = do npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job npmInstall projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["install"] J.Db + let (npmCmd, args) = buildNpmCmdWithArgs ["install"] + runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Db data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 799e99cd71..32d9554a17 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -113,8 +113,7 @@ runCommandThatRequiresNodeAsJob fromDir command args jobType chan = do Right nodeVersion -> if SV.isVersionInRange nodeVersion C.nodeVersionRange then do - let (specificCommand, specificArgs) = C.buildNpmCmdWithArgs command args - let process = (P.proc specificCommand specificArgs) {P.cwd = Just $ SP.fromAbsDir fromDir} + let process = (P.proc command args) {P.cwd = Just $ SP.fromAbsDir fromDir} runProcessAsJob process jobType chan else exitWithError diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index b41dbceba8..b7f74f07ce 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -4,7 +4,7 @@ module Wasp.Generator.ServerGenerator.Setup where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) +import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common @@ -12,4 +12,5 @@ import qualified Wasp.Generator.ServerGenerator.Common as Common installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do let serverDir = projectDir Common.serverRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["install"] J.Server + let (npmCmd, args) = buildNpmCmdWithArgs ["install"] + runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Server diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index b312d9de75..51a795a19c 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -4,7 +4,7 @@ module Wasp.Generator.ServerGenerator.Start where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) +import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common @@ -12,4 +12,5 @@ import qualified Wasp.Generator.ServerGenerator.Common as Common startServer :: Path' Abs (Dir ProjectRootDir) -> J.Job startServer projectDir = do let serverDir = projectDir Common.serverRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob serverDir oSSpecificNpm ["start"] J.Server + let (npmCmd, args) = buildNpmCmdWithArgs ["start"] + runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Server diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index 2f9c15f0a7..a2706bfc94 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -4,7 +4,7 @@ module Wasp.Generator.WebAppGenerator.Setup where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) +import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common @@ -12,4 +12,5 @@ import qualified Wasp.Generator.WebAppGenerator.Common as Common installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do let webAppDir = projectDir Common.webAppRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob webAppDir oSSpecificNpm ["install"] J.WebApp + let (npmCmd, args) = buildNpmCmdWithArgs ["install"] + runCommandThatRequiresNodeAsJob webAppDir npmCmd args J.WebApp diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 3694529024..1b89096126 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -4,7 +4,7 @@ module Wasp.Generator.WebAppGenerator.Start where import StrongPath (Abs, Dir, Path', ()) -import Wasp.Generator.Common (ProjectRootDir, oSSpecificNpm) +import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common @@ -12,4 +12,5 @@ import qualified Wasp.Generator.WebAppGenerator.Common as Common startWebApp :: Path' Abs (Dir ProjectRootDir) -> J.Job startWebApp projectDir = do let webAppDir = projectDir Common.webAppRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob webAppDir oSSpecificNpm ["start"] J.WebApp + let (npmCmd, args) = buildNpmCmdWithArgs ["start"] + runCommandThatRequiresNodeAsJob webAppDir npmCmd args J.WebApp From 8f7d68dfdd8a41ad7dbc39b20d8c188d98ecb97c Mon Sep 17 00:00:00 2001 From: metamodern Date: Sat, 2 Jul 2022 21:23:53 +0300 Subject: [PATCH 12/20] fix: comments placement --- waspc/src/Wasp/Generator/Common.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 35b1adb112..4e4a7b5cf0 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -45,16 +45,17 @@ prismaVersion = SV.Version 3 15 2 npmCmd :: String npmCmd = - -- Windows add ".exe" to command, when calling it programmatically, if it doesn't + -- 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 . "npm" ++ if os /= "mingw32" then "" else ".cmd" --- | Changes an npm command to a cmd.exe command on Windows only. Calling npm from API causes troubles. --- The reason and solution exaplined here: https://stackoverflow.com/a/44820337 buildNpmCmdWithArgs :: [String] -> (String, [String]) buildNpmCmdWithArgs arguments = + -- Changes an npm command to a cmd.exe command on Windows only. + -- Calling npm from API causes troubles, because Haskell changes work directory, which is used inside npm.cmd script. + -- Extra info: https://stackoverflow.com/a/44820337 if os /= "mingw32" then (oSSpecificNpm, arguments) else ("cmd.exe", [intercalate " " (["/c", oSSpecificNpm] ++ arguments)]) From 9b531fb1f56ea716bcf154069ff8ea96e13c8df3 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 4 Jul 2022 12:02:35 +0200 Subject: [PATCH 13/20] Fixed compiler errors, refactored node job function a bit. --- waspc/src/Wasp/Generator/Common.hs | 22 +++++++++---------- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 9 ++++---- waspc/src/Wasp/Generator/Job/Process.hs | 6 +++-- .../Wasp/Generator/ServerGenerator/Setup.hs | 3 +-- .../Wasp/Generator/ServerGenerator/Start.hs | 3 +-- .../Wasp/Generator/WebAppGenerator/Setup.hs | 3 +-- .../Wasp/Generator/WebAppGenerator/Start.hs | 3 +-- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 4e4a7b5cf0..3efc92ad0e 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -3,12 +3,11 @@ module Wasp.Generator.Common nodeVersionRange, npmVersionRange, prismaVersion, - oSSpecificNpm, + npmCmd, buildNpmCmdWithArgs, ) where -import Data.List (intercalate) import System.Info (os) import qualified Wasp.SemanticVersion as SV @@ -44,18 +43,19 @@ prismaVersion :: SV.Version prismaVersion = SV.Version 3 15 2 npmCmd :: String -npmCmd = +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 . - "npm" ++ if os /= "mingw32" then "" else ".cmd" + "mingw32" -> "npm.cmd" + _ -> "npm" buildNpmCmdWithArgs :: [String] -> (String, [String]) -buildNpmCmdWithArgs arguments = - -- Changes an npm command to a cmd.exe command on Windows only. - -- Calling npm from API causes troubles, because Haskell changes work directory, which is used inside npm.cmd script. - -- Extra info: https://stackoverflow.com/a/44820337 - if os /= "mingw32" - then (oSSpecificNpm, arguments) - else ("cmd.exe", [intercalate " " (["/c", oSSpecificNpm] ++ arguments)]) +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) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 18977000dc..b2cfc1da88 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -59,7 +59,7 @@ migrateDev projectDir maybeMigrationName = do -- This should also work on Windows, if `script` command is installed via Cygwin. ["-feqc", unwords npxPrismaMigrateCmd, "/dev/null"] - let job = runCommandThatRequiresNodeAsJob serverDir "script" scriptArgs J.Db + let job = runCommandThatRequiresNodeAsJob J.Db serverDir ("script", scriptArgs) retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -70,7 +70,7 @@ runStudio projectDir = do let schemaFile = projectDir dbSchemaFileInProjectRootDir let npxPrismaStudioCmd = npxPrismaCmd ++ ["studio", "--schema", SP.toFilePath schemaFile] - let job = runCommandThatRequiresNodeAsJob serverDir (head npxPrismaStudioCmd) (tail npxPrismaStudioCmd) J.Db + let job = runCommandThatRequiresNodeAsJob J.Db serverDir (head npxPrismaStudioCmd, tail npxPrismaStudioCmd) retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -80,7 +80,7 @@ generatePrismaClient projectDir = do let schemaFile = projectDir dbSchemaFileInProjectRootDir let npxPrismaGenerateCmd = npxPrismaCmd ++ ["generate", "--schema", SP.toFilePath schemaFile] - let job = runCommandThatRequiresNodeAsJob serverDir (head npxPrismaGenerateCmd) (tail npxPrismaGenerateCmd) J.Db + let job = runCommandThatRequiresNodeAsJob J.Db serverDir (head npxPrismaGenerateCmd, tail npxPrismaGenerateCmd) retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors @@ -89,8 +89,7 @@ generatePrismaClient projectDir = do npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job npmInstall projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir - let (npmCmd, args) = buildNpmCmdWithArgs ["install"] - runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Db + runCommandThatRequiresNodeAsJob J.Db serverDir $ buildNpmCmdWithArgs ["install"] data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 32d9554a17..c42c1178f4 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -105,8 +105,10 @@ decodeEncoding enc where onErrorUseReplacementChar _ _ = Just '\xfffd' -runCommandThatRequiresNodeAsJob :: Path' Abs (Dir a) -> String -> [String] -> J.JobType -> J.Job -runCommandThatRequiresNodeAsJob 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). +runCommandThatRequiresNodeAsJob :: J.JobType -> Path' Abs (Dir a) -> (String, [String]) -> J.Job +runCommandThatRequiresNodeAsJob jobType fromDir (command, args) chan = do errorOrNodeVersion <- getNodeVersion case errorOrNodeVersion of Left errorMsg -> exitWithError (ExitFailure 1) (T.pack errorMsg) diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index b7f74f07ce..45b65edb85 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -12,5 +12,4 @@ import qualified Wasp.Generator.ServerGenerator.Common as Common installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do let serverDir = projectDir Common.serverRootDirInProjectRootDir - let (npmCmd, args) = buildNpmCmdWithArgs ["install"] - runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Server + runCommandThatRequiresNodeAsJob J.Server serverDir $ buildNpmCmdWithArgs ["install"] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index 51a795a19c..7374421c58 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -12,5 +12,4 @@ import qualified Wasp.Generator.ServerGenerator.Common as Common startServer :: Path' Abs (Dir ProjectRootDir) -> J.Job startServer projectDir = do let serverDir = projectDir Common.serverRootDirInProjectRootDir - let (npmCmd, args) = buildNpmCmdWithArgs ["start"] - runCommandThatRequiresNodeAsJob serverDir npmCmd args J.Server + runCommandThatRequiresNodeAsJob J.Server serverDir $ buildNpmCmdWithArgs ["start"] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index a2706bfc94..83d96fbae6 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -12,5 +12,4 @@ import qualified Wasp.Generator.WebAppGenerator.Common as Common installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do let webAppDir = projectDir Common.webAppRootDirInProjectRootDir - let (npmCmd, args) = buildNpmCmdWithArgs ["install"] - runCommandThatRequiresNodeAsJob webAppDir npmCmd args J.WebApp + runCommandThatRequiresNodeAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["install"] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 1b89096126..1fcae09e23 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -12,5 +12,4 @@ import qualified Wasp.Generator.WebAppGenerator.Common as Common startWebApp :: Path' Abs (Dir ProjectRootDir) -> J.Job startWebApp projectDir = do let webAppDir = projectDir Common.webAppRootDirInProjectRootDir - let (npmCmd, args) = buildNpmCmdWithArgs ["start"] - runCommandThatRequiresNodeAsJob webAppDir npmCmd args J.WebApp + runCommandThatRequiresNodeAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["start"] From 09a6a513bc641504a817be2b574eb83735564d42 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 4 Jul 2022 12:06:09 +0200 Subject: [PATCH 14/20] Renamed node-cmd-as-job function. --- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 10 +++++----- waspc/src/Wasp/Generator/Job/Process.hs | 6 +++--- waspc/src/Wasp/Generator/ServerGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/ServerGenerator/Start.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs | 4 ++-- waspc/src/Wasp/Generator/WebAppGenerator/Start.hs | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index b2cfc1da88..56fb4882f2 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -15,7 +15,7 @@ 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 (runCommandThatRequiresNodeAsJob) +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 @@ -59,7 +59,7 @@ migrateDev projectDir maybeMigrationName = do -- This should also work on Windows, if `script` command is installed via Cygwin. ["-feqc", unwords npxPrismaMigrateCmd, "/dev/null"] - let job = runCommandThatRequiresNodeAsJob J.Db serverDir ("script", scriptArgs) + let job = runNodeDependentCommandAsJob J.Db serverDir ("script", scriptArgs) retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -70,7 +70,7 @@ runStudio projectDir = do let schemaFile = projectDir dbSchemaFileInProjectRootDir let npxPrismaStudioCmd = npxPrismaCmd ++ ["studio", "--schema", SP.toFilePath schemaFile] - let job = runCommandThatRequiresNodeAsJob J.Db serverDir (head npxPrismaStudioCmd, tail npxPrismaStudioCmd) + let job = runNodeDependentCommandAsJob J.Db serverDir (head npxPrismaStudioCmd, tail npxPrismaStudioCmd) retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -80,7 +80,7 @@ generatePrismaClient projectDir = do let schemaFile = projectDir dbSchemaFileInProjectRootDir let npxPrismaGenerateCmd = npxPrismaCmd ++ ["generate", "--schema", SP.toFilePath schemaFile] - let job = runCommandThatRequiresNodeAsJob J.Db serverDir (head npxPrismaGenerateCmd, tail npxPrismaGenerateCmd) + let job = runNodeDependentCommandAsJob J.Db serverDir (head npxPrismaGenerateCmd, tail npxPrismaGenerateCmd) retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors @@ -89,7 +89,7 @@ generatePrismaClient projectDir = do npmInstall :: Path' Abs (Dir ProjectRootDir) -> J.Job npmInstall projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir - runCommandThatRequiresNodeAsJob J.Db serverDir $ buildNpmCmdWithArgs ["install"] + runNodeDependentCommandAsJob J.Db serverDir $ buildNpmCmdWithArgs ["install"] data JobMessageForwardingStrategy = ForwardEverything | ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index c42c1178f4..5047f9ae8f 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -2,7 +2,7 @@ module Wasp.Generator.Job.Process ( runProcessAsJob, - runCommandThatRequiresNodeAsJob, + runNodeDependentCommandAsJob, parseNodeVersion, ) where @@ -107,8 +107,8 @@ decodeEncoding enc -- | 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). -runCommandThatRequiresNodeAsJob :: J.JobType -> Path' Abs (Dir a) -> (String, [String]) -> J.Job -runCommandThatRequiresNodeAsJob jobType fromDir (command, args) chan = do +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) diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index 45b65edb85..df90704c98 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) +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 - runCommandThatRequiresNodeAsJob J.Server serverDir $ buildNpmCmdWithArgs ["install"] + runNodeDependentCommandAsJob J.Server serverDir $ buildNpmCmdWithArgs ["install"] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index 7374421c58..f5787b87e6 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) +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 - runCommandThatRequiresNodeAsJob J.Server serverDir $ buildNpmCmdWithArgs ["start"] + runNodeDependentCommandAsJob J.Server serverDir $ buildNpmCmdWithArgs ["start"] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index 83d96fbae6..46a7a099bc 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) +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 - runCommandThatRequiresNodeAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["install"] + runNodeDependentCommandAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["install"] diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 1fcae09e23..d44d0eaebc 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -6,10 +6,10 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir, buildNpmCmdWithArgs) import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runCommandThatRequiresNodeAsJob) +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 - runCommandThatRequiresNodeAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["start"] + runNodeDependentCommandAsJob J.WebApp webAppDir $ buildNpmCmdWithArgs ["start"] From 9115d5e4cfabe36cd86d9101e38ec4f78df0242d Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 4 Jul 2022 15:08:57 +0200 Subject: [PATCH 15/20] Added additional encodings support to Wasp. --- waspc/src/Wasp/Generator/Job/Process.hs | 22 +---------- waspc/src/Wasp/Util/Encoding.hs | 50 +++++++++++++++++++++++++ waspc/waspc.cabal | 2 + 3 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 waspc/src/Wasp/Util/Encoding.hs diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 5047f9ae8f..881bdff120 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -9,18 +9,13 @@ 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 Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeLatin1, decodeUtf8With) -import GHC.IO.Encoding (TextEncoding, textEncodingName) import StrongPath (Abs, Dir, Path') import qualified StrongPath as SP import System.Exit (ExitCode (..)) -import System.IO (latin1, localeEncoding, utf8) import System.IO.Error (catchIOError, isDoesNotExistError) import qualified System.Info import qualified System.Process as P @@ -30,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. @@ -73,7 +69,7 @@ runProcessAsJob process jobType chan = -- 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 (decodeLocaleEncoding bs) jobOutputType, + J._data = J.JobOutput (E.decodeLocaleEncoding bs) jobOutputType, J._jobType = jobType } @@ -91,20 +87,6 @@ runProcessAsJob process jobType chan = else P.interruptProcessGroupOf processHandle return $ ExitFailure 1 --- | Decodes given byte string while assuming it is using locale encoding (which is system'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 - | textEncodingName enc == textEncodingName latin1 = decodeLatin1 - -- This will replace any invalid characters with \xfffd. - | textEncodingName enc == textEncodingName utf8 = decodeUtf8With onErrorUseReplacementChar - | otherwise = error $ "Encoding " ++ textEncodingName localeEncoding ++ " is not supported." - where - onErrorUseReplacementChar _ _ = Just '\xfffd' - -- | 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 diff --git a/waspc/src/Wasp/Util/Encoding.hs b/waspc/src/Wasp/Util/Encoding.hs new file mode 100644 index 0000000000..b1e20343fc --- /dev/null +++ b/waspc/src/Wasp/Util/Encoding.hs @@ -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 diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index dd0bd5bd9f..eefa19dab1 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -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 @@ -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 From 061af908ae7db04aafc2cdbaa0fa0286ab10fcab Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Mon, 4 Jul 2022 17:08:01 +0200 Subject: [PATCH 16/20] Modified encodings flag. --- waspc/cabal.project | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/waspc/cabal.project b/waspc/cabal.project index 0b6a54506a..a167e7f089 100644 --- a/waspc/cabal.project +++ b/waspc/cabal.project @@ -21,3 +21,7 @@ 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 + +-- We turned off the @systemencoding@ flag for @encoding@ package because it doesn't compile on Win otherwise, +-- and we don't need that functionality anyway. +constraints: encoding -systemencoding From b18bfb58d1ddf5802eb2f4593209fbaabca9902b Mon Sep 17 00:00:00 2001 From: Martinsos Date: Mon, 4 Jul 2022 21:01:13 +0200 Subject: [PATCH 17/20] Fixed more for Windows --- waspc/src/Wasp/Generator/Common.hs | 19 ++++++- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 53 +++++++++++--------- waspc/src/Wasp/Generator/Job/Process.hs | 7 +-- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs index 3efc92ad0e..b44fbc553f 100644 --- a/waspc/src/Wasp/Generator/Common.hs +++ b/waspc/src/Wasp/Generator/Common.hs @@ -5,6 +5,8 @@ module Wasp.Generator.Common prismaVersion, npmCmd, buildNpmCmdWithArgs, + npxCmd, + buildNpxCmdWithArgs, ) where @@ -51,11 +53,26 @@ npmCmd = case os of "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" -> ("cmd.exe", [unwords $ "/c" : npmCmd : args]) + "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]) diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 56fb4882f2..711c9d9170 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -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, buildNpmCmdWithArgs, 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 (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 @@ -33,33 +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). - - -- 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 = - -- 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 + let osSpecificCmdAndArgs = case System.Info.os of + "darwin" -> osxCmdAndArgs + "mingw32" -> winCmdAndArgs + _ -> posixCmdAndArgs where - osxScriptArgs = + osxCmdAndArgs = -- NOTE(martin): On MacOS, command that `script` should execute is treated as multiple arguments. - ["-Fq", "/dev/null"] ++ npxPrismaMigrateCmd - posixScriptArgs = + ("script", ["-Fq", "/dev/null"] ++ uncurry (:) npxPrismaMigrateCmdWithArgs) + posixCmdAndArgs = -- 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"] + ("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 - let job = runNodeDependentCommandAsJob J.Db serverDir ("script", scriptArgs) + -- 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 @@ -69,8 +72,8 @@ runStudio projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir let schemaFile = projectDir dbSchemaFileInProjectRootDir - let npxPrismaStudioCmd = npxPrismaCmd ++ ["studio", "--schema", SP.toFilePath schemaFile] - let job = runNodeDependentCommandAsJob J.Db serverDir (head npxPrismaStudioCmd, tail npxPrismaStudioCmd) + let npxPrismaStudioCmdWithArgs = buildNpxCmdWithArgs $ npxPrismaArgs ++ ["studio", "--schema", SP.toFilePath schemaFile] + let job = runNodeDependentCommandAsJob J.Db serverDir npxPrismaStudioCmdWithArgs retryJobOnErrorWith job (npmInstall projectDir) ForwardEverything @@ -79,8 +82,8 @@ generatePrismaClient projectDir = do let serverDir = projectDir serverRootDirInProjectRootDir let schemaFile = projectDir dbSchemaFileInProjectRootDir - let npxPrismaGenerateCmd = npxPrismaCmd ++ ["generate", "--schema", SP.toFilePath schemaFile] - let job = runNodeDependentCommandAsJob J.Db serverDir (head npxPrismaGenerateCmd, tail npxPrismaGenerateCmd) + let npxPrismaGenerateCmdWithArgs = buildNpxCmdWithArgs $ npxPrismaArgs ++ ["generate", "--schema", SP.toFilePath schemaFile] + let job = runNodeDependentCommandAsJob J.Db serverDir npxPrismaGenerateCmdWithArgs retryJobOnErrorWith job (npmInstall projectDir) ForwardOnlyRetryErrors diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index 881bdff120..a4154bf68d 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -25,7 +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 +import Data.Text.Encoding (decodeUtf8) -- TODO: -- Switch from Data.Conduit.Process to Data.Conduit.Process.Typed. @@ -65,11 +65,12 @@ runProcessAsJob process jobType chan = forwardByteStringChunkToChan bs = writeChan chan $ J.JobMessage - { -- Since this is output of a command that was supposed to be shown in the terminal, + { -- TODO: 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, + -- However, right now we just assume it is utf8, due to not prioritizing finding a proper solution. + J._data = J.JobOutput (decodeUtf8 bs) jobOutputType, J._jobType = jobType } From f0a3771af1709a7054c55ed915a8467c49f69f17 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Tue, 5 Jul 2022 13:04:17 +0200 Subject: [PATCH 18/20] Finally fixed encodings? --- waspc/cabal.project | 6 +-- waspc/src/Wasp/Generator/Job/Process.hs | 35 +++++++++++++---- waspc/src/Wasp/Util/Encoding.hs | 50 ------------------------- waspc/waspc.cabal | 3 +- 4 files changed, 29 insertions(+), 65 deletions(-) delete mode 100644 waspc/src/Wasp/Util/Encoding.hs diff --git a/waspc/cabal.project b/waspc/cabal.project index a167e7f089..2a9dff5494 100644 --- a/waspc/cabal.project +++ b/waspc/cabal.project @@ -20,8 +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 - --- We turned off the @systemencoding@ flag for @encoding@ package because it doesn't compile on Win otherwise, --- and we don't need that functionality anyway. -constraints: encoding -systemencoding +index-state: 2022-07-05T07:45:23Z diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index a4154bf68d..b1aea0cb53 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} module Wasp.Generator.Job.Process ( runProcessAsJob, @@ -9,13 +10,19 @@ 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.Knob as K import qualified Data.Text as T +import qualified Data.Text.IO as T +import GHC.IO.Encoding (initLocaleEncoding) +import GHC.IO.Handle (hSetEncoding) import StrongPath (Abs, Dir, Path') import qualified StrongPath as SP import System.Exit (ExitCode (..)) +import System.IO (IOMode (ReadMode)) import System.IO.Error (catchIOError, isDoesNotExistError) import qualified System.Info import qualified System.Process as P @@ -25,7 +32,6 @@ 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 Data.Text.Encoding (decodeUtf8) -- TODO: -- Switch from Data.Conduit.Process to Data.Conduit.Process.Typed. @@ -62,18 +68,31 @@ runProcessAsJob process jobType chan = -- @stream@ can be stdout stream or stderr stream. forwardStandardOutputStreamToChan stream jobOutputType = runConduit $ stream .| CL.mapM_ forwardByteStringChunkToChan where - forwardByteStringChunkToChan bs = + forwardByteStringChunkToChan bs = do + content <- decodeLocaleEncoding bs writeChan chan $ J.JobMessage - { -- TODO: 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. - -- However, right now we just assume it is utf8, due to not prioritizing finding a proper solution. - J._data = J.JobOutput (decodeUtf8 bs) jobOutputType, + { J._data = J.JobOutput content jobOutputType, J._jobType = jobType } + decodeLocaleEncoding :: ByteString -> IO T.Text + decodeLocaleEncoding bs = do + -- TODO: We turn bytestring into handle, because we want to use Haskell's capabilities + -- for decoding encodings, and those are most easily available when dealing with handles. + -- This is somewhat silly, since we go from handles to bytestring back to handles, and we have to use + -- `knob` library which is cool but is very small and it would be nicer to not have to use anything. + -- We set encoding to locale encoding, since that is the best option when dealing with standard in/outputs. + -- Here is a blog explaining this in more details: https://serokell.io/blog/haskell-with-utf8 . + -- Nicer solutions could be: + -- 1. Run the streaming process differently, so that we get handles from it, and not bytestrings. + -- Then we just encoding to initLocaleEncoding, do hGetContents, and that is it. + -- Basically we can skip the first step with the `knob`. + -- 2. Use (mkTextDecoder initLocaleEncoding) to decode the ByteString (it has pretty complex types though). + handle <- K.newKnob bs >>= \k -> K.newFileHandle k "job_stdout/stderr" ReadMode + hSetEncoding handle initLocaleEncoding + T.hGetContents handle + -- 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. -- We want to ensure the entire process chain is stopped. diff --git a/waspc/src/Wasp/Util/Encoding.hs b/waspc/src/Wasp/Util/Encoding.hs deleted file mode 100644 index b1e20343fc..0000000000 --- a/waspc/src/Wasp/Util/Encoding.hs +++ /dev/null @@ -1,50 +0,0 @@ -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 diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index eefa19dab1..dbad9d35df 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -108,7 +108,7 @@ library , fsnotify ^>= 0.3.0 , http-conduit ^>= 2.3.8 , uuid ^>= 1.3.15 - , encoding ^>= 0.8.6 + , knob ^>= 0.1.1 -- 'array' is used by code generated by Alex for src/Analyzer/Parser/Lexer.x , array ^>= 0.5.4 other-modules: Paths_waspc @@ -236,7 +236,6 @@ library Wasp.SemanticVersion Wasp.Util Wasp.Util.Control.Monad - Wasp.Util.Encoding Wasp.Util.Fib Wasp.Util.IO Wasp.Util.Terminal From ed900ae41b6ca3055a3af5a7d12eb2e8218ff840 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 7 Jul 2022 15:10:19 +0200 Subject: [PATCH 19/20] Implemented encoding function for ByteString. --- waspc/src/Wasp/Util/Encoding.hs | 55 +++++++++++++++++++++++++++++++++ waspc/waspc.cabal | 2 ++ 2 files changed, 57 insertions(+) create mode 100644 waspc/src/Wasp/Util/Encoding.hs diff --git a/waspc/src/Wasp/Util/Encoding.hs b/waspc/src/Wasp/Util/Encoding.hs new file mode 100644 index 0000000000..2d3a1288f7 --- /dev/null +++ b/waspc/src/Wasp/Util/Encoding.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE TypeApplications #-} + +module Wasp.Util.Encoding + ( decodeWithTE, + decodeWithTELenient, + encodeWithTE, + ) +where + +import Control.DeepSeq (force) +import Control.Exception (Exception (displayException), SomeException, evaluate, try) +import Data.Bifunctor (first) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS +import Data.List (isSuffixOf) +import qualified GHC.Foreign as GHC +import GHC.IO (unsafePerformIO) +import GHC.IO.Encoding (mkTextEncoding) +import GHC.IO.Encoding.Types (TextEncoding (textEncodingName)) + +-- decodeWithTE and encodeWithTE are modeled by this reddit comment: +-- https://www.reddit.com/r/haskell/comments/vrw476/comment/iezwd0t/?utm_source=share&utm_medium=web2x&context=3 +-- which points to this code in Filepath (with some small modifications): +-- https://gitlab.haskell.org/haskell/filepath/-/blob/master/System/OsPath/Encoding/Internal.hs#L298 + +-- | Decode with the given 'TextEncoding'. +decodeWithTE :: TextEncoding -> ByteString -> Either String String +decodeWithTE enc bs = unsafePerformIO $ do + decodedOrError <- decodeWithTEIO enc bs + evaluate $ force decodedOrError + +-- | Same like decodeWithTE, but it will choose a replacement character for illegal sequences or code points. +decodeWithTELenient :: TextEncoding -> ByteString -> String +decodeWithTELenient enc bs = unsafePerformIO $ do + encTranslit <- mkTextEncoding $ textEncodingName enc `addSuffixIfMissing` "//TRANSLIT" + decodedOrError <- decodeWithTEIO encTranslit bs + evaluate $ force $ fromRight' error decodedOrError + +decodeWithTEIO :: TextEncoding -> ByteString -> IO (Either String String) +decodeWithTEIO enc bs = do + decodedStrOrError <- try @SomeException $ BS.useAsCStringLen bs $ GHC.peekCStringLen enc + return $ first displayException decodedStrOrError + +-- | Encode with the given 'TextEncoding'. +encodeWithTE :: TextEncoding -> String -> Either String ByteString +encodeWithTE enc str = unsafePerformIO $ do + encodedBsOrError <- try @SomeException $ GHC.withCStringLen enc str BS.packCStringLen + evaluate $ force $ first displayException encodedBsOrError + +fromRight' :: (a -> b) -> Either a b -> b +fromRight' f (Left x) = f x +fromRight' _ (Right x) = x + +addSuffixIfMissing :: (Eq a) => [a] -> [a] -> [a] +addSuffixIfMissing t suffix = if suffix `isSuffixOf` t then t else t <> suffix diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index dbad9d35df..985f15da27 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -108,7 +108,9 @@ library , fsnotify ^>= 0.3.0 , http-conduit ^>= 2.3.8 , uuid ^>= 1.3.15 + -- TODO: remove knob , knob ^>= 0.1.1 + , deepseq ^>= 1.4.4 -- 'array' is used by code generated by Alex for src/Analyzer/Parser/Lexer.x , array ^>= 0.5.4 other-modules: Paths_waspc From 6d8c37656f80d80452c5b159661eda42b2b6cf21 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 7 Jul 2022 15:31:22 +0200 Subject: [PATCH 20/20] Removed knob library and now using direct decoding. --- waspc/src/Wasp/Generator/Job/Process.hs | 29 ++++++------------------- waspc/src/Wasp/Util/Encoding.hs | 8 ++++--- waspc/waspc.cabal | 3 +-- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Generator/Job/Process.hs index b1aea0cb53..e640ab6e6b 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Generator/Job/Process.hs @@ -14,15 +14,11 @@ 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.Knob as K import qualified Data.Text as T -import qualified Data.Text.IO as T import GHC.IO.Encoding (initLocaleEncoding) -import GHC.IO.Handle (hSetEncoding) import StrongPath (Abs, Dir, Path') import qualified StrongPath as SP import System.Exit (ExitCode (..)) -import System.IO (IOMode (ReadMode)) import System.IO.Error (catchIOError, isDoesNotExistError) import qualified System.Info import qualified System.Process as P @@ -32,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. @@ -69,29 +66,17 @@ runProcessAsJob process jobType chan = forwardStandardOutputStreamToChan stream jobOutputType = runConduit $ stream .| CL.mapM_ forwardByteStringChunkToChan where forwardByteStringChunkToChan bs = do - content <- decodeLocaleEncoding bs writeChan chan $ J.JobMessage - { J._data = J.JobOutput content jobOutputType, + { -- 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 -> IO T.Text - decodeLocaleEncoding bs = do - -- TODO: We turn bytestring into handle, because we want to use Haskell's capabilities - -- for decoding encodings, and those are most easily available when dealing with handles. - -- This is somewhat silly, since we go from handles to bytestring back to handles, and we have to use - -- `knob` library which is cool but is very small and it would be nicer to not have to use anything. - -- We set encoding to locale encoding, since that is the best option when dealing with standard in/outputs. - -- Here is a blog explaining this in more details: https://serokell.io/blog/haskell-with-utf8 . - -- Nicer solutions could be: - -- 1. Run the streaming process differently, so that we get handles from it, and not bytestrings. - -- Then we just encoding to initLocaleEncoding, do hGetContents, and that is it. - -- Basically we can skip the first step with the `knob`. - -- 2. Use (mkTextDecoder initLocaleEncoding) to decode the ByteString (it has pretty complex types though). - handle <- K.newKnob bs >>= \k -> K.newFileHandle k "job_stdout/stderr" ReadMode - hSetEncoding handle initLocaleEncoding - T.hGetContents handle + 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. diff --git a/waspc/src/Wasp/Util/Encoding.hs b/waspc/src/Wasp/Util/Encoding.hs index 2d3a1288f7..066d314997 100644 --- a/waspc/src/Wasp/Util/Encoding.hs +++ b/waspc/src/Wasp/Util/Encoding.hs @@ -23,8 +23,10 @@ import GHC.IO.Encoding.Types (TextEncoding (textEncodingName)) -- which points to this code in Filepath (with some small modifications): -- https://gitlab.haskell.org/haskell/filepath/-/blob/master/System/OsPath/Encoding/Internal.hs#L298 +type EncodingError = String + -- | Decode with the given 'TextEncoding'. -decodeWithTE :: TextEncoding -> ByteString -> Either String String +decodeWithTE :: TextEncoding -> ByteString -> Either EncodingError String decodeWithTE enc bs = unsafePerformIO $ do decodedOrError <- decodeWithTEIO enc bs evaluate $ force decodedOrError @@ -36,13 +38,13 @@ decodeWithTELenient enc bs = unsafePerformIO $ do decodedOrError <- decodeWithTEIO encTranslit bs evaluate $ force $ fromRight' error decodedOrError -decodeWithTEIO :: TextEncoding -> ByteString -> IO (Either String String) +decodeWithTEIO :: TextEncoding -> ByteString -> IO (Either EncodingError String) decodeWithTEIO enc bs = do decodedStrOrError <- try @SomeException $ BS.useAsCStringLen bs $ GHC.peekCStringLen enc return $ first displayException decodedStrOrError -- | Encode with the given 'TextEncoding'. -encodeWithTE :: TextEncoding -> String -> Either String ByteString +encodeWithTE :: TextEncoding -> String -> Either EncodingError ByteString encodeWithTE enc str = unsafePerformIO $ do encodedBsOrError <- try @SomeException $ GHC.withCStringLen enc str BS.packCStringLen evaluate $ force $ first displayException encodedBsOrError diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 985f15da27..12ee398c45 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -108,8 +108,6 @@ library , fsnotify ^>= 0.3.0 , http-conduit ^>= 2.3.8 , uuid ^>= 1.3.15 - -- TODO: remove knob - , knob ^>= 0.1.1 , deepseq ^>= 1.4.4 -- 'array' is used by code generated by Alex for src/Analyzer/Parser/Lexer.x , array ^>= 0.5.4 @@ -238,6 +236,7 @@ library Wasp.SemanticVersion Wasp.Util Wasp.Util.Control.Monad + Wasp.Util.Encoding Wasp.Util.Fib Wasp.Util.IO Wasp.Util.Terminal