Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3e2f284
Unique package names
cprecioso Sep 15, 2025
e094a41
Add workspaces
cprecioso Sep 15, 2025
dda141b
Snapshots
cprecioso Sep 16, 2025
1e8ad7d
Do not remove dependencies from the framework packages
cprecioso Sep 17, 2025
544a5a9
Add `package.json#workspaces` check
cprecioso Sep 17, 2025
7fd0290
Update snaphsot
cprecioso Sep 17, 2025
c1a1da0
Fix tests
cprecioso Sep 17, 2025
ac9bd17
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Sep 17, 2025
a7d71d7
Add changelog
cprecioso Sep 17, 2025
7a6441f
Add migration guide
cprecioso Sep 17, 2025
c3883b9
Fix example apps
cprecioso Sep 17, 2025
ca9bb9d
Format
cprecioso Sep 17, 2025
4ea1332
Update dockerfile
cprecioso Sep 17, 2025
25b7e1c
Update snaphsot
cprecioso Sep 17, 2025
ac348fe
Add ts-spec notice
cprecioso Sep 18, 2025
d4c84a9
Update migration guide
cprecioso Sep 18, 2025
636434b
Update package locks
cprecioso Sep 18, 2025
2173f34
Merge remote-tracking branch 'origin/main' into cprecioso/npm-workspaces
cprecioso Sep 18, 2025
063e210
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Sep 18, 2025
e267c5b
Update wasp-app-runner version
cprecioso Sep 18, 2025
641e6cd
Add SDK note
cprecioso Sep 18, 2025
6ef08f7
Add breaking change to changelog
cprecioso Sep 19, 2025
be519d2
Extract naming logic
cprecioso Sep 19, 2025
3bf16e6
Update package names
cprecioso Sep 19, 2025
2fdca35
Remove unused npm install for server and generator
cprecioso Sep 19, 2025
1a83660
Rename NpmWorkspaces
cprecioso Sep 19, 2025
99767ec
Fix comment
cprecioso Sep 19, 2025
f2100ff
Leftover rename
cprecioso Sep 19, 2025
820ed7a
Extract workspace list
cprecioso Sep 19, 2025
cca26c3
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Sep 19, 2025
94e673f
Fix
cprecioso Sep 19, 2025
a8b11c3
Fix Windows syntax
cprecioso Sep 19, 2025
800688c
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Sep 25, 2025
9454f77
Better render the paths in package.json
cprecioso Sep 25, 2025
beb8846
Update without path
cprecioso Sep 25, 2025
317c2f6
Update snaphsot
cprecioso Sep 25, 2025
256c4ac
Conditionally output .npmrc
cprecioso Sep 25, 2025
a931703
Update snaphsot
cprecioso Sep 25, 2025
4657896
Format
cprecioso Sep 25, 2025
95db53f
Laxer checking
cprecioso Sep 25, 2025
0a691b3
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Sep 29, 2025
13e891f
Review
cprecioso Oct 7, 2025
06d4bf3
Merge branch 'main' into cprecioso/npm-workspaces
cprecioso Oct 7, 2025
7385ad1
Refactor
cprecioso Oct 7, 2025
b005124
Review
cprecioso Oct 7, 2025
2b7dcf4
Review
cprecioso Oct 7, 2025
7ae0b78
Fixes
cprecioso Oct 7, 2025
aeba0c0
Format
cprecioso Oct 7, 2025
c34d38e
Fix tests
cprecioso Oct 7, 2025
45febe1
Update snaphsot
cprecioso Oct 7, 2025
3036606
Merge remote-tracking branch 'origin/main' into cprecioso/npm-workspaces
cprecioso Oct 9, 2025
3ed37c7
Update
cprecioso Oct 9, 2025
6a6868f
Merge remote-tracking branch 'origin/main' into cprecioso/npm-workspaces
cprecioso Oct 9, 2025
e776b53
Refactor workspace globs
cprecioso Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ RUN cd .wasp/build/server && npm run bundle

# Depending on `npm`'s dependency resolution, these folders may or may not exist.
# We ensure they are created so that the COPY instruction later does not fail.
RUN mkdir -p /app/node_modules /app/.wasp/build/server/node_modules
RUN mkdir -p ./node_modules ./.wasp/build/server/node_modules


# TODO: Use pm2?
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 18 additions & 33 deletions waspc/src/Wasp/Generator/NpmDependencies.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module Wasp.Generator.NpmDependencies
getUserNpmDepsForPackage,
getNpmDepsConflicts,
NpmDepsForPackage (..),
NpmDepsForPackageError (..),
conflictErrorToMessage,
genNpmDepsForPackage,
NpmDepsForFramework,
Expand Down Expand Up @@ -68,12 +67,6 @@ instance ToJSON NpmDepsForUser

instance FromJSON NpmDepsForUser

data NpmDepsForPackageError = NpmDepsForPackageError
{ dependenciesConflictErrors :: [DependencyConflictError],
devDependenciesConflictErrors :: [DependencyConflictError]
}
deriving (Show, Eq)

data DependencyConflictError = DependencyConflictError
{ waspDependency :: D.Dependency,
userDependency :: D.Dependency
Expand All @@ -83,33 +76,32 @@ data DependencyConflictError = DependencyConflictError
-- | Generate a NpmDepsForPackage by combining wasp dependencies with user dependencies
-- derived from AppSpec, or if there are conflicts, fail with error messages.
genNpmDepsForPackage :: AppSpec -> NpmDepsForWasp -> Generator NpmDepsForPackage
genNpmDepsForPackage spec npmDepsForWasp =
case getNpmDepsConflicts npmDepsForWasp (getUserNpmDepsForPackage spec) of
Nothing -> return $ waspDepsToPackageDeps npmDepsForWasp
Just conflictErrorDeps ->
genNpmDepsForPackage spec npmDepsForWasp
| null conflictErrors = return $ waspDepsToPackageDeps npmDepsForWasp
| otherwise =
logAndThrowGeneratorError $
GenericGeneratorError $
intercalate "\n " $
map
conflictErrorToMessage
( dependenciesConflictErrors conflictErrorDeps
++ devDependenciesConflictErrors conflictErrorDeps
)
conflictErrors
where
conflictErrors = getNpmDepsConflicts npmDepsForWasp (getUserNpmDepsForPackage spec)

buildWaspFrameworkNpmDeps :: AppSpec -> NpmDepsForWasp -> NpmDepsForWasp -> Either String NpmDepsForFramework
buildWaspFrameworkNpmDeps spec forServer forWebApp =
case (serverDepConflicts, webAppDepConflicts) of
(Nothing, Nothing) ->
Right
buildWaspFrameworkNpmDeps spec forServer forWebApp
| hasConflicts = Left "Could not construct npm dependencies due to a previously reported conflict."
| otherwise =
Right $
NpmDepsForFramework
{ npmDepsForServer = waspDepsToPackageDeps forServer,
npmDepsForWebApp = waspDepsToPackageDeps forWebApp
}
_ -> Left "Could not construct npm dependencies due to a previously reported conflict."
where
userDeps = getUserNpmDepsForPackage spec
hasConflicts = not $ null serverDepConflicts && null webAppDepConflicts
serverDepConflicts = getNpmDepsConflicts forServer userDeps
webAppDepConflicts = getNpmDepsConflicts forWebApp userDeps
userDeps = getUserNpmDepsForPackage spec

getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser
getUserNpmDepsForPackage spec =
Expand Down Expand Up @@ -150,27 +142,20 @@ waspDepsToPackageDeps npmDepsForWasp =
}

-- | Checks the user's dependencies compatibility against Wasp's declared npm dependencies.
getNpmDepsConflicts :: NpmDepsForWasp -> NpmDepsForUser -> Maybe NpmDepsForPackageError
getNpmDepsConflicts :: NpmDepsForWasp -> NpmDepsForUser -> [DependencyConflictError]
getNpmDepsConflicts npmDepsForWasp npmDepsForUser =
if null conflictErrors && null devConflictErrors
then Nothing
else
Just $
NpmDepsForPackageError
{ dependenciesConflictErrors = conflictErrors,
devDependenciesConflictErrors = devConflictErrors
}
conflictErrors ++ devConflictErrors
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we lose something by not separating dev dependency conflicts from regular dependency conflicts?

where
conflictErrors = determineConflictErrors allWaspDepsByName userDepsByName
devConflictErrors = determineConflictErrors allWaspDepsByName userDevDepsByName

allWaspDepsByName = (Map.union `on` makeDepsByName) waspDeps waspDevDeps
waspDeps = waspDependencies npmDepsForWasp
waspDevDeps = waspDevDependencies npmDepsForWasp
allWaspDepsByName = (Map.union `on` makeDepsByName) waspDeps waspDevDeps

userDepsByName = makeDepsByName $ userDependencies npmDepsForUser
userDevDepsByName = makeDepsByName $ userDevDependencies npmDepsForUser

conflictErrors = determineConflictErrors allWaspDepsByName userDepsByName
devConflictErrors = determineConflictErrors allWaspDepsByName userDevDepsByName

type DepsByName = Map.Map String D.Dependency

-- Given a map of wasp dependencies and a map of user dependencies, construct a
Expand Down
73 changes: 34 additions & 39 deletions waspc/src/Wasp/Generator/NpmWorkspaces.hs
Original file line number Diff line number Diff line change
@@ -1,54 +1,47 @@
module Wasp.Generator.NpmWorkspaces
( serverPackageName,
toWorkspacesField,
webAppPackageName,
workspaces,
workspaceGlobs,
)
where

import Control.Exception (Exception (displayException))
import Data.List (sort)
import StrongPath (Dir, Path, Posix, Rel, fromRelDirP, relDirToPosix, reldirP, (</>))
import Data.Either (fromRight)
import StrongPath (Dir, Path, Path', Posix, Rel, (</>))
import qualified StrongPath as SP
import qualified System.FilePath.Posix as FP.Posix
import Wasp.AppSpec (AppSpec, isBuild)
import Wasp.Project.Common (WaspProjectDir, buildDirInDotWaspDir, dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir)
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Project.Common
( WaspProjectDir,
buildDirInDotWaspDir,
dotWaspDirInWaspProjectDir,
generatedCodeDirInDotWaspDir,
)

-- | Returns the list of workspaces that should be included in the generated package.json file. Each
-- | Returns the list of workspaces that should be included in the user's `package.json` file. Each
-- workspace is a glob that matches all packages in a certain directory.
-- The path syntax here is always POSIX, because Windows syntax does not allow globs, and `npm`
-- prefers it.
--
-- Order doesn't matter, but we sort the packages to ensure a deterministic order. Otherwise, we
-- might inadvertently change the order and the compiler will complain about it in user's projects.
workspaces :: [Path Posix (Rel WaspProjectDir) (Dir ())]
workspaces =
sort
[ globFor generatedCodeDirInDotWaspDir,
globFor buildDirInDotWaspDir
-- TODO: Add SDK as a workspace:
-- Currently a overly-zealous resolution makes an incompatible resolution for `@types/react`
-- that would make the workspace installation fail.
-- Review when we upgrade React 19 (#2482).
]
-- The glob syntax is POSIX-path-like, but not actually a path, so it's represented as a String.
workspaceGlobs :: [String]
workspaceGlobs =
FP.Posix.dropTrailingPathSeparator . SP.fromRelDirP
<$> [ makeGlobFromProjectRoot $ dotWaspDirInWaspProjectDir </> generatedCodeDirInDotWaspDir,
makeGlobFromProjectRoot $ dotWaspDirInWaspProjectDir </> buildDirInDotWaspDir
-- TODO: Add SDK as a workspace (#3233)
]
where
globFor dir =
forceRelDirToPosix (dotWaspDirInWaspProjectDir </> dir)
</> packageWildcard
packageWildcard = [reldirP|*|]
makeGlobFromProjectRoot :: Path' (Rel WaspProjectDir) (Dir ProjectRootDir) -> Path Posix (Rel WaspProjectDir) (Dir a)
makeGlobFromProjectRoot projectRootDir = (forceRelDirToPosix projectRootDir) </> packageWildcard

forceRelDirToPosix = either nonPosixError id . relDirToPosix
nonPosixError exception =
error $
unlines
[ "This should never happen: our paths should always be POSIX-compatible, but they're not.",
displayException exception
]
-- We force this to be POSIX because Windows-style paths do not accept wildcard characters.
packageWildcard = [SP.reldirP|*|]
Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

This is also something that probably doesn't need to be in StrongPath. When we're constructing wildcards, we're already in string teritorry, so there's no point in pretending we're dealing with real paths, discussing Posix etc.


toWorkspacesField :: [Path Posix (Rel WaspProjectDir) (Dir ())] -> [String]
toWorkspacesField =
-- While the trailing slashes do not matter, we drop them because they will be user-visible in
-- their `package.json`, and it is more customary without them.
fmap (FP.Posix.dropTrailingPathSeparator . fromRelDirP)
forceRelDirToPosix inputDir = fromRight (makeNonPosixError inputDir) $ SP.relDirToPosix inputDir
makeNonPosixError inputDir =
error $
"This should never happen: our paths should always be POSIX-compatible, but they're not. (Received: "
++ show inputDir
++ ")"

serverPackageName :: AppSpec -> String
serverPackageName = workspacePackageName "server"
Expand All @@ -57,6 +50,8 @@ webAppPackageName :: AppSpec -> String
webAppPackageName = workspacePackageName "webapp"

workspacePackageName :: String -> AppSpec -> String
workspacePackageName baseName spec = "@wasp.sh/generated-" ++ baseName ++ "-" ++ mode
workspacePackageName baseName spec = "@wasp.sh/generated-" ++ baseName ++ "-" ++ modeName
where
mode = if isBuild spec then "build" else "dev"
modeName
| isBuild spec = "build"
| otherwise = "dev"
3 changes: 1 addition & 2 deletions waspc/src/Wasp/Generator/Valid/PackageJson.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import qualified Data.Map as M
import qualified Wasp.ExternalConfig.Npm.PackageJson as P
import Wasp.Generator.DepVersions (prismaVersion, typescriptVersion)
import Wasp.Generator.Monad (GeneratorError (GenericGeneratorError))
import Wasp.Generator.NpmWorkspaces (toWorkspacesField)
import qualified Wasp.Generator.NpmWorkspaces as NW
import Wasp.Generator.ServerGenerator.DepVersions (expressTypesVersion)
import Wasp.Generator.Valid.Common (FullyQualifiedFieldName (FieldName), validateArrayFieldIncludesRequired)
Expand Down Expand Up @@ -67,7 +66,7 @@ validateWorkspaces packageJson =
validateArrayFieldIncludesRequired
"package.json"
(FieldName ["workspaces"])
(toWorkspacesField NW.workspaces)
NW.workspaceGlobs
(P.workspaces packageJson)

validatePackageJsonDependency :: P.PackageJson -> PackageSpecification -> PackageRequirement -> [GeneratorError]
Expand Down
3 changes: 1 addition & 2 deletions waspc/tests/AppSpec/ValidTest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import qualified Wasp.AppSpec.Route as AS.Route
import qualified Wasp.AppSpec.Valid as ASV
import qualified Wasp.ExternalConfig.Npm.PackageJson as Npm.PackageJson
import qualified Wasp.ExternalConfig.TsConfig as T
import Wasp.Generator.NpmWorkspaces (toWorkspacesField)
import qualified Wasp.Generator.NpmWorkspaces as NW
import qualified Wasp.Psl.Ast.Argument as Psl.Argument
import qualified Wasp.Psl.Ast.Attribute as Psl.Attribute
Expand Down Expand Up @@ -508,7 +507,7 @@ spec_AppSpecValid = do
{ Npm.PackageJson.name = "testApp",
Npm.PackageJson.dependencies = M.empty,
Npm.PackageJson.devDependencies = M.empty,
Npm.PackageJson.workspaces = Just $ toWorkspacesField NW.workspaces
Npm.PackageJson.workspaces = Just $ NW.workspaceGlobs
},
AS.isBuild = False,
AS.migrationsDir = Nothing,
Expand Down
Loading