Replace Wasp with AppSpec and Parser with Analyzer. (#423)

This commit is contained in:
Martin Šošić 2022-01-20 11:45:14 +01:00 committed by GitHub
parent b74eb88ff0
commit 8d2c33ab28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 1381 additions and 3383 deletions

View File

@ -38,8 +38,8 @@ app TodoApp {
title: "Todo App" title: "Todo App"
} }
route "/" -> page Main route RootRoute { path: "/", to: MainPage }
page Main { page MainPage {
component: import Main from "@ext/pages/Main.js" // Importing React component. component: import Main from "@ext/pages/Main.js" // Importing React component.
} }

View File

@ -3,55 +3,64 @@ app Conduit {
head: [ head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />" "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
] ],
}
auth { auth: {
userEntity: User, userEntity: User,
methods: [ EmailAndPassword ], methods: [ EmailAndPassword ],
onAuthFailedRedirectTo: "/login" onAuthFailedRedirectTo: "/login"
},
db: { system: PostgreSQL },
dependencies: [
("prop-types", "15.7.2"),
("react-markdown", "5.0.3"),
("moment", "2.29.1"),
("@material-ui/core", "4.11.3"),
("@material-ui/icons", "4.11.2"),
("slug", "4.0.2")
]
} }
db {
system: PostgreSQL
}
// ----------------- Pages ------------------ // // ----------------- Pages ------------------ //
route "/" -> page Main route RootRoute { path: "/", to: MainPage }
page Main { page MainPage {
component: import Main from "@ext/MainPage.js" component: import Main from "@ext/MainPage.js"
} }
route "/login" -> page LogIn route LogInRoute { path: "/login", to: LogInPage }
page LogIn { page LogInPage {
component: import LogIn from "@ext/auth/LoginPage.js" component: import LogIn from "@ext/auth/LoginPage.js"
} }
route "/register" -> page SignUp route RegisterRoute { path: "/register", to: SignUpPage }
page SignUp { page SignUpPage {
component: import SignUp from "@ext/auth/SignupPage.js" component: import SignUp from "@ext/auth/SignupPage.js"
} }
route "/settings" -> page UserSettings route UserSettingsRoute { path: "/settings", to: UserSettingsPage }
page UserSettings { page UserSettingsPage {
authRequired: true, authRequired: true,
component: import UserSettings from "@ext/user/components/UserSettingsPage.js" component: import UserSettings from "@ext/user/components/UserSettingsPage.js"
} }
route "/@:username" -> page UserProfile route UserProfileRoute { path: "/@:username", to: UserProfilePage }
page UserProfile { page UserProfilePage {
component: import UserProfile from "@ext/user/components/UserProfilePage.js" component: import UserProfile from "@ext/user/components/UserProfilePage.js"
} }
route "/editor/:articleSlug?" -> page ArticleEditor route ArticleEditorRoute { path: "/editor/:articleSlug?", to: ArticleEditorPage }
page ArticleEditor { page ArticleEditorPage {
authRequired: true, authRequired: true,
component: import ArticleEditor from "@ext/article/components/ArticleEditorPage.js" component: import ArticleEditor from "@ext/article/components/ArticleEditorPage.js"
} }
route "/article/:articleSlug" -> page ArticleView route ArticleViewRoute { path: "/article/:articleSlug", to: ArticleViewPage }
page ArticleView { page ArticleViewPage {
component: import ArticleView from "@ext/article/components/ArticleViewPage.js" component: import ArticleView from "@ext/article/components/ArticleViewPage.js"
} }
@ -190,12 +199,3 @@ query getTags {
} }
// -------------------------------------------- // // -------------------------------------------- //
dependencies {=json
"prop-types": "15.7.2",
"react-markdown": "5.0.3",
"moment": "2.29.1",
"@material-ui/core": "4.11.3",
"@material-ui/icons": "4.11.2",
"slug": "4.0.2"
json=}

View File

@ -1,36 +1,36 @@
app Thoughts { app Thoughts {
title: "Thoughts" title: "Thoughts",
} db: { system: PostgreSQL },
auth: {
db {
system: PostgreSQL
}
auth {
userEntity: User, userEntity: User,
methods: [ EmailAndPassword ], methods: [ EmailAndPassword ],
onAuthFailedRedirectTo: "/login" onAuthFailedRedirectTo: "/login"
},
dependencies: [
("react-markdown", "6.0.1"),
("color-hash", "2.0.1")
]
} }
route "/" -> page Main route MainRoute { path: "/", to: MainPage }
page Main { page MainPage {
component: import Main from "@ext/MainPage.js", component: import Main from "@ext/MainPage.js",
authRequired: true authRequired: true
} }
route "/thoughts" -> page Thoughts route ThoughtsRoute { path: "/thoughts", to: ThoughtsPage }
page Thoughts { page ThoughtsPage {
component: import Thoughts from "@ext/ThoughtsPage.js", component: import Thoughts from "@ext/ThoughtsPage.js",
authRequired: true authRequired: true
} }
route "/login" -> page Login route LoginRoute { path: "/login", to: LoginPage }
page Login { page LoginPage {
component: import Login from "@ext/LoginPage.js" component: import Login from "@ext/LoginPage.js"
} }
route "/signup" -> page Signup route SignupRoute { path: "/signup", to: SignupPage }
page Signup { page SignupPage {
component: import Signup from "@ext/SignupPage" component: import Signup from "@ext/SignupPage"
} }
@ -80,8 +80,3 @@ entity User {=psl
thoughts Thought[] thoughts Thought[]
tags Tag[] tags Tag[]
psl=} psl=}
dependencies {=json
"react-markdown": "6.0.1",
"color-hash": "2.0.1"
json=}

View File

@ -1,26 +1,30 @@
app TodoApp { app TodoApp {
title: "Todo app" title: "Todo app",
}
auth { auth: {
userEntity: User, userEntity: User,
methods: [ EmailAndPassword ], methods: [ EmailAndPassword ],
onAuthFailedRedirectTo: "/login" onAuthFailedRedirectTo: "/login"
},
dependencies: [
("react-clock", "3.0.0")
]
} }
route "/" -> page Main route RootRoute { path: "/", to: MainPage }
page Main { page MainPage {
authRequired: true, authRequired: true,
component: import Main from "@ext/MainPage.js" component: import Main from "@ext/MainPage.js"
} }
route "/signup" -> page Signup route SignupRoute { path: "/signup", to: SignupPage }
page Signup { page SignupPage {
component: import Signup from "@ext/SignupPage" component: import Signup from "@ext/SignupPage"
} }
route "/login" -> page Login route LoginRoute { path: "/login", to: LoginPage }
page Login { page LoginPage {
component: import Login from "@ext/LoginPage" component: import Login from "@ext/LoginPage"
} }
@ -53,7 +57,3 @@ action updateTask {
fn: import { updateTask } from "@ext/actions.js", fn: import { updateTask } from "@ext/actions.js",
entities: [Task] entities: [Task]
} }
dependencies {=json
"react-clock": "3.0.0"
json=}

View File

@ -1,57 +0,0 @@
# Migration `20201023121126-a`
This migration has been generated by Martin Sosic at 10/23/2020, 2:11:26 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL
)
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false
)
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201023121126-a
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,23 @@
+
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ output = "../server/node_modules/.prisma/client"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ password String
+}
+
+model Task {
+ id Int @id @default(autoincrement())
+ description String
+ isDone Boolean @default(false)
+}
+
```

View File

@ -1,23 +0,0 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
}

View File

@ -1,185 +0,0 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "password",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateModel",
"model": "Task"
},
{
"tag": "CreateField",
"model": "Task",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateField",
"model": "Task",
"field": "description",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Task",
"field": "isDone",
"type": "Boolean",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "isDone"
},
"directive": "default"
},
"argument": "",
"value": "false"
}
]
}

View File

@ -1,56 +0,0 @@
# Migration `20201023121536-b`
This migration has been generated by Martin Sosic at 10/23/2020, 2:15:36 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false,
"userId" INTEGER,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Task" ("id", "description", "isDone") SELECT "id", "description", "isDone" FROM "Task";
DROP TABLE "Task";
ALTER TABLE "new_Task" RENAME TO "Task";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201023121126-a..20201023121536-b
--- datamodel.dml
+++ datamodel.dml
@@ -1,8 +1,8 @@
datasource db {
provider = "sqlite"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -12,12 +12,15 @@
model User {
id Int @id @default(autoincrement())
email String @unique
password String
+ tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
+ user User? @relation(fields: [userId], references: [id])
+ userId Int?
}
```

View File

@ -1,26 +0,0 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
}

View File

@ -1,65 +0,0 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "User",
"field": "tasks",
"type": "Task",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Task",
"field": "user",
"type": "User",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Task",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Task",
"field": "userId",
"type": "Int",
"arity": "Optional"
}
]
}

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false,
"userId" INTEGER,
FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");

View File

@ -1,4 +0,0 @@
# Prisma Migrate lockfile v1
20201023121126-a
20201023121536-b

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View File

@ -136,12 +136,12 @@ CLI is actually `wasp` executable, and it uses the library, where most of the lo
Wasp compiler takes .wasp files + everything in the `ext/` dir (JS, HTML, ...) and generates a web app that consists of client, server and database. Wasp compiler takes .wasp files + everything in the `ext/` dir (JS, HTML, ...) and generates a web app that consists of client, server and database.
Wasp compiler code is split into 2 basic layers: Parser and Generator. Wasp compiler code is split into 2 basic layers: Analyzer (frontend) and Generator (backend).
Wasp file(s) are parsed by Parser into an AST (Abstract Syntax Tree) described in `src/Wasp.hs`. Wasp file(s) are analyzed by Analyzer, where they are first parsed, then typechecked, and then evaluated into a central IR (Intermediate Representation), which is `AppSpec` (`src/Wasp/AppSpec.hs`).
Parser is implemented via parser combinators and has no distinct tokenization, syntax and semantic analysis steps, currently it is all one big step. Check `src/Wasp/Analyzer.hs` for more details.
AST generated by Parser is passed to the Generator, which based on it decides how to generate a web app. AppSpec is passed to the Generator, which based on it decides how to generate a web app.
Output of Generator is a list of FileDrafts, where each FileDraft explains how to create a file on the disk. Output of Generator is a list of FileDrafts, where each FileDraft explains how to create a file on the disk.
Therefore, Generator doesn't generate anything itself, instead it provides instructions (FileDrafts) on how to generate the web app. Therefore, Generator doesn't generate anything itself, instead it provides instructions (FileDrafts) on how to generate the web app.
FileDrafts are using mustache templates a lot (they can be found in `data/Generator/templates`). FileDrafts are using mustache templates a lot (they can be found in `data/Generator/templates`).

View File

@ -106,8 +106,8 @@ createNewProject' (ProjectName projectName) = do
" title: \"%s\"" `printf` projectName, " title: \"%s\"" `printf` projectName,
"}", "}",
"", "",
"route \"/\" -> page Main", "route RootRoute { path: \"/\", to: MainPage }",
"page Main {", "page MainPage {",
" component: import Main from \"@ext/MainPage.js\"", " component: import Main from \"@ext/MainPage.js\"",
"}" "}"
] ]

View File

@ -4,11 +4,12 @@ module Wasp.Cli.Command.Deps
where where
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.Cli.Command (Command) import Wasp.Cli.Command (Command)
import Wasp.Cli.Terminal (title) import Wasp.Cli.Terminal (title)
import qualified Wasp.Generator.ServerGenerator as ServerGenerator import qualified Wasp.Generator.ServerGenerator as ServerGenerator
import qualified Wasp.Generator.WebAppGenerator as WebAppGenerator import qualified Wasp.Generator.WebAppGenerator as WebAppGenerator
import Wasp.NpmDependency (printDep) import qualified Wasp.Util.Terminal as Term
deps :: Command () deps :: Command ()
deps = deps =
@ -25,3 +26,9 @@ deps =
title "Webapp dependencies:" title "Webapp dependencies:"
] ]
++ map printDep WebAppGenerator.waspNpmDeps ++ map printDep WebAppGenerator.waspNpmDeps
printDep :: AS.Dependency.Dependency -> String
printDep dep =
Term.applyStyles [Term.Cyan] (AS.Dependency.name dep)
++ "@"
++ Term.applyStyles [Term.Yellow] (AS.Dependency.version dep)

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Cli.Command.Info module Wasp.Cli.Command.Info
( info, ( info,
) )
@ -8,19 +10,18 @@ import Control.Monad.IO.Class (liftIO)
import StrongPath (Abs, Dir, Path', fromAbsFile, fromRelFile, toFilePath) import StrongPath (Abs, Dir, Path', fromAbsFile, fromRelFile, toFilePath)
import StrongPath.Operations import StrongPath.Operations
import System.Directory (doesFileExist, getFileSize) import System.Directory (doesFileExist, getFileSize)
import qualified Wasp.Analyzer as Analyzer
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import Wasp.Cli.Command (Command) import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Common import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd, waspSaysC)
( findWaspProjectRootDirFromCwd,
waspSaysC,
)
import qualified Wasp.Cli.Common as Cli.Common import qualified Wasp.Cli.Common as Cli.Common
import Wasp.Cli.Terminal (title) import Wasp.Cli.Terminal (title)
import Wasp.Common (WaspProjectDir) import Wasp.Common (WaspProjectDir)
import Wasp.Error (showCompilerErrorForTerminal)
import Wasp.Lib (findWaspFile) import Wasp.Lib (findWaspFile)
import qualified Wasp.Parser
import Wasp.Util.IO (listDirectoryDeep) import Wasp.Util.IO (listDirectoryDeep)
import qualified Wasp.Util.Terminal as Term import qualified Wasp.Util.Terminal as Term
import Wasp.Wasp (Wasp, appName, getApp)
info :: Command () info :: Command ()
info = info =
@ -28,17 +29,17 @@ info =
waspDir <- findWaspProjectRootDirFromCwd waspDir <- findWaspProjectRootDirFromCwd
compileInfo <- liftIO $ readCompileInformation waspDir compileInfo <- liftIO $ readCompileInformation waspDir
projectSize <- liftIO $ readDirectorySizeMB waspDir projectSize <- liftIO $ readDirectorySizeMB waspDir
waspAstOrError <- liftIO $ parseWaspFile waspDir declsOrError <- liftIO $ parseWaspFile waspDir
case waspAstOrError of case declsOrError of
Left err -> waspSaysC err Left err -> waspSaysC err
Right wasp -> do Right decls -> do
waspSaysC $ waspSaysC $
unlines unlines
[ "", [ "",
title "Project information", title "Project information",
printInfo printInfo
"Name" "Name"
(appName $ getApp wasp), (fst $ head $ AS.takeDecls @AS.App.App decls),
printInfo printInfo
"Last compile" "Last compile"
compileInfo, compileInfo,
@ -55,17 +56,28 @@ readDirectorySizeMB path = (++ " MB") . show . (`div` 1000000) . sum <$> (listDi
readCompileInformation :: Path' Abs (Dir WaspProjectDir) -> IO String readCompileInformation :: Path' Abs (Dir WaspProjectDir) -> IO String
readCompileInformation waspDir = do readCompileInformation waspDir = do
let dotWaspInfoFile = fromAbsFile $ waspDir </> Cli.Common.dotWaspDirInWaspProjectDir </> Cli.Common.generatedCodeDirInDotWaspDir </> Cli.Common.dotWaspInfoFileInGeneratedCodeDir let dotWaspInfoFile =
fromAbsFile $
waspDir </> Cli.Common.dotWaspDirInWaspProjectDir
</> Cli.Common.generatedCodeDirInDotWaspDir
</> Cli.Common.dotWaspInfoFileInGeneratedCodeDir
dotWaspInfoFileExists <- doesFileExist dotWaspInfoFile dotWaspInfoFileExists <- doesFileExist dotWaspInfoFile
if dotWaspInfoFileExists if dotWaspInfoFileExists
then do readFile dotWaspInfoFile then do readFile dotWaspInfoFile
else return "No compile information found" else return "No compile information found"
parseWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String Wasp) parseWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String [AS.Decl])
parseWaspFile waspDir = do parseWaspFile waspDir = do
maybeWaspFile <- findWaspFile waspDir maybeWaspFile <- findWaspFile waspDir
case maybeWaspFile of case maybeWaspFile of
Nothing -> return (Left "Couldn't find a single *.wasp file.") Nothing -> return (Left "Couldn't find a single *.wasp file.")
Just waspFile -> do Just waspFile ->
do
waspStr <- readFile (toFilePath waspFile) waspStr <- readFile (toFilePath waspFile)
return $ left (("Couldn't parse .wasp file: " <>) . show) $ Wasp.Parser.parseWasp waspStr return $
left
( ("Couldn't parse .wasp file:\n" <>)
. showCompilerErrorForTerminal (waspFile, waspStr)
. Analyzer.getErrorMessageAndCtx
)
$ Analyzer.analyze waspStr

View File

@ -12,7 +12,7 @@ const MainPage = () => {
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2> <h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle"> <h3 className="welcome-subtitle">
This is page <code>Main</code> located at route <code>/</code>. This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it. Open <code>ext/MainPage.js</code> to edit it.
</h3> </h3>

View File

@ -1,4 +1,3 @@
{{={{> <}}=}}
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts ## Available Scripts

View File

@ -1,4 +1,3 @@
{{={{> <}}=}}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies

View File

@ -1,6 +1,6 @@
{{={= =}=}} {{={= =}=}}
{ {
"name": "{= wasp.app.name =}", "name": "{= appName =}",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
{=& depsChunk =}, {=& depsChunk =},

View File

@ -1,6 +1,6 @@
{{={{> <}}=}} {{={= =}=}}
{ {
"name": "{{> app.name <}}", "name": "{= appName =}",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View File

@ -1,4 +1,3 @@
{{={{> <}}=}}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -1,4 +1,3 @@
{{={= =}=}}
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { ReactQueryCacheProvider } from 'react-query' import { ReactQueryCacheProvider } from 'react-query'

View File

@ -1,4 +1,3 @@
{{={= =}=}}
import api, { handleApiError } from '../api.js' import api, { handleApiError } from '../api.js'
import config from '../config.js' import config from '../config.js'

View File

@ -1,4 +1,3 @@
{{={{> <}}=}}
// This optional code is used to register a service worker. // This optional code is used to register a service worker.
// register() is not called by default. // register() is not called by default.

View File

@ -2,18 +2,19 @@ app todoApp {
title: "ToDo App", title: "ToDo App",
head: [ head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />" "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
] ],
} dependencies: [
("@material-ui/core", "4.11.3")
dependencies {=json ],
"@material-ui/core": "4.11.3" auth: {
json=}
auth {
userEntity: User, userEntity: User,
methods: [ EmailAndPassword ], methods: [ EmailAndPassword ],
onAuthFailedRedirectTo: "/login", onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/profile" onAuthSucceededRedirectTo: "/profile"
},
server: {
setupFn: import setup from "@ext/serverSetup.js"
}
} }
entity User {=psl entity User {=psl
@ -31,41 +32,38 @@ entity Task {=psl
userId Int userId Int
psl=} psl=}
server {
setupFn: import setup from "@ext/serverSetup.js"
}
route "/signup" -> page Signup route SignupRoute { path: "/signup", to: SignupPage }
page Signup { page SignupPage {
component: import Signup from "@ext/pages/auth/Signup" component: import Signup from "@ext/pages/auth/Signup"
} }
route "/login" -> page Login route LoginRoute { path: "/login", to: LoginPage }
page Login { page LoginPage {
component: import Login from "@ext/pages/auth/Login" component: import Login from "@ext/pages/auth/Login"
} }
route "/" -> page Main route HomeRoute { path: "/", to: MainPage }
page Main { page MainPage {
authRequired: true, authRequired: true,
component: import Main from "@ext/pages/Main" component: import Main from "@ext/pages/Main"
} }
route "/about" -> page About route AboutRoute { path: "/about", to: AboutPage }
page About { page AboutPage {
component: import About from "@ext/pages/About" component: import About from "@ext/pages/About"
} }
route "/profile" -> page Profile route ProfileRoute { path: "/profile", to: ProfilePage }
page Profile { page ProfilePage {
authRequired: true, authRequired: true,
component: import { ProfilePage } from "@ext/pages/ProfilePage" component: import { ProfilePage } from "@ext/pages/ProfilePage"
} }
// Page for viewing a specific task // Page for viewing a specific task
// //
route "/task/:id" -> page Task route TaskRoute { path: "/task/:id", to: TaskPage }
page Task { page TaskPage {
authRequired: true, authRequired: true,
component: import Task from "@ext/pages/Task" component: import Task from "@ext/pages/Task"
} }
@ -79,7 +77,7 @@ query getTasks {
query getNumTasks { query getNumTasks {
fn: import { getNumTasks } from "@ext/queries.js", fn: import { getNumTasks } from "@ext/queries.js",
entities: [Task],
auth: false auth: false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -76,7 +76,7 @@ library:
- unliftio - unliftio
- array # Used by code generated by Alex for src/Analyzer/Parser/Lexer.x - array # Used by code generated by Alex for src/Analyzer/Parser/Lexer.x
- mtl - mtl
- strong-path - strong-path >= 1.1.2.0
- template-haskell - template-haskell
- path-io - path-io

View File

@ -19,6 +19,8 @@ module Wasp.Analyzer.Evaluator.Evaluation.TypedExpr.Combinators
where where
import Control.Arrow (left) import Control.Arrow (left)
import Data.List (stripPrefix)
import qualified StrongPath as SP
import Wasp.Analyzer.Evaluator.Evaluation.Internal (evaluation, evaluation', runEvaluation) import Wasp.Analyzer.Evaluator.Evaluation.Internal (evaluation, evaluation', runEvaluation)
import Wasp.Analyzer.Evaluator.Evaluation.TypedExpr (TypedExprEvaluation) import Wasp.Analyzer.Evaluator.Evaluation.TypedExpr (TypedExprEvaluation)
import qualified Wasp.Analyzer.Evaluator.EvaluationError as ER import qualified Wasp.Analyzer.Evaluator.EvaluationError as ER
@ -152,8 +154,24 @@ tuple4 eval1 eval2 eval3 eval4 = evaluation $ \(typeDefs, bindings) -> withCtx $
-- | An evaluation that expects an "ExtImport". -- | An evaluation that expects an "ExtImport".
extImport :: TypedExprEvaluation AppSpec.ExtImport.ExtImport extImport :: TypedExprEvaluation AppSpec.ExtImport.ExtImport
extImport = evaluation' . withCtx $ \ctx -> \case extImport = evaluation' . withCtx $ \ctx -> \case
TypedAST.ExtImport name file -> pure $ AppSpec.ExtImport.ExtImport name file TypedAST.ExtImport name extFileFP ->
-- NOTE(martin): This parsing here could instead be done in Parser.
-- I don't have a very good reason for doing it here instead of Parser, except
-- for being somewhat simpler to implement.
-- So we might want to move it to Parser at some point in the future, if we
-- figure out that is better (it sounds/feels like it could be).
case stripPrefix extPrefix extFileFP of
Just relFileFP -> case SP.parseRelFileP relFileFP of
Left err -> Left $ ER.mkEvaluationError ctx $ ER.ParseError $ ER.EvaluationParseError $ show err
Right relFileSP -> pure $ AppSpec.ExtImport.ExtImport name relFileSP
Nothing ->
Left $
ER.mkEvaluationError ctx $
ER.ParseError $
ER.EvaluationParseError $ "Path in external import must start with \"" ++ extPrefix ++ "\"!"
expr -> Left $ ER.mkEvaluationError ctx $ ER.ExpectedType T.ExtImportType (TypedAST.exprType expr) expr -> Left $ ER.mkEvaluationError ctx $ ER.ExpectedType T.ExtImportType (TypedAST.exprType expr)
where
extPrefix = "@ext/"
-- | An evaluation that expects a "JSON". -- | An evaluation that expects a "JSON".
json :: TypedExprEvaluation AppSpec.JSON.JSON json :: TypedExprEvaluation AppSpec.JSON.JSON

View File

@ -29,12 +29,16 @@ $any = [.$white]
@double = "-"? $digit+ "." $digit+ @double = "-"? $digit+ "." $digit+
@integer = "-"? $digit+ @integer = "-"? $digit+
@ident = $identstart $ident* "'"* @ident = $identstart $ident* "'"*
@linecomment = "//" [^\n\r]*
@blockcomment = "/*" (("*"[^\/]) | [^\*] | $white)* "*/" -- Based on https://stackoverflow.com/a/16165598/1509394 .
-- Tokenization rules (regex -> token) -- Tokenization rules (regex -> token)
tokens :- tokens :-
-- Skips whitespace -- Skips whitespace and comments
<0> $white+ ; <0> $white+ ;
<0> @linecomment ;
<0> @blockcomment ;
-- Quoter rules: -- Quoter rules:
-- Uses Alex start codes to lex quoted characters with different rules: -- Uses Alex start codes to lex quoted characters with different rules:

View File

@ -7,7 +7,7 @@ module Wasp.Analyzer.Parser.ParseError
where where
import Wasp.Analyzer.Parser.Ctx (Ctx, WithCtx (..), ctxFromPos, ctxFromRgn, getCtxRgn) import Wasp.Analyzer.Parser.Ctx (Ctx, WithCtx (..), ctxFromPos, ctxFromRgn, getCtxRgn)
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition) import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..))
import Wasp.Analyzer.Parser.SourceRegion (getRgnEnd, getRgnStart) import Wasp.Analyzer.Parser.SourceRegion (getRgnEnd, getRgnStart)
import Wasp.Analyzer.Parser.Token (Token (..)) import Wasp.Analyzer.Parser.Token (Token (..))
@ -40,7 +40,9 @@ getErrorMessageAndCtx = \case
"Expected one of the following tokens instead: " "Expected one of the following tokens instead: "
++ unwords expectedTokens ++ unwords expectedTokens
in unexpectedTokenMessage ++ if not (null expectedTokens) then "\n" ++ expectedTokensMessage else "", in unexpectedTokenMessage ++ if not (null expectedTokens) then "\n" ++ expectedTokensMessage else "",
ctxFromPos $ tokenStartPosition unexpectedToken let tokenStartPos@(SourcePosition sl sc) = tokenStartPosition unexpectedToken
tokenEndPos = SourcePosition sl (sc + length (tokenLexeme unexpectedToken) - 1)
in ctxFromRgn tokenStartPos tokenEndPos
) )
QuoterDifferentTags (WithCtx lctx ltag) (WithCtx rctx rtag) -> QuoterDifferentTags (WithCtx lctx ltag) (WithCtx rctx rtag) ->
let ctx = ctxFromRgn (getRgnStart $ getCtxRgn lctx) (getRgnEnd $ getCtxRgn rctx) let ctx = ctxFromRgn (getRgnStart $ getCtxRgn lctx) (getRgnEnd $ getCtxRgn rctx)

View File

@ -275,12 +275,11 @@ waspKindOfHaskellType typ = do
maybeCustomEvaluationKind <- tryCastingToCustomEvaluationKind typ maybeCustomEvaluationKind <- tryCastingToCustomEvaluationKind typ
maybeRecordKind <- tryCastingToRecordKind typ maybeRecordKind <- tryCastingToRecordKind typ
maybe (fail $ "No translation to wasp type for type " ++ show typ) return $ maybe (fail $ "No translation to wasp type for type " ++ show typ) return $
maybeDeclRefKind -- NOTE: It is important to have @maybeCustomEvaluationKind@ first, since we want it to override
-- any of the hardcoded kind assignments below.
maybeCustomEvaluationKind
<|> maybeDeclRefKind
<|> maybeEnumKind <|> maybeEnumKind
-- NOTE: It is important that @maybeCustomEvaluationKind@ is before @maybeRecordKind@,
-- since having a custom evaluation should override typical record evalution, if type is a record.
<|> maybeCustomEvaluationKind
<|> maybeRecordKind
<|> case typ of <|> case typ of
ConT name ConT name
| name == ''String -> pure KString | name == ''String -> pure KString
@ -295,6 +294,10 @@ waspKindOfHaskellType typ = do
TupleT 3 `AppT` t1 `AppT` t2 `AppT` t3 -> pure (KTuple (t1, t2, [t3])) TupleT 3 `AppT` t1 `AppT` t2 `AppT` t3 -> pure (KTuple (t1, t2, [t3]))
TupleT 4 `AppT` t1 `AppT` t2 `AppT` t3 `AppT` t4 -> pure (KTuple (t1, t2, [t3, t4])) TupleT 4 `AppT` t1 `AppT` t2 `AppT` t3 `AppT` t4 -> pure (KTuple (t1, t2, [t3, t4]))
_ -> Nothing _ -> Nothing
-- NOTE: It is important that @maybeRecordKind@ is last, in case there are some other custom/specialized
-- kind assignments for a specific record type -> in that case we want those to be used,
-- instead of a generic record kind assignment.
<|> maybeRecordKind
where where
tryCastingToDeclRefKind :: Type -> Q (Maybe WaspKind) tryCastingToDeclRefKind :: Type -> Q (Maybe WaspKind)
tryCastingToDeclRefKind (ConT name `AppT` subType) | name == ''Ref = do tryCastingToDeclRefKind (ConT name `AppT` subType) | name == ''Ref = do

View File

@ -1,11 +1,34 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.AppSpec module Wasp.AppSpec
( AppSpec (..), ( AppSpec (..),
Decl,
getDecls,
takeDecls,
Ref,
refName,
getApp,
getActions,
getQueries,
getEntities,
getPages,
getRoutes,
isAuthEnabled,
) )
where where
import Data.Maybe (isJust)
import StrongPath (Abs, Dir, File', Path') import StrongPath (Abs, Dir, File', Path')
import Wasp.AppSpec.Core.Decl (Decl) import Wasp.AppSpec.Action (Action)
import Wasp.AppSpec.App (App)
import qualified Wasp.AppSpec.App as App
import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls)
import Wasp.AppSpec.Core.Ref (Ref, refName)
import Wasp.AppSpec.Entity (Entity)
import qualified Wasp.AppSpec.ExternalCode as ExternalCode import qualified Wasp.AppSpec.ExternalCode as ExternalCode
import Wasp.AppSpec.Page (Page)
import Wasp.AppSpec.Query (Query)
import Wasp.AppSpec.Route (Route)
import Wasp.Common (DbMigrationsDir) import Wasp.Common (DbMigrationsDir)
-- | AppSpec is the main/central intermediate representation (IR) of the whole Wasp compiler, -- | AppSpec is the main/central intermediate representation (IR) of the whole Wasp compiler,
@ -21,6 +44,46 @@ data AppSpec = AppSpec
externalCodeDirPath :: !(Path' Abs (Dir ExternalCode.SourceExternalCodeDir)), externalCodeDirPath :: !(Path' Abs (Dir ExternalCode.SourceExternalCodeDir)),
-- | Absolute path to the directory in wasp project source that contains database migrations. -- | Absolute path to the directory in wasp project source that contains database migrations.
migrationsDir :: Maybe (Path' Abs (Dir DbMigrationsDir)), migrationsDir :: Maybe (Path' Abs (Dir DbMigrationsDir)),
-- | Absolute path to the .env file in wasp project source. It contains env variables to be
-- provided to the server only during the development.
dotEnvFile :: Maybe (Path' Abs File'), dotEnvFile :: Maybe (Path' Abs File'),
-- | If true, it means project is being compiled for production/deployment -> it is being "built".
-- If false, it means project is being compiled for development purposes (e.g. "wasp start").
isBuild :: Bool isBuild :: Bool
} }
-- TODO: Make this return "Named" declarations?
-- We would have something like NamedDecl or smth like that. Or at least have a @type Named@ or smth like that.
-- Or @WithName@ or just @Named@.
-- I like the best: `newtype Named a = Named (String, a)`
-- I created a github issue for it: https://github.com/wasp-lang/wasp/issues/426 .
getDecls :: IsDecl a => AppSpec -> [(String, a)]
getDecls = takeDecls . decls
-- TODO: This will fail with an error if there is no `app` declaration (because of `head`)!
-- However, returning a Maybe here would be PITA later in the code.
-- It would be cool instead if we had an extra step that somehow ensures that app exists and
-- throws nice error if it doesn't. Some step that validated AppSpec. Maybe we could
-- have a function that returns `Validated AppSpec` -> so basically smart constructor,
-- validates AppSpec and returns it wrapped with `Validated`,
-- I created a github issue for it: https://github.com/wasp-lang/wasp/issues/425 .
getApp :: AppSpec -> (String, App)
getApp spec = head $ takeDecls @App (decls spec)
getQueries :: AppSpec -> [(String, Query)]
getQueries spec = takeDecls @Query (decls spec)
getActions :: AppSpec -> [(String, Action)]
getActions spec = takeDecls @Action (decls spec)
getEntities :: AppSpec -> [(String, Entity)]
getEntities spec = takeDecls @Entity (decls spec)
getPages :: AppSpec -> [(String, Page)]
getPages spec = takeDecls @Page (decls spec)
getRoutes :: AppSpec -> [(String, Route)]
getRoutes spec = takeDecls @Route (decls spec)
isAuthEnabled :: AppSpec -> Bool
isAuthEnabled spec = isJust (App.auth $ snd $ getApp spec)

View File

@ -12,12 +12,9 @@ import Wasp.AppSpec.Core.Decl (IsDecl)
data App = App data App = App
{ title :: String, { title :: String,
head :: Maybe [String], head :: Maybe [String],
auth :: Maybe Auth, -- NOTE: This is new. Before, `auth` was a standalone declaration. auth :: Maybe Auth,
server :: Maybe Server, -- NOTE: This is new. Before, `server` was a standalone declaration. server :: Maybe Server,
db :: Maybe Db, -- NOTE: This is new. Before, `db` was a standalone declaration. db :: Maybe Db,
-- | NOTE: This is new. Before, `dependencies` was a standalone declaration and it was a {=json json=},
-- while now it is a [{ name :: String, version :: String }].
dependencies :: Maybe [Dependency] dependencies :: Maybe [Dependency]
} }
deriving (Show, Eq, Data) deriving (Show, Eq, Data)

View File

@ -2,6 +2,7 @@
module Wasp.AppSpec.App.Dependency module Wasp.AppSpec.App.Dependency
( Dependency (..), ( Dependency (..),
fromList,
) )
where where
@ -12,3 +13,6 @@ data Dependency = Dependency
version :: String version :: String
} }
deriving (Show, Eq, Data) deriving (Show, Eq, Data)
fromList :: [(String, String)] -> [Dependency]
fromList = map (\(n, v) -> Dependency {name = n, version = v})

View File

@ -4,6 +4,7 @@
module Wasp.AppSpec.Core.Ref module Wasp.AppSpec.Core.Ref
( Ref (..), ( Ref (..),
refName,
) )
where where
@ -21,3 +22,6 @@ deriving instance Eq a => Eq (Ref a)
deriving instance Show a => Show (Ref a) deriving instance Show a => Show (Ref a)
deriving instance (IsDecl a, Data a) => Data (Ref a) deriving instance (IsDecl a, Data a) => Data (Ref a)
refName :: Ref a -> String
refName (Ref name) = name

View File

@ -7,11 +7,18 @@ module Wasp.AppSpec.ExtImport
where where
import Data.Data (Data) import Data.Data (Data)
import StrongPath (File', Path, Posix, Rel)
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
data ExtImport = ExtImport ExtImportName ExtImportPath data ExtImport = ExtImport
{ -- | What is being imported.
name :: ExtImportName,
-- | Path from which we are importing.
path :: ExtImportPath
}
deriving (Show, Eq, Data) deriving (Show, Eq, Data)
type ExtImportPath = String type ExtImportPath = Path Posix (Rel SourceExternalCodeDir) File'
type Identifier = String type Identifier = String

View File

@ -1,3 +1,5 @@
{-# LANGUAGE DeriveDataTypeable #-}
module Wasp.AppSpec.ExternalCode module Wasp.AppSpec.ExternalCode
( -- | Wasp project consists of Wasp code (.wasp files) and external code (e.g. .js files) that is ( -- | Wasp project consists of Wasp code (.wasp files) and external code (e.g. .js files) that is
-- used/referenced by the Wasp code. -- used/referenced by the Wasp code.
@ -14,13 +16,14 @@ module Wasp.AppSpec.ExternalCode
) )
where where
import Data.Data (Data)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Lazy as TextL import qualified Data.Text.Lazy as TextL
import StrongPath (Abs, Dir, File', Path', Rel, (</>)) import StrongPath (Abs, Dir, File', Path', Rel, (</>))
-- | Directory in Wasp source that contains external code. -- | Directory in Wasp source that contains external code.
-- External code files are obtained from it. -- External code files are obtained from it.
data SourceExternalCodeDir data SourceExternalCodeDir deriving (Data)
data File = File data File = File
{ _pathInExtCodeDir :: !(Path' (Rel SourceExternalCodeDir) File'), { _pathInExtCodeDir :: !(Path' (Rel SourceExternalCodeDir) File'),

View File

@ -0,0 +1,38 @@
module Wasp.AppSpec.Operation
( Operation (..),
getName,
getFn,
getEntities,
getAuth,
)
where
import Wasp.AppSpec.Action (Action)
import qualified Wasp.AppSpec.Action as Action
import Wasp.AppSpec.Core.Ref (Ref)
import Wasp.AppSpec.Entity (Entity)
import Wasp.AppSpec.ExtImport (ExtImport)
import Wasp.AppSpec.Query (Query)
import qualified Wasp.AppSpec.Query as Query
-- | Common "interface" for queries and actions.
data Operation
= QueryOp String Query
| ActionOp String Action
deriving (Show)
getName :: Operation -> String
getName (QueryOp name _) = name
getName (ActionOp name _) = name
getFn :: Operation -> ExtImport
getFn (QueryOp _ query) = Query.fn query
getFn (ActionOp _ action) = Action.fn action
getEntities :: Operation -> Maybe [Ref Entity]
getEntities (QueryOp _ query) = Query.entities query
getEntities (ActionOp _ action) = Action.entities action
getAuth :: Operation -> Maybe Bool
getAuth (QueryOp _ query) = Query.auth query
getAuth (ActionOp _ action) = Action.auth action

View File

@ -10,7 +10,6 @@ import Wasp.AppSpec.Core.Decl (IsDecl)
import Wasp.AppSpec.Core.Ref (Ref) import Wasp.AppSpec.Core.Ref (Ref)
import Wasp.AppSpec.Page import Wasp.AppSpec.Page
-- | NOTE: We have new syntax for route, before it was `route "/task" -> page Task`, now it is a dictionary.
data Route = Route data Route = Route
{ path :: String, { path :: String,
-- TODO: In the future we might want to add other types of targets, for example another Route. -- TODO: In the future we might want to add other types of targets, for example another Route.

76
waspc/src/Wasp/Error.hs Normal file
View File

@ -0,0 +1,76 @@
module Wasp.Error (showCompilerErrorForTerminal) where
import Data.List (intercalate)
import StrongPath (Abs, File', Path')
import qualified StrongPath as SP
import Wasp.Analyzer.Parser.Ctx (Ctx, getCtxRgn)
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..))
import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (..))
import Wasp.Util (indent, insertAt, leftPad)
import qualified Wasp.Util.Terminal as T
-- | Transforms compiler error (error with parse context) into an informative, pretty String that
-- can be printed directly into the terminal. It uses terminal features like escape codes
-- (colors, styling, ...).
showCompilerErrorForTerminal :: (Path' Abs File', String) -> (String, Ctx) -> String
showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) =
let srcRegion = getCtxRgn errCtx
in intercalate
"\n"
[ SP.fromAbsFile waspFilePath ++ " @ " ++ showRgn srcRegion,
indent 2 errMsg,
"",
indent 2 $ prettyShowSrcLinesOfErrorRgn waspFileContent srcRegion
]
showRgn :: SourceRegion -> String
showRgn (SourceRegion (SourcePosition l1 c1) (SourcePosition l2 c2))
| l1 == l2 && c1 == c2 = showPos l1 c1
| l1 == l2 && c1 /= c2 = show l1 ++ ":" ++ show c1 ++ "-" ++ show c2
| otherwise = showPos l1 c1 ++ " - " ++ showPos l2 c2
where
showPos l c = show l ++ ":" ++ show c
-- | Given wasp source and error region in it, extracts source lines
-- that are in the given error region and then nicely displays them,
-- by coloring in red the exact error region part of the code and also
-- by prefixing all the lines with their line number (colored yellow).
-- Uses terminal features for styling, like escape codes and similar.
prettyShowSrcLinesOfErrorRgn :: String -> SourceRegion -> String
prettyShowSrcLinesOfErrorRgn
waspFileContent
( SourceRegion
(SourcePosition startLineNum startColNum)
(SourcePosition endLineNum endColNum)
) =
let srcLines =
zip [max 1 (startLineNum - numCtxLines) ..] $
take (endLineNum - startLineNum + 1 + numCtxLines * 2) $
drop (startLineNum - 1 - numCtxLines) $
lines waspFileContent
srcLinesWithMarkedErrorRgn =
map
( \(lineNum, line) ->
let lineContainsError = lineNum >= startLineNum && lineNum <= endLineNum
lineWithStylingStartAndEnd =
if lineNum == startLineNum
then insertAt stylingStart (startColNum - 1) lineWithStylingEnd
else stylingStart ++ lineWithStylingEnd
lineWithStylingEnd =
if lineNum == endLineNum
then insertAt stylingEnd endColNum line
else line ++ stylingEnd
stylingStart = T.escapeCode ++ T.styleCode T.Red
stylingEnd = T.escapeCode ++ T.resetCode
in (lineNum, if lineContainsError then lineWithStylingStartAndEnd else line)
)
srcLines
srcLinesWithMarkedErrorRgnAndLineNumber =
map
(\(lineNum, line) -> T.applyStyles [T.Yellow] (leftPad ' ' 6 (show lineNum) ++ " | ") ++ line)
srcLinesWithMarkedErrorRgn
in intercalate "\n" srcLinesWithMarkedErrorRgnAndLineNumber
where
-- Number of lines to show before and after the source region containing error.
numCtxLines :: Int
numCtxLines = 1

View File

@ -12,7 +12,7 @@ import qualified Data.Version
import qualified Paths_waspc import qualified Paths_waspc
import StrongPath (Abs, Dir, Path', relfile, (</>)) import StrongPath (Abs, Dir, Path', relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.CompileOptions (CompileOptions) import Wasp.AppSpec (AppSpec)
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator (genDb) import Wasp.Generator.DbGenerator (genDb)
import qualified Wasp.Generator.DbGenerator as DbGenerator import qualified Wasp.Generator.DbGenerator as DbGenerator
@ -23,21 +23,20 @@ import qualified Wasp.Generator.ServerGenerator as ServerGenerator
import qualified Wasp.Generator.Setup import qualified Wasp.Generator.Setup
import qualified Wasp.Generator.Start import qualified Wasp.Generator.Start
import Wasp.Generator.WebAppGenerator (generateWebApp) import Wasp.Generator.WebAppGenerator (generateWebApp)
import Wasp.Wasp (Wasp)
-- | Generates web app code from given Wasp and writes it to given destination directory. -- | Generates web app code from given Wasp and writes it to given destination directory.
-- If dstDir does not exist yet, it will be created. -- If dstDir does not exist yet, it will be created.
-- NOTE(martin): What if there is already smth in the dstDir? It is probably best -- NOTE(martin): What if there is already smth in the dstDir? It is probably best
-- if we clean it up first? But we don't want this to end up with us deleting stuff -- if we clean it up first? But we don't want this to end up with us deleting stuff
-- from user's machine. Maybe we just overwrite and we are good? -- from user's machine. Maybe we just overwrite and we are good?
writeWebAppCode :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () writeWebAppCode :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ()
writeWebAppCode wasp dstDir compileOptions = do writeWebAppCode spec dstDir = do
writeFileDrafts dstDir (generateWebApp wasp compileOptions) writeFileDrafts dstDir (generateWebApp spec)
ServerGenerator.preCleanup wasp dstDir compileOptions ServerGenerator.preCleanup spec dstDir
writeFileDrafts dstDir (genServer wasp compileOptions) writeFileDrafts dstDir (genServer spec)
DbGenerator.preCleanup wasp dstDir compileOptions DbGenerator.preCleanup spec dstDir
writeFileDrafts dstDir (genDb wasp compileOptions) writeFileDrafts dstDir (genDb spec)
writeFileDrafts dstDir (genDockerFiles wasp compileOptions) writeFileDrafts dstDir (genDockerFiles spec)
writeDotWaspInfo dstDir writeDotWaspInfo dstDir
-- | Writes file drafts while using given destination dir as root dir. -- | Writes file drafts while using given destination dir as root dir.

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.DbGenerator module Wasp.Generator.DbGenerator
( genDb, ( genDb,
preCleanup, preCleanup,
@ -9,22 +11,21 @@ where
import Control.Monad (when) import Control.Monad (when)
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import Data.Maybe (isNothing, maybeToList) import Data.Maybe (fromMaybe, isNothing, maybeToList)
import StrongPath (Abs, Dir, File', Path', Rel, reldir, relfile, (</>)) import StrongPath (Abs, Dir, File', Path', Rel, reldir, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import System.Directory (doesDirectoryExist, removeDirectoryRecursive) import System.Directory (doesDirectoryExist, removeDirectoryRecursive)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Db as AS.Db
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Common (DbMigrationsDir) import Wasp.Common (DbMigrationsDir)
import Wasp.CompileOptions (CompileOptions)
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir) import Wasp.Generator.Templates (TemplatesDir)
import qualified Wasp.Psl.Ast.Model as Psl.Ast.Model import qualified Wasp.Psl.Ast.Model as Psl.Ast.Model
import qualified Wasp.Psl.Generator.Model as Psl.Generator.Model import qualified Wasp.Psl.Generator.Model as Psl.Generator.Model
import Wasp.Wasp (Wasp, getMigrationsDir)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.Db as Wasp.Db
import Wasp.Wasp.Entity (Entity)
import qualified Wasp.Wasp.Entity as Wasp.Entity
data DbRootDir data DbRootDir
@ -50,13 +51,18 @@ dbSchemaFileInProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaFileInDbRoo
dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir) dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir)
dbMigrationsDirInDbRootDir = [reldir|migrations|] dbMigrationsDirInDbRootDir = [reldir|migrations|]
preCleanup :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () preCleanup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ()
preCleanup wasp projectRootDir _ = do preCleanup spec projectRootDir = do
deleteGeneratedMigrationsDirIfRedundant wasp projectRootDir deleteGeneratedMigrationsDirIfRedundant spec projectRootDir
deleteGeneratedMigrationsDirIfRedundant :: Wasp -> Path' Abs (Dir ProjectRootDir) -> IO () -- * Db generator
deleteGeneratedMigrationsDirIfRedundant wasp projectRootDir = do
let waspMigrationsDirMissing = isNothing $ getMigrationsDir wasp genDb :: AppSpec -> [FileDraft]
genDb spec = genPrismaSchema spec : maybeToList (genMigrationsDir spec)
deleteGeneratedMigrationsDirIfRedundant :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ()
deleteGeneratedMigrationsDirIfRedundant spec projectRootDir = do
let waspMigrationsDirMissing = isNothing $ AS.migrationsDir spec
projectMigrationsDirExists <- doesDirectoryExist projectMigrationsDirAbsFilePath projectMigrationsDirExists <- doesDirectoryExist projectMigrationsDirAbsFilePath
when (waspMigrationsDirMissing && projectMigrationsDirExists) $ do when (waspMigrationsDirMissing && projectMigrationsDirExists) $ do
putStrLn "A migrations directory does not exist in this Wasp root directory, but does in the generated project output directory." putStrLn "A migrations directory does not exist in this Wasp root directory, but does in the generated project output directory."
@ -66,40 +72,36 @@ deleteGeneratedMigrationsDirIfRedundant wasp projectRootDir = do
where where
projectMigrationsDirAbsFilePath = SP.fromAbsDir $ projectRootDir </> dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir projectMigrationsDirAbsFilePath = SP.fromAbsDir $ projectRootDir </> dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir
genDb :: Wasp -> CompileOptions -> [FileDraft] genPrismaSchema :: AppSpec -> FileDraft
genDb wasp _ = genPrismaSchema spec = createTemplateFileDraft dstPath tmplSrcPath (Just templateData)
genPrismaSchema wasp : maybeToList (genMigrationsDir wasp)
genPrismaSchema :: Wasp -> FileDraft
genPrismaSchema wasp = createTemplateFileDraft dstPath tmplSrcPath (Just templateData)
where where
dstPath = dbSchemaFileInProjectRootDir dstPath = dbSchemaFileInProjectRootDir
tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir
templateData = templateData =
object object
[ "modelSchemas" .= map entityToPslModelSchema (Wasp.getPSLEntities wasp), [ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec),
"datasourceProvider" .= (datasourceProvider :: String), "datasourceProvider" .= (datasourceProvider :: String),
"datasourceUrl" .= (datasourceUrl :: String) "datasourceUrl" .= (datasourceUrl :: String)
] ]
dbSystem = maybe Wasp.Db.SQLite Wasp.Db._system (Wasp.getDb wasp) dbSystem = fromMaybe AS.Db.SQLite (AS.Db.system =<< AS.App.db (snd $ AS.getApp spec))
(datasourceProvider, datasourceUrl) = case dbSystem of (datasourceProvider, datasourceUrl) = case dbSystem of
Wasp.Db.PostgreSQL -> ("postgresql", "env(\"DATABASE_URL\")") AS.Db.PostgreSQL -> ("postgresql", "env(\"DATABASE_URL\")")
-- TODO: Report this error with some better mechanism, not `error`. -- TODO: Report this error with some better mechanism, not `error`.
Wasp.Db.SQLite -> AS.Db.SQLite ->
if Wasp.getIsBuild wasp if AS.isBuild spec
then error "SQLite (a default database) is not supported in production. To build your Wasp app for production, switch to a different database. Switching to PostgreSQL: https://wasp-lang.dev/docs/language/basic-elements/#migrating-from-sqlite-to-postgresql ." then error "SQLite (a default database) is not supported in production. To build your Wasp app for production, switch to a different database. Switching to PostgreSQL: https://wasp-lang.dev/docs/language/features/#migrating-from-sqlite-to-postgresql ."
else ("sqlite", "\"file:./dev.db\"") else ("sqlite", "\"file:./dev.db\"")
entityToPslModelSchema :: Entity -> String entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
entityToPslModelSchema entity = entityToPslModelSchema (entityName, entity) =
Psl.Generator.Model.generateModel $ Psl.Generator.Model.generateModel $
Psl.Ast.Model.Model (Wasp.Entity._name entity) (Wasp.Entity._pslModelBody entity) Psl.Ast.Model.Model entityName (AS.Entity.getPslModelBody entity)
genMigrationsDir :: Wasp -> Maybe FileDraft genMigrationsDir :: AppSpec -> Maybe FileDraft
genMigrationsDir wasp = genMigrationsDir spec =
(getMigrationsDir wasp) >>= \waspMigrationsDir -> AS.migrationsDir spec >>= \waspMigrationsDir ->
Just $ createCopyDirFileDraft (SP.castDir genProjectMigrationsDir) (SP.castDir waspMigrationsDir) Just $ createCopyDirFileDraft (SP.castDir genProjectMigrationsDir) (SP.castDir waspMigrationsDir)
where where
genProjectMigrationsDir = dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir genProjectMigrationsDir = dbRootDirInProjectRootDir </> dbMigrationsDirInDbRootDir

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.DockerGenerator module Wasp.Generator.DockerGenerator
( genDockerFiles, ( genDockerFiles,
) )
@ -5,29 +7,29 @@ where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import StrongPath (File', Path', Rel, relfile) import StrongPath (File', Path', Rel, relfile)
import Wasp.CompileOptions (CompileOptions) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir) import Wasp.Generator.Templates (TemplatesDir)
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
genDockerFiles :: Wasp -> CompileOptions -> [FileDraft] genDockerFiles :: AppSpec -> [FileDraft]
genDockerFiles wasp _ = genDockerfile wasp : [genDockerignore wasp] genDockerFiles spec = genDockerfile spec : [genDockerignore spec]
-- TODO: Inject paths to server and db files/dirs, right now they are hardcoded in the templates. -- TODO: Inject paths to server and db files/dirs, right now they are hardcoded in the templates.
genDockerfile :: Wasp -> FileDraft genDockerfile :: AppSpec -> FileDraft
genDockerfile wasp = genDockerfile spec =
createTemplateFileDraft createTemplateFileDraft
([relfile|Dockerfile|] :: Path' (Rel ProjectRootDir) File') ([relfile|Dockerfile|] :: Path' (Rel ProjectRootDir) File')
([relfile|Dockerfile|] :: Path' (Rel TemplatesDir) File') ([relfile|Dockerfile|] :: Path' (Rel TemplatesDir) File')
( Just $ ( Just $
object object
[ "usingPrisma" .= not (null $ Wasp.getPSLEntities wasp) [ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec)
] ]
) )
genDockerignore :: Wasp -> FileDraft genDockerignore :: AppSpec -> FileDraft
genDockerignore _ = genDockerignore _ =
createTemplateFileDraft createTemplateFileDraft
([relfile|.dockerignore|] :: Path' (Rel ProjectRootDir) File') ([relfile|.dockerignore|] :: Path' (Rel ProjectRootDir) File')

View File

@ -10,17 +10,14 @@ import qualified Wasp.AppSpec.ExternalCode as EC
import qualified Wasp.Generator.ExternalCodeGenerator.Common as C import qualified Wasp.Generator.ExternalCodeGenerator.Common as C
import Wasp.Generator.ExternalCodeGenerator.Js (generateJsFile) import Wasp.Generator.ExternalCodeGenerator.Js (generateJsFile)
import qualified Wasp.Generator.FileDraft as FD import qualified Wasp.Generator.FileDraft as FD
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
-- | Takes external code files from Wasp and generates them in new location as part of the generated project. -- | Takes external code files from Wasp and generates them in new location as part of the generated project.
-- It might not just copy them but also do some changes on them, as needed. -- It might not just copy them but also do some changes on them, as needed.
generateExternalCodeDir :: generateExternalCodeDir ::
C.ExternalCodeGeneratorStrategy -> C.ExternalCodeGeneratorStrategy ->
Wasp -> [EC.File] ->
[FD.FileDraft] [FD.FileDraft]
generateExternalCodeDir strategy wasp = generateExternalCodeDir strategy = map (generateFile strategy)
map (generateFile strategy) (Wasp.getExternalCodeFiles wasp)
generateFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> FD.FileDraft generateFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> FD.FileDraft
generateFile strategy file generateFile strategy file

View File

@ -1,27 +1,26 @@
module Wasp.Generator.JsImport module Wasp.Generator.JsImport
( getImportDetailsForJsFnImport, ( getJsImportDetailsForExtFnImport,
) )
where where
import StrongPath (Dir, Path, Posix, Rel, (</>)) import StrongPath (Dir, Path, Posix, Rel, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import qualified Wasp.Wasp.JsImport as Wasp.JsImport
getImportDetailsForJsFnImport :: getJsImportDetailsForExtFnImport ::
-- | Path to generated external code directory, relative to the directory in which file doing the importing is. -- | Path to generated external code directory, relative to the directory in which file doing the importing is.
Path Posix (Rel (Dir a)) (Dir GeneratedExternalCodeDir) -> Path Posix (Rel (Dir a)) (Dir GeneratedExternalCodeDir) ->
Wasp.JsImport.JsImport -> AS.ExtImport.ExtImport ->
-- | (importIdentifier, importStmt) -- | (importIdentifier, importStmt)
-- - importIdentifier -> Identifier via which you can access js function after you import it with importStmt. -- - importIdentifier -> Identifier via which you can access ext js function after you import it with importStmt.
-- - importStmt -> Import statement via which you should do the import. -- - importStmt -> Javascript import statement via which you should do the import.
(String, String) (String, String)
getImportDetailsForJsFnImport relPosixPathToExtCodeDir jsImport = (importIdentifier, importStmt) getJsImportDetailsForExtFnImport relPosixPathToExtCodeDir extImport = (importIdentifier, importStmt)
where where
importStmt = "import " ++ importWhat ++ " from '" ++ importFrom ++ "'" importStmt = "import " ++ importWhat ++ " from '" ++ importFrom ++ "'"
importFrom = "./" ++ SP.fromRelFileP (relPosixPathToExtCodeDir </> SP.castRel (Wasp.JsImport._from jsImport)) importFrom = "./" ++ SP.fromRelFileP (relPosixPathToExtCodeDir </> SP.castRel (AS.ExtImport.path extImport))
(importIdentifier, importWhat) = (importIdentifier, importWhat) =
case (Wasp.JsImport._defaultImport jsImport, Wasp.JsImport._namedImports jsImport) of case AS.ExtImport.name extImport of
(Just defaultImport, []) -> (defaultImport, defaultImport) AS.ExtImport.ExtImportModule defaultImport -> (defaultImport, defaultImport)
(Nothing, [namedImport]) -> (namedImport, "{ " ++ namedImport ++ " }") AS.ExtImport.ExtImportField namedImport -> (namedImport, "{ " ++ namedImport ++ " }")
_ -> error $ "Expected from " ++ show jsImport ++ " to be either default import or single named import, due to it being used to import a single js function."

View File

@ -8,7 +8,7 @@ where
import Data.Bifunctor (second) import Data.Bifunctor (second)
import Data.List (find, intercalate) import Data.List (find, intercalate)
import Data.Maybe (fromJust, isJust) import Data.Maybe (fromJust, isJust)
import qualified Wasp.NpmDependency as ND import qualified Wasp.AppSpec.App.Dependency as D
type NpmDependenciesConflictError = String type NpmDependenciesConflictError = String
@ -20,56 +20,56 @@ type NpmDependenciesConflictError = String
-- On error (Left), returns list of conflicting user deps together with the error message -- On error (Left), returns list of conflicting user deps together with the error message
-- explaining what the error is. -- explaining what the error is.
resolveNpmDeps :: resolveNpmDeps ::
[ND.NpmDependency] -> [D.Dependency] ->
[ND.NpmDependency] -> [D.Dependency] ->
Either Either
[(ND.NpmDependency, NpmDependenciesConflictError)] [(D.Dependency, NpmDependenciesConflictError)]
([ND.NpmDependency], [ND.NpmDependency]) ([D.Dependency], [D.Dependency])
resolveNpmDeps waspDeps userDeps = resolveNpmDeps waspDeps userDeps =
if null conflictingUserDeps if null conflictingUserDeps
then Right (waspDeps, userDepsNotInWaspDeps) then Right (waspDeps, userDepsNotInWaspDeps)
else Left conflictingUserDeps else Left conflictingUserDeps
where where
conflictingUserDeps :: [(ND.NpmDependency, NpmDependenciesConflictError)] conflictingUserDeps :: [(D.Dependency, NpmDependenciesConflictError)]
conflictingUserDeps = conflictingUserDeps =
map (second fromJust) $ map (second fromJust) $
filter (isJust . snd) $ filter (isJust . snd) $
map (\dep -> (dep, checkIfConflictingUserDep dep)) userDeps map (\dep -> (dep, checkIfConflictingUserDep dep)) userDeps
checkIfConflictingUserDep :: ND.NpmDependency -> Maybe NpmDependenciesConflictError checkIfConflictingUserDep :: D.Dependency -> Maybe NpmDependenciesConflictError
checkIfConflictingUserDep userDep = checkIfConflictingUserDep userDep =
let attachErrorMessage dep = let attachErrorMessage dep =
"Error: Dependency conflict for user npm dependency (" "Error: Dependency conflict for user dependency ("
++ ND._name dep ++ D.name dep
++ ", " ++ ", "
++ ND._version dep ++ D.version dep
++ "): " ++ "): "
++ "Version must be set to the exactly the same version as" ++ "Version must be set to the exactly the same version as"
++ " the one wasp is using: " ++ " the one wasp is using: "
++ ND._version dep ++ D.version dep
in attachErrorMessage <$> find (areTwoDepsInConflict userDep) waspDeps in attachErrorMessage <$> find (areTwoDepsInConflict userDep) waspDeps
areTwoDepsInConflict :: ND.NpmDependency -> ND.NpmDependency -> Bool areTwoDepsInConflict :: D.Dependency -> D.Dependency -> Bool
areTwoDepsInConflict d1 d2 = areTwoDepsInConflict d1 d2 =
ND._name d1 == ND._name d2 D.name d1 == D.name d2
&& ND._version d1 /= ND._version d2 && D.version d1 /= D.version d2
userDepsNotInWaspDeps :: [ND.NpmDependency] userDepsNotInWaspDeps :: [D.Dependency]
userDepsNotInWaspDeps = filter (not . isDepWithNameInWaspDeps . ND._name) userDeps userDepsNotInWaspDeps = filter (not . isDepWithNameInWaspDeps . D.name) userDeps
isDepWithNameInWaspDeps :: String -> Bool isDepWithNameInWaspDeps :: String -> Bool
isDepWithNameInWaspDeps name = any ((name ==) . ND._name) waspDeps isDepWithNameInWaspDeps name = any ((name ==) . D.name) waspDeps
npmDepsToPackageJsonEntryWithKey :: [ND.NpmDependency] -> String -> String npmDepsToPackageJsonEntryWithKey :: [D.Dependency] -> String -> String
npmDepsToPackageJsonEntryWithKey deps key = npmDepsToPackageJsonEntryWithKey deps key =
"\"" "\""
++ key ++ key
++ "\": {" ++ "\": {"
++ intercalate ",\n " (map (\dep -> "\"" ++ ND._name dep ++ "\": \"" ++ ND._version dep ++ "\"") deps) ++ intercalate ",\n " (map (\dep -> "\"" ++ D.name dep ++ "\": \"" ++ D.version dep ++ "\"") deps)
++ "\n}" ++ "\n}"
npmDepsToPackageJsonEntry :: [ND.NpmDependency] -> String npmDepsToPackageJsonEntry :: [D.Dependency] -> String
npmDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "dependencies" npmDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "dependencies"
npmDevDepsToPackageJsonEntry :: [ND.NpmDependency] -> String npmDevDepsToPackageJsonEntry :: [D.Dependency] -> String
npmDevDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "devDependencies" npmDevDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "devDependencies"

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.ServerGenerator module Wasp.Generator.ServerGenerator
( genServer, ( genServer,
preCleanup, preCleanup,
@ -20,12 +22,18 @@ import qualified StrongPath as SP
import System.Directory (removeFile) import System.Directory (removeFile)
import System.IO.Error (isDoesNotExistError) import System.IO.Error (isDoesNotExistError)
import UnliftIO.Exception (catch, throwIO) import UnliftIO.Exception (catch, throwIO)
import Wasp.CompileOptions (CompileOptions) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.App.Server as AS.App.Server
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Generator.Common (ProjectRootDir, nodeVersionAsText) import Wasp.Generator.Common (ProjectRootDir, nodeVersionAsText)
import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft) import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft)
import Wasp.Generator.JsImport (getImportDetailsForJsFnImport) import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import Wasp.Generator.PackageJsonGenerator import Wasp.Generator.PackageJsonGenerator
( npmDepsToPackageJsonEntry, ( npmDepsToPackageJsonEntry,
npmDevDepsToPackageJsonEntry, npmDevDepsToPackageJsonEntry,
@ -42,24 +50,18 @@ import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
import qualified Wasp.Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator import qualified Wasp.Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator
import Wasp.Generator.ServerGenerator.OperationsG (genOperations) import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
import Wasp.Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes) import Wasp.Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes)
import qualified Wasp.NpmDependency as ND
import Wasp.Wasp (Wasp, getAuth)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.Auth as Wasp.Auth
import qualified Wasp.Wasp.NpmDependencies as WND
import qualified Wasp.Wasp.Server as Wasp.Server
genServer :: Wasp -> CompileOptions -> [FileDraft] genServer :: AppSpec -> [FileDraft]
genServer wasp _ = genServer spec =
concat concat
[ [genReadme wasp], [ [genReadme],
[genPackageJson wasp waspNpmDeps waspNpmDevDeps], [genPackageJson spec waspNpmDeps waspNpmDevDeps],
[genNpmrc wasp], [genNpmrc],
[genNvmrc wasp], [genNvmrc],
[genGitignore wasp], [genGitignore],
genSrcDir wasp, genSrcDir spec,
generateExternalCodeDir ServerExternalCodeGenerator.generatorStrategy wasp, generateExternalCodeDir ServerExternalCodeGenerator.generatorStrategy (AS.externalCodeFiles spec),
genDotEnv wasp genDotEnv spec
] ]
-- Cleanup to be performed before generating new server code. -- Cleanup to be performed before generating new server code.
@ -67,8 +69,8 @@ genServer wasp _ =
-- TODO: Once we implement a fancier method of removing old/redundant files in outDir, -- TODO: Once we implement a fancier method of removing old/redundant files in outDir,
-- we will not need this method any more. Check https://github.com/wasp-lang/wasp/issues/209 -- we will not need this method any more. Check https://github.com/wasp-lang/wasp/issues/209
-- for progress of this. -- for progress of this.
preCleanup :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () preCleanup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ()
preCleanup _ outDir _ = do preCleanup _ outDir = do
-- If .env gets removed but there is old .env file in generated project from previous attempts, -- If .env gets removed but there is old .env file in generated project from previous attempts,
-- we need to make sure we remove it. -- we need to make sure we remove it.
removeFile dotEnvAbsFilePath removeFile dotEnvAbsFilePath
@ -76,9 +78,9 @@ preCleanup _ outDir _ = do
where where
dotEnvAbsFilePath = SP.toFilePath $ outDir </> C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir dotEnvAbsFilePath = SP.toFilePath $ outDir </> C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir
genDotEnv :: Wasp -> [FileDraft] genDotEnv :: AppSpec -> [FileDraft]
genDotEnv wasp = genDotEnv spec =
case Wasp.getDotEnvFile wasp of case AS.dotEnvFile spec of
Just srcFilePath -> Just srcFilePath ->
[ createCopyFileDraft [ createCopyFileDraft
(C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir) (C.serverRootDirInProjectRootDir </> dotEnvInServerRootDir)
@ -89,22 +91,21 @@ genDotEnv wasp =
dotEnvInServerRootDir :: Path' (Rel C.ServerRootDir) File' dotEnvInServerRootDir :: Path' (Rel C.ServerRootDir) File'
dotEnvInServerRootDir = [relfile|.env|] dotEnvInServerRootDir = [relfile|.env|]
genReadme :: Wasp -> FileDraft genReadme :: FileDraft
genReadme _ = C.copyTmplAsIs (asTmplFile [relfile|README.md|]) genReadme = C.mkTmplFd (asTmplFile [relfile|README.md|])
genPackageJson :: Wasp -> [ND.NpmDependency] -> [ND.NpmDependency] -> FileDraft genPackageJson :: AppSpec -> [AS.Dependency.Dependency] -> [AS.Dependency.Dependency] -> FileDraft
genPackageJson wasp waspDeps waspDevDeps = genPackageJson spec waspDeps waspDevDeps =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|package.json|]) (asTmplFile [relfile|package.json|])
(asServerFile [relfile|package.json|]) (asServerFile [relfile|package.json|])
( Just $ ( Just $
object object
[ "wasp" .= wasp, [ "depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps),
"depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps),
"devDepsChunk" .= npmDevDepsToPackageJsonEntry waspDevDeps, "devDepsChunk" .= npmDevDepsToPackageJsonEntry waspDevDeps,
"nodeVersion" .= nodeVersionAsText, "nodeVersion" .= nodeVersionAsText,
"startProductionScript" "startProductionScript"
.= if not (null $ Wasp.getPSLEntities wasp) .= if not (null $ AS.getDecls @AS.Entity.Entity spec)
then "npm run db-migrate-prod && " then "npm run db-migrate-prod && "
else else
"" ""
@ -117,12 +118,12 @@ genPackageJson wasp waspDeps waspDevDeps =
Right deps -> deps Right deps -> deps
Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors
userDeps :: [ND.NpmDependency] userDeps :: [AS.Dependency.Dependency]
userDeps = WND._dependencies $ Wasp.getNpmDependencies wasp userDeps = fromMaybe [] $ AS.App.dependencies $ snd $ AS.getApp spec
waspNpmDeps :: [ND.NpmDependency] waspNpmDeps :: [AS.Dependency.Dependency]
waspNpmDeps = waspNpmDeps =
ND.fromList AS.Dependency.fromList
[ ("cookie-parser", "~1.4.4"), [ ("cookie-parser", "~1.4.4"),
("cors", "^2.8.5"), ("cors", "^2.8.5"),
("debug", "~2.6.9"), ("debug", "~2.6.9"),
@ -135,56 +136,56 @@ waspNpmDeps =
("helmet", "^4.6.0") ("helmet", "^4.6.0")
] ]
waspNpmDevDeps :: [ND.NpmDependency] waspNpmDevDeps :: [AS.Dependency.Dependency]
waspNpmDevDeps = waspNpmDevDeps =
ND.fromList AS.Dependency.fromList
[ ("nodemon", "^2.0.4"), [ ("nodemon", "^2.0.4"),
("standard", "^14.3.4"), ("standard", "^14.3.4"),
("prisma", "2.22.1") ("prisma", "2.22.1")
] ]
genNpmrc :: Wasp -> FileDraft genNpmrc :: FileDraft
genNpmrc _ = genNpmrc =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|npmrc|]) (asTmplFile [relfile|npmrc|])
(asServerFile [relfile|.npmrc|]) (asServerFile [relfile|.npmrc|])
Nothing Nothing
genNvmrc :: Wasp -> FileDraft genNvmrc :: FileDraft
genNvmrc _ = genNvmrc =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|nvmrc|]) (asTmplFile [relfile|nvmrc|])
(asServerFile [relfile|.nvmrc|]) (asServerFile [relfile|.nvmrc|])
(Just (object ["nodeVersion" .= ('v' : nodeVersionAsText)])) (Just (object ["nodeVersion" .= ('v' : nodeVersionAsText)]))
genGitignore :: Wasp -> FileDraft genGitignore :: FileDraft
genGitignore _ = genGitignore =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|gitignore|]) (asTmplFile [relfile|gitignore|])
(asServerFile [relfile|.gitignore|]) (asServerFile [relfile|.gitignore|])
Nothing Nothing
genSrcDir :: Wasp -> [FileDraft] genSrcDir :: AppSpec -> [FileDraft]
genSrcDir wasp = genSrcDir spec =
concat concat
[ [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|app.js|]], [ [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|app.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|server.js|]], [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|server.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|utils.js|]], [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|utils.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/AuthError.js|]], [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/AuthError.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/HttpError.js|]], [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/HttpError.js|]],
[genDbClient wasp], [genDbClient spec],
[genConfigFile wasp], [genConfigFile spec],
genRoutesDir wasp, genRoutesDir spec,
genOperationsRoutes wasp, genOperationsRoutes spec,
genOperations wasp, genOperations spec,
genAuth wasp, genAuth spec,
[genServerJs wasp] [genServerJs spec]
] ]
genDbClient :: Wasp -> FileDraft genDbClient :: AppSpec -> FileDraft
genDbClient wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData) genDbClient spec = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
maybeAuth = getAuth wasp maybeAuth = AS.App.auth $ snd $ AS.getApp spec
dbClientRelToSrcP = [relfile|dbClient.js|] dbClientRelToSrcP = [relfile|dbClient.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> dbClientRelToSrcP tmplFile = C.asTmplFile $ [reldir|src|] </> dbClientRelToSrcP
@ -195,13 +196,13 @@ genDbClient wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData)
then then
object object
[ "isAuthEnabled" .= True, [ "isAuthEnabled" .= True,
"userEntityUpper" .= Wasp.Auth._userEntity (fromJust maybeAuth) "userEntityUpper" .= (AS.refName (AS.App.Auth.userEntity $ fromJust maybeAuth) :: String)
] ]
else object [] else object []
genServerJs :: Wasp -> FileDraft genServerJs :: AppSpec -> FileDraft
genServerJs wasp = genServerJs spec =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/server.js|]) (asTmplFile [relfile|src/server.js|])
(asServerFile [relfile|src/server.js|]) (asServerFile [relfile|src/server.js|])
( Just $ ( Just $
@ -212,8 +213,8 @@ genServerJs wasp =
] ]
) )
where where
maybeSetupJsFunction = Wasp.Server._setupJsFunction <$> Wasp.getServer wasp maybeSetupJsFunction = AS.App.Server.setupFn =<< AS.App.server (snd $ AS.getApp spec)
maybeSetupJsFnImportDetails = getImportDetailsForJsFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction maybeSetupJsFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction
(maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) = (maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails) (fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
@ -221,17 +222,17 @@ genServerJs wasp =
relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir ServerSrcDir)) (Dir GeneratedExternalCodeDir) relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir ServerSrcDir)) (Dir GeneratedExternalCodeDir)
relPosixPathFromSrcDirToExtSrcDir = [reldirP|./ext-src|] relPosixPathFromSrcDirToExtSrcDir = [reldirP|./ext-src|]
genRoutesDir :: Wasp -> [FileDraft] genRoutesDir :: AppSpec -> [FileDraft]
genRoutesDir wasp = genRoutesDir spec =
-- TODO(martin): We will probably want to extract "routes" path here same as we did with "src", to avoid hardcoding, -- TODO(martin): We will probably want to extract "routes" path here same as we did with "src", to avoid hardcoding,
-- but I did not bother with it yet since it is used only here for now. -- but I did not bother with it yet since it is used only here for now.
[ C.makeTemplateFD [ C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/routes/index.js|]) (asTmplFile [relfile|src/routes/index.js|])
(asServerFile [relfile|src/routes/index.js|]) (asServerFile [relfile|src/routes/index.js|])
( Just $ ( Just $
object object
[ "operationsRouteInRootRouter" .= operationsRouteInRootRouter, [ "operationsRouteInRootRouter" .= (operationsRouteInRootRouter :: String),
"isAuthEnabled" .= isJust (getAuth wasp) "isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool)
] ]
) )
] ]

View File

@ -5,14 +5,16 @@ where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import StrongPath (reldir, relfile, (</>)) import StrongPath (reldir, relfile, (</>))
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import qualified Wasp.Util as Util import qualified Wasp.Util as Util
import Wasp.Wasp (Wasp, getAuth)
import qualified Wasp.Wasp.Auth as Wasp.Auth
genAuth :: Wasp -> [FileDraft] genAuth :: AppSpec -> [FileDraft]
genAuth wasp = case maybeAuth of genAuth spec = case maybeAuth of
Just auth -> Just auth ->
[ genCoreAuth auth, [ genCoreAuth auth,
genAuthMiddleware auth, genAuthMiddleware auth,
@ -24,55 +26,55 @@ genAuth wasp = case maybeAuth of
] ]
Nothing -> [] Nothing -> []
where where
maybeAuth = getAuth wasp maybeAuth = AS.App.auth $ snd $ AS.getApp spec
-- | Generates core/auth file which contains auth middleware and createUser() function. -- | Generates core/auth file which contains auth middleware and createUser() function.
genCoreAuth :: Wasp.Auth.Auth -> FileDraft genCoreAuth :: AS.Auth.Auth -> FileDraft
genCoreAuth auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) genCoreAuth auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
coreAuthRelToSrc = [relfile|core/auth.js|] coreAuthRelToSrc = [relfile|core/auth.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> coreAuthRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> coreAuthRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile coreAuthRelToSrc dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile coreAuthRelToSrc
tmplData = tmplData =
let userEntity = Wasp.Auth._userEntity auth let userEntityName = AS.refName $ AS.Auth.userEntity auth
in object in object
[ "userEntityUpper" .= userEntity, [ "userEntityUpper" .= (userEntityName :: String),
"userEntityLower" .= Util.toLowerFirst userEntity "userEntityLower" .= (Util.toLowerFirst userEntityName :: String)
] ]
genAuthMiddleware :: Wasp.Auth.Auth -> FileDraft genAuthMiddleware :: AS.Auth.Auth -> FileDraft
genAuthMiddleware auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) genAuthMiddleware auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
authMiddlewareRelToSrc = [relfile|core/auth/prismaMiddleware.js|] authMiddlewareRelToSrc = [relfile|core/auth/prismaMiddleware.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> authMiddlewareRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> authMiddlewareRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile authMiddlewareRelToSrc dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile authMiddlewareRelToSrc
tmplData = tmplData =
let userEntity = Wasp.Auth._userEntity auth let userEntityName = AS.refName $ AS.Auth.userEntity auth
in object in object
[ "userEntityUpper" .= userEntity [ "userEntityUpper" .= (userEntityName :: String)
] ]
genAuthRoutesIndex :: FileDraft genAuthRoutesIndex :: FileDraft
genAuthRoutesIndex = C.copySrcTmplAsIs (C.asTmplSrcFile [relfile|routes/auth/index.js|]) genAuthRoutesIndex = C.mkSrcTmplFd (C.asTmplSrcFile [relfile|routes/auth/index.js|])
genLoginRoute :: Wasp.Auth.Auth -> FileDraft genLoginRoute :: AS.Auth.Auth -> FileDraft
genLoginRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) genLoginRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
loginRouteRelToSrc = [relfile|routes/auth/login.js|] loginRouteRelToSrc = [relfile|routes/auth/login.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> loginRouteRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> loginRouteRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile loginRouteRelToSrc dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile loginRouteRelToSrc
tmplData = tmplData =
let userEntity = Wasp.Auth._userEntity auth let userEntityName = AS.refName $ AS.Auth.userEntity auth
in object in object
[ "userEntityUpper" .= userEntity, [ "userEntityUpper" .= (userEntityName :: String),
"userEntityLower" .= Util.toLowerFirst userEntity "userEntityLower" .= (Util.toLowerFirst userEntityName :: String)
] ]
genSignupRoute :: Wasp.Auth.Auth -> FileDraft genSignupRoute :: AS.Auth.Auth -> FileDraft
genSignupRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) genSignupRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
signupRouteRelToSrc = [relfile|routes/auth/signup.js|] signupRouteRelToSrc = [relfile|routes/auth/signup.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> signupRouteRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> signupRouteRelToSrc
@ -80,11 +82,11 @@ genSignupRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
tmplData = tmplData =
object object
[ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth) [ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)
] ]
genMeRoute :: Wasp.Auth.Auth -> FileDraft genMeRoute :: AS.Auth.Auth -> FileDraft
genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) genMeRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
meRouteRelToSrc = [relfile|routes/auth/me.js|] meRouteRelToSrc = [relfile|routes/auth/me.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> meRouteRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> meRouteRelToSrc
@ -92,5 +94,5 @@ genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData)
tmplData = tmplData =
object object
[ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth) [ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)
] ]

View File

@ -2,10 +2,9 @@ module Wasp.Generator.ServerGenerator.Common
( serverRootDirInProjectRootDir, ( serverRootDirInProjectRootDir,
serverSrcDirInServerRootDir, serverSrcDirInServerRootDir,
serverSrcDirInProjectRootDir, serverSrcDirInProjectRootDir,
copyTmplAsIs, mkTmplFd,
makeSimpleTemplateFD, mkTmplFdWithDstAndData,
makeTemplateFD, mkSrcTmplFd,
copySrcTmplAsIs,
srcDirInServerTemplatesDir, srcDirInServerTemplatesDir,
asTmplFile, asTmplFile,
asTmplSrcFile, asTmplSrcFile,
@ -24,7 +23,6 @@ import qualified StrongPath as SP
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir) import Wasp.Generator.Templates (TemplatesDir)
import Wasp.Wasp (Wasp)
data ServerRootDir data ServerRootDir
@ -61,29 +59,24 @@ serverSrcDirInProjectRootDir = serverRootDirInProjectRootDir </> serverSrcDirInS
-- * Templates -- * Templates
copyTmplAsIs :: Path' (Rel ServerTemplatesDir) File' -> FileDraft mkTmplFd :: Path' (Rel ServerTemplatesDir) File' -> FileDraft
copyTmplAsIs srcPath = makeTemplateFD srcPath dstPath Nothing mkTmplFd srcPath = mkTmplFdWithDstAndData srcPath dstPath Nothing
where where
dstPath = SP.castRel srcPath :: Path' (Rel ServerRootDir) File' dstPath = SP.castRel srcPath :: Path' (Rel ServerRootDir) File'
makeSimpleTemplateFD :: Path' (Rel ServerTemplatesDir) File' -> Wasp -> FileDraft mkTmplFdWithDstAndData ::
makeSimpleTemplateFD srcPath wasp = makeTemplateFD srcPath dstPath (Just $ Aeson.toJSON wasp)
where
dstPath = SP.castRel srcPath :: Path' (Rel ServerRootDir) File'
makeTemplateFD ::
Path' (Rel ServerTemplatesDir) File' -> Path' (Rel ServerTemplatesDir) File' ->
Path' (Rel ServerRootDir) File' -> Path' (Rel ServerRootDir) File' ->
Maybe Aeson.Value -> Maybe Aeson.Value ->
FileDraft FileDraft
makeTemplateFD relSrcPath relDstPath tmplData = mkTmplFdWithDstAndData relSrcPath relDstPath tmplData =
createTemplateFileDraft createTemplateFileDraft
(serverRootDirInProjectRootDir </> relDstPath) (serverRootDirInProjectRootDir </> relDstPath)
(serverTemplatesDirInTemplatesDir </> relSrcPath) (serverTemplatesDirInTemplatesDir </> relSrcPath)
tmplData tmplData
copySrcTmplAsIs :: Path' (Rel ServerTemplatesSrcDir) File' -> FileDraft mkSrcTmplFd :: Path' (Rel ServerTemplatesSrcDir) File' -> FileDraft
copySrcTmplAsIs pathInTemplatesSrcDir = makeTemplateFD srcPath dstPath Nothing mkSrcTmplFd pathInTemplatesSrcDir = mkTmplFdWithDstAndData srcPath dstPath Nothing
where where
srcPath = srcDirInServerTemplatesDir </> pathInTemplatesSrcDir srcPath = srcDirInServerTemplatesDir </> pathInTemplatesSrcDir
dstPath = dstPath =

View File

@ -5,21 +5,21 @@ module Wasp.Generator.ServerGenerator.ConfigG
where where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import Data.Maybe (isJust)
import StrongPath (File', Path', Rel, relfile, (</>)) import StrongPath (File', Path', Rel, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Wasp (Wasp, getAuth)
genConfigFile :: Wasp -> FileDraft genConfigFile :: AppSpec -> FileDraft
genConfigFile wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData) genConfigFile spec = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
tmplFile = C.srcDirInServerTemplatesDir </> SP.castRel configFileInSrcDir tmplFile = C.srcDirInServerTemplatesDir </> SP.castRel configFileInSrcDir
dstFile = C.serverSrcDirInServerRootDir </> configFileInSrcDir dstFile = C.serverSrcDirInServerRootDir </> configFileInSrcDir
tmplData = tmplData =
object object
[ "isAuthEnabled" .= isJust (getAuth wasp) [ "isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool)
] ]
configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.ServerGenerator.OperationsG module Wasp.Generator.ServerGenerator.OperationsG
( genOperations, ( genOperations,
queryFileInSrcDir, queryFileInSrcDir,
@ -12,80 +14,76 @@ import Data.Char (toLower)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import StrongPath (Dir, Dir', File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>)) import StrongPath (Dir, Dir', File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Action as AS.Action
import qualified Wasp.AppSpec.Operation as AS.Operation
import qualified Wasp.AppSpec.Query as AS.Query
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.JsImport (getImportDetailsForJsFnImport) import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.Action as Wasp.Action
import qualified Wasp.Wasp.Operation as Wasp.Operation
import qualified Wasp.Wasp.Query as Wasp.Query
genOperations :: Wasp -> [FileDraft] genOperations :: AppSpec -> [FileDraft]
genOperations wasp = genOperations spec = genQueries spec ++ genActions spec
genQueries wasp
++ genActions wasp
genQueries :: Wasp -> [FileDraft] genQueries :: AppSpec -> [FileDraft]
genQueries wasp = genQueries spec = map (genQuery spec) (AS.getQueries spec)
map (genQuery wasp) (Wasp.getQueries wasp)
genActions :: Wasp -> [FileDraft] genActions :: AppSpec -> [FileDraft]
genActions wasp = genActions spec = map (genAction spec) (AS.getActions spec)
map (genAction wasp) (Wasp.getActions wasp)
-- | Here we generate JS file that basically imports JS query function provided by user, -- | Here we generate JS file that basically imports JS query function provided by user,
-- decorates it (mostly injects stuff into it) and exports. Idea is that the rest of the server, -- decorates it (mostly injects stuff into it) and exports. Idea is that the rest of the server,
-- and user also, should use this new JS function, and not the old one directly. -- and user also, should use this new JS function, and not the old one directly.
genQuery :: Wasp -> Wasp.Query.Query -> FileDraft genQuery :: AppSpec -> (String, AS.Query.Query) -> FileDraft
genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData) genQuery _ (queryName, query) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
operation = Wasp.Operation.QueryOp query operation = AS.Operation.QueryOp queryName query
tmplFile = C.asTmplFile [relfile|src/queries/_query.js|] tmplFile = C.asTmplFile [relfile|src/queries/_query.js|]
dstFile = C.serverSrcDirInServerRootDir </> queryFileInSrcDir query dstFile = C.serverSrcDirInServerRootDir </> queryFileInSrcDir queryName
tmplData = operationTmplData operation tmplData = operationTmplData operation
-- | Analogous to genQuery. -- | Analogous to genQuery.
genAction :: Wasp -> Wasp.Action.Action -> FileDraft genAction :: AppSpec -> (String, AS.Action.Action) -> FileDraft
genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData) genAction _ (actionName, action) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
operation = Wasp.Operation.ActionOp action operation = AS.Operation.ActionOp actionName action
tmplFile = [relfile|src/actions/_action.js|] tmplFile = [relfile|src/actions/_action.js|]
dstFile = C.serverSrcDirInServerRootDir </> actionFileInSrcDir action dstFile = C.serverSrcDirInServerRootDir </> actionFileInSrcDir actionName
tmplData = operationTmplData operation tmplData = operationTmplData operation
queryFileInSrcDir :: Wasp.Query.Query -> Path' (Rel C.ServerSrcDir) File' queryFileInSrcDir :: String -> Path' (Rel C.ServerSrcDir) File'
queryFileInSrcDir query = queryFileInSrcDir queryName =
[reldir|queries|] [reldir|queries|]
-- TODO: fromJust here could fail if there is some problem with the name, we should handle this. -- TODO: fromJust here could fail if there is some problem with the name, we should handle this.
</> fromJust (SP.parseRelFile $ Wasp.Query._name query ++ ".js") </> fromJust (SP.parseRelFile $ queryName ++ ".js")
actionFileInSrcDir :: Wasp.Action.Action -> Path' (Rel C.ServerSrcDir) File' actionFileInSrcDir :: String -> Path' (Rel C.ServerSrcDir) File'
actionFileInSrcDir action = actionFileInSrcDir actionName =
[reldir|actions|] [reldir|actions|]
-- TODO: fromJust here could fail if there is some problem with the name, we should handle this. -- TODO: fromJust here could fail if there is some problem with the name, we should handle this.
</> fromJust (SP.parseRelFile $ Wasp.Action._name action ++ ".js") </> fromJust (SP.parseRelFile $ actionName ++ ".js")
operationFileInSrcDir :: Wasp.Operation.Operation -> Path' (Rel C.ServerSrcDir) File' operationFileInSrcDir :: AS.Operation.Operation -> Path' (Rel C.ServerSrcDir) File'
operationFileInSrcDir (Wasp.Operation.QueryOp query) = queryFileInSrcDir query operationFileInSrcDir (AS.Operation.QueryOp name _) = queryFileInSrcDir name
operationFileInSrcDir (Wasp.Operation.ActionOp action) = actionFileInSrcDir action operationFileInSrcDir (AS.Operation.ActionOp name _) = actionFileInSrcDir name
-- | TODO: Make this not hardcoded! -- | TODO: Make this not hardcoded!
relPosixPathFromOperationFileToExtSrcDir :: Path Posix (Rel Dir') (Dir GeneratedExternalCodeDir) relPosixPathFromOperationFileToExtSrcDir :: Path Posix (Rel Dir') (Dir GeneratedExternalCodeDir)
relPosixPathFromOperationFileToExtSrcDir = [reldirP|../ext-src/|] relPosixPathFromOperationFileToExtSrcDir = [reldirP|../ext-src/|]
operationTmplData :: Wasp.Operation.Operation -> Aeson.Value operationTmplData :: AS.Operation.Operation -> Aeson.Value
operationTmplData operation = operationTmplData operation =
object object
[ "jsFnImportStatement" .= importStmt, [ "jsFnImportStatement" .= importStmt,
"jsFnIdentifier" .= importIdentifier, "jsFnIdentifier" .= importIdentifier,
"entities" .= maybe [] (map buildEntityData) (Wasp.Operation.getEntities operation) "entities" .= maybe [] (map (buildEntityData . AS.refName)) (AS.Operation.getEntities operation)
] ]
where where
(importIdentifier, importStmt) = (importIdentifier, importStmt) =
getImportDetailsForJsFnImport relPosixPathFromOperationFileToExtSrcDir $ getJsImportDetailsForExtFnImport relPosixPathFromOperationFileToExtSrcDir $
Wasp.Operation.getJsFn operation AS.Operation.getFn operation
buildEntityData :: String -> Aeson.Value buildEntityData :: String -> Aeson.Value
buildEntityData entityName = buildEntityData entityName =
object object

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.ServerGenerator.OperationsRoutesG module Wasp.Generator.ServerGenerator.OperationsRoutesG
( genOperationsRoutes, ( genOperationsRoutes,
operationRouteInOperationsRouter, operationRouteInOperationsRouter,
@ -9,54 +11,55 @@ import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust, fromMaybe, isJust) import Data.Maybe (fromJust, fromMaybe, isJust)
import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>)) import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec, getApp)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Action as AS.Action
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import qualified Wasp.AppSpec.Operation as AS.Operation
import qualified Wasp.AppSpec.Query as AS.Query
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Generator.ServerGenerator.OperationsG (operationFileInSrcDir) import Wasp.Generator.ServerGenerator.OperationsG (operationFileInSrcDir)
import qualified Wasp.Util as U import qualified Wasp.Util as U
import Wasp.Wasp (Wasp, getAuth)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.Action as Wasp.Action
import qualified Wasp.Wasp.Auth as Wasp.Auth
import qualified Wasp.Wasp.Operation as Wasp.Operation
import qualified Wasp.Wasp.Query as Wasp.Query
genOperationsRoutes :: Wasp -> [FileDraft] genOperationsRoutes :: AppSpec -> [FileDraft]
genOperationsRoutes wasp = genOperationsRoutes spec =
concat concat
[ map (genActionRoute wasp) (Wasp.getActions wasp), [ map (genActionRoute spec) (AS.getActions spec),
map (genQueryRoute wasp) (Wasp.getQueries wasp), map (genQueryRoute spec) (AS.getQueries spec),
[genOperationsRouter wasp] [genOperationsRouter spec]
] ]
genActionRoute :: Wasp -> Wasp.Action.Action -> FileDraft genActionRoute :: AppSpec -> (String, AS.Action.Action) -> FileDraft
genActionRoute wasp action = genOperationRoute wasp op tmplFile genActionRoute spec (actionName, action) = genOperationRoute spec op tmplFile
where where
op = Wasp.Operation.ActionOp action op = AS.Operation.ActionOp actionName action
tmplFile = C.asTmplFile [relfile|src/routes/operations/_action.js|] tmplFile = C.asTmplFile [relfile|src/routes/operations/_action.js|]
genQueryRoute :: Wasp -> Wasp.Query.Query -> FileDraft genQueryRoute :: AppSpec -> (String, AS.Query.Query) -> FileDraft
genQueryRoute wasp query = genOperationRoute wasp op tmplFile genQueryRoute spec (queryName, query) = genOperationRoute spec op tmplFile
where where
op = Wasp.Operation.QueryOp query op = AS.Operation.QueryOp queryName query
tmplFile = C.asTmplFile [relfile|src/routes/operations/_query.js|] tmplFile = C.asTmplFile [relfile|src/routes/operations/_query.js|]
genOperationRoute :: Wasp -> Wasp.Operation.Operation -> Path' (Rel C.ServerTemplatesDir) File' -> FileDraft genOperationRoute :: AppSpec -> AS.Operation.Operation -> Path' (Rel C.ServerTemplatesDir) File' -> FileDraft
genOperationRoute wasp operation tmplFile = C.makeTemplateFD tmplFile dstFile (Just tmplData) genOperationRoute spec operation tmplFile = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
dstFile = operationsRoutesDirInServerRootDir </> operationRouteFileInOperationsRoutesDir operation dstFile = operationsRoutesDirInServerRootDir </> operationRouteFileInOperationsRoutesDir operation
baseTmplData = baseTmplData =
object object
[ "operationImportPath" .= operationImportPath, [ "operationImportPath" .= (operationImportPath :: FilePath),
"operationName" .= Wasp.Operation.getName operation "operationName" .= (AS.Operation.getName operation :: String)
] ]
tmplData = case Wasp.getAuth wasp of tmplData = case AS.App.auth (snd $ getApp spec) of
Nothing -> baseTmplData Nothing -> baseTmplData
Just auth -> Just auth ->
U.jsonSet U.jsonSet
"userEntityLower" "userEntityLower"
(Aeson.toJSON (U.toLowerFirst $ Wasp.Auth._userEntity auth)) (Aeson.toJSON (U.toLowerFirst $ AS.refName $ AS.Auth.userEntity auth))
baseTmplData baseTmplData
operationImportPath = operationImportPath =
@ -72,40 +75,45 @@ operationsRoutesDirInServerSrcDir = [reldir|routes/operations/|]
operationsRoutesDirInServerRootDir :: Path' (Rel C.ServerRootDir) (Dir OperationsRoutesDir) operationsRoutesDirInServerRootDir :: Path' (Rel C.ServerRootDir) (Dir OperationsRoutesDir)
operationsRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir </> operationsRoutesDirInServerSrcDir operationsRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir </> operationsRoutesDirInServerSrcDir
operationRouteFileInOperationsRoutesDir :: Wasp.Operation.Operation -> Path' (Rel OperationsRoutesDir) File' operationRouteFileInOperationsRoutesDir :: AS.Operation.Operation -> Path' (Rel OperationsRoutesDir) File'
operationRouteFileInOperationsRoutesDir operation = fromJust $ SP.parseRelFile $ Wasp.Operation.getName operation ++ ".js" operationRouteFileInOperationsRoutesDir operation = fromJust $ SP.parseRelFile $ AS.Operation.getName operation ++ ".js"
relPosixPathFromOperationsRoutesDirToSrcDir :: Path Posix (Rel OperationsRoutesDir) (Dir C.ServerSrcDir) relPosixPathFromOperationsRoutesDirToSrcDir :: Path Posix (Rel OperationsRoutesDir) (Dir C.ServerSrcDir)
relPosixPathFromOperationsRoutesDirToSrcDir = [reldirP|../..|] relPosixPathFromOperationsRoutesDirToSrcDir = [reldirP|../..|]
genOperationsRouter :: Wasp -> FileDraft genOperationsRouter :: AppSpec -> FileDraft
genOperationsRouter wasp genOperationsRouter spec
-- TODO: Right now we are throwing error here, but we should instead perform this check in parsing/analyzer phase, as a semantic check, since we have all the info we need then already. -- TODO: Right now we are throwing error here, but we should instead perform this check in parsing/analyzer phase, as a semantic check, since we have all the info we need then already.
| any isAuthSpecifiedForOperation operations && not isAuthEnabledGlobally = error "`auth` cannot be specified for specific operations if it is not enabled for the whole app!" | any isAuthSpecifiedForOperation operations && not isAuthEnabledGlobally = error "`auth` cannot be specified for specific operations if it is not enabled for the whole app!"
| otherwise = C.makeTemplateFD tmplFile dstFile (Just tmplData) | otherwise = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
tmplFile = C.asTmplFile [relfile|src/routes/operations/index.js|] tmplFile = C.asTmplFile [relfile|src/routes/operations/index.js|]
dstFile = operationsRoutesDirInServerRootDir </> [relfile|index.js|] dstFile = operationsRoutesDirInServerRootDir </> [relfile|index.js|]
operations = operations =
map Wasp.Operation.ActionOp (Wasp.getActions wasp) map (uncurry AS.Operation.ActionOp) (AS.getActions spec)
++ map Wasp.Operation.QueryOp (Wasp.getQueries wasp) ++ map (uncurry AS.Operation.QueryOp) (AS.getQueries spec)
tmplData = tmplData =
object object
[ "operationRoutes" .= map makeOperationRoute operations, [ "operationRoutes" .= map makeOperationRoute operations,
"isAuthEnabled" .= isAuthEnabledGlobally "isAuthEnabled" .= isAuthEnabledGlobally
] ]
makeOperationRoute operation = makeOperationRoute operation =
let operationName = Wasp.Operation.getName operation let operationName = AS.Operation.getName operation
in object in object
[ "importIdentifier" .= operationName, [ "importIdentifier" .= operationName,
"importPath" .= ("./" ++ SP.fromRelFileP (fromJust $ SP.relFileToPosix $ operationRouteFileInOperationsRoutesDir operation)), "importPath"
.= ( "./"
++ SP.fromRelFileP
( fromJust $ SP.relFileToPosix $ operationRouteFileInOperationsRoutesDir operation
)
),
"routePath" .= ("/" ++ operationRouteInOperationsRouter operation), "routePath" .= ("/" ++ operationRouteInOperationsRouter operation),
"isUsingAuth" .= isAuthEnabledForOperation operation "isUsingAuth" .= isAuthEnabledForOperation operation
] ]
isAuthEnabledGlobally = isJust $ getAuth wasp isAuthEnabledGlobally = AS.isAuthEnabled spec
isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (Wasp.Operation.getAuth operation) isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (AS.Operation.getAuth operation)
isAuthSpecifiedForOperation operation = isJust $ Wasp.Operation.getAuth operation isAuthSpecifiedForOperation operation = isJust $ AS.Operation.getAuth operation
operationRouteInOperationsRouter :: Wasp.Operation.Operation -> String operationRouteInOperationsRouter :: AS.Operation.Operation -> String
operationRouteInOperationsRouter = U.camelToKebabCase . Wasp.Operation.getName operationRouteInOperationsRouter = U.camelToKebabCase . AS.Operation.getName

View File

@ -4,12 +4,9 @@ module Wasp.Generator.WebAppGenerator
) )
where where
import Data.Aeson import Data.Aeson (object, (.=))
( ToJSON (..),
object,
(.=),
)
import Data.List (intercalate) import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import StrongPath import StrongPath
( Dir, ( Dir,
Path', Path',
@ -18,7 +15,10 @@ import StrongPath
relfile, relfile,
(</>), (</>),
) )
import Wasp.CompileOptions (CompileOptions) import Wasp.AppSpec (AppSpec, getApp)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Wasp.Generator.FileDraft import Wasp.Generator.FileDraft
import Wasp.Generator.PackageJsonGenerator import Wasp.Generator.PackageJsonGenerator
@ -35,34 +35,30 @@ import qualified Wasp.Generator.WebAppGenerator.Common as C
import qualified Wasp.Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator import qualified Wasp.Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator
import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations) import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations)
import qualified Wasp.Generator.WebAppGenerator.RouterGenerator as RouterGenerator import qualified Wasp.Generator.WebAppGenerator.RouterGenerator as RouterGenerator
import qualified Wasp.NpmDependency as ND
import Wasp.Wasp
import qualified Wasp.Wasp.App as Wasp.App
import qualified Wasp.Wasp.NpmDependencies as WND
generateWebApp :: Wasp -> CompileOptions -> [FileDraft] generateWebApp :: AppSpec -> [FileDraft]
generateWebApp wasp _ = generateWebApp spec =
concat concat
[ [generateReadme wasp], [ [generateReadme],
[genPackageJson wasp waspNpmDeps], [genPackageJson spec waspNpmDeps],
[generateGitignore wasp], [generateGitignore],
generatePublicDir wasp, generatePublicDir spec,
generateSrcDir wasp, generateSrcDir spec,
generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy wasp, generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy (AS.externalCodeFiles spec),
[C.makeSimpleTemplateFD (asTmplFile [relfile|netlify.toml|]) wasp] [C.mkTmplFd $ asTmplFile [relfile|netlify.toml|]]
] ]
generateReadme :: Wasp -> FileDraft generateReadme :: FileDraft
generateReadme wasp = C.makeSimpleTemplateFD (asTmplFile [relfile|README.md|]) wasp generateReadme = C.mkTmplFd $ asTmplFile [relfile|README.md|]
genPackageJson :: Wasp -> [ND.NpmDependency] -> FileDraft genPackageJson :: AppSpec -> [AS.Dependency.Dependency] -> FileDraft
genPackageJson wasp waspDeps = genPackageJson spec waspDeps =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(C.asTmplFile [relfile|package.json|]) (C.asTmplFile [relfile|package.json|])
(C.asWebAppFile [relfile|package.json|]) (C.asWebAppFile [relfile|package.json|])
( Just $ ( Just $
object object
[ "wasp" .= wasp, [ "appName" .= (fst (getApp spec) :: String),
"depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps) "depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps)
] ]
) )
@ -72,12 +68,12 @@ genPackageJson wasp waspDeps =
Right deps -> deps Right deps -> deps
Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors
userDeps :: [ND.NpmDependency] userDeps :: [AS.Dependency.Dependency]
userDeps = WND._dependencies $ Wasp.Wasp.getNpmDependencies wasp userDeps = fromMaybe [] $ AS.App.dependencies $ snd $ getApp spec
waspNpmDeps :: [ND.NpmDependency] waspNpmDeps :: [AS.Dependency.Dependency]
waspNpmDeps = waspNpmDeps =
ND.fromList AS.Dependency.fromList
[ ("axios", "^0.21.1"), [ ("axios", "^0.21.1"),
("lodash", "^4.17.15"), ("lodash", "^4.17.15"),
("react", "^16.12.0"), ("react", "^16.12.0"),
@ -90,25 +86,26 @@ waspNpmDeps =
-- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps). -- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps).
generateGitignore :: Wasp -> FileDraft generateGitignore :: FileDraft
generateGitignore wasp = generateGitignore =
C.makeTemplateFD C.mkTmplFdWithDst
(asTmplFile [relfile|gitignore|]) (asTmplFile [relfile|gitignore|])
(asWebAppFile [relfile|.gitignore|]) (asWebAppFile [relfile|.gitignore|])
(Just $ toJSON wasp)
generatePublicDir :: Wasp -> [FileDraft] generatePublicDir :: AppSpec -> [FileDraft]
generatePublicDir wasp = generatePublicDir spec =
C.copyTmplAsIs (asTmplFile [relfile|public/favicon.ico|]) : C.mkTmplFd (asTmplFile [relfile|public/favicon.ico|]) :
generatePublicIndexHtml wasp : generatePublicIndexHtml spec :
map ( let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
(\path -> C.makeSimpleTemplateFD (asTmplFile $ [reldir|public|] </> path) wasp) processPublicTmpl path = C.mkTmplFdWithData (asTmplFile $ [reldir|public|] </> path) tmplData
[ [relfile|manifest.json|] in processPublicTmpl
<$> [ [relfile|manifest.json|]
] ]
)
generatePublicIndexHtml :: Wasp -> FileDraft generatePublicIndexHtml :: AppSpec -> FileDraft
generatePublicIndexHtml wasp = generatePublicIndexHtml spec =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|public/index.html|]) (asTmplFile [relfile|public/index.html|])
targetPath targetPath
(Just templateData) (Just templateData)
@ -116,8 +113,8 @@ generatePublicIndexHtml wasp =
targetPath = [relfile|public/index.html|] targetPath = [relfile|public/index.html|]
templateData = templateData =
object object
[ "title" .= Wasp.App.appTitle (getApp wasp), [ "title" .= (AS.App.title (snd $ getApp spec) :: String),
"head" .= maybe "" (intercalate "\n") (Wasp.App.appHead $ getApp wasp) "head" .= (maybe "" (intercalate "\n") (AS.App.head $ snd $ getApp spec) :: String)
] ]
-- * Src dir -- * Src dir
@ -132,15 +129,15 @@ srcDir = C.webAppSrcDirInWebAppRootDir
-- | Generates api.js file which contains token management and configured api (e.g. axios) instance. -- | Generates api.js file which contains token management and configured api (e.g. axios) instance.
genApi :: FileDraft genApi :: FileDraft
genApi = C.copyTmplAsIs (C.asTmplFile [relfile|src/api.js|]) genApi = C.mkTmplFd (C.asTmplFile [relfile|src/api.js|])
generateSrcDir :: Wasp -> [FileDraft] generateSrcDir :: AppSpec -> [FileDraft]
generateSrcDir wasp = generateSrcDir spec =
generateLogo : generateLogo :
RouterGenerator.generateRouter wasp : RouterGenerator.generateRouter spec :
genApi : genApi :
map map
makeSimpleSrcTemplateFD processSrcTmpl
[ [relfile|index.js|], [ [relfile|index.js|],
[relfile|index.css|], [relfile|index.css|],
[relfile|serviceWorker.js|], [relfile|serviceWorker.js|],
@ -148,16 +145,15 @@ generateSrcDir wasp =
[relfile|queryCache.js|], [relfile|queryCache.js|],
[relfile|utils.js|] [relfile|utils.js|]
] ]
++ genOperations wasp ++ genOperations spec
++ AuthG.genAuth wasp ++ AuthG.genAuth spec
where where
generateLogo = generateLogo =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/logo.png|]) (asTmplFile [relfile|src/logo.png|])
(srcDir </> asWebAppSrcFile [relfile|logo.png|]) (srcDir </> asWebAppSrcFile [relfile|logo.png|])
Nothing Nothing
makeSimpleSrcTemplateFD path = processSrcTmpl path =
C.makeTemplateFD C.mkTmplFdWithDst
(asTmplFile $ [reldir|src|] </> path) (asTmplFile $ [reldir|src|] </> path)
(srcDir </> asWebAppSrcFile path) (srcDir </> asWebAppSrcFile path)
(Just $ toJSON wasp)

View File

@ -5,14 +5,17 @@ where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import Data.Aeson.Types (Pair) import Data.Aeson.Types (Pair)
import Data.Maybe (fromMaybe)
import StrongPath (File', Path', Rel', reldir, relfile, (</>)) import StrongPath (File', Path', Rel', reldir, relfile, (</>))
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.Common as C
import Wasp.Wasp (Wasp, getAuth)
import qualified Wasp.Wasp.Auth as Wasp.Auth
genAuth :: Wasp -> [FileDraft] genAuth :: AppSpec -> [FileDraft]
genAuth wasp = case maybeAuth of genAuth spec = case maybeAuth of
Just auth -> Just auth ->
[ genSignup, [ genSignup,
genLogin, genLogin,
@ -23,54 +26,56 @@ genAuth wasp = case maybeAuth of
++ genAuthForms auth ++ genAuthForms auth
Nothing -> [] Nothing -> []
where where
maybeAuth = getAuth wasp maybeAuth = AS.App.auth $ snd $ AS.getApp spec
-- | Generates file with signup function to be used by Wasp developer. -- | Generates file with signup function to be used by Wasp developer.
genSignup :: FileDraft genSignup :: FileDraft
genSignup = C.copyTmplAsIs (C.asTmplFile [relfile|src/auth/signup.js|]) genSignup = C.mkTmplFd (C.asTmplFile [relfile|src/auth/signup.js|])
-- | Generates file with login function to be used by Wasp developer. -- | Generates file with login function to be used by Wasp developer.
genLogin :: FileDraft genLogin :: FileDraft
genLogin = C.copyTmplAsIs (C.asTmplFile [relfile|src/auth/login.js|]) genLogin = C.mkTmplFd (C.asTmplFile [relfile|src/auth/login.js|])
-- | Generates file with logout function to be used by Wasp developer. -- | Generates file with logout function to be used by Wasp developer.
genLogout :: FileDraft genLogout :: FileDraft
genLogout = C.copyTmplAsIs (C.asTmplFile [relfile|src/auth/logout.js|]) genLogout = C.mkTmplFd (C.asTmplFile [relfile|src/auth/logout.js|])
-- | Generates HOC that handles auth for the given page. -- | Generates HOC that handles auth for the given page.
genCreateAuthRequiredPage :: Wasp.Auth.Auth -> FileDraft genCreateAuthRequiredPage :: AS.Auth.Auth -> FileDraft
genCreateAuthRequiredPage auth = genCreateAuthRequiredPage auth =
compileTmplToSamePath compileTmplToSamePath
[relfile|auth/pages/createAuthRequiredPage.js|] [relfile|auth/pages/createAuthRequiredPage.js|]
["onAuthFailedRedirectTo" .= Wasp.Auth._onAuthFailedRedirectTo auth] ["onAuthFailedRedirectTo" .= AS.Auth.onAuthFailedRedirectTo auth]
-- | Generates React hook that Wasp developer can use in a component to get -- | Generates React hook that Wasp developer can use in a component to get
-- access to the currently logged in user (and check whether user is logged in -- access to the currently logged in user (and check whether user is logged in
-- ot not). -- ot not).
genUseAuth :: FileDraft genUseAuth :: FileDraft
genUseAuth = C.copyTmplAsIs (C.asTmplFile [relfile|src/auth/useAuth.js|]) genUseAuth = C.mkTmplFd (C.asTmplFile [relfile|src/auth/useAuth.js|])
genAuthForms :: Wasp.Auth.Auth -> [FileDraft] genAuthForms :: AS.Auth.Auth -> [FileDraft]
genAuthForms auth = genAuthForms auth =
[ genLoginForm auth, [ genLoginForm auth,
genSignupForm auth genSignupForm auth
] ]
genLoginForm :: Wasp.Auth.Auth -> FileDraft genLoginForm :: AS.Auth.Auth -> FileDraft
genLoginForm auth = genLoginForm auth =
-- TODO: Logic that says "/" is a default redirect on success is duplicated here and in the function below.
-- We should remove that duplication.
compileTmplToSamePath compileTmplToSamePath
[relfile|auth/forms/Login.js|] [relfile|auth/forms/Login.js|]
["onAuthSucceededRedirectTo" .= Wasp.Auth._onAuthSucceededRedirectTo auth] ["onAuthSucceededRedirectTo" .= fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)]
genSignupForm :: Wasp.Auth.Auth -> FileDraft genSignupForm :: AS.Auth.Auth -> FileDraft
genSignupForm auth = genSignupForm auth =
compileTmplToSamePath compileTmplToSamePath
[relfile|auth/forms/Signup.js|] [relfile|auth/forms/Signup.js|]
["onAuthSucceededRedirectTo" .= Wasp.Auth._onAuthSucceededRedirectTo auth] ["onAuthSucceededRedirectTo" .= fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)]
compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> FileDraft compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> FileDraft
compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs = compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile $ [reldir|src|] </> tmplFileInTmplSrcDir) (asTmplFile $ [reldir|src|] </> tmplFileInTmplSrcDir)
targetPath targetPath
(Just templateData) (Just templateData)

View File

@ -1,9 +1,10 @@
module Wasp.Generator.WebAppGenerator.Common module Wasp.Generator.WebAppGenerator.Common
( webAppRootDirInProjectRootDir, ( webAppRootDirInProjectRootDir,
webAppSrcDirInWebAppRootDir, webAppSrcDirInWebAppRootDir,
copyTmplAsIs, mkTmplFd,
makeSimpleTemplateFD, mkTmplFdWithDst,
makeTemplateFD, mkTmplFdWithData,
mkTmplFdWithDstAndData,
webAppSrcDirInProjectRootDir, webAppSrcDirInProjectRootDir,
webAppTemplatesDirInTemplatesDir, webAppTemplatesDirInTemplatesDir,
asTmplFile, asTmplFile,
@ -21,7 +22,6 @@ import qualified StrongPath as SP
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Templates (TemplatesDir) import Wasp.Generator.Templates (TemplatesDir)
import Wasp.Wasp (Wasp)
data WebAppRootDir data WebAppRootDir
@ -57,18 +57,21 @@ webAppSrcDirInProjectRootDir = webAppRootDirInProjectRootDir </> webAppSrcDirInW
webAppTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir WebAppTemplatesDir) webAppTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir WebAppTemplatesDir)
webAppTemplatesDirInTemplatesDir = [reldir|react-app|] webAppTemplatesDirInTemplatesDir = [reldir|react-app|]
copyTmplAsIs :: Path' (Rel WebAppTemplatesDir) File' -> FileDraft mkTmplFd :: Path' (Rel WebAppTemplatesDir) File' -> FileDraft
copyTmplAsIs path = makeTemplateFD path (SP.castRel path) Nothing mkTmplFd path = mkTmplFdWithDst path (SP.castRel path)
makeSimpleTemplateFD :: Path' (Rel WebAppTemplatesDir) File' -> Wasp -> FileDraft mkTmplFdWithDst :: Path' (Rel WebAppTemplatesDir) File' -> Path' (Rel WebAppRootDir) File' -> FileDraft
makeSimpleTemplateFD path wasp = makeTemplateFD path (SP.castRel path) (Just $ Aeson.toJSON wasp) mkTmplFdWithDst src dst = mkTmplFdWithDstAndData src dst Nothing
makeTemplateFD :: mkTmplFdWithData :: Path' (Rel WebAppTemplatesDir) File' -> Aeson.Value -> FileDraft
mkTmplFdWithData src tmplData = mkTmplFdWithDstAndData src (SP.castRel src) (Just tmplData)
mkTmplFdWithDstAndData ::
Path' (Rel WebAppTemplatesDir) File' -> Path' (Rel WebAppTemplatesDir) File' ->
Path' (Rel WebAppRootDir) File' -> Path' (Rel WebAppRootDir) File' ->
Maybe Aeson.Value -> Maybe Aeson.Value ->
FileDraft FileDraft
makeTemplateFD srcPathInWebAppTemplatesDir dstPathInWebAppRootDir tmplData = mkTmplFdWithDstAndData srcPathInWebAppTemplatesDir dstPathInWebAppRootDir tmplData =
createTemplateFileDraft createTemplateFileDraft
(webAppRootDirInProjectRootDir </> dstPathInWebAppRootDir) (webAppRootDirInProjectRootDir </> dstPathInWebAppRootDir)
(webAppTemplatesDirInTemplatesDir </> srcPathInWebAppTemplatesDir) (webAppTemplatesDirInTemplatesDir </> srcPathInWebAppTemplatesDir)

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.WebAppGenerator.OperationsGenerator module Wasp.Generator.WebAppGenerator.OperationsGenerator
( genOperations, ( genOperations,
) )
@ -10,44 +12,44 @@ import Data.Aeson
import Data.List (intercalate) import Data.List (intercalate)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import StrongPath (File', Path', Rel', parseRelFile, reldir, relfile, (</>)) import StrongPath (File', Path', Rel', parseRelFile, reldir, relfile, (</>))
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Action as AS.Action
import qualified Wasp.AppSpec.Operation as AS.Operation
import qualified Wasp.AppSpec.Query as AS.Query
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.ServerGenerator as ServerGenerator import qualified Wasp.Generator.ServerGenerator as ServerGenerator
import qualified Wasp.Generator.ServerGenerator.OperationsRoutesG as ServerOperationsRoutesG import qualified Wasp.Generator.ServerGenerator.OperationsRoutesG as ServerOperationsRoutesG
import qualified Wasp.Generator.WebAppGenerator.Common as C import qualified Wasp.Generator.WebAppGenerator.Common as C
import qualified Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG as Resources import qualified Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG as Resources
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.Action as Wasp.Action
import qualified Wasp.Wasp.Operation as Wasp.Operation
import qualified Wasp.Wasp.Query as Wasp.Query
genOperations :: Wasp -> [FileDraft] genOperations :: AppSpec -> [FileDraft]
genOperations wasp = genOperations spec =
concat concat
[ genQueries wasp, [ genQueries spec,
genActions wasp, genActions spec,
[C.makeSimpleTemplateFD (C.asTmplFile [relfile|src/operations/index.js|]) wasp], [C.mkTmplFd $ C.asTmplFile [relfile|src/operations/index.js|]],
Resources.genResources wasp Resources.genResources spec
] ]
genQueries :: Wasp -> [FileDraft] genQueries :: AppSpec -> [FileDraft]
genQueries wasp = genQueries spec =
map (genQuery wasp) (Wasp.getQueries wasp) map (genQuery spec) (AS.getQueries spec)
++ [C.makeSimpleTemplateFD (C.asTmplFile [relfile|src/queries/index.js|]) wasp] ++ [C.mkTmplFd $ C.asTmplFile [relfile|src/queries/index.js|]]
genActions :: Wasp -> [FileDraft] genActions :: AppSpec -> [FileDraft]
genActions wasp = genActions spec =
map (genAction wasp) (Wasp.getActions wasp) map (genAction spec) (AS.getActions spec)
genQuery :: Wasp -> Wasp.Query.Query -> FileDraft genQuery :: AppSpec -> (String, AS.Query.Query) -> FileDraft
genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData) genQuery _ (queryName, query) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
tmplFile = C.asTmplFile [relfile|src/queries/_query.js|] tmplFile = C.asTmplFile [relfile|src/queries/_query.js|]
dstFile = C.asWebAppFile $ [reldir|src/queries/|] </> fromJust (getOperationDstFileName operation) dstFile = C.asWebAppFile $ [reldir|src/queries/|] </> fromJust (getOperationDstFileName operation)
tmplData = tmplData =
object object
[ "queryFnName" .= Wasp.Query._name query, [ "queryFnName" .= (queryName :: String),
"queryRoute" "queryRoute"
.= ( ServerGenerator.operationsRouteInRootRouter .= ( ServerGenerator.operationsRouteInRootRouter
++ "/" ++ "/"
@ -55,17 +57,17 @@ genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData)
), ),
"entitiesArray" .= makeJsArrayOfEntityNames operation "entitiesArray" .= makeJsArrayOfEntityNames operation
] ]
operation = Wasp.Operation.QueryOp query operation = AS.Operation.QueryOp queryName query
genAction :: Wasp -> Wasp.Action.Action -> FileDraft genAction :: AppSpec -> (String, AS.Action.Action) -> FileDraft
genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData) genAction _ (actionName, action) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
tmplFile = C.asTmplFile [relfile|src/actions/_action.js|] tmplFile = C.asTmplFile [relfile|src/actions/_action.js|]
dstFile = C.asWebAppFile $ [reldir|src/actions/|] </> fromJust (getOperationDstFileName operation) dstFile = C.asWebAppFile $ [reldir|src/actions/|] </> fromJust (getOperationDstFileName operation)
tmplData = tmplData =
object object
[ "actionFnName" .= Wasp.Action._name action, [ "actionFnName" .= (actionName :: String),
"actionRoute" "actionRoute"
.= ( ServerGenerator.operationsRouteInRootRouter .= ( ServerGenerator.operationsRouteInRootRouter
++ "/" ++ "/"
@ -73,14 +75,14 @@ genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData)
), ),
"entitiesArray" .= makeJsArrayOfEntityNames operation "entitiesArray" .= makeJsArrayOfEntityNames operation
] ]
operation = Wasp.Operation.ActionOp action operation = AS.Operation.ActionOp actionName action
-- | Generates string that is JS array containing names (as strings) of entities being used by given operation. -- | Generates string that is JS array containing names (as strings) of entities being used by given operation.
-- E.g. "['Task', 'Project']" -- E.g. "['Task', 'Project']"
makeJsArrayOfEntityNames :: Wasp.Operation.Operation -> String makeJsArrayOfEntityNames :: AS.Operation.Operation -> String
makeJsArrayOfEntityNames operation = "[" ++ intercalate ", " entityStrings ++ "]" makeJsArrayOfEntityNames operation = "[" ++ intercalate ", " entityStrings ++ "]"
where where
entityStrings = maybe [] (map (\x -> "'" ++ x ++ "'")) (Wasp.Operation.getEntities operation) entityStrings = maybe [] (map $ \x -> "'" ++ AS.refName x ++ "'") (AS.Operation.getEntities operation)
getOperationDstFileName :: Wasp.Operation.Operation -> Maybe (Path' Rel' File') getOperationDstFileName :: AS.Operation.Operation -> Maybe (Path' Rel' File')
getOperationDstFileName operation = parseRelFile (Wasp.Operation.getName operation ++ ".js") getOperationDstFileName operation = parseRelFile (AS.Operation.getName operation ++ ".js")

View File

@ -5,12 +5,12 @@ where
import Data.Aeson (object) import Data.Aeson (object)
import StrongPath (relfile) import StrongPath (relfile)
import Wasp.AppSpec (AppSpec)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import qualified Wasp.Generator.WebAppGenerator.Common as C import qualified Wasp.Generator.WebAppGenerator.Common as C
import Wasp.Wasp (Wasp)
genResources :: Wasp -> [FileDraft] genResources :: AppSpec -> [FileDraft]
genResources _ = [C.makeTemplateFD tmplFile dstFile (Just tmplData)] genResources _ = [C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)]
where where
tmplFile = C.asTmplFile [relfile|src/operations/resources.js|] tmplFile = C.asTmplFile [relfile|src/operations/resources.js|]
dstFile = C.asWebAppFile [relfile|src/operations/resources.js|] -- TODO: Un-hardcode this by combining path to operations dir with path to resources file in it. dstFile = C.asWebAppFile [relfile|src/operations/resources.js|] -- TODO: Un-hardcode this by combining path to operations dir with path to resources file in it.

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.WebAppGenerator.RouterGenerator module Wasp.Generator.WebAppGenerator.RouterGenerator
( generateRouter, ( generateRouter,
) )
@ -5,17 +7,18 @@ where
import Data.Aeson (ToJSON (..), object, (.=)) import Data.Aeson (ToJSON (..), object, (.=))
import Data.List (find) import Data.List (find)
import Data.Maybe (fromJust, fromMaybe, isJust) import Data.Maybe (fromMaybe)
import StrongPath (reldir, relfile, (</>)) import StrongPath (reldir, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import qualified System.FilePath as FP
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Route as AS.Route
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile) import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile)
import qualified Wasp.Generator.WebAppGenerator.Common as C import qualified Wasp.Generator.WebAppGenerator.Common as C
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
import qualified Wasp.Wasp.JsImport as Wasp.JsImport
import qualified Wasp.Wasp.Page as Wasp.Page
import qualified Wasp.Wasp.Route as Wasp.Route
data RouterTemplateData = RouterTemplateData data RouterTemplateData = RouterTemplateData
{ _routes :: ![RouteTemplateData], { _routes :: ![RouteTemplateData],
@ -56,47 +59,52 @@ instance ToJSON PageTemplateData where
"importFrom" .= _importFrom pageTD "importFrom" .= _importFrom pageTD
] ]
generateRouter :: Wasp -> FileDraft generateRouter :: AppSpec -> FileDraft
generateRouter wasp = generateRouter spec =
C.makeTemplateFD C.mkTmplFdWithDstAndData
(asTmplFile $ [reldir|src|] </> routerPath) (asTmplFile $ [reldir|src|] </> routerPath)
targetPath targetPath
(Just $ toJSON templateData) (Just $ toJSON templateData)
where where
routerPath = [relfile|router.js|] routerPath = [relfile|router.js|]
templateData = createRouterTemplateData wasp templateData = createRouterTemplateData spec
targetPath = C.webAppSrcDirInWebAppRootDir </> asWebAppSrcFile routerPath targetPath = C.webAppSrcDirInWebAppRootDir </> asWebAppSrcFile routerPath
createRouterTemplateData :: Wasp -> RouterTemplateData createRouterTemplateData :: AppSpec -> RouterTemplateData
createRouterTemplateData wasp = createRouterTemplateData spec =
RouterTemplateData RouterTemplateData
{ _routes = routes, { _routes = routes,
_pagesToImport = pages, _pagesToImport = pages,
_isAuthEnabled = isJust $ Wasp.getAuth wasp _isAuthEnabled = AS.isAuthEnabled spec
} }
where where
routes = map (createRouteTemplateData wasp) $ Wasp.getRoutes wasp routes = map (createRouteTemplateData spec) $ AS.getRoutes spec
pages = map createPageTemplateData $ Wasp.getPages wasp pages = map createPageTemplateData $ AS.getPages spec
createRouteTemplateData :: Wasp -> Wasp.Route.Route -> RouteTemplateData createRouteTemplateData :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData
createRouteTemplateData wasp route = createRouteTemplateData spec namedRoute@(_, route) =
RouteTemplateData RouteTemplateData
{ _urlPath = Wasp.Route._urlPath route, { _urlPath = AS.Route.path route,
_targetComponent = determineRouteTargetComponent wasp route _targetComponent = determineRouteTargetComponent spec namedRoute
} }
determineRouteTargetComponent :: Wasp -> Wasp.Route.Route -> String determineRouteTargetComponent :: AppSpec -> (String, AS.Route.Route) -> String
determineRouteTargetComponent wasp route = determineRouteTargetComponent spec (_, route) =
maybe maybe
targetPageName targetPageName
determineRouteTargetComponent' determineRouteTargetComponent'
(Wasp.Page._authRequired targetPage) (AS.Page.authRequired $ snd targetPage)
where where
targetPageName = Wasp.Route._targetPage route targetPageName = AS.refName (AS.Route.to route :: AS.Ref AS.Page.Page)
targetPage = targetPage =
fromMaybe fromMaybe
(error $ "Can't find page with name '" ++ targetPageName ++ "', pointed to by route '" ++ Wasp.Route._urlPath route ++ "'") ( error $
(find ((==) targetPageName . Wasp.Page._name) (Wasp.getPages wasp)) "Can't find page with name '" ++ targetPageName
++ "', pointed to by route '"
++ AS.Route.path route
++ "'"
)
(find ((==) targetPageName . fst) (AS.getPages spec))
determineRouteTargetComponent' :: Bool -> String determineRouteTargetComponent' :: Bool -> String
determineRouteTargetComponent' authRequired = determineRouteTargetComponent' authRequired =
@ -105,21 +113,20 @@ determineRouteTargetComponent wasp route =
"createAuthRequiredPage(" ++ targetPageName ++ ")" "createAuthRequiredPage(" ++ targetPageName ++ ")"
else targetPageName else targetPageName
createPageTemplateData :: Wasp.Page.Page -> PageTemplateData createPageTemplateData :: (String, AS.Page.Page) -> PageTemplateData
createPageTemplateData page = createPageTemplateData page =
PageTemplateData PageTemplateData
{ _importFrom = { _importFrom = relPathToExtSrcDir FP.</> SP.fromRelFileP (AS.ExtImport.path pageComponent),
relPathToExtSrcDir _importWhat = case AS.ExtImport.name pageComponent of
++ SP.fromRelFileP (fromJust $ SP.relFileToPosix $ Wasp.JsImport._from pageComponent), AS.ExtImport.ExtImportModule _ -> pageName
_importWhat = case Wasp.JsImport._namedImports pageComponent of AS.ExtImport.ExtImportField identifier -> "{ " ++ identifier ++ " as " ++ pageName ++ " }"
-- If no named imports, we go with the default import.
[] -> pageName
[namedImport] -> "{ " ++ namedImport ++ " as " ++ pageName ++ " }"
_ -> error "Only one named import can be provided for a page."
} }
where where
relPathToExtSrcDir :: FilePath relPathToExtSrcDir :: FilePath
relPathToExtSrcDir = "./ext-src/" relPathToExtSrcDir = "./ext-src/"
pageName = Wasp.Page._name page pageName :: String
pageComponent = Wasp.Page._component page pageName = fst page
pageComponent :: AS.ExtImport.ExtImport
pageComponent = AS.Page.component $ snd page

View File

@ -11,16 +11,17 @@ import Data.List (find, isSuffixOf)
import StrongPath (Abs, Dir, File', Path', relfile) import StrongPath (Abs, Dir, File', Path', relfile)
import qualified StrongPath as SP import qualified StrongPath as SP
import System.Directory (doesDirectoryExist, doesFileExist) import System.Directory (doesDirectoryExist, doesFileExist)
import qualified Wasp.Analyzer as Analyzer
import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx)
import qualified Wasp.AppSpec as AS
import Wasp.Common (DbMigrationsDir, WaspProjectDir, dbMigrationsDirInWaspProjectDir) import Wasp.Common (DbMigrationsDir, WaspProjectDir, dbMigrationsDirInWaspProjectDir)
import Wasp.CompileOptions (CompileOptions) import Wasp.CompileOptions (CompileOptions)
import qualified Wasp.CompileOptions as CompileOptions import qualified Wasp.CompileOptions as CompileOptions
import Wasp.Error (showCompilerErrorForTerminal)
import qualified Wasp.ExternalCode as ExternalCode import qualified Wasp.ExternalCode as ExternalCode
import qualified Wasp.Generator as Generator import qualified Wasp.Generator as Generator
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Parser as Parser
import qualified Wasp.Util.IO as Util.IO import qualified Wasp.Util.IO as Util.IO
import Wasp.Wasp (Wasp)
import qualified Wasp.Wasp as Wasp
type CompileError = String type CompileError = String
@ -30,34 +31,33 @@ compile ::
CompileOptions -> CompileOptions ->
IO (Either CompileError ()) IO (Either CompileError ())
compile waspDir outDir options = do compile waspDir outDir options = do
maybeWaspFile <- findWaspFile waspDir maybeWaspFilePath <- findWaspFile waspDir
case maybeWaspFile of case maybeWaspFilePath of
Nothing -> return $ Left "Couldn't find a single *.wasp file." Nothing -> return $ Left "Couldn't find a single *.wasp file."
Just waspFile -> do Just waspFilePath -> do
waspStr <- readFile (SP.toFilePath waspFile) waspFileContent <- readFile (SP.fromAbsFile waspFilePath)
case Analyzer.analyze waspFileContent of
case Parser.parseWasp waspStr of Left analyzeError ->
Left err -> return $ Left (show err) return $
Right wasp -> do Left $
showCompilerErrorForTerminal
(waspFilePath, waspFileContent)
(getErrorMessageAndCtx analyzeError)
Right decls -> do
externalCodeFiles <-
ExternalCode.readFiles (CompileOptions.externalCodeDirPath options)
maybeDotEnvFile <- findDotEnvFile waspDir maybeDotEnvFile <- findDotEnvFile waspDir
maybeMigrationsDir <- findMigrationsDir waspDir maybeMigrationsDir <- findMigrationsDir waspDir
( wasp let appSpec =
`Wasp.setDotEnvFile` maybeDotEnvFile AS.AppSpec
`Wasp.setMigrationsDir` maybeMigrationsDir { AS.decls = decls,
`enrichWaspASTBasedOnCompileOptions` options AS.externalCodeFiles = externalCodeFiles,
) AS.externalCodeDirPath = CompileOptions.externalCodeDirPath options,
>>= generateCode AS.migrationsDir = maybeMigrationsDir,
where AS.dotEnvFile = maybeDotEnvFile,
generateCode wasp = Generator.writeWebAppCode wasp outDir options >> return (Right ()) AS.isBuild = CompileOptions.isBuild options
}
enrichWaspASTBasedOnCompileOptions :: Wasp -> CompileOptions -> IO Wasp Right <$> Generator.writeWebAppCode appSpec outDir
enrichWaspASTBasedOnCompileOptions wasp options = do
externalCodeFiles <- ExternalCode.readFiles (CompileOptions.externalCodeDirPath options)
return
( wasp
`Wasp.setExternalCodeFiles` externalCodeFiles
`Wasp.setIsBuild` CompileOptions.isBuild options
)
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File')) findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findWaspFile waspDir = do findWaspFile waspDir = do
@ -74,7 +74,9 @@ findDotEnvFile waspDir = do
dotEnvExists <- doesFileExist (SP.toFilePath dotEnvAbsPath) dotEnvExists <- doesFileExist (SP.toFilePath dotEnvAbsPath)
return $ if dotEnvExists then Just dotEnvAbsPath else Nothing return $ if dotEnvExists then Just dotEnvAbsPath else Nothing
findMigrationsDir :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs (Dir DbMigrationsDir))) findMigrationsDir ::
Path' Abs (Dir WaspProjectDir) ->
IO (Maybe (Path' Abs (Dir DbMigrationsDir)))
findMigrationsDir waspDir = do findMigrationsDir waspDir = do
let migrationsAbsPath = waspDir SP.</> dbMigrationsDirInWaspProjectDir let migrationsAbsPath = waspDir SP.</> dbMigrationsDirInWaspProjectDir
migrationsExists <- doesDirectoryExist $ SP.fromAbsDir migrationsAbsPath migrationsExists <- doesDirectoryExist $ SP.fromAbsDir migrationsAbsPath

View File

@ -1,88 +0,0 @@
module Wasp.Parser
( parseWasp,
)
where
import Text.Parsec (ParseError, eof, many, many1, (<|>))
import Text.Parsec.String (Parser)
import Wasp.Lexer
import qualified Wasp.Parser.Action as Parser.Action
import Wasp.Parser.App (app)
import Wasp.Parser.Auth (auth)
import Wasp.Parser.Common (runWaspParser)
import Wasp.Parser.Db (db)
import Wasp.Parser.Entity (entity)
import Wasp.Parser.JsImport (jsImport)
import qualified Wasp.Parser.NpmDependencies as Parser.NpmDependencies
import Wasp.Parser.Page (page)
import qualified Wasp.Parser.Query as Parser.Query
import Wasp.Parser.Route (route)
import qualified Wasp.Parser.Server as Parser.Server
import qualified Wasp.Wasp as Wasp
waspElement :: Parser Wasp.WaspElement
waspElement =
waspElementApp
<|> waspElementAuth
<|> waspElementPage
<|> waspElementDb
<|> waspElementRoute
<|> waspElementEntity
<|> waspElementQuery
<|> waspElementAction
<|> waspElementNpmDependencies
<|> waspElementServer
waspElementApp :: Parser Wasp.WaspElement
waspElementApp = Wasp.WaspElementApp <$> app
waspElementAuth :: Parser Wasp.WaspElement
waspElementAuth = Wasp.WaspElementAuth <$> auth
waspElementDb :: Parser Wasp.WaspElement
waspElementDb = Wasp.WaspElementDb <$> db
waspElementPage :: Parser Wasp.WaspElement
waspElementPage = Wasp.WaspElementPage <$> page
waspElementRoute :: Parser Wasp.WaspElement
waspElementRoute = Wasp.WaspElementRoute <$> route
waspElementEntity :: Parser Wasp.WaspElement
waspElementEntity = Wasp.WaspElementEntity <$> entity
waspElementQuery :: Parser Wasp.WaspElement
waspElementQuery = Wasp.WaspElementQuery <$> Parser.Query.query
waspElementAction :: Parser Wasp.WaspElement
waspElementAction = Wasp.WaspElementAction <$> Parser.Action.action
waspElementServer :: Parser Wasp.WaspElement
waspElementServer = Wasp.WaspElementServer <$> Parser.Server.server
waspElementNpmDependencies :: Parser Wasp.WaspElement
waspElementNpmDependencies = Wasp.WaspElementNpmDependencies <$> Parser.NpmDependencies.npmDependencies
-- | Top level parser, produces Wasp.
waspParser :: Parser Wasp.Wasp
waspParser = do
-- NOTE(matija): this is the only place we need to use whiteSpace, to skip empty lines
-- and comments in the beginning of file. All other used parsers are lexeme parsers
-- so they do it themselves.
whiteSpace
jsImports <- many jsImport
waspElems <- many1 waspElement
eof
-- TODO(matija): after we parsed everything, we should do semantic analysis
-- e.g. check there is only 1 title - if not, throw a meaningful error.
-- Also, check there is at least one Page defined.
return $ Wasp.fromWaspElems waspElems `Wasp.setJsImports` jsImports
-- | Top level parser executor.
parseWasp :: String -> Either ParseError Wasp.Wasp
parseWasp = runWaspParser waspParser

View File

@ -1,24 +0,0 @@
module Wasp.Parser.Action
( action,
)
where
import Data.Maybe (fromMaybe)
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as C
import qualified Wasp.Parser.Operation as Operation
import Wasp.Wasp.Action (Action)
import qualified Wasp.Wasp.Action as Action
action :: Parser Action
action = do
(name, props) <- C.waspElementNameAndClosureContent L.reservedNameAction Operation.properties
return
Action.Action
{ Action._name = name,
Action._jsFunction =
fromMaybe (error "Action js function is missing.") (Operation.getJsFunctionFromProps props),
Action._entities = Operation.getEntitiesFromProps props,
Action._auth = Operation.getAuthEnabledFromProps props
}

View File

@ -1,57 +0,0 @@
module Wasp.Parser.App
( app,
)
where
import Data.Maybe (listToMaybe)
import Text.Parsec
import Text.Parsec.String (Parser)
import Wasp.Lexer
import qualified Wasp.Lexer as L
import Wasp.Parser.Common
import qualified Wasp.Wasp.App as App
-- | A type that describes supported app properties.
data AppProperty
= Title !String
| Favicon !String
| Head [String]
deriving (Show, Eq)
-- | Parses supported app properties, expects format "key1: value1, key2: value2, ..."
appProperties :: Parser [AppProperty]
appProperties =
commaSep1 $
appPropertyTitle
<|> appPropertyFavicon
<|> appPropertyHead
appPropertyTitle :: Parser AppProperty
appPropertyTitle = Title <$> waspPropertyStringLiteral "title"
appPropertyFavicon :: Parser AppProperty
-- TODO(matija): 'fav.png' currently does not work because of '.'. Support it.
appPropertyFavicon = Favicon <$> waspPropertyStringLiteral "favicon"
appPropertyHead :: Parser AppProperty
appPropertyHead = Head <$> waspProperty "head" (L.brackets $ L.commaSep1 L.stringLiteral)
-- TODO(matija): unsafe, what if empty list?
getAppTitle :: [AppProperty] -> String
getAppTitle ps = head $ [t | Title t <- ps]
getAppHead :: [AppProperty] -> Maybe [String]
getAppHead ps = listToMaybe [hs | Head hs <- ps]
-- | Top level parser, parses App.
app :: Parser App.App
app = do
(appName, appProps) <- waspElementNameAndClosureContent reservedNameApp appProperties
return
App.App
{ App.appName = appName,
App.appTitle = getAppTitle appProps,
App.appHead = getAppHead appProps
-- TODO(matija): add favicon.
}

View File

@ -1,85 +0,0 @@
module Wasp.Parser.Auth
( auth,
)
where
import Control.Monad (when)
import Text.Parsec (try, (<|>))
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as P
import qualified Wasp.Wasp.Auth as Wasp.Auth
auth :: Parser Wasp.Auth.Auth
auth = do
L.reserved L.reservedNameAuth
authProperties <- P.waspClosure (L.commaSep1 authProperty)
let userEntityProps = [s | AuthPropertyUserEntity s <- authProperties]
failIfPropMissing propUserEntityName userEntityProps
let methodsProps = [ms | AuthPropertyMethods ms <- authProperties]
failIfPropMissing propMethodsName methodsProps
let failRedirectProps = [r | AuthPropertyOnAuthFailedRedirectTo r <- authProperties]
failIfPropMissing propOnAuthFailedRedirectToName failRedirectProps
let successRedirectProps = [r | AuthPropertyOnAuthSucceededRedirectTo r <- authProperties]
return
Wasp.Auth.Auth
{ Wasp.Auth._userEntity = head userEntityProps,
Wasp.Auth._methods = head methodsProps,
Wasp.Auth._onAuthFailedRedirectTo = head failRedirectProps,
Wasp.Auth._onAuthSucceededRedirectTo = headWithDefault "/" successRedirectProps
}
headWithDefault :: a -> [a] -> a
headWithDefault d ps = if null ps then d else head ps
-- TODO(matija): this should be extracted if we want to use in other places too.
failIfPropMissing :: (Applicative m, MonadFail m) => String -> [p] -> m ()
failIfPropMissing propName ps = when (null ps) $ fail errorMsg
where
errorMsg = propName ++ " is required!"
-- Auxiliary data structure used by parser.
data AuthProperty
= AuthPropertyUserEntity String
| AuthPropertyMethods [Wasp.Auth.AuthMethod]
| AuthPropertyOnAuthFailedRedirectTo String
| AuthPropertyOnAuthSucceededRedirectTo String
propUserEntityName :: String
propUserEntityName = "userEntity"
propMethodsName :: String
propMethodsName = "methods"
propOnAuthFailedRedirectToName :: String
propOnAuthFailedRedirectToName = "onAuthFailedRedirectTo"
-- Sub-parsers
authProperty :: Parser AuthProperty
authProperty =
authPropertyUserEntity
<|> authPropertyMethods
<|> (try authPropertyOnAuthFailedRedirectTo <|> authPropertyOnAuthSucceededRedirectTo)
authPropertyOnAuthFailedRedirectTo :: Parser AuthProperty
authPropertyOnAuthFailedRedirectTo =
AuthPropertyOnAuthFailedRedirectTo <$> P.waspPropertyStringLiteral "onAuthFailedRedirectTo"
authPropertyOnAuthSucceededRedirectTo :: Parser AuthProperty
authPropertyOnAuthSucceededRedirectTo =
AuthPropertyOnAuthSucceededRedirectTo <$> P.waspPropertyStringLiteral "onAuthSucceededRedirectTo"
authPropertyUserEntity :: Parser AuthProperty
authPropertyUserEntity = AuthPropertyUserEntity <$> P.waspProperty "userEntity" L.identifier
authPropertyMethods :: Parser AuthProperty
authPropertyMethods = AuthPropertyMethods <$> P.waspProperty "methods" (L.brackets $ L.commaSep1 authMethod)
authMethod :: Parser Wasp.Auth.AuthMethod
authMethod = L.symbol "EmailAndPassword" *> pure Wasp.Auth.EmailAndPassword

View File

@ -1,180 +0,0 @@
{-
Common functions used among Wasp parsers.
-}
module Wasp.Parser.Common where
import qualified Data.Text as T
import StrongPath (File, Path, Posix, Rel, System)
import qualified StrongPath as SP
import Text.Parsec
( ParseError,
anyChar,
manyTill,
parse,
try,
unexpected,
)
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
-- | Runs given wasp parser on a specified input.
runWaspParser :: Parser a -> String -> Either ParseError a
runWaspParser waspParser input = parse waspParser sourceName input
where
-- NOTE(matija): this is used by Parsec only when reporting errors, but we currently
-- don't provide source name (e.g. .wasp file name) to this method so leaving it empty
-- for now.
sourceName = ""
-- TODO(matija): rename to just "waspElement"?
-- | Parses declaration of a wasp element (e.g. App or Page) and the closure content.
waspElementNameAndClosureContent ::
-- | Type of the wasp element (e.g. "app" or "page").
String ->
-- | Parser to be used for parsing closure content of the wasp element.
Parser a ->
-- | Name of the element and parsed closure content.
Parser (String, a)
waspElementNameAndClosureContent elementType closureContent =
waspElementNameAndClosure elementType (waspClosure closureContent)
-- | Parses declaration of a wasp element (e.g. App or Page) and the belonging closure.
waspElementNameAndClosure ::
-- | Element type
String ->
-- | Closure parser (needs to parse braces as well, not just the content)
Parser a ->
-- | Name of the element and parsed closure content.
Parser (String, a)
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
-- "entity" and would consume "entity", so entity-form parser would also fail.
-- This way when entity parser fails, it will backtrack and allow
-- entity-form parser to succeed.
--
-- TODO(matija): should I push this try higher, to the specific case of entity parser
-- which is causing the trouble?
-- This way try will be executed in more cases where it is not neccessary, this
-- might not be the best for the performance and the clarity of error messages.
-- On the other hand, it is safer?
try $ do
L.reserved elementType
elementName <- L.identifier
closureContent <- closure
return (elementName, closureContent)
-- | Parses declaration of a wasp element linked to an entity.
-- E.g. "entity-form<Task> ..." or "action<Task> ..."
waspElementLinkedToEntity ::
-- | Type of the linked wasp element (e.g. "entity-form").
String ->
-- | Parser to be used for parsing body of the wasp element.
Parser a ->
-- | Name of the linked entity, element name and body.
Parser (String, String, a)
waspElementLinkedToEntity elementType bodyParser = do
L.reserved elementType
linkedEntityName <- L.angles L.identifier
elementName <- L.identifier
body <- bodyParser
return (linkedEntityName, elementName, body)
-- | Parses wasp property along with the key, "key: value".
waspProperty :: String -> Parser a -> Parser a
waspProperty key value = L.symbol key <* L.colon *> value
-- | Parses wasp property which has a string literal for a value.
-- e.g.: title: "my first app"
waspPropertyStringLiteral :: String -> Parser String
waspPropertyStringLiteral key = waspProperty key L.stringLiteral
-- | Parses wasp property which has a bool for a value. E.g.: "onEnter: true".
waspPropertyBool :: String -> Parser Bool
waspPropertyBool key = waspProperty key L.bool
-- | Parses wasp property which has an identifier as a key (E.g. field name or filter name).
-- E.g. within an entity-form {} we can set properties for a specific field with a closure of
-- form "FIELD_NAME: {...}" -> FIELD_NAME is then an identifier we need.
waspPropertyWithIdentifierAsKey :: Parser a -> Parser (String, a)
waspPropertyWithIdentifierAsKey valueP = do
identifier <- L.identifier <* L.colon
value <- valueP
return (identifier, value)
-- | Parses wasp closure, which is {...}. Returns parsed content within the closure.
waspClosure :: Parser a -> Parser a
waspClosure = L.braces
-- | Parses wasp property closure where property is an identifier whose value we also
-- need to retrieve.
-- E.g. within an entity-form {} we can set properties for a specific field with a closure of
-- form "FIELD_NAME: {...}" -> FIELD_NAME is then an identifier we need.
waspIdentifierClosure :: Parser a -> Parser (String, a)
waspIdentifierClosure = waspPropertyWithIdentifierAsKey . waspClosure
-- | Parses wasp property which has a closure for a value. Returns parsed content within the
-- closure.
waspPropertyClosure :: String -> Parser a -> Parser a
waspPropertyClosure key closureContent = waspProperty key (waspClosure closureContent)
-- | Parses wasp property which has a jsx closure for a value. Returns the content
-- within the closure.
waspPropertyJsxClosure :: String -> Parser String
waspPropertyJsxClosure key = waspProperty key waspJsxClosure
-- | Parses wasp property which has a css closure for a value. Returns the content
-- within the closure.
waspPropertyCssClosure :: String -> Parser String
waspPropertyCssClosure key = waspProperty key waspCssClosure
-- | Parses wasp jsx closure, which is {=jsx...jsx=}. Returns content within the closure.
waspJsxClosure :: Parser String
waspJsxClosure = waspNamedClosure "jsx"
-- | Parses wasp css closure, which is {=css...css=}. Returns content within the closure.
waspCssClosure :: Parser String
waspCssClosure = waspNamedClosure "css"
-- TODO(martin): write tests and comments.
-- | Parses named wasp closure, which is {=name...name=}. Returns content within the closure.
waspNamedClosure :: String -> Parser String
waspNamedClosure name = do
_ <- closureStart
strip <$> manyTill anyChar (try closureEnd)
where
closureStart = L.symbol ("{=" ++ name)
closureEnd = L.symbol (name ++ "=}")
-- | Parses a list of items that can be parsed with given parser.
-- For example, `waspList L.identifier` will parse "[foo, bar, t]" into ["foo", "bar", "t"].
waspList :: Parser a -> Parser [a]
waspList elementParser = L.brackets $ L.commaSep elementParser
-- | Removes leading and trailing spaces from a string.
strip :: String -> String
strip = T.unpack . T.strip . T.pack
-- | Parses relative file path, e.g. "my/file.txt".
relFilePathString :: Parser (Path System (Rel d) (File f))
relFilePathString = do
path <- L.stringLiteral
maybe
(unexpected $ "string \"" ++ path ++ "\": Expected relative file path.")
return
(SP.parseRelFile path)
-- | Parses relative posix file path, e.g. "my/file.txt".
relPosixFilePathString :: Parser (Path Posix (Rel d) (File f))
relPosixFilePathString = do
path <- L.stringLiteral
maybe
(unexpected $ "string \"" ++ path ++ "\": Expected relative file path.")
return
(SP.parseRelFileP path)

View File

@ -1,41 +0,0 @@
module Wasp.Parser.Db
( db,
)
where
import Data.Maybe (listToMaybe)
import Text.Parsec (try, (<|>))
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as P
import qualified Wasp.Wasp.Db as Wasp.Db
db :: Parser Wasp.Db.Db
db = do
L.reserved L.reservedNameDb
dbProperties <- P.waspClosure (L.commaSep1 dbProperty)
system <-
maybe
(fail "'system' property is required!")
return
(listToMaybe [p | DbPropertySystem p <- dbProperties])
return
Wasp.Db.Db
{ Wasp.Db._system = system
}
data DbProperty
= DbPropertySystem Wasp.Db.DbSystem
dbProperty :: Parser DbProperty
dbProperty =
dbPropertySystem
dbPropertySystem :: Parser DbProperty
dbPropertySystem = DbPropertySystem <$> P.waspProperty "system" dbPropertySystemValue
where
dbPropertySystemValue =
try (L.symbol "PostgreSQL" >> return Wasp.Db.PostgreSQL)
<|> try (L.symbol "SQLite" >> return Wasp.Db.SQLite)

View File

@ -1,66 +0,0 @@
module Wasp.Parser.Entity
( entity,
)
where
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Psl.Ast.Model as PslModel
import qualified Wasp.Psl.Parser.Model
import qualified Wasp.Wasp.Entity as Entity
entity :: Parser Entity.Entity
entity = do
_ <- L.reserved L.reservedNameEntity
name <- L.identifier
_ <- L.symbol "{=psl"
pslModelBody <- Wasp.Psl.Parser.Model.body
_ <- L.symbol "psl=}"
return
Entity.Entity
{ Entity._name = name,
Entity._fields = getEntityFields pslModelBody,
Entity._pslModelBody = pslModelBody
}
getEntityFields :: PslModel.Body -> [Entity.Field]
getEntityFields (PslModel.Body pslElements) = map pslFieldToEntityField pslFields
where
pslFields = [field | (PslModel.ElementField field) <- pslElements]
pslFieldToEntityField :: PslModel.Field -> Entity.Field
pslFieldToEntityField pslField =
Entity.Field
{ Entity._fieldName = PslModel._name pslField,
Entity._fieldType =
pslFieldTypeToEntityFieldType
(PslModel._type pslField)
(PslModel._typeModifiers pslField)
}
pslFieldTypeToEntityFieldType ::
PslModel.FieldType ->
[PslModel.FieldTypeModifier] ->
Entity.FieldType
pslFieldTypeToEntityFieldType fType fTypeModifiers =
let scalar = pslFieldTypeToScalar fType
in case fTypeModifiers of
[] -> Entity.FieldTypeScalar scalar
[PslModel.List] -> Entity.FieldTypeComposite $ Entity.List scalar
[PslModel.Optional] -> Entity.FieldTypeComposite $ Entity.Optional scalar
_ -> error "Not a valid list of modifiers."
pslFieldTypeToScalar :: PslModel.FieldType -> Entity.Scalar
pslFieldTypeToScalar fType = case fType of
PslModel.String -> Entity.String
PslModel.Boolean -> Entity.Boolean
PslModel.Int -> Entity.Int
PslModel.BigInt -> Entity.BigInt
PslModel.Float -> Entity.Float
PslModel.Decimal -> Entity.Decimal
PslModel.DateTime -> Entity.DateTime
PslModel.Json -> Entity.Json
PslModel.Bytes -> Entity.Bytes
PslModel.UserType typeName -> Entity.UserType typeName
PslModel.Unsupported typeName -> Entity.Unsupported typeName

View File

@ -1,24 +0,0 @@
module Wasp.Parser.ExternalCode
( extCodeFilePathString,
)
where
import qualified Path.Posix as PPosix
import StrongPath (File', Path, Posix, Rel)
import qualified StrongPath.Path as SP.Path
import Text.Parsec (unexpected)
import Text.Parsec.String (Parser)
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
import qualified Wasp.Parser.Common
-- Parses string literal that is file path to file in source external code dir.
-- Returns file path relative to the external code dir.
-- Example of input: "@ext/some/file.txt". Output would be: "some/file.txt".
extCodeFilePathString :: Parser (Path Posix (Rel SourceExternalCodeDir) File')
extCodeFilePathString = do
path <- Wasp.Parser.Common.relPosixFilePathString
maybe
(unexpected $ "string \"" ++ show path ++ "\": External code file path should start with \"@ext/\".")
return
-- TODO: Once StrongPath supports stripProperPrefix method, use that instead of transforming it to Path and back.
(SP.Path.fromPathRelFileP <$> PPosix.stripProperPrefix [PPosix.reldir|@ext|] (SP.Path.toPathRelFileP path))

View File

@ -1,12 +0,0 @@
module Wasp.Parser.JsCode
( jsCode,
)
where
import qualified Data.Text as Text
import Text.Parsec.String (Parser)
import qualified Wasp.Parser.Common as P
import qualified Wasp.Wasp.JsCode as WJS
jsCode :: Parser WJS.JsCode
jsCode = WJS.JsCode . Text.pack <$> P.waspNamedClosure "js"

View File

@ -1,32 +0,0 @@
module Wasp.Parser.JsImport
( jsImport,
)
where
import Text.Parsec ((<|>))
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.ExternalCode
import qualified Wasp.Wasp.JsImport as Wasp.JsImport
-- | Parses subset of JS import statement (only default or single named import, and only external code files):
-- import <identifier> from "@ext/..."
-- import { <identifier> } from "@ext/..."
jsImport :: Parser Wasp.JsImport.JsImport
jsImport = do
L.whiteSpace
_ <- L.reserved L.reservedNameImport
-- For now we support only default import or one named import.
(defaultImport, namedImports) <-
((\i -> (Just i, [])) <$> L.identifier)
<|> ((\i -> (Nothing, [i])) <$> L.braces L.identifier)
_ <- L.reserved L.reservedNameFrom
-- TODO: For now we only support double quotes here, we should also support single quotes.
-- We would need to write this from scratch, with single quote escaping enabled.
from <- Wasp.Parser.ExternalCode.extCodeFilePathString
return
Wasp.JsImport.JsImport
{ Wasp.JsImport._defaultImport = defaultImport,
Wasp.JsImport._namedImports = namedImports,
Wasp.JsImport._from = from
}

View File

@ -1,31 +0,0 @@
module Wasp.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 Wasp.Lexer as L
import qualified Wasp.NpmDependency as ND
import qualified Wasp.Parser.Common as P
import Wasp.Wasp.NpmDependencies (NpmDependencies)
import qualified Wasp.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

@ -1,50 +0,0 @@
module Wasp.Parser.Operation
( jsFunctionPropParser,
entitiesPropParser,
getJsFunctionFromProps,
getEntitiesFromProps,
getAuthEnabledFromProps,
properties,
-- FOR TESTS:
Property (..),
)
where
import Data.Maybe (listToMaybe)
import Text.Parsec ((<|>))
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as C
import qualified Wasp.Parser.JsImport
import qualified Wasp.Wasp.JsImport as Wasp.JsImport
data Property
= JsFunction !Wasp.JsImport.JsImport
| Entities ![String]
| AuthEnabled !Bool
deriving (Show, Eq)
properties :: Parser [Property]
properties =
L.commaSep1 $
jsFunctionPropParser
<|> entitiesPropParser
<|> authEnabledPropParser
jsFunctionPropParser :: Parser Property
jsFunctionPropParser = JsFunction <$> C.waspProperty "fn" Wasp.Parser.JsImport.jsImport
getJsFunctionFromProps :: [Property] -> Maybe Wasp.JsImport.JsImport
getJsFunctionFromProps ps = listToMaybe [f | JsFunction f <- ps]
entitiesPropParser :: Parser Property
entitiesPropParser = Entities <$> C.waspProperty "entities" (C.waspList L.identifier)
getEntitiesFromProps :: [Property] -> Maybe [String]
getEntitiesFromProps ps = listToMaybe [es | Entities es <- ps]
authEnabledPropParser :: Parser Property
authEnabledPropParser = AuthEnabled <$> C.waspProperty "auth" L.bool
getAuthEnabledFromProps :: [Property] -> Maybe Bool
getAuthEnabledFromProps ps = listToMaybe [aE | AuthEnabled aE <- ps]

View File

@ -1,55 +0,0 @@
module Wasp.Parser.Page
( page,
)
where
import Data.Maybe (fromMaybe, listToMaybe)
import Text.Parsec
import Text.Parsec.String (Parser)
import Wasp.Lexer
import Wasp.Parser.Common
import qualified Wasp.Parser.JsImport
import Wasp.Wasp.JsImport (JsImport)
import qualified Wasp.Wasp.Page as Page
data PageProperty
= Title !String
| Component !JsImport
| AuthRequired !Bool
deriving (Show, Eq)
-- | Parses Page properties, separated by a comma.
pageProperties :: Parser [PageProperty]
pageProperties =
commaSep1 $
pagePropertyTitle
<|> pagePropertyComponent
<|> pagePropertyAuthRequired
-- NOTE(matija): this is currently unused?
pagePropertyTitle :: Parser PageProperty
pagePropertyTitle = Title <$> waspPropertyStringLiteral "title"
pagePropertyComponent :: Parser PageProperty
pagePropertyComponent = Component <$> waspProperty "component" Wasp.Parser.JsImport.jsImport
pagePropertyAuthRequired :: Parser PageProperty
pagePropertyAuthRequired = AuthRequired <$> waspPropertyBool "authRequired"
getPageAuthRequired :: [PageProperty] -> Maybe Bool
getPageAuthRequired ps = listToMaybe [a | AuthRequired a <- ps]
getPageComponent :: [PageProperty] -> Maybe JsImport
getPageComponent ps = listToMaybe [c | Component c <- ps]
-- | Top level parser, parses Page.
page :: Parser Page.Page
page = do
(pageName, pageProps) <- waspElementNameAndClosureContent reservedNamePage pageProperties
return
Page.Page
{ Page._name = pageName,
Page._component = fromMaybe (error "Page component is missing.") (getPageComponent pageProps),
Page._authRequired = getPageAuthRequired pageProps
}

View File

@ -1,24 +0,0 @@
module Wasp.Parser.Query
( query,
)
where
import Data.Maybe (fromMaybe)
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as C
import qualified Wasp.Parser.Operation as Operation
import Wasp.Wasp.Query (Query)
import qualified Wasp.Wasp.Query as Query
query :: Parser Query
query = do
(name, props) <- C.waspElementNameAndClosureContent L.reservedNameQuery Operation.properties
return
Query.Query
{ Query._name = name,
Query._jsFunction =
fromMaybe (error "Query js function is missing.") (Operation.getJsFunctionFromProps props),
Query._entities = Operation.getEntitiesFromProps props,
Query._auth = Operation.getAuthEnabledFromProps props
}

View File

@ -1,26 +0,0 @@
module Wasp.Parser.Route
( route,
)
where
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Wasp.Route as Route
-- | Top level parser, parses route Wasp element.
route :: Parser Route.Route
route = do
-- route "some/url/path"
L.reserved L.reservedNameRoute
urlPath <- L.stringLiteral
-- -> page somePage
L.reserved "->"
L.reserved L.reservedNamePage
targetPage <- L.identifier
return
Route.Route
{ Route._urlPath = urlPath,
Route._targetPage = targetPage
}

View File

@ -1,38 +0,0 @@
module Wasp.Parser.Server
( server,
)
where
import Data.Maybe (fromMaybe, listToMaybe)
import Text.Parsec.String (Parser)
import qualified Wasp.Lexer as L
import qualified Wasp.Parser.Common as C
import qualified Wasp.Parser.JsImport
import qualified Wasp.Wasp.JsImport as Wasp.JsImport
import Wasp.Wasp.Server (Server)
import qualified Wasp.Wasp.Server as Server
server :: Parser Server
server = do
L.reserved L.reservedNameServer
props <- C.waspClosure properties
return
Server.Server
{ Server._setupJsFunction =
fromMaybe (error "Server js function is missing.") (getSetupJsFunctionFromProps props)
}
data Property
= SetupJsFunction !Wasp.JsImport.JsImport
deriving (Show, Eq)
properties :: Parser [Property]
properties =
L.commaSep1 setupJsFunctionPropParser
setupJsFunctionPropParser :: Parser Property
setupJsFunctionPropParser = SetupJsFunction <$> C.waspProperty "setupFn" Wasp.Parser.JsImport.jsImport
getSetupJsFunctionFromProps :: [Property] -> Maybe Wasp.JsImport.JsImport
getSetupJsFunctionFromProps ps = listToMaybe [f | SetupJsFunction f <- ps]

View File

@ -1,20 +0,0 @@
module Wasp.Parser.Style
( style,
)
where
import qualified Data.Text as Text
import Text.Parsec ((<|>))
import Text.Parsec.String (Parser)
import qualified Wasp.Parser.Common
import qualified Wasp.Parser.ExternalCode
import qualified Wasp.Wasp.Style as Wasp.Style
style :: Parser Wasp.Style.Style
style = cssFile <|> cssCode
cssFile :: Parser Wasp.Style.Style
cssFile = Wasp.Style.ExtCodeCssFile <$> Wasp.Parser.ExternalCode.extCodeFilePathString
cssCode :: Parser Wasp.Style.Style
cssCode = Wasp.Style.CssCode . Text.pack <$> Wasp.Parser.Common.waspNamedClosure "css"

View File

@ -8,6 +8,8 @@ module Wasp.Util
indent, indent,
concatShortPrefixAndText, concatShortPrefixAndText,
concatPrefixAndText, concatPrefixAndText,
insertAt,
leftPad,
) )
where where
@ -102,3 +104,17 @@ concatShortPrefixAndText prefix text =
concatPrefixAndText :: String -> String -> String concatPrefixAndText :: String -> String -> String
concatPrefixAndText prefix text = concatPrefixAndText prefix text =
if length (lines text) <= 1 then prefix ++ text else prefix ++ "\n" ++ indent 2 text if length (lines text) <= 1 then prefix ++ text else prefix ++ "\n" ++ indent 2 text
-- | Adds given element to the start of the given list until the list is of specified length.
-- leftPad ' ' 4 "hi" == " hi"
-- leftPad ' ' 4 "hihihi" == "hihihi"
leftPad :: a -> Int -> [a] -> [a]
leftPad padElem n list = replicate (max 0 (n - length list)) padElem ++ list
-- | Inserts a given @theInsert@ list into the given @host@ list so that @theInsert@
-- starts at index @idx@ in the @host@.
-- Example: @insertAt "hi" 2 "hoho" == "hohiho"@
insertAt :: [a] -> Int -> [a] -> [a]
insertAt theInsert idx host =
let (before, after) = splitAt idx host
in before ++ theInsert ++ after

View File

@ -1,6 +1,9 @@
module Wasp.Util.Terminal module Wasp.Util.Terminal
( Style (..), ( Style (..),
applyStyles, applyStyles,
styleCode,
escapeCode,
resetCode,
) )
where where

View File

@ -1,259 +0,0 @@
module Wasp.Wasp
( Wasp,
WaspElement (..),
fromWaspElems,
module Wasp.Wasp.JsImport,
getJsImports,
setJsImports,
module Wasp.Wasp.App,
fromApp,
getApp,
setApp,
getAuth,
getServer,
getPSLEntities,
getDb,
module Wasp.Wasp.Page,
getPages,
addPage,
getRoutes,
getQueries,
addQuery,
getQueryByName,
getActions,
addAction,
getActionByName,
setExternalCodeFiles,
getExternalCodeFiles,
setDotEnvFile,
getDotEnvFile,
setMigrationsDir,
getMigrationsDir,
setIsBuild,
getIsBuild,
setNpmDependencies,
getNpmDependencies,
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import StrongPath (Abs, Dir, File', Path')
import qualified Wasp.AppSpec.ExternalCode as ExternalCode
import Wasp.Common (DbMigrationsDir)
import qualified Wasp.Util as U
import qualified Wasp.Wasp.Action as Wasp.Action
import Wasp.Wasp.App
import qualified Wasp.Wasp.Auth as Wasp.Auth
import qualified Wasp.Wasp.Db as Wasp.Db
import Wasp.Wasp.Entity
import Wasp.Wasp.JsImport
import Wasp.Wasp.NpmDependencies (NpmDependencies)
import qualified Wasp.Wasp.NpmDependencies as Wasp.NpmDependencies
import Wasp.Wasp.Page
import qualified Wasp.Wasp.Query as Wasp.Query
import Wasp.Wasp.Route
import qualified Wasp.Wasp.Server as Wasp.Server
-- * Wasp
data Wasp = Wasp
{ waspElements :: [WaspElement],
waspJsImports :: [JsImport],
externalCodeFiles :: [ExternalCode.File],
dotEnvFile :: Maybe (Path' Abs File'),
migrationsDir :: Maybe (Path' Abs (Dir DbMigrationsDir)),
isBuild :: Bool
}
deriving (Show, Eq)
data WaspElement
= WaspElementApp !App
| WaspElementAuth !Wasp.Auth.Auth
| WaspElementDb !Wasp.Db.Db
| WaspElementPage !Page
| WaspElementNpmDependencies !NpmDependencies
| WaspElementRoute !Route
| WaspElementEntity !Wasp.Wasp.Entity.Entity
| WaspElementQuery !Wasp.Query.Query
| WaspElementAction !Wasp.Action.Action
| WaspElementServer !Wasp.Server.Server
deriving (Show, Eq)
fromWaspElems :: [WaspElement] -> Wasp
fromWaspElems elems =
Wasp
{ waspElements = elems,
waspJsImports = [],
externalCodeFiles = [],
dotEnvFile = Nothing,
migrationsDir = Nothing,
isBuild = False
}
-- * Build
getIsBuild :: Wasp -> Bool
getIsBuild = isBuild
setIsBuild :: Wasp -> Bool -> Wasp
setIsBuild wasp isBuildNew = wasp {isBuild = isBuildNew}
-- * External code files
getExternalCodeFiles :: Wasp -> [ExternalCode.File]
getExternalCodeFiles = externalCodeFiles
setExternalCodeFiles :: Wasp -> [ExternalCode.File] -> Wasp
setExternalCodeFiles wasp files = wasp {externalCodeFiles = files}
-- * Dot env files
getDotEnvFile :: Wasp -> Maybe (Path' Abs File')
getDotEnvFile = dotEnvFile
setDotEnvFile :: Wasp -> Maybe (Path' Abs File') -> Wasp
setDotEnvFile wasp file = wasp {dotEnvFile = file}
-- * Migrations dir
getMigrationsDir :: Wasp -> Maybe (Path' Abs (Dir DbMigrationsDir))
getMigrationsDir = migrationsDir
setMigrationsDir :: Wasp -> Maybe (Path' Abs (Dir DbMigrationsDir)) -> Wasp
setMigrationsDir wasp dir = wasp {migrationsDir = dir}
-- * Js imports
getJsImports :: Wasp -> [JsImport]
getJsImports = waspJsImports
setJsImports :: Wasp -> [JsImport] -> Wasp
setJsImports wasp jsImports = wasp {waspJsImports = jsImports}
-- * App
getApp :: Wasp -> App
getApp wasp =
let apps = getApps wasp
in if length apps /= 1
then error "Wasp has to contain exactly one WaspElementApp element!"
else head apps
isAppElem :: WaspElement -> Bool
isAppElem WaspElementApp {} = True
isAppElem _ = False
getApps :: Wasp -> [App]
getApps wasp = [app | (WaspElementApp app) <- waspElements wasp]
setApp :: Wasp -> App -> Wasp
setApp wasp app = wasp {waspElements = WaspElementApp app : filter (not . isAppElem) (waspElements wasp)}
fromApp :: App -> Wasp
fromApp app = fromWaspElems [WaspElementApp app]
-- * Auth
getAuth :: Wasp -> Maybe Wasp.Auth.Auth
getAuth wasp =
let auths = [a | WaspElementAuth a <- waspElements wasp]
in case auths of
[] -> Nothing
[a] -> Just a
_ -> error "Wasp can't contain more than one WaspElementAuth element!"
-- * Db
getDb :: Wasp -> Maybe Wasp.Db.Db
getDb wasp =
let dbs = [db | WaspElementDb db <- waspElements wasp]
in case dbs of
[] -> Nothing
[db] -> Just db
_ -> error "Wasp can't contain more than one Db element!"
-- * 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]
getRoutes wasp = [route | (WaspElementRoute route) <- waspElements wasp]
-- * Pages
getPages :: Wasp -> [Page]
getPages wasp = [page | (WaspElementPage page) <- waspElements wasp]
addPage :: Wasp -> Page -> Wasp
addPage wasp page = wasp {waspElements = WaspElementPage page : waspElements wasp}
-- * Query
getQueries :: Wasp -> [Wasp.Query.Query]
getQueries wasp = [query | (WaspElementQuery query) <- waspElements wasp]
addQuery :: Wasp -> Wasp.Query.Query -> Wasp
addQuery wasp query = wasp {waspElements = WaspElementQuery query : waspElements wasp}
-- | Gets query with a specified name from wasp, if such an action exists.
-- We assume here that there are no two queries with same name.
getQueryByName :: Wasp -> String -> Maybe Wasp.Query.Query
getQueryByName wasp name = U.headSafe $ filter (\a -> Wasp.Query._name a == name) (getQueries wasp)
-- * Action
getActions :: Wasp -> [Wasp.Action.Action]
getActions wasp = [action | (WaspElementAction action) <- waspElements wasp]
addAction :: Wasp -> Wasp.Action.Action -> Wasp
addAction wasp action = wasp {waspElements = WaspElementAction action : waspElements wasp}
-- | Gets action with a specified name from wasp, if such an action exists.
-- We assume here that there are no two actions with same name.
getActionByName :: Wasp -> String -> Maybe Wasp.Action.Action
getActionByName wasp name = U.headSafe $ filter (\a -> Wasp.Action._name a == name) (getActions wasp)
-- * Entities
getPSLEntities :: Wasp -> [Wasp.Wasp.Entity.Entity]
getPSLEntities wasp = [entity | (WaspElementEntity entity) <- waspElements wasp]
-- * Get server
getServer :: Wasp -> Maybe Wasp.Server.Server
getServer wasp =
let servers = [s | WaspElementServer s <- waspElements wasp]
in case servers of
[] -> Nothing
[s] -> Just s
_ -> error "Wasp can't contain more than one WaspElementAuth element!"
-- * ToJSON instances.
instance ToJSON Wasp where
toJSON wasp =
object
[ "app" .= getApp wasp,
"pages" .= getPages wasp,
"routes" .= getRoutes wasp,
"jsImports" .= getJsImports wasp,
"server" .= getServer wasp
]

View File

@ -1,26 +0,0 @@
module Wasp.Wasp.Action
( Action (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import Wasp.Wasp.JsImport (JsImport)
-- TODO: Very similar to Wasp.Query, consider extracting duplication.
data Action = Action
{ _name :: !String,
_jsFunction :: !JsImport,
_entities :: !(Maybe [String]),
_auth :: !(Maybe Bool)
}
deriving (Show, Eq)
instance ToJSON Action where
toJSON action =
object
[ "name" .= _name action,
"jsFunction" .= _jsFunction action,
"entities" .= _entities action,
"auth" .= _auth action
]

View File

@ -1,20 +0,0 @@
module Wasp.Wasp.App
( App (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
data App = App
{ appName :: !String, -- Identifier
appTitle :: !String,
appHead :: !(Maybe [String])
}
deriving (Show, Eq)
instance ToJSON App where
toJSON app =
object
[ "name" .= appName app,
"title" .= appTitle app
]

View File

@ -1,17 +0,0 @@
module Wasp.Wasp.Auth
( Auth (..),
AuthMethod (..),
)
where
data Auth = Auth
{ _userEntity :: !String,
_methods :: [AuthMethod],
_onAuthFailedRedirectTo :: !String,
_onAuthSucceededRedirectTo :: !String
}
deriving (Show, Eq)
data AuthMethod
= EmailAndPassword
deriving (Show, Eq)

View File

@ -1,15 +0,0 @@
module Wasp.Wasp.Db
( Db (..),
DbSystem (..),
)
where
data Db = Db
{ _system :: !DbSystem
}
deriving (Show, Eq)
data DbSystem
= PostgreSQL
| SQLite
deriving (Show, Eq)

View File

@ -1,55 +0,0 @@
module Wasp.Wasp.Entity
( Entity (..),
Field (..),
FieldType (..),
Scalar (..),
Composite (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import qualified Wasp.Psl.Ast.Model
data Entity = Entity
{ _name :: !String,
_fields :: ![Field],
_pslModelBody :: !Wasp.Psl.Ast.Model.Body
}
deriving (Show, Eq)
data Field = Field
{ _fieldName :: !String,
_fieldType :: !FieldType
}
deriving (Show, Eq)
data FieldType = FieldTypeScalar Scalar | FieldTypeComposite Composite
deriving (Show, Eq)
data Composite = Optional Scalar | List Scalar
deriving (Show, Eq)
data Scalar
= String
| Boolean
| Int
| BigInt
| Float
| Decimal
| DateTime
| Json
| Bytes
| -- | Name of the user-defined type.
-- This could be another entity, or maybe an enum,
-- we don't know here yet.
UserType String
| Unsupported String
deriving (Show, Eq)
instance ToJSON Entity where
toJSON entity =
object
[ "name" .= _name entity,
"fields" .= show (_fields entity),
"pslModelBody" .= show (_pslModelBody entity)
]

View File

@ -1,15 +0,0 @@
module Wasp.Wasp.JsCode
( JsCode (..),
)
where
import Data.Aeson (ToJSON (..))
import Data.Text (Text)
data JsCode = JsCode !Text deriving (Show, Eq)
-- TODO(matija): Currently generator is relying on this implementation, which is not
-- ideal. Ideally all the generation logic would be in the generator. But for now this was
-- the simplest way to implement it.
instance ToJSON JsCode where
toJSON (JsCode code) = toJSON code

View File

@ -1,25 +0,0 @@
module Wasp.Wasp.JsImport
( JsImport (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import StrongPath (File', Path, Posix, Rel)
import qualified StrongPath as SP
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
-- | Represents javascript import -> "import <what> from <from>".
data JsImport = JsImport
{ _defaultImport :: !(Maybe String),
_namedImports :: ![String],
_from :: Path Posix (Rel SourceExternalCodeDir) File'
}
deriving (Show, Eq)
instance ToJSON JsImport where
toJSON jsImport =
object
[ "defaultImport" .= _defaultImport jsImport,
"namedImports" .= _namedImports jsImport,
"from" .= SP.fromRelFileP (_from jsImport)
]

View File

@ -1,22 +0,0 @@
module Wasp.Wasp.NpmDependencies
( NpmDependencies (..),
empty,
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import Wasp.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

@ -1,38 +0,0 @@
module Wasp.Wasp.Operation
( Operation (..),
getName,
getJsFn,
getEntities,
getAuth,
)
where
-- TODO: Is this ok approach, should I instead use typeclass?
-- So far, all usages in the codebase could be easily replaced with the Typeclass.
import Wasp.Wasp.Action (Action)
import qualified Wasp.Wasp.Action as Action
import Wasp.Wasp.JsImport (JsImport)
import Wasp.Wasp.Query (Query)
import qualified Wasp.Wasp.Query as Query
data Operation
= QueryOp Query
| ActionOp Action
deriving (Show)
getName :: Operation -> String
getName (QueryOp query) = Query._name query
getName (ActionOp action) = Action._name action
getJsFn :: Operation -> JsImport
getJsFn (QueryOp query) = Query._jsFunction query
getJsFn (ActionOp action) = Action._jsFunction action
getEntities :: Operation -> Maybe [String]
getEntities (QueryOp query) = Query._entities query
getEntities (ActionOp action) = Action._entities action
getAuth :: Operation -> Maybe Bool
getAuth (QueryOp query) = Query._auth query
getAuth (ActionOp action) = Action._auth action

View File

@ -1,21 +0,0 @@
module Wasp.Wasp.Page
( Page (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import Wasp.Wasp.JsImport (JsImport)
data Page = Page
{ _name :: !String,
_component :: !JsImport,
_authRequired :: Maybe Bool
}
deriving (Show, Eq)
instance ToJSON Page where
toJSON page =
object
[ "name" .= _name page,
"component" .= _component page
]

View File

@ -1,26 +0,0 @@
module Wasp.Wasp.Query
( Query (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import Wasp.Wasp.JsImport (JsImport)
-- TODO: Very similar to Wasp.Action, consider extracting duplication.
data Query = Query
{ _name :: !String,
_jsFunction :: !JsImport,
_entities :: !(Maybe [String]),
_auth :: !(Maybe Bool)
}
deriving (Show, Eq)
instance ToJSON Query where
toJSON query =
object
[ "name" .= _name query,
"jsFunction" .= _jsFunction query,
"entities" .= _entities query,
"auth" .= _auth query
]

View File

@ -1,21 +0,0 @@
module Wasp.Wasp.Route
( Route (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
data Route = Route
{ _urlPath :: !String,
-- NOTE(matija): for now page is the only possible target, but in
-- the future there might be different types of targets (e.g. another route).
_targetPage :: !String
}
deriving (Show, Eq)
instance ToJSON Route where
toJSON route =
object
[ "urlPath" .= _urlPath route,
"targetPage" .= _targetPage route
]

View File

@ -1,18 +0,0 @@
module Wasp.Wasp.Server
( Server (..),
)
where
import Data.Aeson (ToJSON (..), object, (.=))
import Wasp.Wasp.JsImport (JsImport)
data Server = Server
{ _setupJsFunction :: !JsImport
}
deriving (Show, Eq)
instance ToJSON Server where
toJSON server =
object
[ "setupJsFunction" .= _setupJsFunction server
]

View File

@ -1,18 +0,0 @@
module Wasp.Wasp.Style
( Style (..),
)
where
import Data.Aeson (ToJSON (..))
import Data.Text (Text)
import StrongPath (File', Path, Posix, Rel, toFilePath)
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
data Style
= ExtCodeCssFile !(Path Posix (Rel SourceExternalCodeDir) File')
| CssCode !Text
deriving (Show, Eq)
instance ToJSON Style where
toJSON (ExtCodeCssFile path) = toJSON $ toFilePath path
toJSON (CssCode code) = toJSON code

View File

@ -1 +1 @@
resolver: lts-18.15 resolver: lts-18.21

View File

@ -2,8 +2,8 @@ resolver: ./stack-snapshot.yaml
packages: packages:
- . - .
extra-deps: extra-deps:
- strong-path-1.1.0.0 - strong-path-1.1.2.0
- path-0.9.0 - path-0.9.2
- path-io-1.6.3 - path-io-1.6.3
# (Martin): I added this per instructions from haskell-language-server, in order to # (Martin): I added this per instructions from haskell-language-server, in order to
# enable type information and documentation on hover for dependencies. # enable type information and documentation on hover for dependencies.

View File

@ -8,6 +8,8 @@ module Analyzer.EvaluatorTest where
import Data.Data (Data) import Data.Data (Data)
import Data.List.Split (splitOn) import Data.List.Split (splitOn)
import Data.Maybe (fromJust)
import qualified StrongPath as SP
import Test.Tasty.Hspec import Test.Tasty.Hspec
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Wasp.Analyzer.Evaluator import Wasp.Analyzer.Evaluator
@ -182,7 +184,7 @@ spec_Evaluator = do
let typeDefs = TD.addDeclType @Special $ TD.empty let typeDefs = TD.addDeclType @Special $ TD.empty
let source = let source =
[ "special Test {", [ "special Test {",
" imps: [import { field } from \"main.js\", import main from \"main.js\"],", " imps: [import { field } from \"@ext/main.js\", import main from \"@ext/main.js\"],",
" json: {=json \"key\": 1 json=}", " json: {=json \"key\": 1 json=}",
"}" "}"
] ]
@ -191,8 +193,8 @@ spec_Evaluator = do
`shouldBe` Right `shouldBe` Right
[ ( "Test", [ ( "Test",
Special Special
[ ExtImport (ExtImportField "field") "main.js", [ ExtImport (ExtImportField "field") (fromJust $ SP.parseRelFileP "main.js"),
ExtImport (ExtImportModule "main") "main.js" ExtImport (ExtImportModule "main") (fromJust $ SP.parseRelFileP "main.js")
] ]
(JSON " \"key\": 1 ") (JSON " \"key\": 1 ")
) )

View File

@ -19,14 +19,15 @@ spec_Parser = do
" yes: true,", " yes: true,",
" no: false,", " no: false,",
" ident: Wasp,", " ident: Wasp,",
" // This is a comment",
" innerDict: { innerDictReal: 2.17 }", " innerDict: { innerDictReal: 2.17 }",
"}" "}"
] ]
let ast = let ast =
AST AST
[ wctx (1, 1) (10, 1) $ [ wctx (1, 1) (11, 1) $
Decl "test" "Decl" $ Decl "test" "Decl" $
wctx (1, 11) (10, 1) $ wctx (1, 11) (11, 1) $
Dict Dict
[ ("string", wctx (2, 11) (2, 25) $ StringLiteral "Hello Wasp =}"), [ ("string", wctx (2, 11) (2, 25) $ StringLiteral "Hello Wasp =}"),
("escapedString", wctx (3, 18) (3, 29) $ StringLiteral "Look, a \""), ("escapedString", wctx (3, 18) (3, 29) $ StringLiteral "Look, a \""),
@ -36,15 +37,32 @@ spec_Parser = do
("no", wctx (7, 7) (7, 11) $ BoolLiteral False), ("no", wctx (7, 7) (7, 11) $ BoolLiteral False),
("ident", wctx (8, 10) (8, 13) $ Var "Wasp"), ("ident", wctx (8, 10) (8, 13) $ Var "Wasp"),
( "innerDict", ( "innerDict",
wctx (9, 14) (9, 36) $ wctx (10, 14) (10, 36) $
Dict Dict
[ ("innerDictReal", wctx (9, 31) (9, 34) $ DoubleLiteral 2.17) [ ("innerDictReal", wctx (10, 31) (10, 34) $ DoubleLiteral 2.17)
] ]
) )
] ]
] ]
parse source `shouldBe` Right ast parse source `shouldBe` Right ast
it "Parses comments" $ do
let source =
unlines
[ " // This is some // comment",
"/* comment",
" span//ning",
"multi/*ple lines */",
"test /* *hi* */ Decl 42 // One more comment",
"// And here is final comment"
]
let ast =
AST
[ wctx (5, 1) (5, 23) $
Decl "test" "Decl" $ wctx (5, 22) (5, 23) $ IntegerLiteral 42
]
parse source `shouldBe` Right ast
it "Parses external imports" $ do it "Parses external imports" $ do
let source = let source =
unlines unlines

View File

@ -5,6 +5,8 @@ module AnalyzerTest where
import Analyzer.TestUtil (ctx) import Analyzer.TestUtil (ctx)
import Data.Either (isRight) import Data.Either (isRight)
import Data.List (intercalate) import Data.List (intercalate)
import Data.Maybe (fromJust)
import qualified StrongPath as SP
import Test.Tasty.Hspec import Test.Tasty.Hspec
import Wasp.Analyzer import Wasp.Analyzer
import Wasp.Analyzer.Parser (Ctx) import Wasp.Analyzer.Parser (Ctx)
@ -103,7 +105,11 @@ spec_Analyzer = do
App.server = App.server =
Just Just
Server.Server Server.Server
{ Server.setupFn = Just $ ExtImport (ExtImportField "setupServer") "@ext/bar.js" { Server.setupFn =
Just $
ExtImport
(ExtImportField "setupServer")
(fromJust $ SP.parseRelFileP "bar.js")
}, },
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL} App.db = Just Db.Db {Db.system = Just Db.PostgreSQL}
} }
@ -114,13 +120,19 @@ spec_Analyzer = do
let expectedPages = let expectedPages =
[ ( "HomePage", [ ( "HomePage",
Page.Page Page.Page
{ Page.component = ExtImport (ExtImportModule "Home") "@ext/pages/Main", { Page.component =
ExtImport
(ExtImportModule "Home")
(fromJust $ SP.parseRelFileP "pages/Main"),
Page.authRequired = Nothing Page.authRequired = Nothing
} }
), ),
( "ProfilePage", ( "ProfilePage",
Page.Page Page.Page
{ Page.component = ExtImport (ExtImportField "profilePage") "@ext/pages/Profile", { Page.component =
ExtImport
(ExtImportField "profilePage")
(fromJust $ SP.parseRelFileP "pages/Profile"),
Page.authRequired = Just True Page.authRequired = Just True
} }
) )
@ -153,7 +165,10 @@ spec_Analyzer = do
let expectedQueries = let expectedQueries =
[ ( "getUsers", [ ( "getUsers",
Query.Query Query.Query
{ Query.fn = ExtImport (ExtImportField "getAllUsers") "@ext/foo.js", { Query.fn =
ExtImport
(ExtImportField "getAllUsers")
(fromJust $ SP.parseRelFileP "foo.js"),
Query.entities = Just [Ref "User"], Query.entities = Just [Ref "User"],
Query.auth = Nothing Query.auth = Nothing
} }
@ -164,7 +179,10 @@ spec_Analyzer = do
let expectedAction = let expectedAction =
[ ( "updateUser", [ ( "updateUser",
Action.Action Action.Action
{ Action.fn = ExtImport (ExtImportField "updateUser") "@ext/foo.js", { Action.fn =
ExtImport
(ExtImportField "updateUser")
(fromJust $ SP.parseRelFileP "foo.js"),
Action.entities = Just [Ref "User"], Action.entities = Just [Ref "User"],
Action.auth = Just True Action.auth = Just True
} }

Some files were not shown because too many files have changed in this diff Show More