Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions waspc/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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)).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go on "unreleased". Also I'd remove or at least rephrase the last part. Now it is not clear if this change means Wasp on Windows finally works, or if it doesn't yet.

- Fixed a type error with the default `NODE_ENV` value in the server env validation schema. ([#3189](https://github.com/wasp-lang/wasp/pull/3189))

### 🔧 Small improvements
Expand All @@ -30,6 +31,9 @@ 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 ([#3085](github.com/wasp-lang/wasp/issues/3085))
- Added documentation on how to setup the Chrome DevTools workspace mapping with Wasp (by @0xTaneja). ([#3103](https://github.com/wasp-lang/wasp/pull/3103))

### 🌟 Contributors
- Big thanks to @MetaMModern and @nodtem66 for crucial work in releasing the fix for node commands resolution on Windows.

## 0.18.2

### 🐞 Bug fixes
Expand Down
5 changes: 3 additions & 2 deletions waspc/cli/src/Wasp/Cli/Command/BuildStart/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
"--",
Expand Down
3 changes: 2 additions & 1 deletion waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
12 changes: 10 additions & 2 deletions waspc/packages/wasp-config/__tests__/cli.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "[]"]);
});
Expand Down Expand Up @@ -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({
Expand Down
9 changes: 6 additions & 3 deletions waspc/src/Wasp/Generator/DbGenerator/Jobs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are now being system-agnostic here, when figuring out abs path to prisma binary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment on the NodeModules.hs file

5 changes: 3 additions & 2 deletions waspc/src/Wasp/Generator/SdkGenerator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Generator/ServerGenerator/Start.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Generator/WebAppGenerator/Start.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Generator/WebAppGenerator/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
22 changes: 22 additions & 0 deletions waspc/src/Wasp/Node/Executables.hs
Original file line number Diff line number Diff line change
@@ -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"
25 changes: 25 additions & 0 deletions waspc/src/Wasp/Node/NodeModules.hs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this logic is only used for calling prisma

idea: Could it be possible to call npx prisma, instead of duplicating npm's executable resolution algorithm ourselves? Then we can remove this file.

Original file line number Diff line number Diff line change
@@ -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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a comment with a link to somewhere that says that npm adds .cmd on Windows?
I found this, don't know if there's a better one: https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin

| otherwise = execName
5 changes: 3 additions & 2 deletions waspc/src/Wasp/Node/Version.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions waspc/src/Wasp/NodePackageFFI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions waspc/src/Wasp/Project/WaspFile/TypeScript.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Util/Json.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
62 changes: 62 additions & 0 deletions waspc/src/Wasp/Util/System.hs
Original file line number Diff line number Diff line change
@@ -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"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get this list from the PATHEXT environment variable since it is configurable.

| otherwise = [execName]

isSystemWindows :: Bool
isSystemWindows = System.Info.os == "mingw32"

isSystemMacOS :: Bool
isSystemMacOS = System.Info.os == "darwin"
3 changes: 3 additions & 0 deletions waspc/waspc.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,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
Expand Down Expand Up @@ -448,6 +450,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
Expand Down