diff --git a/waspc/cli/src/Wasp/Cli/Command/Build.hs b/waspc/cli/src/Wasp/Cli/Command/Build.hs index f978fa058c..8f8e034190 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Build.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Build.hs @@ -3,23 +3,40 @@ module Wasp.Cli.Command.Build ) where +import Control.Lens import Control.Monad (unless, when) -import Control.Monad.Except (throwError) +import Control.Monad.Except (ExceptT (ExceptT), runExceptT, throwError) import Control.Monad.IO.Class (liftIO) +import Data.Aeson (Value (..)) +import Data.Aeson.Lens +import qualified Data.HashMap.Strict as HM +import Data.List (isSuffixOf) +import Data.Text (Text, unpack) import StrongPath (Abs, Dir, Path', castRel, ()) +import qualified System.FilePath as FP import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command.Compile (compileIOWithOptions, printCompilationResult) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) import Wasp.Cli.Message (cliSendMessage) import Wasp.CompileOptions (CompileOptions (..)) -import qualified Wasp.Generator +import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning)) import Wasp.Generator.SdkGenerator.Common (sdkRootDirInGeneratedCodeDir, sdkRootDirInProjectRootDir) import qualified Wasp.Message as Msg -import Wasp.Project (CompileError, CompileWarning, WaspProjectDir) -import Wasp.Project.Common (buildDirInDotWaspDir, dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir, packageJsonInWaspProjectDir, packageLockJsonInWaspProjectDir, srcDirInWaspProjectDir) +import Wasp.Project.Common + ( CompileError, + CompileWarning, + WaspProjectDir, + buildDirInDotWaspDir, + dotWaspDirInWaspProjectDir, + generatedCodeDirInDotWaspDir, + packageJsonInWaspProjectDir, + packageLockJsonInWaspProjectDir, + srcDirInWaspProjectDir, + ) import Wasp.Util.IO (copyDirectory, copyFile, doesDirectoryExist, removeDirectory) +import Wasp.Util.Json (updateJsonFile) -- | Builds Wasp project that the current working directory is part of. -- Does all the steps, from analysis to generation, and at the end writes generated code @@ -58,40 +75,79 @@ build = do throwError $ CommandError "Building of wasp project failed" $ show (length errors) ++ " errors found." - liftIO $ copyUserFilesNecessaryForBuild waspProjectDir buildDir + liftIO (prepareFilesNecessaryForDockerBuild waspProjectDir buildDir) >>= \case + Left err -> throwError $ CommandError "Failed to prepare files necessary for docker build" err + Right () -> return () cliSendMessageC $ Msg.Success "Your wasp project has been successfully built! Check it out in the .wasp/build directory." where - -- Until we implement the solution described in https://github.com/wasp-lang/wasp/issues/1769, - -- we're copying all files and folders necessary for the build into the .wasp/build directory. - -- We chose this approach for 0.12.0 (instead of building from the project root) because: - -- - The build context remains small (~1.5 MB vs ~900 MB). - -- - We don't risk copying possible secrets from the project root into the build context. - -- - The commands for building the project stay the same as before - -- 0.12.0, which is good for both us (e.g., for fly deployment) and our - -- users (no changes in CI/CD scripts). - -- For more details, read the issue linked above. - copyUserFilesNecessaryForBuild waspProjectDir buildDir = do - copyDirectory - (waspProjectDir srcDirInWaspProjectDir) - (buildDir castRel srcDirInWaspProjectDir) - - copyDirectory - (waspProjectDir dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir sdkRootDirInGeneratedCodeDir) - (buildDir sdkRootDirInGeneratedCodeDir) - - copyFile - (waspProjectDir packageJsonInWaspProjectDir) - (buildDir castRel packageJsonInWaspProjectDir) - - copyFile - (waspProjectDir packageLockJsonInWaspProjectDir) - (buildDir castRel packageLockJsonInWaspProjectDir) + prepareFilesNecessaryForDockerBuild waspProjectDir buildDir = runExceptT $ do + -- Until we implement the solution described in https://github.com/wasp-lang/wasp/issues/1769, + -- we're copying all files and folders necessary for Docker build into the .wasp/build directory. + -- We chose this approach for 0.12.0 (instead of building from the project root) because: + -- - The Docker build context remains small (~1.5 MB vs ~900 MB). + -- - We don't risk copying possible secrets from the project root into Docker's build context. + -- - The commands for building the project stay the same as before + -- 0.12.0, which is good for both us (e.g., for fly deployment) and our + -- users (no changes in CI/CD scripts). + -- For more details, read the issue linked above. + liftIO $ + copyDirectory + (waspProjectDir srcDirInWaspProjectDir) + (buildDir castRel srcDirInWaspProjectDir) + + liftIO $ + copyDirectory + (waspProjectDir dotWaspDirInWaspProjectDir generatedCodeDirInDotWaspDir sdkRootDirInGeneratedCodeDir) + (buildDir sdkRootDirInGeneratedCodeDir) + + let packageJsonInBuildDir = buildDir castRel packageJsonInWaspProjectDir + let packageLockJsonInBuildDir = buildDir castRel packageLockJsonInWaspProjectDir + + liftIO $ + copyFile + (waspProjectDir packageJsonInWaspProjectDir) + packageJsonInBuildDir + + liftIO $ + copyFile + (waspProjectDir packageLockJsonInWaspProjectDir) + packageLockJsonInBuildDir + + -- A hacky quick fix for https://github.com/wasp-lang/wasp/issues/2368 + -- We should remove this code once we implement a proper solution. + ExceptT $ updateJsonFile removeWaspConfigFromDevDependenciesArray packageJsonInBuildDir + ExceptT $ updateJsonFile removeAllMentionsOfWaspConfigInPackageLockJson packageLockJsonInBuildDir + + removeAllMentionsOfWaspConfigInPackageLockJson :: Value -> Value + removeAllMentionsOfWaspConfigInPackageLockJson packageLockJsonObject = + -- We want to: + -- 1. Remove the `wasp-config` dev dependency from the root package in package-lock.json. + -- This is at `packageLock["packages"][""]["wasp-config"]`. + -- 2. Remove all package location entries for the `wasp-config` package + -- (i.e., entries whose location keys end in `/wasp-config`). + -- Example locations include: + -- packageLock["packages"]["../../data/packages/wasp-config"] + -- packageLock["packages"]["node_modules/wasp-config"] + -- packageLock["packages"]["/home/filip/../wasp-config"] + packageLockJsonObject + & key "packages" . key "" %~ removeWaspConfigFromDevDependenciesArray + & key "packages" . _Object + %~ HM.filterWithKey + (\packageLocation _ -> not $ isWaspConfigPackageLocation packageLocation) + + isWaspConfigPackageLocation :: Text -> Bool + isWaspConfigPackageLocation packageLocation = + (FP.pathSeparator : "wasp-config") `isSuffixOf` unpack packageLocation + + removeWaspConfigFromDevDependenciesArray :: Value -> Value + removeWaspConfigFromDevDependenciesArray original = + original & key "devDependencies" . _Object . at "wasp-config" .~ Nothing buildIO :: Path' Abs (Dir WaspProjectDir) -> - Path' Abs (Dir Wasp.Generator.ProjectRootDir) -> + Path' Abs (Dir ProjectRootDir) -> IO ([CompileWarning], [CompileError]) buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir buildDir where diff --git a/waspc/src/Wasp/Util/IO.hs b/waspc/src/Wasp/Util/IO.hs index 39b33c2ec1..f91aaf8e0e 100644 --- a/waspc/src/Wasp/Util/IO.hs +++ b/waspc/src/Wasp/Util/IO.hs @@ -18,6 +18,7 @@ module Wasp.Util.IO isDirectoryEmpty, writeFileFromText, readFileBytes, + writeFileBytes, ) where @@ -115,6 +116,9 @@ readFileStrict = T.IO.readFile . SP.toFilePath writeFile :: Path' Abs (File f) -> String -> IO () writeFile = P.writeFile . SP.fromAbsFile +writeFileBytes :: Path' Abs (File f) -> B.ByteString -> IO () +writeFileBytes = B.writeFile . SP.fromAbsFile + writeFileFromText :: Path' Abs (File f) -> Text -> IO () writeFileFromText = T.IO.writeFile . SP.fromAbsFile diff --git a/waspc/src/Wasp/Util/Json.hs b/waspc/src/Wasp/Util/Json.hs index dc7128ec1b..6a2262a703 100644 --- a/waspc/src/Wasp/Util/Json.hs +++ b/waspc/src/Wasp/Util/Json.hs @@ -1,9 +1,17 @@ -module Wasp.Util.Json (parseJsonWithComments) where +module Wasp.Util.Json + ( parseJsonWithComments, + updateJsonFile, + ) +where -import Data.Aeson (FromJSON) +import Control.Monad.Except (ExceptT (..), runExceptT) +import Control.Monad.IO.Class (liftIO) +import Data.Aeson (FromJSON, Value (..), eitherDecode, encode) +import StrongPath (Abs, File, Path') import System.Exit (ExitCode (..)) import qualified System.Process as P import Wasp.Util.Aeson (decodeFromString) +import qualified Wasp.Util.IO as IOUtil -- | Uses Node.js to parse JSON with comments by treating it as a JavaScript object. -- We use this technique because Aeson can't read JSON with comments and we didn't want to write @@ -16,3 +24,11 @@ parseJsonWithComments jsonStr = do case exitCode of ExitSuccess -> return $ decodeFromString response _exitFailure -> return $ Left stderr + +updateJsonFile :: (Value -> Value) -> Path' Abs (File a) -> IO (Either String ()) +updateJsonFile updateFn jsonFilePath = runExceptT $ do + jsonContent <- ExceptT $ eitherDecode <$> IOUtil.readFileBytes jsonFilePath + liftIO $ writeJsonValue jsonFilePath $ updateFn jsonContent + +writeJsonValue :: Path' Abs (File f) -> Value -> IO () +writeJsonValue file = IOUtil.writeFileBytes file . encode diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 9fd3e69690..177320f9f2 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -505,6 +505,8 @@ library cli-lib , mtl , async , exceptions + , lens ^>=5.1 + , lens-aeson ^>=1.1.3 , cryptonite , fsnotify , http-conduit @@ -521,6 +523,7 @@ library cli-lib , waspls , neat-interpolation , unliftio ^>= 0.2.20 + , unordered-containers ^>= 0.2.16 , bytestring ^>= 0.10.12 , tar ^>=0.5.1.1 , zlib ^>=0.6.3.0