Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Userland files validation #2297

Merged
merged 24 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import qualified Data.Text as T
import StrongPath (Abs, Dir, File, Path')
import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName)
import Wasp.Project.Analyze (findPackageJsonFile, findWaspFile)
import Wasp.ExternalConfig.PackageJson (findPackageJsonFile)
import Wasp.Project.Analyze (findWaspFile)
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Util.IO as IOUtil

Expand Down
2 changes: 1 addition & 1 deletion waspc/src/Wasp/AppSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ import qualified Wasp.AppSpec.ExternalFiles as ExternalFiles
import Wasp.AppSpec.Job (Job)
import Wasp.AppSpec.Operation (Operation)
import qualified Wasp.AppSpec.Operation as AS.Operation
import Wasp.AppSpec.PackageJson (PackageJson)
import Wasp.AppSpec.Page (Page)
import Wasp.AppSpec.Query (Query)
import Wasp.AppSpec.Route (Route)
import Wasp.Env (EnvVar)
import Wasp.ExternalConfig.PackageJson (PackageJson)
import Wasp.Node.Version (oldestWaspSupportedNodeVersion)
import Wasp.Project.Common (WaspProjectDir)
import Wasp.Project.Db.Migrations (DbMigrationsDir)
Expand Down
25 changes: 0 additions & 25 deletions waspc/src/Wasp/AppSpec/PackageJson.hs

This file was deleted.

31 changes: 31 additions & 0 deletions waspc/src/Wasp/ExternalConfig.hs
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Wasp.ExternalConfig
( ExternalConfigs (..),
analyzeExternalConfigs,
)
where

import Control.Monad.Except (ExceptT (ExceptT), runExceptT)
import StrongPath (Abs, Dir, Path')
import Wasp.ExternalConfig.PackageJson (PackageJson, analyzePackageJsonContent)
import Wasp.ExternalConfig.TsConfig (TsConfig, analyzeTsConfigContent)
import Wasp.Project.Common
( CompileError,
WaspProjectDir,
)

data ExternalConfigs = ExternalConfigs
{ packageJson :: PackageJson,
tsConfig :: TsConfig
}
deriving (Show)

analyzeExternalConfigs :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] ExternalConfigs)
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
analyzeExternalConfigs waspDir = runExceptT $ do
packageJsonContent <- ExceptT $ analyzePackageJsonContent waspDir
tsConfigContent <- ExceptT $ analyzeTsConfigContent waspDir

return $
ExternalConfigs
{ packageJson = packageJsonContent,
tsConfig = tsConfigContent
}
127 changes: 127 additions & 0 deletions waspc/src/Wasp/ExternalConfig/PackageJson.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{-# LANGUAGE DeriveGeneric #-}

module Wasp.ExternalConfig.PackageJson
( PackageJson (..),
getDependencies,
getDevDependencies,
analyzePackageJsonContent,
findPackageJsonFile,
)
where

import Control.Monad.Except (ExceptT (ExceptT), runExceptT)
import Data.Aeson (FromJSON)
import qualified Data.Aeson as Aeson
import Data.Map (Map)
import qualified Data.Map as M
import GHC.Generics (Generic)
import StrongPath (Abs, Dir, File', Path', toFilePath)
import Wasp.AppSpec.App.Dependency (Dependency)
import qualified Wasp.AppSpec.App.Dependency as D
import Wasp.Generator.Common
( prismaVersion,
reactRouterVersion,
)
import Wasp.Project.Common
( CompileError,
WaspProjectDir,
findFileInWaspProjectDir,
packageJsonInWaspProjectDir,
)
import Wasp.Util (maybeToEither)
import qualified Wasp.Util.IO as IOUtil

data PackageJson = PackageJson
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
{ name :: !String,
dependencies :: !DependenciesMap,
devDependencies :: !DependenciesMap
}
deriving (Show, Generic)

getDependencies :: PackageJson -> [Dependency]
getDependencies packageJson = D.fromList $ M.toList $ dependencies packageJson

getDevDependencies :: PackageJson -> [Dependency]
getDevDependencies packageJson = D.fromList $ M.toList $ devDependencies packageJson

type DependenciesMap = Map PackageName PackageVersion

type PackageName = String

type PackageVersion = String

instance FromJSON PackageJson
Martinsos marked this conversation as resolved.
Show resolved Hide resolved

analyzePackageJsonContent :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] PackageJson)
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
analyzePackageJsonContent waspProjectDir = runExceptT $ do
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
packageJsonFile <- ExceptT findPackageJsonFileOrError
packageJson <- ExceptT $ readPackageJsonFile packageJsonFile
ExceptT $ validatePackageJson packageJson
where
findPackageJsonFileOrError = maybeToEither [fileNotFoundMessage] <$> findPackageJsonFile waspProjectDir
fileNotFoundMessage = "Couldn't find the package.json file in the " ++ toFilePath waspProjectDir ++ " directory"

validatePackageJson :: PackageJson -> IO (Either [CompileError] PackageJson)
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
validatePackageJson packageJson =
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
return $
if null packageJsonErrors
then Right packageJson
else Left packageJsonErrors
where
packageJsonErrors =
concat
[ -- Wasp needs the Wasp SDK to be installed in the project.
validate ("wasp", "file:.wasp/out/sdk/wasp", RequiredPackage),
-- Wrong version of Prisma will break the generated code.
validate ("prisma", show prismaVersion, RequiredDevPackage),
-- Installing the wrong version of "react-router-dom" can make users believe that they
-- can use features that are not available in the version that Wasp supports.
validate ("react-router-dom", show reactRouterVersion, OptionalPackage)
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
]
validate = validatePackageInDeps packageJson

findPackageJsonFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
findPackageJsonFile waspProjectDir = findFileInWaspProjectDir waspProjectDir packageJsonInWaspProjectDir

readPackageJsonFile :: Path' Abs File' -> IO (Either [CompileError] PackageJson)
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
readPackageJsonFile packageJsonFile = do
byteString <- IOUtil.readFileBytes packageJsonFile
return $ maybeToEither ["Error parsing the package.json file"] $ Aeson.decode byteString

data PackageValidationType = RequiredPackage | RequiredDevPackage | OptionalPackage

validatePackageInDeps :: PackageJson -> (PackageName, PackageVersion, PackageValidationType) -> [CompileError]
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
validatePackageInDeps packageJson (packageName, expectedPackageVersion, validationType) =
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
case map (M.lookup packageName) depsToCheck of
(Just actualPackageVersion : _) ->
if actualPackageVersion == expectedPackageVersion
then []
else [incorrectVersionMessage]
_rest -> case validationType of
RequiredPackage -> [requiredPackageMessage "dependencies"]
RequiredDevPackage -> [requiredPackageMessage "devDependencies"]
OptionalPackage -> []
where
depsToCheck = case validationType of
RequiredPackage -> [dependencies packageJson]
RequiredDevPackage -> [devDependencies packageJson]
-- Users can install packages that don't need to be strictly in dependencies or devDependencies
infomiho marked this conversation as resolved.
Show resolved Hide resolved
-- which means Wasp needs to check both to validate the correct version of the package.
OptionalPackage -> [dependencies packageJson, devDependencies packageJson]

incorrectVersionMessage :: String
incorrectVersionMessage =
unwords
["The", show packageName, "package must have version", show expectedPackageVersion, "in package.json."]
Martinsos marked this conversation as resolved.
Show resolved Hide resolved

requiredPackageMessage :: String -> String
requiredPackageMessage packageJsonLocation =
unwords
[ "The",
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
show packageName,
"package with version",
show expectedPackageVersion,
"must be defined in",
show packageJsonLocation,
"in package.json."
]
130 changes: 130 additions & 0 deletions waspc/src/Wasp/ExternalConfig/TsConfig.hs
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}

module Wasp.ExternalConfig.TsConfig
( TsConfig (..),
analyzeTsConfigContent,
)
where

import Control.Monad.Except
import Data.Aeson (FromJSON, parseJSON, withObject, (.:?))
import qualified Data.ByteString.Lazy.UTF8 as BS
import Data.Either.Extra (maybeToEither)
import GHC.Generics (Generic)
import StrongPath (Abs, Dir, File', Path', toFilePath)
import Wasp.Project.Common
( CompileError,
WaspProjectDir,
findFileInWaspProjectDir,
tsConfigInWaspProjectDir,
)
import qualified Wasp.Util.IO as IOUtil
import Wasp.Util.Json (parseJsonWithComments)

data TsConfig = TsConfig
{ compilerOptions :: !CompilerOptions
}
deriving (Show, Generic)

instance FromJSON TsConfig

data CompilerOptions = CompilerOptions
{ _module :: !(Maybe String),
target :: !(Maybe String),
moduleResolution :: !(Maybe String),
jsx :: !(Maybe String),
strict :: !(Maybe Bool),
esModuleInterop :: !(Maybe Bool),
lib :: !(Maybe [String]),
allowJs :: !(Maybe Bool),
typeRoots :: !(Maybe [String]),
outDir :: !(Maybe String)
}
deriving (Show)

instance FromJSON CompilerOptions where
parseJSON = withObject "CompilerOptions" $ \v ->
CompilerOptions
-- We couldn't use the Generic deriving for this because of the _ prefix in the "module" field name.
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
<$> v .:? "module"
<*> v .:? "target"
<*> v .:? "moduleResolution"
<*> v .:? "jsx"
<*> v .:? "strict"
<*> v .:? "esModuleInterop"
<*> v .:? "lib"
<*> v .:? "allowJs"
<*> v .:? "typeRoots"
<*> v .:? "outDir"

analyzeTsConfigContent :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] TsConfig)
analyzeTsConfigContent waspDir = runExceptT $ do
tsConfigFile <- ExceptT findTsConfigOrError
tsConfig <- ExceptT $ readTsConfigFile tsConfigFile
ExceptT $ validateTsConfig tsConfig
where
findTsConfigOrError = maybeToEither [fileNotFoundMessage] <$> findTsConfigFile waspDir
fileNotFoundMessage = "Couldn't find the tsconfig.json file in the " ++ toFilePath waspDir ++ " directory"

findTsConfigFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findTsConfigFile waspProjectDir = findFileInWaspProjectDir waspProjectDir tsConfigInWaspProjectDir

readTsConfigFile :: Path' Abs File' -> IO (Either [CompileError] TsConfig)
readTsConfigFile tsConfigFile = do
tsConfigContent <- IOUtil.readFileBytes tsConfigFile

parseResult <- parseJsonWithComments . BS.toString $ tsConfigContent

case parseResult of
Right tsConfig -> return $ Right tsConfig
Left err -> return $ Left ["Failed to parse tsconfig.json file: " ++ err]

validateTsConfig :: TsConfig -> IO (Either [CompileError] TsConfig)
validateTsConfig tsConfig =
return $
if null tsConfigErrors
then Right tsConfig
else Left tsConfigErrors
where
tsConfigErrors =
concat
[ validateRequiredField "module" "esnext" (_module compilerOptionsValues),
validateRequiredField "target" "esnext" (target compilerOptionsValues),
validateRequiredField "moduleResolution" "bundler" (moduleResolution compilerOptionsValues),
validateRequiredField "jsx" "preserve" (jsx compilerOptionsValues),
validateRequiredField "strict" True (strict compilerOptionsValues),
validateRequiredField "esModuleInterop" True (esModuleInterop compilerOptionsValues),
validateRequiredField "lib" ["dom", "dom.iterable", "esnext"] (lib compilerOptionsValues),
validateRequiredField "allowJs" True (allowJs compilerOptionsValues),
validateRequiredField "typeRoots" ["node_modules/@testing-library", "node_modules/@types"] (typeRoots compilerOptionsValues),
validateRequiredField "outDir" ".wasp/phantom" (outDir compilerOptionsValues)
]
compilerOptionsValues = compilerOptions tsConfig

-- | Used to show expected values in error messages.
infomiho marked this conversation as resolved.
Show resolved Hide resolved
class ShowJs a where
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
showJs :: a -> String

instance ShowJs String where
showJs = show

instance ShowJs [String] where
showJs = show

instance ShowJs Bool where
showJs True = "true"
showJs False = "false"

type FieldName = String

validateRequiredField :: (Eq value, ShowJs value) => FieldName -> value -> Maybe value -> [CompileError]
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
validateRequiredField fieldName expectedValue maybeUserProvidedValue = case maybeUserProvidedValue of
Nothing -> [missingFieldErrorMessage]
Just userProvidedValue ->
if userProvidedValue /= expectedValue
then [invalidValueErrorMessage]
else []
where
invalidValueErrorMessage = unwords ["Invalid value for the", show fieldName, "field in tsconfig.json file, expected value:", showJs expectedValue ++ "."]
missingFieldErrorMessage = unwords ["The", show fieldName, "field is missing in tsconfig.json. Expected value:", showJs expectedValue ++ "."]
4 changes: 4 additions & 0 deletions waspc/src/Wasp/Generator/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Wasp.Generator.Common
GeneratedSrcDir,
makeJsArrayFromHaskellList,
dropExtensionFromImportPath,
reactRouterVersion,
)
where

Expand Down Expand Up @@ -57,6 +58,9 @@ prismaVersion :: SV.Version
-- Then, make sure `data/Generator/templates/sdk/wasp/prisma-runtime-library.d.ts` is up to date.
prismaVersion = SV.Version 5 19 1

reactRouterVersion :: SV.ComparatorSet
reactRouterVersion = SV.backwardsCompatibleWith $ SV.Version 5 3 3
infomiho marked this conversation as resolved.
Show resolved Hide resolved

makeJsonWithEntityData :: String -> Aeson.Value
makeJsonWithEntityData name =
object
Expand Down
6 changes: 3 additions & 3 deletions waspc/src/Wasp/Generator/NpmDependencies.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import GHC.Generics
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App.Dependency as D
import qualified Wasp.AppSpec.PackageJson as AS.PackageJson
import qualified Wasp.ExternalConfig.PackageJson as EC.PackageJson
import Wasp.Generator.Monad (Generator, GeneratorError (..), logAndThrowGeneratorError)

data NpmDepsForFramework = NpmDepsForFramework
Expand Down Expand Up @@ -110,9 +110,9 @@ buildWaspFrameworkNpmDeps spec forServer forWebApp =
getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser
getUserNpmDepsForPackage spec =
NpmDepsForUser
{ userDependencies = AS.PackageJson.getDependencies $ AS.packageJson spec,
{ userDependencies = EC.PackageJson.getDependencies $ AS.packageJson spec,
-- Should we allow user devDependencies? https://github.com/wasp-lang/wasp/issues/456
userDevDependencies = AS.PackageJson.getDevDependencies $ AS.packageJson spec
userDevDependencies = EC.PackageJson.getDevDependencies $ AS.packageJson spec
}

conflictErrorToMessage :: DependencyConflictError -> String
Expand Down
Loading
Loading