Wasper can now specify npm dependencies.

This commit is contained in:
Martin Sosic 2020-09-25 14:36:31 +02:00 committed by Martin Šošić
parent d936eeba28
commit ca039304f9
17 changed files with 366 additions and 77 deletions

View File

@ -1,22 +1,9 @@
{{={= =}=}}
{
"name": "{= app.name =}",
"name": "{= wasp.app.name =}",
"version": "0.0.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.9.1",
"@reduxjs/toolkit": "^1.2.3",
"axios": "^0.20.0",
"lodash": "^4.17.15",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-query": "^2.14.1",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
"redux": "^4.0.5",
"uuid": "^3.4.0"
},
{=& depsChunk =},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",

View File

@ -12,14 +12,7 @@
"engines": {
"node": ">={= nodeVersion =}"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"morgan": "~1.9.1",
"@prisma/client": "2.x"
},
{=& depsChunk =},
"devDependencies": {
"nodemon": "^2.0.4",
"standard": "^14.3.4",

View File

@ -59,6 +59,7 @@ library:
- async
- bytestring
- regex-tdfa
- utf8-string
executables:
wasp:
@ -114,3 +115,4 @@ tests:
- parsec
- deepseq
- path
- unordered-containers

View File

@ -0,0 +1,58 @@
module Generator.PackageJsonGenerator
( resolveNpmDeps
, toPackageJsonDependenciesString
) where
import Data.List (find, intercalate)
import Data.Maybe (fromJust, isJust)
import qualified NpmDependency as ND
type NpmDependenciesConflictError = String
-- | Takes wasp npm dependencies and user npm dependencies and figures out how to
-- combine them together, returning (Right) a list of npm dependencies to be used on
-- behalf of wasp and then also a list of npm dependencies to be used on behalf
-- of user. These lists might be the same as the initial ones, but might also
-- be different.
-- On error (Left), returns list of conflicting user deps together with the error message
-- explaining what the error is.
resolveNpmDeps
:: [ND.NpmDependency]
-> [ND.NpmDependency]
-> Either [(ND.NpmDependency, NpmDependenciesConflictError)]
([ND.NpmDependency], [ND.NpmDependency])
resolveNpmDeps waspDeps userDeps = if null conflictingUserDeps
then Right (waspDeps, userDepsNotInWaspDeps)
else Left conflictingUserDeps
where
conflictingUserDeps :: [(ND.NpmDependency, NpmDependenciesConflictError)]
conflictingUserDeps = map (\(dep, err) -> (dep, fromJust err))
$ filter (isJust . snd)
$ map (\dep -> (dep, checkIfConflictingUserDep dep)) userDeps
checkIfConflictingUserDep :: ND.NpmDependency -> Maybe NpmDependenciesConflictError
checkIfConflictingUserDep userDep =
let attachErrorMessage dep = "Error: Dependency conflict for user npm dependency ("
++ ND._name dep ++ ", " ++ ND._version dep ++ "): "
++ "Version must be set to the exactly the same version as"
++ " the one wasp is using: "
++ ND._version dep
in attachErrorMessage <$> find (areTwoDepsInConflict userDep) waspDeps
areTwoDepsInConflict :: ND.NpmDependency -> ND.NpmDependency -> Bool
areTwoDepsInConflict d1 d2 = ND._name d1 == ND._name d2
&& ND._version d1 /= ND._version d2
userDepsNotInWaspDeps :: [ND.NpmDependency]
userDepsNotInWaspDeps = filter (not . isDepWithNameInWaspDeps . ND._name) userDeps
isDepWithNameInWaspDeps :: String -> Bool
isDepWithNameInWaspDeps name = any ((name ==). ND._name) waspDeps
toPackageJsonDependenciesString :: [ND.NpmDependency] -> String
toPackageJsonDependenciesString deps =
"\"dependencies\": {"
++ intercalate ",\n " (map (\dep -> "\"" ++ ND._name dep ++ "\": \"" ++ ND._version dep ++ "\"") deps)
++ "\n}"

View File

@ -4,27 +4,32 @@ module Generator.ServerGenerator
) where
import Data.Aeson (object, (.=))
import Data.List (intercalate)
import qualified Path as P
import CompileOptions (CompileOptions)
import Generator.Common (nodeVersionAsText)
import Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Generator.FileDraft (FileDraft)
import Generator.PackageJsonGenerator (resolveNpmDeps, toPackageJsonDependenciesString)
import Generator.ServerGenerator.Common (asServerFile,
asTmplFile)
import qualified Generator.ServerGenerator.Common as C
import qualified Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator
import Generator.ServerGenerator.OperationsGenerator (genOperations)
import qualified NpmDependency as ND
import StrongPath (File, Path,
Rel)
import qualified StrongPath as SP
import Wasp (Wasp)
import qualified Wasp
import qualified Wasp.NpmDependencies as WND
genServer :: Wasp -> CompileOptions -> [FileDraft]
genServer wasp _ = concat
[ [genReadme wasp]
, [genPackageJson wasp]
, [genPackageJson wasp waspNpmDeps]
, [genNpmrc wasp]
, [genNvmrc wasp]
, [genGitignore wasp]
@ -35,10 +40,35 @@ genServer wasp _ = concat
genReadme :: Wasp -> FileDraft
genReadme _ = C.copyTmplAsIs (asTmplFile [P.relfile|README.md|])
genPackageJson :: Wasp -> FileDraft
genPackageJson _ = C.makeTemplateFD (asTmplFile [P.relfile|package.json|])
(asServerFile [P.relfile|package.json|])
(Just (object ["nodeVersion" .= nodeVersionAsText]))
genPackageJson :: Wasp -> [ND.NpmDependency] -> FileDraft
genPackageJson wasp waspDeps = C.makeTemplateFD
(asTmplFile [P.relfile|package.json|])
(asServerFile [P.relfile|package.json|])
(Just $ object
[ "wasp" .= wasp
, "depsChunk" .= toPackageJsonDependenciesString (resolvedWaspDeps ++ resolvedUserDeps)
, "nodeVersion" .= nodeVersionAsText
])
where
(resolvedWaspDeps, resolvedUserDeps) =
case resolveNpmDeps waspDeps userDeps of
Right deps -> deps
Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors
userDeps :: [ND.NpmDependency]
userDeps = WND._dependencies $ Wasp.getNpmDependencies wasp
waspNpmDeps :: [ND.NpmDependency]
waspNpmDeps = ND.fromList
[ ("cookie-parser", "~1.4.4")
, ("cors", "^2.8.5")
, ("debug", "~2.6.9")
, ("express", "~4.16.1")
, ("morgan", "~1.9.1")
, ("@prisma/client", "2.x")
]
-- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps).
genNpmrc :: Wasp -> FileDraft
genNpmrc _ = C.makeTemplateFD (asTmplFile [P.relfile|npmrc|])

View File

@ -2,40 +2,79 @@ module Generator.WebAppGenerator
( generateWebApp
) where
import Data.Aeson (ToJSON(..), (.=), object)
import qualified Path as P
import Data.Aeson (ToJSON (..),
object, (.=))
import Data.List (intercalate)
import qualified Path as P
import StrongPath (Path, Rel, Dir, (</>))
import qualified StrongPath as SP
import qualified Util
import CompileOptions (CompileOptions)
import Wasp
import Generator.FileDraft
import Generator.ExternalCodeGenerator (generateExternalCodeDir)
import qualified Generator.WebAppGenerator.EntityGenerator as EntityGenerator
import qualified Generator.WebAppGenerator.RouterGenerator as RouterGenerator
import Generator.WebAppGenerator.Common (asTmplFile, asWebAppFile, asWebAppSrcFile)
import qualified Generator.WebAppGenerator.Common as C
import CompileOptions (CompileOptions)
import Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Generator.FileDraft
import Generator.PackageJsonGenerator (resolveNpmDeps, toPackageJsonDependenciesString)
import Generator.WebAppGenerator.Common (asTmplFile,
asWebAppFile,
asWebAppSrcFile)
import qualified Generator.WebAppGenerator.Common as C
import qualified Generator.WebAppGenerator.EntityGenerator as EntityGenerator
import qualified Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator
import Generator.WebAppGenerator.OperationsGenerator (genOperations)
import Generator.WebAppGenerator.OperationsGenerator (genOperations)
import qualified Generator.WebAppGenerator.RouterGenerator as RouterGenerator
import qualified NpmDependency as ND
import StrongPath (Dir, Path,
Rel, (</>))
import qualified StrongPath as SP
import qualified Util
import Wasp
import qualified Wasp.NpmDependencies as WND
generateWebApp :: Wasp -> CompileOptions -> [FileDraft]
generateWebApp wasp _ = concatMap ($ wasp)
[ (:[]) . generateReadme
, (:[]) . generatePackageJson
, (:[]) . generateGitignore
, generatePublicDir
, generateSrcDir
, generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy
generateWebApp wasp _ = concat
[ [generateReadme wasp]
, [genPackageJson wasp waspNpmDeps]
, [generateGitignore wasp]
, generatePublicDir wasp
, generateSrcDir wasp
, generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy wasp
]
generateReadme :: Wasp -> FileDraft
generateReadme wasp = C.makeSimpleTemplateFD (asTmplFile [P.relfile|README.md|]) wasp
generatePackageJson :: Wasp -> FileDraft
generatePackageJson wasp = C.makeSimpleTemplateFD (asTmplFile [P.relfile|package.json|]) wasp
genPackageJson :: Wasp -> [ND.NpmDependency] -> FileDraft
genPackageJson wasp waspDeps = C.makeTemplateFD
(C.asTmplFile [P.relfile|package.json|])
(C.asWebAppFile [P.relfile|package.json|])
(Just $ object
[ "wasp" .= wasp
, "depsChunk" .= toPackageJsonDependenciesString (resolvedWaspDeps ++ resolvedUserDeps)
])
where
(resolvedWaspDeps, resolvedUserDeps) =
case resolveNpmDeps waspDeps userDeps of
Right deps -> deps
Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors
userDeps :: [ND.NpmDependency]
userDeps = WND._dependencies $ Wasp.getNpmDependencies wasp
waspNpmDeps :: [ND.NpmDependency]
waspNpmDeps = ND.fromList
[ ("@material-ui/core", "^4.9.1")
, ("@reduxjs/toolkit", "^1.2.3")
, ("axios", "^0.20.0")
, ("lodash", "^4.17.15")
, ("react", "^16.12.0")
, ("react-dom", "^16.12.0")
, ("react-query", "^2.14.1")
, ("react-redux", "^7.1.3")
, ("react-router-dom", "^5.1.2")
, ("react-scripts", "3.4.0")
, ("redux", "^4.0.5")
, ("uuid", "^3.4.0")
]
-- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps).
generateGitignore :: Wasp -> FileDraft
generateGitignore wasp = C.makeTemplateFD (asTmplFile [P.relfile|gitignore|])

View File

@ -43,6 +43,9 @@ reservedNameQuery = "query"
reservedNameAction :: String
reservedNameAction = "action"
reservedNameDependencies :: String
reservedNameDependencies = "dependencies"
-- * Data types.
reservedNameString :: String
@ -63,6 +66,7 @@ reservedNames =
, reservedNameFrom
-- * Wasp element types
, reservedNameApp
, reservedNameDependencies
, reservedNamePage
, reservedNameRoute
, reservedNameEntityPSL

View File

@ -0,0 +1,21 @@
module NpmDependency
( NpmDependency (..)
, fromList
) where
import Data.Aeson (ToJSON (..), object, (.=))
data NpmDependency = NpmDependency
{ _name :: !String
, _version :: !String }
deriving (Show, Eq)
fromList :: [(String, String)] -> [NpmDependency]
fromList = map (\(name, version) -> NpmDependency { _name = name, _version = version })
instance ToJSON NpmDependency where
toJSON npmDep = object
[ "name" .= _name npmDep
, "version" .= _version npmDep
]

View File

@ -23,6 +23,7 @@ import Parser.JsImport (jsImport)
import Parser.Common (runWaspParser)
import qualified Parser.Query
import qualified Parser.Action
import qualified Parser.NpmDependencies
waspElement :: Parser Wasp.WaspElement
waspElement
@ -37,6 +38,7 @@ waspElement
<|> waspElementEntity
<|> waspElementEntityForm
<|> waspElementEntityList
<|> waspElementNpmDependencies
waspElementApp :: Parser Wasp.WaspElement
waspElementApp = Wasp.WaspElementApp <$> app
@ -68,6 +70,10 @@ waspElementQuery = Wasp.WaspElementQuery <$> Parser.Query.query
waspElementAction :: Parser Wasp.WaspElement
waspElementAction = Wasp.WaspElementAction <$> Parser.Action.action
waspElementNpmDependencies :: Parser Wasp.WaspElement
waspElementNpmDependencies = Wasp.WaspElementNpmDependencies <$> Parser.NpmDependencies.npmDependencies
-- | Top level parser, produces Wasp.
waspParser :: Parser Wasp.Wasp
waspParser = do

View File

@ -36,7 +36,7 @@ waspElementNameAndClosure
:: String -- ^ Element type
-> Parser a -- ^ Closure parser (needs to parse braces as well, not just the content)
-> Parser (String, a) -- ^ Name of the element and parsed closure content.
waspElementNameAndClosure elementType closure =
waspElementNameAndClosure elementType closure =
-- NOTE(matija): It is important to have `try` here because we don't want to consume the
-- content intended for other parsers.
-- E.g. if we tried to parse "entity-form" this parser would have been tried first for
@ -54,7 +54,7 @@ waspElementNameAndClosure elementType closure =
elementName <- L.identifier
closureContent <- closure
return (elementName, closureContent)
return (elementName, closureContent)
-- | Parses declaration of a wasp element linked to an entity.
-- E.g. "entity-form<Task> ..." or "action<Task> ..."

View File

@ -0,0 +1,31 @@
module Parser.NpmDependencies
( npmDependencies
) where
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy.UTF8 as BLU
import qualified Data.HashMap.Strict as M
import Text.Parsec (try)
import Text.Parsec.String (Parser)
import qualified Lexer as L
import qualified NpmDependency as ND
import qualified Parser.Common as P
import Wasp.NpmDependencies (NpmDependencies)
import qualified Wasp.NpmDependencies as NpmDependencies
npmDependencies :: Parser NpmDependencies
npmDependencies = try $ do
L.reserved L.reservedNameDependencies
closureContent <- P.waspNamedClosure "json"
let jsonBytestring = BLU.fromString $ "{ " ++ closureContent ++ " }"
npmDeps <- case Aeson.eitherDecode' jsonBytestring :: Either String (M.HashMap String String) of
Left errorMessage -> fail $ "Failed to parse dependencies JSON: " ++ errorMessage
Right rawDeps -> return $ map rawDepToNpmDep (M.toList rawDeps)
return NpmDependencies.NpmDependencies
{ NpmDependencies._dependencies = npmDeps
}
where
rawDepToNpmDep :: (String, String) -> ND.NpmDependency
rawDepToNpmDep (name, version) = ND.NpmDependency { ND._name = name, ND._version = version }

View File

@ -38,38 +38,43 @@ module Wasp
, setExternalCodeFiles
, getExternalCodeFiles
, setNpmDependencies
, getNpmDependencies
) where
import Data.Aeson ((.=), object, ToJSON(..))
import Data.Aeson (ToJSON (..), object, (.=))
import qualified ExternalCode
import Wasp.App
import qualified Util as U
import qualified Wasp.Action
import Wasp.App
import Wasp.EntityPSL
import Wasp.JsImport
import Wasp.NpmDependencies (NpmDependencies)
import qualified Wasp.NpmDependencies
import Wasp.Page
import qualified Wasp.Query
import Wasp.Route
-- TODO(matija): old Entity stuff, to be removed
import Wasp.Entity
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityList as EL
import Wasp.Entity
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityList as EL
import Wasp.EntityPSL
import Wasp.JsImport
import Wasp.Page
import Wasp.Route
import qualified Wasp.Query
import qualified Wasp.Action
import qualified Util as U
-- * Wasp
data Wasp = Wasp
{ waspElements :: [WaspElement]
, waspJsImports :: [JsImport]
{ waspElements :: [WaspElement]
, waspJsImports :: [JsImport]
, externalCodeFiles :: [ExternalCode.File]
} deriving (Show, Eq)
data WaspElement
= WaspElementApp !App
| WaspElementPage !Page
| WaspElementNpmDependencies !NpmDependencies
| WaspElementRoute !Route
| WaspElementEntityPSL !Wasp.EntityPSL.EntityPSL
| WaspElementQuery !Wasp.Query.Query
@ -115,7 +120,7 @@ getApp wasp = let apps = getApps wasp in
isAppElem :: WaspElement -> Bool
isAppElem WaspElementApp{} = True
isAppElem _ = False
isAppElem _ = False
getApps :: Wasp -> [App]
getApps wasp = [app | (WaspElementApp app) <- waspElements wasp]
@ -126,6 +131,25 @@ setApp wasp app = wasp { waspElements = (WaspElementApp app) : (filter (not . is
fromApp :: App -> Wasp
fromApp app = fromWaspElems [WaspElementApp app]
-- * NpmDependencies
getNpmDependencies :: Wasp -> NpmDependencies
getNpmDependencies wasp
= let depses = [d | (WaspElementNpmDependencies d) <- waspElements wasp]
in case depses of
[] -> Wasp.NpmDependencies.empty
[deps] -> deps
_ -> error "Wasp can't contain more than one NpmDependencies element!"
isNpmDependenciesElem :: WaspElement -> Bool
isNpmDependenciesElem WaspElementNpmDependencies{} = True
isNpmDependenciesElem _ = False
setNpmDependencies :: Wasp -> NpmDependencies -> Wasp
setNpmDependencies wasp deps = wasp
{ waspElements = WaspElementNpmDependencies deps : filter (not . isNpmDependenciesElem) (waspElements wasp)
}
-- * Routes
getRoutes :: Wasp -> [Route]

View File

@ -0,0 +1,20 @@
module Wasp.NpmDependencies
( NpmDependencies(..)
, empty
) where
import Data.Aeson (ToJSON (..), object, (.=))
import NpmDependency
data NpmDependencies = NpmDependencies
{ _dependencies :: ![NpmDependency]
} deriving (Show, Eq)
empty :: NpmDependencies
empty = NpmDependencies { _dependencies = [] }
instance ToJSON NpmDependencies where
toJSON deps = object
[ "dependencies" .= _dependencies deps
]

View File

@ -0,0 +1,34 @@
module Generator.PackageJsonGeneratorTest where
import Test.Tasty.Hspec
import Generator.PackageJsonGenerator (resolveNpmDeps)
import qualified NpmDependency as ND
spec_resolveNpmDeps :: Spec
spec_resolveNpmDeps = do
let waspDeps = [ ("axios", "^0.20.0")
, ("lodash", "^4.17.15")
]
it "Concatenates two distincts lists of deps." $ do
let userDeps = [ ("foo", "bar")
, ("foo2", "bar2")
]
resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps)
`shouldBe` Right (ND.fromList waspDeps, ND.fromList userDeps)
it "Does not repeat dep if it is both user and wasp dep." $ do
let userDeps = [ ("axios", "^0.20.0")
, ("foo", "bar")
]
resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps)
`shouldBe` Right (ND.fromList waspDeps, ND.fromList [("foo", "bar")])
it "Reports error if user dep version does not match wasp dep version." $ do
let userDeps = [ ("axios", "^1.20.0")
, ("foo", "bar")
]
let Left conflicts = resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps)
(map fst conflicts) `shouldBe` ND.fromList [("axios", "^1.20.0")]

View File

@ -0,0 +1,28 @@
module Parser.NpmDependenciesTest where
import Test.Tasty.Hspec
import Data.Aeson ((.=))
import Data.Either (isLeft)
import Data.HashMap.Strict (fromList)
import qualified NpmDependency as ND
import Parser.Common (runWaspParser)
import Parser.NpmDependencies (npmDependencies)
import Wasp.NpmDependencies
spec_parseNpmDependencies :: Spec
spec_parseNpmDependencies = do
describe "Parsing npm dependencies" $ do
it "When given a valid declaration with valid json, parses it correctly" $ do
runWaspParser npmDependencies "dependencies {=json \"foo\": \"test1\", \"bar\": \"test2\" json=}"
`shouldBe` Right NpmDependencies
{ _dependencies =
[ ND.NpmDependency { ND._name = "foo", ND._version = "test1" }
, ND.NpmDependency { ND._name = "bar", ND._version = "test2" }
]
}
it "When given invalid json, reports error" $ do
isLeft (runWaspParser npmDependencies "dependencies {=json foo: 42 json=}")
`shouldBe` True

View File

@ -1,22 +1,24 @@
module Parser.ParserTest where
import Data.Either
import qualified Path.Posix as PPosix
import qualified Path.Posix as PPosix
import Test.Tasty.Hspec
import NpmDependency as ND
import Parser
import qualified StrongPath as SP
import qualified StrongPath as SP
import Wasp
import qualified Wasp.EntityPSL
import qualified Wasp.JsCode
import qualified Wasp.JsImport
import qualified Wasp.NpmDependencies
import qualified Wasp.Page
import qualified Wasp.Query
import qualified Wasp.Route as R
import qualified Wasp.Route as R
-- TODO(matija): old Entity stuff, to be removed.
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityList as EL
import qualified Wasp.EntityForm as EF
import qualified Wasp.EntityList as EL
spec_parseWasp :: Spec
@ -127,6 +129,14 @@ spec_parseWasp =
, Wasp.JsImport._from = SP.fromPathRelFileP [PPosix.relfile|some/path|]
}
}
, WaspElementNpmDependencies $ Wasp.NpmDependencies.NpmDependencies
{ Wasp.NpmDependencies._dependencies =
[ ND.NpmDependency
{ ND._name = "lodash"
, ND._version = "^4.17.15"
}
]
}
]
`setJsImports` [ JsImport (Just "something") [] (SP.fromPathRelFileP [PPosix.relfile|some/file|]) ]
)

View File

@ -69,8 +69,10 @@ entity-list<Task> TaskList {
}
}
query myQuery {
fn: import { myJsQuery } from "@ext/some/path"
}
dependencies {=json
"lodash": "^4.17.15"
json=}