diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index d99509cca7..3861173418 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -26,6 +26,7 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guide ### 🐞 Bug fixes +- Fixed node/npm/npx/prisma command resolution on Windows, which was one of the main stoppers of supporting Wasp on native Windows. ([#3258](https://github.com/wasp-lang/wasp/pull/3258)). - Fixed a type error in the generated server app when `process.env.NODE_ENV` is also declared by another dependency in the project. ([#3189](https://github.com/wasp-lang/wasp/pull/3189)) - Fixed an incompatibility between `wasp deploy fly` and Fly CLI v0.3.214. ([#3372](https://github.com/wasp-lang/wasp/pull/3372)) - Raised the internal requirement of Tailwind CSS from `^3.2.7` to `^3.4.17` to ensure compatibility with ESM config files. ([#3380](https://github.com/wasp-lang/wasp/issues/3380)) @@ -47,6 +48,10 @@ Remember to check out the [migration guide](https://wasp.sh/docs/migration-guide - Wasp's `kitchen-sink` application has been moved to the public examples (`examples/`) directory in our repo. ([#3085](https://github.com/wasp-lang/wasp/pull/3197)) +### 🌟 Contributors + +- Big thanks to @MetaMModern and @nodtem66 for crucial work in releasing the fix for node commands resolution on Windows ([#3258](https://github.com/wasp-lang/wasp/pull/3258)) and other related issues. + ## 0.18.2 ### 🐞 Bug fixes diff --git a/waspc/cli/src/Wasp/Cli/Command/BuildStart/Client.hs b/waspc/cli/src/Wasp/Cli/Command/BuildStart/Client.hs index 9d15b0ae1c..b7ec036ea5 100644 --- a/waspc/cli/src/Wasp/Cli/Command/BuildStart/Client.hs +++ b/waspc/cli/src/Wasp/Cli/Command/BuildStart/Client.hs @@ -12,13 +12,14 @@ import qualified Wasp.Generator.WebAppGenerator.Common as WebApp import qualified Wasp.Job as J import Wasp.Job.Except (ExceptJob, toExceptJob) import Wasp.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv) +import Wasp.Node.Executables (npmExec) buildClient :: BuildStartConfig -> ExceptJob buildClient config = runNodeCommandAsJobWithExtraEnv envVars webAppDir - "npm" + npmExec ["run", "build"] J.WebApp & toExceptJob (("Building the client failed with exit code: " <>) . show) @@ -31,7 +32,7 @@ startClient :: BuildStartConfig -> ExceptJob startClient config = runNodeCommandAsJob webAppDir - "npm" + npmExec [ "run", "preview", -- `preview` launches a static file server for the built client. "--", diff --git a/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs index 62751a780b..f1a115f699 100644 --- a/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs +++ b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs @@ -11,6 +11,7 @@ import Wasp.Cli.Command.Require (InWaspProject (InWaspProject)) import qualified Wasp.Job as J import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npmExec) import Wasp.NodePackageFFI (InstallablePackage (WaspConfigPackage), getPackageInstallationPath) -- | Prepares the project for using Wasp's TypeScript SDK. @@ -32,7 +33,7 @@ installWaspConfigPackage chan projectDir = do (_, exitCode) <- concurrently (readJobMessagesAndPrintThemPrefixed chan) - (runNodeCommandAsJob projectDir "npm" ["install", "--save-dev", "file:" ++ installationPath] J.Wasp chan) + (runNodeCommandAsJob projectDir npmExec ["install", "--save-dev", "file:" ++ installationPath] J.Wasp chan) return $ case exitCode of ExitSuccess -> Right () ExitFailure _ -> Left "Failed to install wasp-config package" diff --git a/waspc/packages/wasp-config/__tests__/cli.unit.test.ts b/waspc/packages/wasp-config/__tests__/cli.unit.test.ts index 127ca1a173..9da2403dfc 100644 --- a/waspc/packages/wasp-config/__tests__/cli.unit.test.ts +++ b/waspc/packages/wasp-config/__tests__/cli.unit.test.ts @@ -10,6 +10,14 @@ describe("parseProcessArgsOrThrow", () => { ]); }); + test("should parse arguments correctly even if node has absolute path", () => { + expectParseProcessArgsToSucceed([ + "main.wasp.js", + "output.json", + JSON.stringify(["entity1"]), + ], "/usr/bin/node"); + }); + test("should parse 0 entities correctly", () => { expectParseProcessArgsToSucceed(["main.wasp.js", "output.json", "[]"]); }); @@ -59,8 +67,8 @@ describe("parseProcessArgsOrThrow", () => { ]); }); - function expectParseProcessArgsToSucceed(args: string[]) { - const result = parseProcessArgsOrThrow(["node", "run.js", ...args]); + function expectParseProcessArgsToSucceed(args: string[], nodeExec: string = "node") { + const result = parseProcessArgsOrThrow([nodeExec, "run.js", ...args]); const [waspTsSpecPath, outputFilePath, entityNames] = args; expect(result).toEqual({ diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 79e528ed14..4aa43d2875 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -15,14 +15,14 @@ where import StrongPath (Abs, Dir, File', Path', ()) import qualified StrongPath as SP -import StrongPath.TH (relfile) import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), dbSchemaFileInProjectRootDir) import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir) import Wasp.Generator.ServerGenerator.Db.Seed (dbSeedNameEnvVarName) import qualified Wasp.Job as J import Wasp.Job.Process (runNodeCommandAsJobWithExtraEnv) -import Wasp.Project.Common (WaspProjectDir, waspProjectDirFromProjectRootDir) +import Wasp.Node.NodeModules (getPathToExecutableInNodeModules) +import Wasp.Project.Common (WaspProjectDir, nodeModulesDirInWaspProjectDir, waspProjectDirFromProjectRootDir) migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job migrateDev projectRootDir migrateArgs = @@ -178,4 +178,7 @@ absPrismaExecutableFp :: Path' Abs (Dir WaspProjectDir) -> FilePath absPrismaExecutableFp waspProjectDir = SP.fromAbsFile prismaExecutableAbs where prismaExecutableAbs :: Path' Abs File' - prismaExecutableAbs = waspProjectDir [relfile|./node_modules/.bin/prisma|] + prismaExecutableAbs = + waspProjectDir + nodeModulesDirInWaspProjectDir + SP.castRel (getPathToExecutableInNodeModules "prisma") diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index 623cac53a8..f74ded9053 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -76,6 +76,7 @@ import Wasp.Generator.WebAppGenerator.DepVersions import qualified Wasp.Job as J import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npmExec) import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Common (WaspProjectDir, waspProjectDirFromAppComponentDir) import qualified Wasp.Project.Db as Db @@ -90,7 +91,7 @@ buildSdk projectRootDir = do (_, exitCode) <- concurrently (readJobMessagesAndPrintThemPrefixed chan) - (runNodeCommandAsJob dstDir "npm" ["run", "build"] J.Wasp chan) + (runNodeCommandAsJob dstDir npmExec ["run", "build"] J.Wasp chan) case exitCode of ExitSuccess -> return $ Right () ExitFailure code -> return $ Left $ "SDK build failed with exit code: " ++ show code @@ -332,7 +333,7 @@ depsRequiredByTailwind spec = -- Also, fix imports for wasp project. installNpmDependencies :: Path' Abs (Dir WaspProjectDir) -> J.Job installNpmDependencies projectDir = - runNodeCommandAsJob projectDir "npm" ["install"] J.Wasp + runNodeCommandAsJob projectDir npmExec ["install"] J.Wasp -- todo(filip): consider reorganizing/splitting the file. diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index a2edb34f87..73f6a5b3f2 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -8,8 +8,9 @@ import Wasp.Generator.Common (ProjectRootDir) import qualified Wasp.Generator.ServerGenerator.Common as Common import qualified Wasp.Job as J import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npmExec) startServer :: Path' Abs (Dir ProjectRootDir) -> J.Job startServer projectDir = do let serverDir = projectDir Common.serverRootDirInProjectRootDir - runNodeCommandAsJob serverDir "npm" ["run", "watch"] J.Server + runNodeCommandAsJob serverDir npmExec ["run", "watch"] J.Server diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 6bd126f6b3..7d22e2c8f7 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -8,8 +8,9 @@ import Wasp.Generator.Common (ProjectRootDir) import qualified Wasp.Generator.WebAppGenerator.Common as Common import qualified Wasp.Job as J import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npmExec) startWebApp :: Path' Abs (Dir ProjectRootDir) -> J.Job startWebApp projectDir = do let webAppDir = projectDir Common.webAppRootDirInProjectRootDir - runNodeCommandAsJob webAppDir "npm" ["start"] J.WebApp + runNodeCommandAsJob webAppDir npmExec ["start"] J.WebApp diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs index fd832ca554..23ff01a0d1 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs @@ -8,11 +8,12 @@ import qualified StrongPath as SP import Wasp.Generator.WebAppGenerator.Common (webAppRootDirInProjectRootDir) import qualified Wasp.Job as J import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npxExec) import Wasp.Project.Common (WaspProjectDir, dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) testWebApp :: [String] -> Path' Abs (Dir WaspProjectDir) -> J.Job testWebApp args projectDir = do - runNodeCommandAsJob projectDir "npx" (vitestCommand ++ args) J.WebApp + runNodeCommandAsJob projectDir npxExec (vitestCommand ++ args) J.WebApp where vitestCommand = ["vitest", "--config", SP.fromRelFile viteConfigPath] viteConfigPath = diff --git a/waspc/src/Wasp/Node/Executables.hs b/waspc/src/Wasp/Node/Executables.hs new file mode 100644 index 0000000000..8f1dde940b --- /dev/null +++ b/waspc/src/Wasp/Node/Executables.hs @@ -0,0 +1,22 @@ +module Wasp.Node.Executables + ( nodeExec, + npmExec, + npxExec, + ) +where + +import GHC.IO (unsafePerformIO) +import StrongPath (fromAbsFile) +import Wasp.Util.System (ExecName, resolveExecNameIO) + +{-# NOINLINE nodeExec #-} -- To ensure single execution, for unsafePerformIO. +nodeExec :: ExecName +nodeExec = fromAbsFile $ unsafePerformIO $ resolveExecNameIO "node" + +{-# NOINLINE npmExec #-} -- To ensure single execution, for unsafePerformIO. +npmExec :: ExecName +npmExec = fromAbsFile $ unsafePerformIO $ resolveExecNameIO "npm" + +{-# NOINLINE npxExec #-} -- To ensure single execution, for unsafePerformIO. +npxExec :: ExecName +npxExec = fromAbsFile $ unsafePerformIO $ resolveExecNameIO "npx" diff --git a/waspc/src/Wasp/Node/NodeModules.hs b/waspc/src/Wasp/Node/NodeModules.hs new file mode 100644 index 0000000000..d36ff6a8d8 --- /dev/null +++ b/waspc/src/Wasp/Node/NodeModules.hs @@ -0,0 +1,25 @@ +module Wasp.Node.NodeModules + ( getPathToExecutableInNodeModules, + ) +where + +import Data.Maybe (fromJust) +import StrongPath (File, Path', Rel, parseRelFile, reldir, ()) +import Wasp.Util.System (isSystemWindows) + +-- | Represents some node_modules dir. +data NodeModulesDir + +-- | Node modules (node_modules) have a place where they put all the executables/binaries +-- produced by the packages/modules. +-- This function returns a path to such an executable with a given name, taking into account +-- details like current operating system. +-- +-- Example: @getPathToExecutableInNodeModules "npm"@ -> @".bin/npm.cmd"@ +getPathToExecutableInNodeModules :: String -> Path' (Rel NodeModulesDir) (File f) +getPathToExecutableInNodeModules execName = + [reldir|.bin|] fromJust (parseRelFile systemSpecificExecFilename) + where + systemSpecificExecFilename + | isSystemWindows = execName <> ".cmd" + | otherwise = execName diff --git a/waspc/src/Wasp/Node/Version.hs b/waspc/src/Wasp/Node/Version.hs index 0e8157185f..fb9971f69e 100644 --- a/waspc/src/Wasp/Node/Version.hs +++ b/waspc/src/Wasp/Node/Version.hs @@ -10,6 +10,7 @@ where import Data.Conduit.Process.Typed (ExitCode (..)) import System.IO.Error (catchIOError, isDoesNotExistError) import System.Process (readProcessWithExitCode) +import Wasp.Node.Executables (nodeExec, npmExec) import Wasp.Node.Internal (parseVersionFromCommandOutput) import qualified Wasp.SemanticVersion as SV import qualified Wasp.SemanticVersion.VersionBound as SV @@ -48,8 +49,8 @@ checkUserNodeAndNpmMeetWaspRequirements = do (VersionCheckFail nodeError, _) -> VersionCheckFail nodeError (_, VersionCheckFail npmError) -> VersionCheckFail npmError where - checkUserNodeVersion = checkUserToolVersion "node" ["--version"] oldestWaspSupportedNodeVersion - checkUserNpmVersion = checkUserToolVersion "npm" ["--version"] oldestWaspSupportedNpmVersion + checkUserNodeVersion = checkUserToolVersion nodeExec ["--version"] oldestWaspSupportedNodeVersion + checkUserNpmVersion = checkUserToolVersion npmExec ["--version"] oldestWaspSupportedNpmVersion checkUserToolVersion :: String -> [String] -> SV.Version -> IO VersionCheckResult checkUserToolVersion commandName commandArgs oldestSupportedToolVersion = do diff --git a/waspc/src/Wasp/NodePackageFFI.hs b/waspc/src/Wasp/NodePackageFFI.hs index c294efedf7..f9382c8d25 100644 --- a/waspc/src/Wasp/NodePackageFFI.hs +++ b/waspc/src/Wasp/NodePackageFFI.hs @@ -21,6 +21,7 @@ import System.IO (hPutStrLn, stderr) import qualified System.Process as P import Wasp.Data (DataDir) import qualified Wasp.Data as Data +import Wasp.Node.Executables (nodeExec, npmExec) import qualified Wasp.Node.Version as NodeVersion -- | This are the globally installed packages waspc runs directly from @@ -85,7 +86,7 @@ getPackageProcessOptions package args = do packageDir <- getRunnablePackageDir package let scriptFile = packageDir scriptInPackageDir ensurePackageDependenciesAreInstalled packageDir - return $ packageCreateProcess packageDir "node" (fromAbsFile scriptFile : args) + return $ packageCreateProcess packageDir nodeExec (fromAbsFile scriptFile : args) getPackageInstallationPath :: InstallablePackage -> IO String getPackageInstallationPath package = do @@ -103,7 +104,7 @@ getRunnablePackageDir package = do ensurePackageDependenciesAreInstalled :: Path' Abs (Dir PackageDir) -> IO () ensurePackageDependenciesAreInstalled packageDir = unlessM nodeModulesDirExists $ do - let npmInstallCreateProcess = packageCreateProcess packageDir "npm" ["install"] + let npmInstallCreateProcess = packageCreateProcess packageDir npmExec ["install"] (exitCode, _out, err) <- P.readCreateProcessWithExitCode npmInstallCreateProcess "" case exitCode of ExitFailure _ -> do diff --git a/waspc/src/Wasp/Project/WaspFile/TypeScript.hs b/waspc/src/Wasp/Project/WaspFile/TypeScript.hs index 0417fd0a7c..a87471e5f2 100644 --- a/waspc/src/Wasp/Project/WaspFile/TypeScript.hs +++ b/waspc/src/Wasp/Project/WaspFile/TypeScript.hs @@ -29,6 +29,7 @@ import Wasp.AppSpec.Core.Decl.JSON () import qualified Wasp.Job as J import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Node.Executables (npxExec) import Wasp.Project.Common ( CompileError, WaspProjectDir, @@ -71,7 +72,7 @@ compileWaspTsFile waspProjectDir tsconfigNodeFileInWaspProjectDir waspFilePath = (readJobMessagesAndPrintThemPrefixed chan) ( runNodeCommandAsJob waspProjectDir - "npx" + npxExec -- We're using tsc to compile the *.wasp.ts file into a JS file. -- -- The tsconfig.wasp.json is configured to give our users with the @@ -125,7 +126,7 @@ executeMainWaspJsFileAndGetDeclsFile waspProjectDir prismaSchemaAst absCompiledM (readJobMessagesAndPrintThemPrefixed chan) ( runNodeCommandAsJob waspProjectDir - "npx" + npxExec -- TODO: Figure out how to keep running instructions in a single -- place (e.g., this is string the same as the package name, but it's -- repeated in two places). diff --git a/waspc/src/Wasp/Util/Json.hs b/waspc/src/Wasp/Util/Json.hs index 1d5f934233..0df455c5f6 100644 --- a/waspc/src/Wasp/Util/Json.hs +++ b/waspc/src/Wasp/Util/Json.hs @@ -10,6 +10,7 @@ import Data.Aeson (FromJSON, Value (..), eitherDecode, encode) import StrongPath (Abs, File, Path') import System.Exit (ExitCode (..)) import qualified System.Process as P +import Wasp.Node.Executables (nodeExec) import Wasp.Util.Aeson (decodeFromString) import qualified Wasp.Util.IO as IOUtil @@ -19,7 +20,7 @@ import qualified Wasp.Util.IO as IOUtil parseJsonWithComments :: (FromJSON a) => String -> IO (Either String a) parseJsonWithComments jsonStr = do let evalScript = "const v = " ++ jsonStr ++ ";console.log(JSON.stringify(v));" - let cp = P.proc "node" ["-e", evalScript] + let cp = P.proc nodeExec ["-e", evalScript] (exitCode, response, stderr) <- P.readCreateProcessWithExitCode cp "" case exitCode of ExitSuccess -> return $ decodeFromString response diff --git a/waspc/src/Wasp/Util/System.hs b/waspc/src/Wasp/Util/System.hs new file mode 100644 index 0000000000..e4cc6a1cef --- /dev/null +++ b/waspc/src/Wasp/Util/System.hs @@ -0,0 +1,62 @@ +module Wasp.Util.System + ( resolveExecNameIO, + isSystemWindows, + isSystemMacOS, + ExecName, + ) +where + +import Control.Exception (throwIO) +import Control.Monad.Extra (firstJustM) +import StrongPath (Abs, File', Path', parseAbsFile) +import System.Directory (findExecutable) +import qualified System.Info + +-- | Executable name as expected by Haskell's "System.Process" and its 'System.Process.RawCommand', +-- therefore suited for passing to their functions for creating/executing processes. +-- It can be just "node", or "node.exe", or relative or full path, ... . +type ExecName = FilePath + +-- | Resolve given executable name (e.g. "node") to the full executable path. +-- +-- Note that filename of the resolved path might not be exactly equal to the provided executable +-- name, but may have additional extension (e.g. "node" might resolve to "/some/path/node.cmd"). +-- +-- The reason why we return resolved absolute executable path and not just the resolved filename +-- (e.g. "node.cmd" for "node") is that, as per Haskell docs, when passing just the filename to +-- 'System.Process.createProcess', we can get unexpected resolution if 'cwd' option is set. +-- For example it can resolve to "npm.cmd" in the local ".node_modules" instead of a global one. +-- So it is better to stick with absolute paths. +-- +-- Motivation for this function was mainly driven by how exec names are resolved when executing a +-- process on Windows. +-- On Linux/MacOS situation is simple, the system will normally do the name resolution for us, so +-- e.g. if we pass "npm" to 'System.Process.proc', that will work out of the box. +-- But on Windows, that will normally fail, since there is no "npm" really but instead "npm.cmd" or +-- "npm.exe". In that case, we want to figure out what exactly is the right exec name to use. +-- Note that we don't have to bother with this when using 'System.Process.shell' instead of +-- 'System.Process.proc', becuase then the shell will do system resolution for us, +-- but at the price of abandoning any argument escaping. +-- +-- Throws IOError if it failed to resolve the name. +-- +-- Example: resolveExecNameIO "npm" -> "C:\...\npm.cmd" +resolveExecNameIO :: ExecName -> IO (Path' Abs File') +resolveExecNameIO execName = do + firstJustM findExecutable execNamesToLookForByPriorityDesc >>= \case + Just execPath -> parseAbsFile execPath + Nothing -> + (throwIO . userError . unlines) + [ "Could not find '" <> execName <> "' executable on your system.", + "Please ensure " <> execName <> " is installed and available in your PATH." + ] + where + execNamesToLookForByPriorityDesc + | isSystemWindows = (execName <>) <$> ["", ".cmd", ".exe", ".ps1"] + | otherwise = [execName] + +isSystemWindows :: Bool +isSystemWindows = System.Info.os == "mingw32" + +isSystemMacOS :: Bool +isSystemMacOS = System.Info.os == "darwin" diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 5c70ba1786..9a3c756e26 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -383,7 +383,9 @@ library Wasp.Job.IO.PrefixedWriter Wasp.Job.Process Wasp.Message + Wasp.Node.Executables Wasp.Node.Internal + Wasp.Node.NodeModules Wasp.Node.Version Wasp.NodePackageFFI Wasp.Project @@ -455,6 +457,7 @@ library Wasp.Util.Network.HTTP Wasp.Util.Network.Socket Wasp.Util.StrongPath + Wasp.Util.System Wasp.Util.Terminal Wasp.Util.TH Wasp.Util.WebRouterPath