From 8d2c33ab28432720cc786459cefd7a0f5afa7a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0o=C5=A1i=C4=87?= Date: Thu, 20 Jan 2022 11:45:14 +0100 Subject: [PATCH] Replace Wasp with AppSpec and Parser with Analyzer. (#423) --- README.md | 6 +- examples/realworld/main.wasp | 62 ++--- examples/thoughts/main.wasp | 45 ++- examples/tutorials/TodoApp/main.wasp | 30 +- .../migrations/20201023121126-a/README.md | 57 ---- .../migrations/20201023121126-a/schema.prisma | 23 -- .../migrations/20201023121126-a/steps.json | 185 ------------- .../migrations/20201023121536-b/README.md | 56 ---- .../migrations/20201023121536-b/schema.prisma | 26 -- .../migrations/20201023121536-b/steps.json | 65 ----- .../20220113204751_init/migration.sql | 18 ++ .../tutorials/TodoApp/migrations/migrate.lock | 4 - .../TodoApp/migrations/migration_lock.toml | 3 + waspc/README.md | 8 +- .../cli/Wasp/Cli/Command/CreateNewProject.hs | 4 +- waspc/cli/Wasp/Cli/Command/Deps.hs | 9 +- waspc/cli/Wasp/Cli/Command/Info.hs | 42 ++- waspc/data/Cli/templates/new/ext/MainPage.js | 2 +- .../Generator/templates/react-app/README.md | 1 - .../Generator/templates/react-app/gitignore | 1 - .../templates/react-app/package.json | 2 +- .../templates/react-app/public/manifest.json | 4 +- .../templates/react-app/src/index.css | 1 - .../templates/react-app/src/index.js | 1 - .../react-app/src/operations/index.js | 1 - .../templates/react-app/src/serviceWorker.js | 1 - waspc/examples/todoApp/todoApp.wasp | 72 +++-- waspc/images/waspc-implementation-diagram.png | Bin 37751 -> 60503 bytes waspc/package.yaml | 2 +- .../Evaluation/TypedExpr/Combinators.hs | 20 +- waspc/src/Wasp/Analyzer/Parser/Lexer.x | 6 +- waspc/src/Wasp/Analyzer/Parser/ParseError.hs | 6 +- .../Wasp/Analyzer/TypeDefinitions/TH/Decl.hs | 13 +- waspc/src/Wasp/AppSpec.hs | 65 ++++- waspc/src/Wasp/AppSpec/App.hs | 9 +- waspc/src/Wasp/AppSpec/App/Dependency.hs | 4 + waspc/src/Wasp/AppSpec/Core/Ref.hs | 4 + waspc/src/Wasp/AppSpec/ExtImport.hs | 11 +- waspc/src/Wasp/AppSpec/ExternalCode.hs | 5 +- waspc/src/Wasp/AppSpec/Operation.hs | 38 +++ waspc/src/Wasp/AppSpec/Route.hs | 1 - waspc/src/Wasp/Error.hs | 76 +++++ waspc/src/Wasp/Generator.hs | 19 +- waspc/src/Wasp/Generator/DbGenerator.hs | 64 ++--- waspc/src/Wasp/Generator/DockerGenerator.hs | 20 +- .../Wasp/Generator/ExternalCodeGenerator.hs | 7 +- waspc/src/Wasp/Generator/JsImport.hs | 23 +- .../Wasp/Generator/PackageJsonGenerator.hs | 42 +-- waspc/src/Wasp/Generator/ServerGenerator.hs | 149 +++++----- .../Wasp/Generator/ServerGenerator/AuthG.hs | 54 ++-- .../Wasp/Generator/ServerGenerator/Common.hs | 25 +- .../Wasp/Generator/ServerGenerator/ConfigG.hs | 10 +- .../Generator/ServerGenerator/OperationsG.hs | 72 +++-- .../ServerGenerator/OperationsRoutesG.hs | 82 +++--- waspc/src/Wasp/Generator/WebAppGenerator.hs | 110 ++++---- .../Wasp/Generator/WebAppGenerator/AuthG.hs | 39 +-- .../Wasp/Generator/WebAppGenerator/Common.hs | 23 +- .../WebAppGenerator/OperationsGenerator.hs | 62 +++-- .../OperationsGenerator/ResourcesG.hs | 6 +- .../WebAppGenerator/RouterGenerator.hs | 79 +++--- waspc/src/Wasp/Lib.hs | 60 ++-- waspc/src/Wasp/Parser.hs | 88 ------ waspc/src/Wasp/Parser/Action.hs | 24 -- waspc/src/Wasp/Parser/App.hs | 57 ---- waspc/src/Wasp/Parser/Auth.hs | 85 ------ waspc/src/Wasp/Parser/Common.hs | 180 ------------ waspc/src/Wasp/Parser/Db.hs | 41 --- waspc/src/Wasp/Parser/Entity.hs | 66 ----- waspc/src/Wasp/Parser/ExternalCode.hs | 24 -- waspc/src/Wasp/Parser/JsCode.hs | 12 - waspc/src/Wasp/Parser/JsImport.hs | 32 --- waspc/src/Wasp/Parser/NpmDependencies.hs | 31 --- waspc/src/Wasp/Parser/Operation.hs | 50 ---- waspc/src/Wasp/Parser/Page.hs | 55 ---- waspc/src/Wasp/Parser/Query.hs | 24 -- waspc/src/Wasp/Parser/Route.hs | 26 -- waspc/src/Wasp/Parser/Server.hs | 38 --- waspc/src/Wasp/Parser/Style.hs | 20 -- waspc/src/Wasp/Util.hs | 16 ++ waspc/src/Wasp/Util/Terminal.hs | 3 + waspc/src/Wasp/Wasp.hs | 259 ------------------ waspc/src/Wasp/Wasp/Action.hs | 26 -- waspc/src/Wasp/Wasp/App.hs | 20 -- waspc/src/Wasp/Wasp/Auth.hs | 17 -- waspc/src/Wasp/Wasp/Db.hs | 15 - waspc/src/Wasp/Wasp/Entity.hs | 55 ---- waspc/src/Wasp/Wasp/JsCode.hs | 15 - waspc/src/Wasp/Wasp/JsImport.hs | 25 -- waspc/src/Wasp/Wasp/NpmDependencies.hs | 22 -- waspc/src/Wasp/Wasp/Operation.hs | 38 --- waspc/src/Wasp/Wasp/Page.hs | 21 -- waspc/src/Wasp/Wasp/Query.hs | 26 -- waspc/src/Wasp/Wasp/Route.hs | 21 -- waspc/src/Wasp/Wasp/Server.hs | 18 -- waspc/src/Wasp/Wasp/Style.hs | 18 -- waspc/stack-snapshot.yaml | 2 +- waspc/stack.yaml | 4 +- waspc/test/Analyzer/EvaluatorTest.hs | 8 +- waspc/test/Analyzer/ParserTest.hs | 26 +- waspc/test/AnalyzerTest.hs | 28 +- waspc/test/ErrorTest.hs | 79 ++++++ waspc/test/Fixtures.hs | 23 -- .../Generator/PackageJsonGeneratorTest.hs | 14 +- waspc/test/Generator/WebAppGeneratorTest.hs | 38 ++- waspc/test/Parser/ActionTest.hs | 57 ---- waspc/test/Parser/CommonTest.hs | 112 -------- waspc/test/Parser/DbTest.hs | 21 -- waspc/test/Parser/ExternalCodeTest.hs | 17 -- waspc/test/Parser/JsImportTest.hs | 44 --- waspc/test/Parser/NpmDependenciesTest.hs | 24 -- waspc/test/Parser/OperationTest.hs | 34 --- waspc/test/Parser/PageTest.hs | 57 ---- waspc/test/Parser/ParserTest.hs | 125 --------- waspc/test/Parser/QueryTest.hs | 53 ---- waspc/test/Parser/RouteTest.hs | 29 -- waspc/test/Parser/ServerTest.hs | 40 --- waspc/test/Parser/StyleTest.hs | 22 -- waspc/test/Parser/valid.wasp | 40 --- waspc/test/Psl/Generator/ModelTest.hs | 6 +- waspc/test/Psl/Parser/ModelTest.hs | 10 +- waspc/test/UtilTest.hs | 22 ++ web/blog/2021-12-02-waspello.md | 4 +- web/docs/deploying.md | 2 +- .../{basic-elements.md => features.md} | 196 +++++++------ web/docs/language/overview.md | 8 +- web/docs/language/syntax.md | 76 +++++ web/docs/tutorials/todo-app/auth.md | 53 ++-- .../todo-app/creating-new-project.md | 14 +- web/docs/tutorials/todo-app/dependencies.md | 18 +- web/docs/tutorials/todo-app/listing-tasks.md | 6 +- web/docs/tutorials/todo-app/task-entity.md | 2 +- web/docusaurus.config.js | 2 +- web/sidebars.js | 3 +- web/src/pages/index.js | 42 +-- 134 files changed, 1381 insertions(+), 3383 deletions(-) delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121126-a/README.md delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121126-a/schema.prisma delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121126-a/steps.json delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121536-b/README.md delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121536-b/schema.prisma delete mode 100644 examples/tutorials/TodoApp/migrations/20201023121536-b/steps.json create mode 100644 examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql delete mode 100644 examples/tutorials/TodoApp/migrations/migrate.lock create mode 100644 examples/tutorials/TodoApp/migrations/migration_lock.toml create mode 100644 waspc/src/Wasp/AppSpec/Operation.hs create mode 100644 waspc/src/Wasp/Error.hs delete mode 100644 waspc/src/Wasp/Parser.hs delete mode 100644 waspc/src/Wasp/Parser/Action.hs delete mode 100644 waspc/src/Wasp/Parser/App.hs delete mode 100644 waspc/src/Wasp/Parser/Auth.hs delete mode 100644 waspc/src/Wasp/Parser/Common.hs delete mode 100644 waspc/src/Wasp/Parser/Db.hs delete mode 100644 waspc/src/Wasp/Parser/Entity.hs delete mode 100644 waspc/src/Wasp/Parser/ExternalCode.hs delete mode 100644 waspc/src/Wasp/Parser/JsCode.hs delete mode 100644 waspc/src/Wasp/Parser/JsImport.hs delete mode 100644 waspc/src/Wasp/Parser/NpmDependencies.hs delete mode 100644 waspc/src/Wasp/Parser/Operation.hs delete mode 100644 waspc/src/Wasp/Parser/Page.hs delete mode 100644 waspc/src/Wasp/Parser/Query.hs delete mode 100644 waspc/src/Wasp/Parser/Route.hs delete mode 100644 waspc/src/Wasp/Parser/Server.hs delete mode 100644 waspc/src/Wasp/Parser/Style.hs delete mode 100644 waspc/src/Wasp/Wasp.hs delete mode 100644 waspc/src/Wasp/Wasp/Action.hs delete mode 100644 waspc/src/Wasp/Wasp/App.hs delete mode 100644 waspc/src/Wasp/Wasp/Auth.hs delete mode 100644 waspc/src/Wasp/Wasp/Db.hs delete mode 100644 waspc/src/Wasp/Wasp/Entity.hs delete mode 100644 waspc/src/Wasp/Wasp/JsCode.hs delete mode 100644 waspc/src/Wasp/Wasp/JsImport.hs delete mode 100644 waspc/src/Wasp/Wasp/NpmDependencies.hs delete mode 100644 waspc/src/Wasp/Wasp/Operation.hs delete mode 100644 waspc/src/Wasp/Wasp/Page.hs delete mode 100644 waspc/src/Wasp/Wasp/Query.hs delete mode 100644 waspc/src/Wasp/Wasp/Route.hs delete mode 100644 waspc/src/Wasp/Wasp/Server.hs delete mode 100644 waspc/src/Wasp/Wasp/Style.hs create mode 100644 waspc/test/ErrorTest.hs delete mode 100644 waspc/test/Parser/ActionTest.hs delete mode 100644 waspc/test/Parser/CommonTest.hs delete mode 100644 waspc/test/Parser/DbTest.hs delete mode 100644 waspc/test/Parser/ExternalCodeTest.hs delete mode 100644 waspc/test/Parser/JsImportTest.hs delete mode 100644 waspc/test/Parser/NpmDependenciesTest.hs delete mode 100644 waspc/test/Parser/OperationTest.hs delete mode 100644 waspc/test/Parser/PageTest.hs delete mode 100644 waspc/test/Parser/ParserTest.hs delete mode 100644 waspc/test/Parser/QueryTest.hs delete mode 100644 waspc/test/Parser/RouteTest.hs delete mode 100644 waspc/test/Parser/ServerTest.hs delete mode 100644 waspc/test/Parser/StyleTest.hs delete mode 100644 waspc/test/Parser/valid.wasp rename web/docs/language/{basic-elements.md => features.md} (80%) create mode 100644 web/docs/language/syntax.md diff --git a/README.md b/README.md index 31d9d1f60..0dcb6a679 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ app TodoApp { title: "Todo App" } -route "/" -> page Main -page Main { - component: import Main from "@ext/pages/Main.js" // Importing React component. +route RootRoute { path: "/", to: MainPage } +page MainPage { + component: import Main from "@ext/pages/Main.js" // Importing React component. } query getTasks { diff --git a/examples/realworld/main.wasp b/examples/realworld/main.wasp index ccb2c6ef6..e0388ac14 100644 --- a/examples/realworld/main.wasp +++ b/examples/realworld/main.wasp @@ -3,55 +3,64 @@ app Conduit { head: [ "" + ], + + auth: { + userEntity: User, + methods: [ EmailAndPassword ], + 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") ] } -auth { - userEntity: User, - methods: [ EmailAndPassword ], - onAuthFailedRedirectTo: "/login" -} -db { - system: PostgreSQL -} // ----------------- Pages ------------------ // -route "/" -> page Main -page Main { +route RootRoute { path: "/", to: MainPage } +page MainPage { component: import Main from "@ext/MainPage.js" } -route "/login" -> page LogIn -page LogIn { +route LogInRoute { path: "/login", to: LogInPage } +page LogInPage { component: import LogIn from "@ext/auth/LoginPage.js" } -route "/register" -> page SignUp -page SignUp { +route RegisterRoute { path: "/register", to: SignUpPage } +page SignUpPage { component: import SignUp from "@ext/auth/SignupPage.js" } -route "/settings" -> page UserSettings -page UserSettings { +route UserSettingsRoute { path: "/settings", to: UserSettingsPage } +page UserSettingsPage { authRequired: true, component: import UserSettings from "@ext/user/components/UserSettingsPage.js" } -route "/@:username" -> page UserProfile -page UserProfile { +route UserProfileRoute { path: "/@:username", to: UserProfilePage } +page UserProfilePage { component: import UserProfile from "@ext/user/components/UserProfilePage.js" } -route "/editor/:articleSlug?" -> page ArticleEditor -page ArticleEditor { +route ArticleEditorRoute { path: "/editor/:articleSlug?", to: ArticleEditorPage } +page ArticleEditorPage { authRequired: true, component: import ArticleEditor from "@ext/article/components/ArticleEditorPage.js" } -route "/article/:articleSlug" -> page ArticleView -page ArticleView { +route ArticleViewRoute { path: "/article/:articleSlug", to: ArticleViewPage } +page ArticleViewPage { 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=} diff --git a/examples/thoughts/main.wasp b/examples/thoughts/main.wasp index 1e3080701..d9a1453b7 100644 --- a/examples/thoughts/main.wasp +++ b/examples/thoughts/main.wasp @@ -1,36 +1,36 @@ app Thoughts { - title: "Thoughts" + title: "Thoughts", + db: { system: PostgreSQL }, + auth: { + userEntity: User, + methods: [ EmailAndPassword ], + onAuthFailedRedirectTo: "/login" + }, + dependencies: [ + ("react-markdown", "6.0.1"), + ("color-hash", "2.0.1") + ] } -db { - system: PostgreSQL -} - -auth { - userEntity: User, - methods: [ EmailAndPassword ], - onAuthFailedRedirectTo: "/login" -} - -route "/" -> page Main -page Main { +route MainRoute { path: "/", to: MainPage } +page MainPage { component: import Main from "@ext/MainPage.js", authRequired: true } -route "/thoughts" -> page Thoughts -page Thoughts { +route ThoughtsRoute { path: "/thoughts", to: ThoughtsPage } +page ThoughtsPage { component: import Thoughts from "@ext/ThoughtsPage.js", authRequired: true } -route "/login" -> page Login -page Login { +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { component: import Login from "@ext/LoginPage.js" } -route "/signup" -> page Signup -page Signup { +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { component: import Signup from "@ext/SignupPage" } @@ -79,9 +79,4 @@ entity User {=psl thoughts Thought[] tags Tag[] -psl=} - -dependencies {=json - "react-markdown": "6.0.1", - "color-hash": "2.0.1" -json=} \ No newline at end of file +psl=} \ No newline at end of file diff --git a/examples/tutorials/TodoApp/main.wasp b/examples/tutorials/TodoApp/main.wasp index f630e0a21..c90f882c5 100644 --- a/examples/tutorials/TodoApp/main.wasp +++ b/examples/tutorials/TodoApp/main.wasp @@ -1,27 +1,31 @@ app TodoApp { - title: "Todo app" -} + title: "Todo app", -auth { + auth: { userEntity: User, methods: [ EmailAndPassword ], onAuthFailedRedirectTo: "/login" + }, + + dependencies: [ + ("react-clock", "3.0.0") + ] } -route "/" -> page Main -page Main { +route RootRoute { path: "/", to: MainPage } +page MainPage { authRequired: true, component: import Main from "@ext/MainPage.js" } -route "/signup" -> page Signup -page Signup { - component: import Signup from "@ext/SignupPage" +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { + component: import Signup from "@ext/SignupPage" } -route "/login" -> page Login -page Login { - component: import Login from "@ext/LoginPage" +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import Login from "@ext/LoginPage" } entity User {=psl @@ -53,7 +57,3 @@ action updateTask { fn: import { updateTask } from "@ext/actions.js", entities: [Task] } - -dependencies {=json - "react-clock": "3.0.0" -json=} diff --git a/examples/tutorials/TodoApp/migrations/20201023121126-a/README.md b/examples/tutorials/TodoApp/migrations/20201023121126-a/README.md deleted file mode 100644 index 7548e4f81..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121126-a/README.md +++ /dev/null @@ -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) -+} -+ -``` - - diff --git a/examples/tutorials/TodoApp/migrations/20201023121126-a/schema.prisma b/examples/tutorials/TodoApp/migrations/20201023121126-a/schema.prisma deleted file mode 100644 index 1a7817e65..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121126-a/schema.prisma +++ /dev/null @@ -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) -} - diff --git a/examples/tutorials/TodoApp/migrations/20201023121126-a/steps.json b/examples/tutorials/TodoApp/migrations/20201023121126-a/steps.json deleted file mode 100644 index 93812f939..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121126-a/steps.json +++ /dev/null @@ -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" - } - ] -} \ No newline at end of file diff --git a/examples/tutorials/TodoApp/migrations/20201023121536-b/README.md b/examples/tutorials/TodoApp/migrations/20201023121536-b/README.md deleted file mode 100644 index 146a148e9..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121536-b/README.md +++ /dev/null @@ -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? - } -``` - - diff --git a/examples/tutorials/TodoApp/migrations/20201023121536-b/schema.prisma b/examples/tutorials/TodoApp/migrations/20201023121536-b/schema.prisma deleted file mode 100644 index 2c344a06b..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121536-b/schema.prisma +++ /dev/null @@ -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? -} - diff --git a/examples/tutorials/TodoApp/migrations/20201023121536-b/steps.json b/examples/tutorials/TodoApp/migrations/20201023121536-b/steps.json deleted file mode 100644 index 30da01335..000000000 --- a/examples/tutorials/TodoApp/migrations/20201023121536-b/steps.json +++ /dev/null @@ -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" - } - ] -} \ No newline at end of file diff --git a/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql b/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql new file mode 100644 index 000000000..7a12b3624 --- /dev/null +++ b/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql @@ -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"); diff --git a/examples/tutorials/TodoApp/migrations/migrate.lock b/examples/tutorials/TodoApp/migrations/migrate.lock deleted file mode 100644 index 30e77881e..000000000 --- a/examples/tutorials/TodoApp/migrations/migrate.lock +++ /dev/null @@ -1,4 +0,0 @@ -# Prisma Migrate lockfile v1 - -20201023121126-a -20201023121536-b \ No newline at end of file diff --git a/examples/tutorials/TodoApp/migrations/migration_lock.toml b/examples/tutorials/TodoApp/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/examples/tutorials/TodoApp/migrations/migration_lock.toml @@ -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" \ No newline at end of file diff --git a/waspc/README.md b/waspc/README.md index 6eb3cc27d..0a4cf4e40 100644 --- a/waspc/README.md +++ b/waspc/README.md @@ -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 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`. -Parser is implemented via parser combinators and has no distinct tokenization, syntax and semantic analysis steps, currently it is all one big step. +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`). +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. 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`). diff --git a/waspc/cli/Wasp/Cli/Command/CreateNewProject.hs b/waspc/cli/Wasp/Cli/Command/CreateNewProject.hs index 51eda62b8..16b92cfa9 100644 --- a/waspc/cli/Wasp/Cli/Command/CreateNewProject.hs +++ b/waspc/cli/Wasp/Cli/Command/CreateNewProject.hs @@ -106,8 +106,8 @@ createNewProject' (ProjectName projectName) = do " title: \"%s\"" `printf` projectName, "}", "", - "route \"/\" -> page Main", - "page Main {", + "route RootRoute { path: \"/\", to: MainPage }", + "page MainPage {", " component: import Main from \"@ext/MainPage.js\"", "}" ] diff --git a/waspc/cli/Wasp/Cli/Command/Deps.hs b/waspc/cli/Wasp/Cli/Command/Deps.hs index 99833ba4b..591b1b64b 100644 --- a/waspc/cli/Wasp/Cli/Command/Deps.hs +++ b/waspc/cli/Wasp/Cli/Command/Deps.hs @@ -4,11 +4,12 @@ module Wasp.Cli.Command.Deps where import Control.Monad.IO.Class (liftIO) +import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import Wasp.Cli.Command (Command) import Wasp.Cli.Terminal (title) import qualified Wasp.Generator.ServerGenerator as ServerGenerator import qualified Wasp.Generator.WebAppGenerator as WebAppGenerator -import Wasp.NpmDependency (printDep) +import qualified Wasp.Util.Terminal as Term deps :: Command () deps = @@ -25,3 +26,9 @@ deps = title "Webapp dependencies:" ] ++ 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) diff --git a/waspc/cli/Wasp/Cli/Command/Info.hs b/waspc/cli/Wasp/Cli/Command/Info.hs index 750dd569e..87a706e1e 100644 --- a/waspc/cli/Wasp/Cli/Command/Info.hs +++ b/waspc/cli/Wasp/Cli/Command/Info.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Cli.Command.Info ( info, ) @@ -8,19 +10,18 @@ import Control.Monad.IO.Class (liftIO) import StrongPath (Abs, Dir, Path', fromAbsFile, fromRelFile, toFilePath) import StrongPath.Operations 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.Common - ( findWaspProjectRootDirFromCwd, - waspSaysC, - ) +import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd, waspSaysC) import qualified Wasp.Cli.Common as Cli.Common import Wasp.Cli.Terminal (title) import Wasp.Common (WaspProjectDir) +import Wasp.Error (showCompilerErrorForTerminal) import Wasp.Lib (findWaspFile) -import qualified Wasp.Parser import Wasp.Util.IO (listDirectoryDeep) import qualified Wasp.Util.Terminal as Term -import Wasp.Wasp (Wasp, appName, getApp) info :: Command () info = @@ -28,17 +29,17 @@ info = waspDir <- findWaspProjectRootDirFromCwd compileInfo <- liftIO $ readCompileInformation waspDir projectSize <- liftIO $ readDirectorySizeMB waspDir - waspAstOrError <- liftIO $ parseWaspFile waspDir - case waspAstOrError of + declsOrError <- liftIO $ parseWaspFile waspDir + case declsOrError of Left err -> waspSaysC err - Right wasp -> do + Right decls -> do waspSaysC $ unlines [ "", title "Project information", printInfo "Name" - (appName $ getApp wasp), + (fst $ head $ AS.takeDecls @AS.App.App decls), printInfo "Last compile" compileInfo, @@ -55,17 +56,28 @@ readDirectorySizeMB path = (++ " MB") . show . (`div` 1000000) . sum <$> (listDi readCompileInformation :: Path' Abs (Dir WaspProjectDir) -> IO String 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 if dotWaspInfoFileExists then do readFile dotWaspInfoFile 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 maybeWaspFile <- findWaspFile waspDir case maybeWaspFile of Nothing -> return (Left "Couldn't find a single *.wasp file.") - Just waspFile -> do - waspStr <- readFile (toFilePath waspFile) - return $ left (("Couldn't parse .wasp file: " <>) . show) $ Wasp.Parser.parseWasp waspStr + Just waspFile -> + do + waspStr <- readFile (toFilePath waspFile) + return $ + left + ( ("Couldn't parse .wasp file:\n" <>) + . showCompilerErrorForTerminal (waspFile, waspStr) + . Analyzer.getErrorMessageAndCtx + ) + $ Analyzer.analyze waspStr diff --git a/waspc/data/Cli/templates/new/ext/MainPage.js b/waspc/data/Cli/templates/new/ext/MainPage.js index 56395e6c0..529a4f286 100644 --- a/waspc/data/Cli/templates/new/ext/MainPage.js +++ b/waspc/data/Cli/templates/new/ext/MainPage.js @@ -12,7 +12,7 @@ const MainPage = () => {

Welcome to Wasp - you just started a new app!

- This is page Main located at route /. + This is page MainPage located at route /. Open ext/MainPage.js to edit it.

diff --git a/waspc/data/Generator/templates/react-app/README.md b/waspc/data/Generator/templates/react-app/README.md index 304158a25..9d9614c4f 100644 --- a/waspc/data/Generator/templates/react-app/README.md +++ b/waspc/data/Generator/templates/react-app/README.md @@ -1,4 +1,3 @@ -{{={{> <}}=}} This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts diff --git a/waspc/data/Generator/templates/react-app/gitignore b/waspc/data/Generator/templates/react-app/gitignore index 21e71a8d8..4d29575de 100644 --- a/waspc/data/Generator/templates/react-app/gitignore +++ b/waspc/data/Generator/templates/react-app/gitignore @@ -1,4 +1,3 @@ -{{={{> <}}=}} # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies diff --git a/waspc/data/Generator/templates/react-app/package.json b/waspc/data/Generator/templates/react-app/package.json index 97137e638..f0e086df5 100644 --- a/waspc/data/Generator/templates/react-app/package.json +++ b/waspc/data/Generator/templates/react-app/package.json @@ -1,6 +1,6 @@ {{={= =}=}} { - "name": "{= wasp.app.name =}", + "name": "{= appName =}", "version": "0.0.0", "private": true, {=& depsChunk =}, diff --git a/waspc/data/Generator/templates/react-app/public/manifest.json b/waspc/data/Generator/templates/react-app/public/manifest.json index b1942a294..302b5fccd 100644 --- a/waspc/data/Generator/templates/react-app/public/manifest.json +++ b/waspc/data/Generator/templates/react-app/public/manifest.json @@ -1,6 +1,6 @@ -{{={{> <}}=}} +{{={= =}=}} { - "name": "{{> app.name <}}", + "name": "{= appName =}", "icons": [ { "src": "favicon.ico", diff --git a/waspc/data/Generator/templates/react-app/src/index.css b/waspc/data/Generator/templates/react-app/src/index.css index 30e3447ab..cee5f348f 100644 --- a/waspc/data/Generator/templates/react-app/src/index.css +++ b/waspc/data/Generator/templates/react-app/src/index.css @@ -1,4 +1,3 @@ -{{={{> <}}=}} body { margin: 0; padding: 0; diff --git a/waspc/data/Generator/templates/react-app/src/index.js b/waspc/data/Generator/templates/react-app/src/index.js index 9c6afda1d..5ad5730d4 100644 --- a/waspc/data/Generator/templates/react-app/src/index.js +++ b/waspc/data/Generator/templates/react-app/src/index.js @@ -1,4 +1,3 @@ -{{={= =}=}} import React from 'react' import ReactDOM from 'react-dom' import { ReactQueryCacheProvider } from 'react-query' diff --git a/waspc/data/Generator/templates/react-app/src/operations/index.js b/waspc/data/Generator/templates/react-app/src/operations/index.js index e38aa06ae..647398dc4 100644 --- a/waspc/data/Generator/templates/react-app/src/operations/index.js +++ b/waspc/data/Generator/templates/react-app/src/operations/index.js @@ -1,4 +1,3 @@ -{{={= =}=}} import api, { handleApiError } from '../api.js' import config from '../config.js' diff --git a/waspc/data/Generator/templates/react-app/src/serviceWorker.js b/waspc/data/Generator/templates/react-app/src/serviceWorker.js index ff8bab1dd..f8c7e50c2 100644 --- a/waspc/data/Generator/templates/react-app/src/serviceWorker.js +++ b/waspc/data/Generator/templates/react-app/src/serviceWorker.js @@ -1,4 +1,3 @@ -{{={{> <}}=}} // This optional code is used to register a service worker. // register() is not called by default. diff --git a/waspc/examples/todoApp/todoApp.wasp b/waspc/examples/todoApp/todoApp.wasp index 613bf6d69..121a6000e 100644 --- a/waspc/examples/todoApp/todoApp.wasp +++ b/waspc/examples/todoApp/todoApp.wasp @@ -1,19 +1,20 @@ app todoApp { - title: "ToDo App", - head: [ - "" - ] -} - -dependencies {=json - "@material-ui/core": "4.11.3" -json=} - -auth { + title: "ToDo App", + head: [ + "" + ], + dependencies: [ + ("@material-ui/core", "4.11.3") + ], + auth: { userEntity: User, methods: [ EmailAndPassword ], onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/profile" + }, + server: { + setupFn: import setup from "@ext/serverSetup.js" + } } entity User {=psl @@ -31,43 +32,40 @@ entity Task {=psl userId Int psl=} -server { - setupFn: import setup from "@ext/serverSetup.js" + +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { + component: import Signup from "@ext/pages/auth/Signup" } -route "/signup" -> page Signup -page Signup { - component: import Signup from "@ext/pages/auth/Signup" +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import Login from "@ext/pages/auth/Login" } -route "/login" -> page Login -page Login { - component: import Login from "@ext/pages/auth/Login" +route HomeRoute { path: "/", to: MainPage } +page MainPage { + authRequired: true, + component: import Main from "@ext/pages/Main" } -route "/" -> page Main -page Main { - authRequired: true, - component: import Main from "@ext/pages/Main" +route AboutRoute { path: "/about", to: AboutPage } +page AboutPage { + component: import About from "@ext/pages/About" } -route "/about" -> page About -page About { - component: import About from "@ext/pages/About" -} - -route "/profile" -> page Profile -page Profile { - authRequired: true, - component: import { ProfilePage } from "@ext/pages/ProfilePage" +route ProfileRoute { path: "/profile", to: ProfilePage } +page ProfilePage { + authRequired: true, + component: import { ProfilePage } from "@ext/pages/ProfilePage" } // Page for viewing a specific task // -route "/task/:id" -> page Task -page Task { - authRequired: true, - component: import Task from "@ext/pages/Task" +route TaskRoute { path: "/task/:id", to: TaskPage } +page TaskPage { + authRequired: true, + component: import Task from "@ext/pages/Task" } // --------- Queries --------- // @@ -79,7 +77,7 @@ query getTasks { query getNumTasks { fn: import { getNumTasks } from "@ext/queries.js", - entities: [Task], + auth: false } diff --git a/waspc/images/waspc-implementation-diagram.png b/waspc/images/waspc-implementation-diagram.png index ac5fae6c451f117ae4fcfecc2e1b9ab0bd57512d..3efc2cecd3a75f456754bdff19ef6334054afbc2 100644 GIT binary patch literal 60503 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 GXa64nws@%k literal 37751 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 diff --git a/waspc/package.yaml b/waspc/package.yaml index 940262e37..390bec117 100644 --- a/waspc/package.yaml +++ b/waspc/package.yaml @@ -76,7 +76,7 @@ library: - unliftio - array # Used by code generated by Alex for src/Analyzer/Parser/Lexer.x - mtl - - strong-path + - strong-path >= 1.1.2.0 - template-haskell - path-io diff --git a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs index bf4b6559e..abf406c20 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs @@ -19,6 +19,8 @@ module Wasp.Analyzer.Evaluator.Evaluation.TypedExpr.Combinators where 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.TypedExpr (TypedExprEvaluation) 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". extImport :: TypedExprEvaluation AppSpec.ExtImport.ExtImport 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) + where + extPrefix = "@ext/" -- | An evaluation that expects a "JSON". json :: TypedExprEvaluation AppSpec.JSON.JSON diff --git a/waspc/src/Wasp/Analyzer/Parser/Lexer.x b/waspc/src/Wasp/Analyzer/Parser/Lexer.x index 27076d474..c5f0fc60b 100644 --- a/waspc/src/Wasp/Analyzer/Parser/Lexer.x +++ b/waspc/src/Wasp/Analyzer/Parser/Lexer.x @@ -29,12 +29,16 @@ $any = [.$white] @double = "-"? $digit+ "." $digit+ @integer = "-"? $digit+ @ident = $identstart $ident* "'"* +@linecomment = "//" [^\n\r]* +@blockcomment = "/*" (("*"[^\/]) | [^\*] | $white)* "*/" -- Based on https://stackoverflow.com/a/16165598/1509394 . -- Tokenization rules (regex -> token) tokens :- --- Skips whitespace +-- Skips whitespace and comments <0> $white+ ; +<0> @linecomment ; +<0> @blockcomment ; -- Quoter rules: -- Uses Alex start codes to lex quoted characters with different rules: diff --git a/waspc/src/Wasp/Analyzer/Parser/ParseError.hs b/waspc/src/Wasp/Analyzer/Parser/ParseError.hs index 3258a5134..92130f967 100644 --- a/waspc/src/Wasp/Analyzer/Parser/ParseError.hs +++ b/waspc/src/Wasp/Analyzer/Parser/ParseError.hs @@ -7,7 +7,7 @@ module Wasp.Analyzer.Parser.ParseError where 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.Token (Token (..)) @@ -40,7 +40,9 @@ getErrorMessageAndCtx = \case "Expected one of the following tokens instead: " ++ unwords expectedTokens 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) -> let ctx = ctxFromRgn (getRgnStart $ getCtxRgn lctx) (getRgnEnd $ getCtxRgn rctx) diff --git a/waspc/src/Wasp/Analyzer/TypeDefinitions/TH/Decl.hs b/waspc/src/Wasp/Analyzer/TypeDefinitions/TH/Decl.hs index 9d29263b3..224f2ae97 100644 --- a/waspc/src/Wasp/Analyzer/TypeDefinitions/TH/Decl.hs +++ b/waspc/src/Wasp/Analyzer/TypeDefinitions/TH/Decl.hs @@ -275,12 +275,11 @@ waspKindOfHaskellType typ = do maybeCustomEvaluationKind <- tryCastingToCustomEvaluationKind typ maybeRecordKind <- tryCastingToRecordKind typ 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 - -- 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 ConT name | 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 4 `AppT` t1 `AppT` t2 `AppT` t3 `AppT` t4 -> pure (KTuple (t1, t2, [t3, t4])) _ -> 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 tryCastingToDeclRefKind :: Type -> Q (Maybe WaspKind) tryCastingToDeclRefKind (ConT name `AppT` subType) | name == ''Ref = do diff --git a/waspc/src/Wasp/AppSpec.hs b/waspc/src/Wasp/AppSpec.hs index dc0fa1baf..2b1a85d18 100644 --- a/waspc/src/Wasp/AppSpec.hs +++ b/waspc/src/Wasp/AppSpec.hs @@ -1,11 +1,34 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.AppSpec ( AppSpec (..), + Decl, + getDecls, + takeDecls, + Ref, + refName, + getApp, + getActions, + getQueries, + getEntities, + getPages, + getRoutes, + isAuthEnabled, ) where +import Data.Maybe (isJust) 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 Wasp.AppSpec.Page (Page) +import Wasp.AppSpec.Query (Query) +import Wasp.AppSpec.Route (Route) import Wasp.Common (DbMigrationsDir) -- | 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)), -- | Absolute path to the directory in wasp project source that contains database migrations. 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'), + -- | 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 } + +-- 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) diff --git a/waspc/src/Wasp/AppSpec/App.hs b/waspc/src/Wasp/AppSpec/App.hs index bc3c7cc4e..a05be3174 100644 --- a/waspc/src/Wasp/AppSpec/App.hs +++ b/waspc/src/Wasp/AppSpec/App.hs @@ -12,12 +12,9 @@ import Wasp.AppSpec.Core.Decl (IsDecl) data App = App { title :: String, head :: Maybe [String], - auth :: Maybe Auth, -- NOTE: This is new. Before, `auth` was a standalone declaration. - server :: Maybe Server, -- NOTE: This is new. Before, `server` was a standalone declaration. - db :: Maybe Db, -- NOTE: This is new. Before, `db` was a standalone declaration. - - -- | 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 }]. + auth :: Maybe Auth, + server :: Maybe Server, + db :: Maybe Db, dependencies :: Maybe [Dependency] } deriving (Show, Eq, Data) diff --git a/waspc/src/Wasp/AppSpec/App/Dependency.hs b/waspc/src/Wasp/AppSpec/App/Dependency.hs index 8f068619a..3dbd8b2c6 100644 --- a/waspc/src/Wasp/AppSpec/App/Dependency.hs +++ b/waspc/src/Wasp/AppSpec/App/Dependency.hs @@ -2,6 +2,7 @@ module Wasp.AppSpec.App.Dependency ( Dependency (..), + fromList, ) where @@ -12,3 +13,6 @@ data Dependency = Dependency version :: String } deriving (Show, Eq, Data) + +fromList :: [(String, String)] -> [Dependency] +fromList = map (\(n, v) -> Dependency {name = n, version = v}) diff --git a/waspc/src/Wasp/AppSpec/Core/Ref.hs b/waspc/src/Wasp/AppSpec/Core/Ref.hs index f9c97cc0e..8a20768cd 100644 --- a/waspc/src/Wasp/AppSpec/Core/Ref.hs +++ b/waspc/src/Wasp/AppSpec/Core/Ref.hs @@ -4,6 +4,7 @@ module Wasp.AppSpec.Core.Ref ( Ref (..), + refName, ) where @@ -21,3 +22,6 @@ deriving instance Eq a => Eq (Ref a) deriving instance Show a => Show (Ref a) deriving instance (IsDecl a, Data a) => Data (Ref a) + +refName :: Ref a -> String +refName (Ref name) = name diff --git a/waspc/src/Wasp/AppSpec/ExtImport.hs b/waspc/src/Wasp/AppSpec/ExtImport.hs index d5809cf7c..22e3cbcd1 100644 --- a/waspc/src/Wasp/AppSpec/ExtImport.hs +++ b/waspc/src/Wasp/AppSpec/ExtImport.hs @@ -7,11 +7,18 @@ module Wasp.AppSpec.ExtImport where 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) -type ExtImportPath = String +type ExtImportPath = Path Posix (Rel SourceExternalCodeDir) File' type Identifier = String diff --git a/waspc/src/Wasp/AppSpec/ExternalCode.hs b/waspc/src/Wasp/AppSpec/ExternalCode.hs index e3f73b7f7..7f83db731 100644 --- a/waspc/src/Wasp/AppSpec/ExternalCode.hs +++ b/waspc/src/Wasp/AppSpec/ExternalCode.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE DeriveDataTypeable #-} + module Wasp.AppSpec.ExternalCode ( -- | Wasp project consists of Wasp code (.wasp files) and external code (e.g. .js files) that is -- used/referenced by the Wasp code. @@ -14,13 +16,14 @@ module Wasp.AppSpec.ExternalCode ) where +import Data.Data (Data) import Data.Text (Text) import qualified Data.Text.Lazy as TextL import StrongPath (Abs, Dir, File', Path', Rel, ()) -- | Directory in Wasp source that contains external code. -- External code files are obtained from it. -data SourceExternalCodeDir +data SourceExternalCodeDir deriving (Data) data File = File { _pathInExtCodeDir :: !(Path' (Rel SourceExternalCodeDir) File'), diff --git a/waspc/src/Wasp/AppSpec/Operation.hs b/waspc/src/Wasp/AppSpec/Operation.hs new file mode 100644 index 000000000..e062d8013 --- /dev/null +++ b/waspc/src/Wasp/AppSpec/Operation.hs @@ -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 diff --git a/waspc/src/Wasp/AppSpec/Route.hs b/waspc/src/Wasp/AppSpec/Route.hs index a2731beff..8f46eac26 100644 --- a/waspc/src/Wasp/AppSpec/Route.hs +++ b/waspc/src/Wasp/AppSpec/Route.hs @@ -10,7 +10,6 @@ import Wasp.AppSpec.Core.Decl (IsDecl) import Wasp.AppSpec.Core.Ref (Ref) 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 { path :: String, -- TODO: In the future we might want to add other types of targets, for example another Route. diff --git a/waspc/src/Wasp/Error.hs b/waspc/src/Wasp/Error.hs new file mode 100644 index 000000000..947d82fb3 --- /dev/null +++ b/waspc/src/Wasp/Error.hs @@ -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 diff --git a/waspc/src/Wasp/Generator.hs b/waspc/src/Wasp/Generator.hs index ce26594c1..7720acc31 100644 --- a/waspc/src/Wasp/Generator.hs +++ b/waspc/src/Wasp/Generator.hs @@ -12,7 +12,7 @@ import qualified Data.Version import qualified Paths_waspc import StrongPath (Abs, Dir, Path', relfile, ()) import qualified StrongPath as SP -import Wasp.CompileOptions (CompileOptions) +import Wasp.AppSpec (AppSpec) import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator (genDb) 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.Start import Wasp.Generator.WebAppGenerator (generateWebApp) -import Wasp.Wasp (Wasp) -- | Generates web app code from given Wasp and writes it to given destination directory. -- 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 -- 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? -writeWebAppCode :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () -writeWebAppCode wasp dstDir compileOptions = do - writeFileDrafts dstDir (generateWebApp wasp compileOptions) - ServerGenerator.preCleanup wasp dstDir compileOptions - writeFileDrafts dstDir (genServer wasp compileOptions) - DbGenerator.preCleanup wasp dstDir compileOptions - writeFileDrafts dstDir (genDb wasp compileOptions) - writeFileDrafts dstDir (genDockerFiles wasp compileOptions) +writeWebAppCode :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO () +writeWebAppCode spec dstDir = do + writeFileDrafts dstDir (generateWebApp spec) + ServerGenerator.preCleanup spec dstDir + writeFileDrafts dstDir (genServer spec) + DbGenerator.preCleanup spec dstDir + writeFileDrafts dstDir (genDb spec) + writeFileDrafts dstDir (genDockerFiles spec) writeDotWaspInfo dstDir -- | Writes file drafts while using given destination dir as root dir. diff --git a/waspc/src/Wasp/Generator/DbGenerator.hs b/waspc/src/Wasp/Generator/DbGenerator.hs index 4bd54e59f..2f7b10e72 100644 --- a/waspc/src/Wasp/Generator/DbGenerator.hs +++ b/waspc/src/Wasp/Generator/DbGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.DbGenerator ( genDb, preCleanup, @@ -9,22 +11,21 @@ where import Control.Monad (when) 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 qualified StrongPath as SP 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.CompileOptions (CompileOptions) import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.FileDraft (FileDraft, createCopyDirFileDraft, createTemplateFileDraft) import Wasp.Generator.Templates (TemplatesDir) import qualified Wasp.Psl.Ast.Model as Psl.Ast.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 @@ -50,13 +51,18 @@ dbSchemaFileInProjectRootDir = dbRootDirInProjectRootDir dbSchemaFileInDbRoo dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir) dbMigrationsDirInDbRootDir = [reldir|migrations|] -preCleanup :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () -preCleanup wasp projectRootDir _ = do - deleteGeneratedMigrationsDirIfRedundant wasp projectRootDir +preCleanup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO () +preCleanup spec projectRootDir = do + deleteGeneratedMigrationsDirIfRedundant spec projectRootDir -deleteGeneratedMigrationsDirIfRedundant :: Wasp -> Path' Abs (Dir ProjectRootDir) -> IO () -deleteGeneratedMigrationsDirIfRedundant wasp projectRootDir = do - let waspMigrationsDirMissing = isNothing $ getMigrationsDir wasp +-- * Db generator + +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 when (waspMigrationsDirMissing && projectMigrationsDirExists) $ do 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 projectMigrationsDirAbsFilePath = SP.fromAbsDir $ projectRootDir dbRootDirInProjectRootDir dbMigrationsDirInDbRootDir -genDb :: Wasp -> CompileOptions -> [FileDraft] -genDb wasp _ = - genPrismaSchema wasp : maybeToList (genMigrationsDir wasp) - -genPrismaSchema :: Wasp -> FileDraft -genPrismaSchema wasp = createTemplateFileDraft dstPath tmplSrcPath (Just templateData) +genPrismaSchema :: AppSpec -> FileDraft +genPrismaSchema spec = createTemplateFileDraft dstPath tmplSrcPath (Just templateData) where dstPath = dbSchemaFileInProjectRootDir tmplSrcPath = dbTemplatesDirInTemplatesDir dbSchemaFileInDbTemplatesDir templateData = object - [ "modelSchemas" .= map entityToPslModelSchema (Wasp.getPSLEntities wasp), + [ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec), "datasourceProvider" .= (datasourceProvider :: 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 - Wasp.Db.PostgreSQL -> ("postgresql", "env(\"DATABASE_URL\")") + AS.Db.PostgreSQL -> ("postgresql", "env(\"DATABASE_URL\")") -- TODO: Report this error with some better mechanism, not `error`. - Wasp.Db.SQLite -> - if Wasp.getIsBuild wasp - 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 ." + AS.Db.SQLite -> + 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/features/#migrating-from-sqlite-to-postgresql ." else ("sqlite", "\"file:./dev.db\"") - entityToPslModelSchema :: Entity -> String - entityToPslModelSchema entity = + entityToPslModelSchema :: (String, AS.Entity.Entity) -> String + entityToPslModelSchema (entityName, entity) = 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 wasp = - (getMigrationsDir wasp) >>= \waspMigrationsDir -> +genMigrationsDir :: AppSpec -> Maybe FileDraft +genMigrationsDir spec = + AS.migrationsDir spec >>= \waspMigrationsDir -> Just $ createCopyDirFileDraft (SP.castDir genProjectMigrationsDir) (SP.castDir waspMigrationsDir) where genProjectMigrationsDir = dbRootDirInProjectRootDir dbMigrationsDirInDbRootDir diff --git a/waspc/src/Wasp/Generator/DockerGenerator.hs b/waspc/src/Wasp/Generator/DockerGenerator.hs index 47b429b49..5af27dc1a 100644 --- a/waspc/src/Wasp/Generator/DockerGenerator.hs +++ b/waspc/src/Wasp/Generator/DockerGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.DockerGenerator ( genDockerFiles, ) @@ -5,29 +7,29 @@ where import Data.Aeson (object, (.=)) 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.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.Templates (TemplatesDir) -import Wasp.Wasp (Wasp) -import qualified Wasp.Wasp as Wasp -genDockerFiles :: Wasp -> CompileOptions -> [FileDraft] -genDockerFiles wasp _ = genDockerfile wasp : [genDockerignore wasp] +genDockerFiles :: AppSpec -> [FileDraft] +genDockerFiles spec = genDockerfile spec : [genDockerignore spec] -- TODO: Inject paths to server and db files/dirs, right now they are hardcoded in the templates. -genDockerfile :: Wasp -> FileDraft -genDockerfile wasp = +genDockerfile :: AppSpec -> FileDraft +genDockerfile spec = createTemplateFileDraft ([relfile|Dockerfile|] :: Path' (Rel ProjectRootDir) File') ([relfile|Dockerfile|] :: Path' (Rel TemplatesDir) File') ( Just $ object - [ "usingPrisma" .= not (null $ Wasp.getPSLEntities wasp) + [ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec) ] ) -genDockerignore :: Wasp -> FileDraft +genDockerignore :: AppSpec -> FileDraft genDockerignore _ = createTemplateFileDraft ([relfile|.dockerignore|] :: Path' (Rel ProjectRootDir) File') diff --git a/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs b/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs index 08ba40896..7a82f589f 100644 --- a/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs +++ b/waspc/src/Wasp/Generator/ExternalCodeGenerator.hs @@ -10,17 +10,14 @@ import qualified Wasp.AppSpec.ExternalCode as EC import qualified Wasp.Generator.ExternalCodeGenerator.Common as C import Wasp.Generator.ExternalCodeGenerator.Js (generateJsFile) 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. -- It might not just copy them but also do some changes on them, as needed. generateExternalCodeDir :: C.ExternalCodeGeneratorStrategy -> - Wasp -> + [EC.File] -> [FD.FileDraft] -generateExternalCodeDir strategy wasp = - map (generateFile strategy) (Wasp.getExternalCodeFiles wasp) +generateExternalCodeDir strategy = map (generateFile strategy) generateFile :: C.ExternalCodeGeneratorStrategy -> EC.File -> FD.FileDraft generateFile strategy file diff --git a/waspc/src/Wasp/Generator/JsImport.hs b/waspc/src/Wasp/Generator/JsImport.hs index ab34724dc..260ed878c 100644 --- a/waspc/src/Wasp/Generator/JsImport.hs +++ b/waspc/src/Wasp/Generator/JsImport.hs @@ -1,27 +1,26 @@ module Wasp.Generator.JsImport - ( getImportDetailsForJsFnImport, + ( getJsImportDetailsForExtFnImport, ) where import StrongPath (Dir, Path, Posix, Rel, ()) import qualified StrongPath as SP +import qualified Wasp.AppSpec.ExtImport as AS.ExtImport 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 Posix (Rel (Dir a)) (Dir GeneratedExternalCodeDir) -> - Wasp.JsImport.JsImport -> + AS.ExtImport.ExtImport -> -- | (importIdentifier, importStmt) - -- - importIdentifier -> Identifier via which you can access js function after you import it with importStmt. - -- - importStmt -> Import statement via which you should do the import. + -- - importIdentifier -> Identifier via which you can access ext js function after you import it with importStmt. + -- - importStmt -> Javascript import statement via which you should do the import. (String, String) -getImportDetailsForJsFnImport relPosixPathToExtCodeDir jsImport = (importIdentifier, importStmt) +getJsImportDetailsForExtFnImport relPosixPathToExtCodeDir extImport = (importIdentifier, importStmt) where 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) = - case (Wasp.JsImport._defaultImport jsImport, Wasp.JsImport._namedImports jsImport) of - (Just defaultImport, []) -> (defaultImport, defaultImport) - (Nothing, [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." + case AS.ExtImport.name extImport of + AS.ExtImport.ExtImportModule defaultImport -> (defaultImport, defaultImport) + AS.ExtImport.ExtImportField namedImport -> (namedImport, "{ " ++ namedImport ++ " }") diff --git a/waspc/src/Wasp/Generator/PackageJsonGenerator.hs b/waspc/src/Wasp/Generator/PackageJsonGenerator.hs index 654ed5489..68bdef3df 100644 --- a/waspc/src/Wasp/Generator/PackageJsonGenerator.hs +++ b/waspc/src/Wasp/Generator/PackageJsonGenerator.hs @@ -8,7 +8,7 @@ where import Data.Bifunctor (second) import Data.List (find, intercalate) import Data.Maybe (fromJust, isJust) -import qualified Wasp.NpmDependency as ND +import qualified Wasp.AppSpec.App.Dependency as D type NpmDependenciesConflictError = String @@ -20,56 +20,56 @@ type NpmDependenciesConflictError = String -- On error (Left), returns list of conflicting user deps together with the error message -- explaining what the error is. resolveNpmDeps :: - [ND.NpmDependency] -> - [ND.NpmDependency] -> + [D.Dependency] -> + [D.Dependency] -> Either - [(ND.NpmDependency, NpmDependenciesConflictError)] - ([ND.NpmDependency], [ND.NpmDependency]) + [(D.Dependency, NpmDependenciesConflictError)] + ([D.Dependency], [D.Dependency]) resolveNpmDeps waspDeps userDeps = if null conflictingUserDeps then Right (waspDeps, userDepsNotInWaspDeps) else Left conflictingUserDeps where - conflictingUserDeps :: [(ND.NpmDependency, NpmDependenciesConflictError)] + conflictingUserDeps :: [(D.Dependency, NpmDependenciesConflictError)] conflictingUserDeps = map (second fromJust) $ filter (isJust . snd) $ map (\dep -> (dep, checkIfConflictingUserDep dep)) userDeps - checkIfConflictingUserDep :: ND.NpmDependency -> Maybe NpmDependenciesConflictError + checkIfConflictingUserDep :: D.Dependency -> Maybe NpmDependenciesConflictError checkIfConflictingUserDep userDep = let attachErrorMessage dep = - "Error: Dependency conflict for user npm dependency (" - ++ ND._name dep + "Error: Dependency conflict for user dependency (" + ++ D.name dep ++ ", " - ++ ND._version dep + ++ D.version dep ++ "): " ++ "Version must be set to the exactly the same version as" ++ " the one wasp is using: " - ++ ND._version dep + ++ D.version dep in attachErrorMessage <$> find (areTwoDepsInConflict userDep) waspDeps - areTwoDepsInConflict :: ND.NpmDependency -> ND.NpmDependency -> Bool + areTwoDepsInConflict :: D.Dependency -> D.Dependency -> Bool areTwoDepsInConflict d1 d2 = - ND._name d1 == ND._name d2 - && ND._version d1 /= ND._version d2 + D.name d1 == D.name d2 + && D.version d1 /= D.version d2 - userDepsNotInWaspDeps :: [ND.NpmDependency] - userDepsNotInWaspDeps = filter (not . isDepWithNameInWaspDeps . ND._name) userDeps + userDepsNotInWaspDeps :: [D.Dependency] + userDepsNotInWaspDeps = filter (not . isDepWithNameInWaspDeps . D.name) userDeps 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 = "\"" ++ key ++ "\": {" - ++ intercalate ",\n " (map (\dep -> "\"" ++ ND._name dep ++ "\": \"" ++ ND._version dep ++ "\"") deps) + ++ intercalate ",\n " (map (\dep -> "\"" ++ D.name dep ++ "\": \"" ++ D.version dep ++ "\"") deps) ++ "\n}" -npmDepsToPackageJsonEntry :: [ND.NpmDependency] -> String +npmDepsToPackageJsonEntry :: [D.Dependency] -> String npmDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "dependencies" -npmDevDepsToPackageJsonEntry :: [ND.NpmDependency] -> String +npmDevDepsToPackageJsonEntry :: [D.Dependency] -> String npmDevDepsToPackageJsonEntry deps = npmDepsToPackageJsonEntryWithKey deps "devDependencies" diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index bc209aaf7..5a6a07f13 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.ServerGenerator ( genServer, preCleanup, @@ -20,12 +22,18 @@ import qualified StrongPath as SP import System.Directory (removeFile) import System.IO.Error (isDoesNotExistError) 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.ExternalCodeGenerator (generateExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.FileDraft (FileDraft, createCopyFileDraft) -import Wasp.Generator.JsImport (getImportDetailsForJsFnImport) +import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport) import Wasp.Generator.PackageJsonGenerator ( npmDepsToPackageJsonEntry, npmDevDepsToPackageJsonEntry, @@ -42,24 +50,18 @@ import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile) import qualified Wasp.Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator import Wasp.Generator.ServerGenerator.OperationsG (genOperations) 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 wasp _ = +genServer :: AppSpec -> [FileDraft] +genServer spec = concat - [ [genReadme wasp], - [genPackageJson wasp waspNpmDeps waspNpmDevDeps], - [genNpmrc wasp], - [genNvmrc wasp], - [genGitignore wasp], - genSrcDir wasp, - generateExternalCodeDir ServerExternalCodeGenerator.generatorStrategy wasp, - genDotEnv wasp + [ [genReadme], + [genPackageJson spec waspNpmDeps waspNpmDevDeps], + [genNpmrc], + [genNvmrc], + [genGitignore], + genSrcDir spec, + generateExternalCodeDir ServerExternalCodeGenerator.generatorStrategy (AS.externalCodeFiles spec), + genDotEnv spec ] -- 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, -- we will not need this method any more. Check https://github.com/wasp-lang/wasp/issues/209 -- for progress of this. -preCleanup :: Wasp -> Path' Abs (Dir ProjectRootDir) -> CompileOptions -> IO () -preCleanup _ outDir _ = do +preCleanup :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO () +preCleanup _ outDir = do -- If .env gets removed but there is old .env file in generated project from previous attempts, -- we need to make sure we remove it. removeFile dotEnvAbsFilePath @@ -76,9 +78,9 @@ preCleanup _ outDir _ = do where dotEnvAbsFilePath = SP.toFilePath $ outDir C.serverRootDirInProjectRootDir dotEnvInServerRootDir -genDotEnv :: Wasp -> [FileDraft] -genDotEnv wasp = - case Wasp.getDotEnvFile wasp of +genDotEnv :: AppSpec -> [FileDraft] +genDotEnv spec = + case AS.dotEnvFile spec of Just srcFilePath -> [ createCopyFileDraft (C.serverRootDirInProjectRootDir dotEnvInServerRootDir) @@ -89,22 +91,21 @@ genDotEnv wasp = dotEnvInServerRootDir :: Path' (Rel C.ServerRootDir) File' dotEnvInServerRootDir = [relfile|.env|] -genReadme :: Wasp -> FileDraft -genReadme _ = C.copyTmplAsIs (asTmplFile [relfile|README.md|]) +genReadme :: FileDraft +genReadme = C.mkTmplFd (asTmplFile [relfile|README.md|]) -genPackageJson :: Wasp -> [ND.NpmDependency] -> [ND.NpmDependency] -> FileDraft -genPackageJson wasp waspDeps waspDevDeps = - C.makeTemplateFD +genPackageJson :: AppSpec -> [AS.Dependency.Dependency] -> [AS.Dependency.Dependency] -> FileDraft +genPackageJson spec waspDeps waspDevDeps = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|package.json|]) (asServerFile [relfile|package.json|]) ( Just $ object - [ "wasp" .= wasp, - "depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps), + [ "depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps), "devDepsChunk" .= npmDevDepsToPackageJsonEntry waspDevDeps, "nodeVersion" .= nodeVersionAsText, "startProductionScript" - .= if not (null $ Wasp.getPSLEntities wasp) + .= if not (null $ AS.getDecls @AS.Entity.Entity spec) then "npm run db-migrate-prod && " else "" @@ -117,12 +118,12 @@ genPackageJson wasp waspDeps waspDevDeps = Right deps -> deps Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors - userDeps :: [ND.NpmDependency] - userDeps = WND._dependencies $ Wasp.getNpmDependencies wasp + userDeps :: [AS.Dependency.Dependency] + userDeps = fromMaybe [] $ AS.App.dependencies $ snd $ AS.getApp spec -waspNpmDeps :: [ND.NpmDependency] +waspNpmDeps :: [AS.Dependency.Dependency] waspNpmDeps = - ND.fromList + AS.Dependency.fromList [ ("cookie-parser", "~1.4.4"), ("cors", "^2.8.5"), ("debug", "~2.6.9"), @@ -135,56 +136,56 @@ waspNpmDeps = ("helmet", "^4.6.0") ] -waspNpmDevDeps :: [ND.NpmDependency] +waspNpmDevDeps :: [AS.Dependency.Dependency] waspNpmDevDeps = - ND.fromList + AS.Dependency.fromList [ ("nodemon", "^2.0.4"), ("standard", "^14.3.4"), ("prisma", "2.22.1") ] -genNpmrc :: Wasp -> FileDraft -genNpmrc _ = - C.makeTemplateFD +genNpmrc :: FileDraft +genNpmrc = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|npmrc|]) (asServerFile [relfile|.npmrc|]) Nothing -genNvmrc :: Wasp -> FileDraft -genNvmrc _ = - C.makeTemplateFD +genNvmrc :: FileDraft +genNvmrc = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|nvmrc|]) (asServerFile [relfile|.nvmrc|]) (Just (object ["nodeVersion" .= ('v' : nodeVersionAsText)])) -genGitignore :: Wasp -> FileDraft -genGitignore _ = - C.makeTemplateFD +genGitignore :: FileDraft +genGitignore = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|gitignore|]) (asServerFile [relfile|.gitignore|]) Nothing -genSrcDir :: Wasp -> [FileDraft] -genSrcDir wasp = +genSrcDir :: AppSpec -> [FileDraft] +genSrcDir spec = concat - [ [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|app.js|]], - [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|server.js|]], - [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|utils.js|]], - [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/AuthError.js|]], - [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/HttpError.js|]], - [genDbClient wasp], - [genConfigFile wasp], - genRoutesDir wasp, - genOperationsRoutes wasp, - genOperations wasp, - genAuth wasp, - [genServerJs wasp] + [ [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|app.js|]], + [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|server.js|]], + [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|utils.js|]], + [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/AuthError.js|]], + [C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/HttpError.js|]], + [genDbClient spec], + [genConfigFile spec], + genRoutesDir spec, + genOperationsRoutes spec, + genOperations spec, + genAuth spec, + [genServerJs spec] ] -genDbClient :: Wasp -> FileDraft -genDbClient wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genDbClient :: AppSpec -> FileDraft +genDbClient spec = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where - maybeAuth = getAuth wasp + maybeAuth = AS.App.auth $ snd $ AS.getApp spec dbClientRelToSrcP = [relfile|dbClient.js|] tmplFile = C.asTmplFile $ [reldir|src|] dbClientRelToSrcP @@ -195,13 +196,13 @@ genDbClient wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData) then object [ "isAuthEnabled" .= True, - "userEntityUpper" .= Wasp.Auth._userEntity (fromJust maybeAuth) + "userEntityUpper" .= (AS.refName (AS.App.Auth.userEntity $ fromJust maybeAuth) :: String) ] else object [] -genServerJs :: Wasp -> FileDraft -genServerJs wasp = - C.makeTemplateFD +genServerJs :: AppSpec -> FileDraft +genServerJs spec = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|src/server.js|]) (asServerFile [relfile|src/server.js|]) ( Just $ @@ -212,8 +213,8 @@ genServerJs wasp = ] ) where - maybeSetupJsFunction = Wasp.Server._setupJsFunction <$> Wasp.getServer wasp - maybeSetupJsFnImportDetails = getImportDetailsForJsFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction + maybeSetupJsFunction = AS.App.Server.setupFn =<< AS.App.server (snd $ AS.getApp spec) + maybeSetupJsFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction (maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) = (fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails) @@ -221,17 +222,17 @@ genServerJs wasp = relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir ServerSrcDir)) (Dir GeneratedExternalCodeDir) relPosixPathFromSrcDirToExtSrcDir = [reldirP|./ext-src|] -genRoutesDir :: Wasp -> [FileDraft] -genRoutesDir wasp = +genRoutesDir :: AppSpec -> [FileDraft] +genRoutesDir spec = -- 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. - [ C.makeTemplateFD + [ C.mkTmplFdWithDstAndData (asTmplFile [relfile|src/routes/index.js|]) (asServerFile [relfile|src/routes/index.js|]) ( Just $ object - [ "operationsRouteInRootRouter" .= operationsRouteInRootRouter, - "isAuthEnabled" .= isJust (getAuth wasp) + [ "operationsRouteInRootRouter" .= (operationsRouteInRootRouter :: String), + "isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool) ] ) ] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs index d50ff66b5..7e47d9155 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs @@ -5,14 +5,16 @@ where import Data.Aeson (object, (.=)) 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 qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Util as Util -import Wasp.Wasp (Wasp, getAuth) -import qualified Wasp.Wasp.Auth as Wasp.Auth -genAuth :: Wasp -> [FileDraft] -genAuth wasp = case maybeAuth of +genAuth :: AppSpec -> [FileDraft] +genAuth spec = case maybeAuth of Just auth -> [ genCoreAuth auth, genAuthMiddleware auth, @@ -24,55 +26,55 @@ genAuth wasp = case maybeAuth of ] Nothing -> [] where - maybeAuth = getAuth wasp + maybeAuth = AS.App.auth $ snd $ AS.getApp spec -- | Generates core/auth file which contains auth middleware and createUser() function. -genCoreAuth :: Wasp.Auth.Auth -> FileDraft -genCoreAuth auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genCoreAuth :: AS.Auth.Auth -> FileDraft +genCoreAuth auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where coreAuthRelToSrc = [relfile|core/auth.js|] tmplFile = C.asTmplFile $ [reldir|src|] coreAuthRelToSrc dstFile = C.serverSrcDirInServerRootDir C.asServerSrcFile coreAuthRelToSrc tmplData = - let userEntity = Wasp.Auth._userEntity auth + let userEntityName = AS.refName $ AS.Auth.userEntity auth in object - [ "userEntityUpper" .= userEntity, - "userEntityLower" .= Util.toLowerFirst userEntity + [ "userEntityUpper" .= (userEntityName :: String), + "userEntityLower" .= (Util.toLowerFirst userEntityName :: String) ] -genAuthMiddleware :: Wasp.Auth.Auth -> FileDraft -genAuthMiddleware auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genAuthMiddleware :: AS.Auth.Auth -> FileDraft +genAuthMiddleware auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where authMiddlewareRelToSrc = [relfile|core/auth/prismaMiddleware.js|] tmplFile = C.asTmplFile $ [reldir|src|] authMiddlewareRelToSrc dstFile = C.serverSrcDirInServerRootDir C.asServerSrcFile authMiddlewareRelToSrc tmplData = - let userEntity = Wasp.Auth._userEntity auth + let userEntityName = AS.refName $ AS.Auth.userEntity auth in object - [ "userEntityUpper" .= userEntity + [ "userEntityUpper" .= (userEntityName :: String) ] 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 auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genLoginRoute :: AS.Auth.Auth -> FileDraft +genLoginRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where loginRouteRelToSrc = [relfile|routes/auth/login.js|] tmplFile = C.asTmplFile $ [reldir|src|] loginRouteRelToSrc dstFile = C.serverSrcDirInServerRootDir C.asServerSrcFile loginRouteRelToSrc tmplData = - let userEntity = Wasp.Auth._userEntity auth + let userEntityName = AS.refName $ AS.Auth.userEntity auth in object - [ "userEntityUpper" .= userEntity, - "userEntityLower" .= Util.toLowerFirst userEntity + [ "userEntityUpper" .= (userEntityName :: String), + "userEntityLower" .= (Util.toLowerFirst userEntityName :: String) ] -genSignupRoute :: Wasp.Auth.Auth -> FileDraft -genSignupRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genSignupRoute :: AS.Auth.Auth -> FileDraft +genSignupRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where signupRouteRelToSrc = [relfile|routes/auth/signup.js|] tmplFile = C.asTmplFile $ [reldir|src|] signupRouteRelToSrc @@ -80,11 +82,11 @@ genSignupRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) tmplData = object - [ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth) + [ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String) ] -genMeRoute :: Wasp.Auth.Auth -> FileDraft -genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genMeRoute :: AS.Auth.Auth -> FileDraft +genMeRoute auth = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where meRouteRelToSrc = [relfile|routes/auth/me.js|] tmplFile = C.asTmplFile $ [reldir|src|] meRouteRelToSrc @@ -92,5 +94,5 @@ genMeRoute auth = C.makeTemplateFD tmplFile dstFile (Just tmplData) tmplData = object - [ "userEntityLower" .= Util.toLowerFirst (Wasp.Auth._userEntity auth) + [ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String) ] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Common.hs b/waspc/src/Wasp/Generator/ServerGenerator/Common.hs index a5e3525b5..8ac180a9c 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Common.hs @@ -2,10 +2,9 @@ module Wasp.Generator.ServerGenerator.Common ( serverRootDirInProjectRootDir, serverSrcDirInServerRootDir, serverSrcDirInProjectRootDir, - copyTmplAsIs, - makeSimpleTemplateFD, - makeTemplateFD, - copySrcTmplAsIs, + mkTmplFd, + mkTmplFdWithDstAndData, + mkSrcTmplFd, srcDirInServerTemplatesDir, asTmplFile, asTmplSrcFile, @@ -24,7 +23,6 @@ import qualified StrongPath as SP import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.Templates (TemplatesDir) -import Wasp.Wasp (Wasp) data ServerRootDir @@ -61,29 +59,24 @@ serverSrcDirInProjectRootDir = serverRootDirInProjectRootDir serverSrcDirInS -- * Templates -copyTmplAsIs :: Path' (Rel ServerTemplatesDir) File' -> FileDraft -copyTmplAsIs srcPath = makeTemplateFD srcPath dstPath Nothing +mkTmplFd :: Path' (Rel ServerTemplatesDir) File' -> FileDraft +mkTmplFd srcPath = mkTmplFdWithDstAndData srcPath dstPath Nothing where dstPath = SP.castRel srcPath :: Path' (Rel ServerRootDir) File' -makeSimpleTemplateFD :: Path' (Rel ServerTemplatesDir) File' -> Wasp -> FileDraft -makeSimpleTemplateFD srcPath wasp = makeTemplateFD srcPath dstPath (Just $ Aeson.toJSON wasp) - where - dstPath = SP.castRel srcPath :: Path' (Rel ServerRootDir) File' - -makeTemplateFD :: +mkTmplFdWithDstAndData :: Path' (Rel ServerTemplatesDir) File' -> Path' (Rel ServerRootDir) File' -> Maybe Aeson.Value -> FileDraft -makeTemplateFD relSrcPath relDstPath tmplData = +mkTmplFdWithDstAndData relSrcPath relDstPath tmplData = createTemplateFileDraft (serverRootDirInProjectRootDir relDstPath) (serverTemplatesDirInTemplatesDir relSrcPath) tmplData -copySrcTmplAsIs :: Path' (Rel ServerTemplatesSrcDir) File' -> FileDraft -copySrcTmplAsIs pathInTemplatesSrcDir = makeTemplateFD srcPath dstPath Nothing +mkSrcTmplFd :: Path' (Rel ServerTemplatesSrcDir) File' -> FileDraft +mkSrcTmplFd pathInTemplatesSrcDir = mkTmplFdWithDstAndData srcPath dstPath Nothing where srcPath = srcDirInServerTemplatesDir pathInTemplatesSrcDir dstPath = diff --git a/waspc/src/Wasp/Generator/ServerGenerator/ConfigG.hs b/waspc/src/Wasp/Generator/ServerGenerator/ConfigG.hs index 7c159e5ee..425a63a36 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/ConfigG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/ConfigG.hs @@ -5,21 +5,21 @@ module Wasp.Generator.ServerGenerator.ConfigG where import Data.Aeson (object, (.=)) -import Data.Maybe (isJust) import StrongPath (File', Path', Rel, relfile, ()) import qualified StrongPath as SP +import Wasp.AppSpec (AppSpec) +import qualified Wasp.AppSpec as AS import Wasp.Generator.FileDraft (FileDraft) import qualified Wasp.Generator.ServerGenerator.Common as C -import Wasp.Wasp (Wasp, getAuth) -genConfigFile :: Wasp -> FileDraft -genConfigFile wasp = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genConfigFile :: AppSpec -> FileDraft +genConfigFile spec = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where tmplFile = C.srcDirInServerTemplatesDir SP.castRel configFileInSrcDir dstFile = C.serverSrcDirInServerRootDir configFileInSrcDir tmplData = object - [ "isAuthEnabled" .= isJust (getAuth wasp) + [ "isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool) ] configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' diff --git a/waspc/src/Wasp/Generator/ServerGenerator/OperationsG.hs b/waspc/src/Wasp/Generator/ServerGenerator/OperationsG.hs index 2e0057676..e78c7c9c8 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/OperationsG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/OperationsG.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.ServerGenerator.OperationsG ( genOperations, queryFileInSrcDir, @@ -12,80 +14,76 @@ import Data.Char (toLower) import Data.Maybe (fromJust) import StrongPath (Dir, Dir', File', Path, Path', Posix, Rel, reldir, reldirP, relfile, ()) 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.FileDraft (FileDraft) -import Wasp.Generator.JsImport (getImportDetailsForJsFnImport) +import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport) 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 wasp = - genQueries wasp - ++ genActions wasp +genOperations :: AppSpec -> [FileDraft] +genOperations spec = genQueries spec ++ genActions spec -genQueries :: Wasp -> [FileDraft] -genQueries wasp = - map (genQuery wasp) (Wasp.getQueries wasp) +genQueries :: AppSpec -> [FileDraft] +genQueries spec = map (genQuery spec) (AS.getQueries spec) -genActions :: Wasp -> [FileDraft] -genActions wasp = - map (genAction wasp) (Wasp.getActions wasp) +genActions :: AppSpec -> [FileDraft] +genActions spec = map (genAction spec) (AS.getActions spec) -- | 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, -- and user also, should use this new JS function, and not the old one directly. -genQuery :: Wasp -> Wasp.Query.Query -> FileDraft -genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genQuery :: AppSpec -> (String, AS.Query.Query) -> FileDraft +genQuery _ (queryName, query) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where - operation = Wasp.Operation.QueryOp query + operation = AS.Operation.QueryOp queryName query tmplFile = C.asTmplFile [relfile|src/queries/_query.js|] - dstFile = C.serverSrcDirInServerRootDir queryFileInSrcDir query + dstFile = C.serverSrcDirInServerRootDir queryFileInSrcDir queryName tmplData = operationTmplData operation -- | Analogous to genQuery. -genAction :: Wasp -> Wasp.Action.Action -> FileDraft -genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genAction :: AppSpec -> (String, AS.Action.Action) -> FileDraft +genAction _ (actionName, action) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where - operation = Wasp.Operation.ActionOp action + operation = AS.Operation.ActionOp actionName action tmplFile = [relfile|src/actions/_action.js|] - dstFile = C.serverSrcDirInServerRootDir actionFileInSrcDir action + dstFile = C.serverSrcDirInServerRootDir actionFileInSrcDir actionName tmplData = operationTmplData operation -queryFileInSrcDir :: Wasp.Query.Query -> Path' (Rel C.ServerSrcDir) File' -queryFileInSrcDir query = +queryFileInSrcDir :: String -> Path' (Rel C.ServerSrcDir) File' +queryFileInSrcDir queryName = [reldir|queries|] -- 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 action = +actionFileInSrcDir :: String -> Path' (Rel C.ServerSrcDir) File' +actionFileInSrcDir actionName = [reldir|actions|] -- 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 (Wasp.Operation.QueryOp query) = queryFileInSrcDir query -operationFileInSrcDir (Wasp.Operation.ActionOp action) = actionFileInSrcDir action +operationFileInSrcDir :: AS.Operation.Operation -> Path' (Rel C.ServerSrcDir) File' +operationFileInSrcDir (AS.Operation.QueryOp name _) = queryFileInSrcDir name +operationFileInSrcDir (AS.Operation.ActionOp name _) = actionFileInSrcDir name -- | TODO: Make this not hardcoded! relPosixPathFromOperationFileToExtSrcDir :: Path Posix (Rel Dir') (Dir GeneratedExternalCodeDir) relPosixPathFromOperationFileToExtSrcDir = [reldirP|../ext-src/|] -operationTmplData :: Wasp.Operation.Operation -> Aeson.Value +operationTmplData :: AS.Operation.Operation -> Aeson.Value operationTmplData operation = object [ "jsFnImportStatement" .= importStmt, "jsFnIdentifier" .= importIdentifier, - "entities" .= maybe [] (map buildEntityData) (Wasp.Operation.getEntities operation) + "entities" .= maybe [] (map (buildEntityData . AS.refName)) (AS.Operation.getEntities operation) ] where (importIdentifier, importStmt) = - getImportDetailsForJsFnImport relPosixPathFromOperationFileToExtSrcDir $ - Wasp.Operation.getJsFn operation + getJsImportDetailsForExtFnImport relPosixPathFromOperationFileToExtSrcDir $ + AS.Operation.getFn operation buildEntityData :: String -> Aeson.Value buildEntityData entityName = object diff --git a/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs b/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs index 5c2614705..932de8ff9 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.ServerGenerator.OperationsRoutesG ( genOperationsRoutes, operationRouteInOperationsRouter, @@ -9,54 +11,55 @@ import qualified Data.Aeson as Aeson import Data.Maybe (fromJust, fromMaybe, isJust) import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, ()) 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 qualified Wasp.Generator.ServerGenerator.Common as C import Wasp.Generator.ServerGenerator.OperationsG (operationFileInSrcDir) 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 wasp = +genOperationsRoutes :: AppSpec -> [FileDraft] +genOperationsRoutes spec = concat - [ map (genActionRoute wasp) (Wasp.getActions wasp), - map (genQueryRoute wasp) (Wasp.getQueries wasp), - [genOperationsRouter wasp] + [ map (genActionRoute spec) (AS.getActions spec), + map (genQueryRoute spec) (AS.getQueries spec), + [genOperationsRouter spec] ] -genActionRoute :: Wasp -> Wasp.Action.Action -> FileDraft -genActionRoute wasp action = genOperationRoute wasp op tmplFile +genActionRoute :: AppSpec -> (String, AS.Action.Action) -> FileDraft +genActionRoute spec (actionName, action) = genOperationRoute spec op tmplFile where - op = Wasp.Operation.ActionOp action + op = AS.Operation.ActionOp actionName action tmplFile = C.asTmplFile [relfile|src/routes/operations/_action.js|] -genQueryRoute :: Wasp -> Wasp.Query.Query -> FileDraft -genQueryRoute wasp query = genOperationRoute wasp op tmplFile +genQueryRoute :: AppSpec -> (String, AS.Query.Query) -> FileDraft +genQueryRoute spec (queryName, query) = genOperationRoute spec op tmplFile where - op = Wasp.Operation.QueryOp query + op = AS.Operation.QueryOp queryName query tmplFile = C.asTmplFile [relfile|src/routes/operations/_query.js|] -genOperationRoute :: Wasp -> Wasp.Operation.Operation -> Path' (Rel C.ServerTemplatesDir) File' -> FileDraft -genOperationRoute wasp operation tmplFile = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genOperationRoute :: AppSpec -> AS.Operation.Operation -> Path' (Rel C.ServerTemplatesDir) File' -> FileDraft +genOperationRoute spec operation tmplFile = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where dstFile = operationsRoutesDirInServerRootDir operationRouteFileInOperationsRoutesDir operation baseTmplData = object - [ "operationImportPath" .= operationImportPath, - "operationName" .= Wasp.Operation.getName operation + [ "operationImportPath" .= (operationImportPath :: FilePath), + "operationName" .= (AS.Operation.getName operation :: String) ] - tmplData = case Wasp.getAuth wasp of + tmplData = case AS.App.auth (snd $ getApp spec) of Nothing -> baseTmplData Just auth -> U.jsonSet "userEntityLower" - (Aeson.toJSON (U.toLowerFirst $ Wasp.Auth._userEntity auth)) + (Aeson.toJSON (U.toLowerFirst $ AS.refName $ AS.Auth.userEntity auth)) baseTmplData operationImportPath = @@ -72,40 +75,45 @@ operationsRoutesDirInServerSrcDir = [reldir|routes/operations/|] operationsRoutesDirInServerRootDir :: Path' (Rel C.ServerRootDir) (Dir OperationsRoutesDir) operationsRoutesDirInServerRootDir = C.serverSrcDirInServerRootDir operationsRoutesDirInServerSrcDir -operationRouteFileInOperationsRoutesDir :: Wasp.Operation.Operation -> Path' (Rel OperationsRoutesDir) File' -operationRouteFileInOperationsRoutesDir operation = fromJust $ SP.parseRelFile $ Wasp.Operation.getName operation ++ ".js" +operationRouteFileInOperationsRoutesDir :: AS.Operation.Operation -> Path' (Rel OperationsRoutesDir) File' +operationRouteFileInOperationsRoutesDir operation = fromJust $ SP.parseRelFile $ AS.Operation.getName operation ++ ".js" relPosixPathFromOperationsRoutesDirToSrcDir :: Path Posix (Rel OperationsRoutesDir) (Dir C.ServerSrcDir) relPosixPathFromOperationsRoutesDirToSrcDir = [reldirP|../..|] -genOperationsRouter :: Wasp -> FileDraft -genOperationsRouter wasp +genOperationsRouter :: AppSpec -> FileDraft +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. | 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 tmplFile = C.asTmplFile [relfile|src/routes/operations/index.js|] dstFile = operationsRoutesDirInServerRootDir [relfile|index.js|] operations = - map Wasp.Operation.ActionOp (Wasp.getActions wasp) - ++ map Wasp.Operation.QueryOp (Wasp.getQueries wasp) + map (uncurry AS.Operation.ActionOp) (AS.getActions spec) + ++ map (uncurry AS.Operation.QueryOp) (AS.getQueries spec) tmplData = object [ "operationRoutes" .= map makeOperationRoute operations, "isAuthEnabled" .= isAuthEnabledGlobally ] makeOperationRoute operation = - let operationName = Wasp.Operation.getName operation + let operationName = AS.Operation.getName operation in object [ "importIdentifier" .= operationName, - "importPath" .= ("./" ++ SP.fromRelFileP (fromJust $ SP.relFileToPosix $ operationRouteFileInOperationsRoutesDir operation)), + "importPath" + .= ( "./" + ++ SP.fromRelFileP + ( fromJust $ SP.relFileToPosix $ operationRouteFileInOperationsRoutesDir operation + ) + ), "routePath" .= ("/" ++ operationRouteInOperationsRouter operation), "isUsingAuth" .= isAuthEnabledForOperation operation ] - isAuthEnabledGlobally = isJust $ getAuth wasp - isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (Wasp.Operation.getAuth operation) - isAuthSpecifiedForOperation operation = isJust $ Wasp.Operation.getAuth operation + isAuthEnabledGlobally = AS.isAuthEnabled spec + isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (AS.Operation.getAuth operation) + isAuthSpecifiedForOperation operation = isJust $ AS.Operation.getAuth operation -operationRouteInOperationsRouter :: Wasp.Operation.Operation -> String -operationRouteInOperationsRouter = U.camelToKebabCase . Wasp.Operation.getName +operationRouteInOperationsRouter :: AS.Operation.Operation -> String +operationRouteInOperationsRouter = U.camelToKebabCase . AS.Operation.getName diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index f1356dfb6..615d3328c 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -4,12 +4,9 @@ module Wasp.Generator.WebAppGenerator ) where -import Data.Aeson - ( ToJSON (..), - object, - (.=), - ) +import Data.Aeson (object, (.=)) import Data.List (intercalate) +import Data.Maybe (fromMaybe) import StrongPath ( Dir, Path', @@ -18,7 +15,10 @@ import StrongPath 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.FileDraft 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 Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations) 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 wasp _ = +generateWebApp :: AppSpec -> [FileDraft] +generateWebApp spec = concat - [ [generateReadme wasp], - [genPackageJson wasp waspNpmDeps], - [generateGitignore wasp], - generatePublicDir wasp, - generateSrcDir wasp, - generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy wasp, - [C.makeSimpleTemplateFD (asTmplFile [relfile|netlify.toml|]) wasp] + [ [generateReadme], + [genPackageJson spec waspNpmDeps], + [generateGitignore], + generatePublicDir spec, + generateSrcDir spec, + generateExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy (AS.externalCodeFiles spec), + [C.mkTmplFd $ asTmplFile [relfile|netlify.toml|]] ] -generateReadme :: Wasp -> FileDraft -generateReadme wasp = C.makeSimpleTemplateFD (asTmplFile [relfile|README.md|]) wasp +generateReadme :: FileDraft +generateReadme = C.mkTmplFd $ asTmplFile [relfile|README.md|] -genPackageJson :: Wasp -> [ND.NpmDependency] -> FileDraft -genPackageJson wasp waspDeps = - C.makeTemplateFD +genPackageJson :: AppSpec -> [AS.Dependency.Dependency] -> FileDraft +genPackageJson spec waspDeps = + C.mkTmplFdWithDstAndData (C.asTmplFile [relfile|package.json|]) (C.asWebAppFile [relfile|package.json|]) ( Just $ object - [ "wasp" .= wasp, + [ "appName" .= (fst (getApp spec) :: String), "depsChunk" .= npmDepsToPackageJsonEntry (resolvedWaspDeps ++ resolvedUserDeps) ] ) @@ -72,12 +68,12 @@ genPackageJson wasp waspDeps = Right deps -> deps Left depsAndErrors -> error $ intercalate " ; " $ map snd depsAndErrors - userDeps :: [ND.NpmDependency] - userDeps = WND._dependencies $ Wasp.Wasp.getNpmDependencies wasp + userDeps :: [AS.Dependency.Dependency] + userDeps = fromMaybe [] $ AS.App.dependencies $ snd $ getApp spec -waspNpmDeps :: [ND.NpmDependency] +waspNpmDeps :: [AS.Dependency.Dependency] waspNpmDeps = - ND.fromList + AS.Dependency.fromList [ ("axios", "^0.21.1"), ("lodash", "^4.17.15"), ("react", "^16.12.0"), @@ -90,25 +86,26 @@ waspNpmDeps = -- TODO: Also extract devDependencies like we did dependencies (waspNpmDeps). -generateGitignore :: Wasp -> FileDraft -generateGitignore wasp = - C.makeTemplateFD +generateGitignore :: FileDraft +generateGitignore = + C.mkTmplFdWithDst (asTmplFile [relfile|gitignore|]) (asWebAppFile [relfile|.gitignore|]) - (Just $ toJSON wasp) -generatePublicDir :: Wasp -> [FileDraft] -generatePublicDir wasp = - C.copyTmplAsIs (asTmplFile [relfile|public/favicon.ico|]) : - generatePublicIndexHtml wasp : - map - (\path -> C.makeSimpleTemplateFD (asTmplFile $ [reldir|public|] path) wasp) - [ [relfile|manifest.json|] - ] +generatePublicDir :: AppSpec -> [FileDraft] +generatePublicDir spec = + C.mkTmplFd (asTmplFile [relfile|public/favicon.ico|]) : + generatePublicIndexHtml spec : + ( let tmplData = object ["appName" .= (fst (getApp spec) :: String)] + processPublicTmpl path = C.mkTmplFdWithData (asTmplFile $ [reldir|public|] path) tmplData + in processPublicTmpl + <$> [ [relfile|manifest.json|] + ] + ) -generatePublicIndexHtml :: Wasp -> FileDraft -generatePublicIndexHtml wasp = - C.makeTemplateFD +generatePublicIndexHtml :: AppSpec -> FileDraft +generatePublicIndexHtml spec = + C.mkTmplFdWithDstAndData (asTmplFile [relfile|public/index.html|]) targetPath (Just templateData) @@ -116,8 +113,8 @@ generatePublicIndexHtml wasp = targetPath = [relfile|public/index.html|] templateData = object - [ "title" .= Wasp.App.appTitle (getApp wasp), - "head" .= maybe "" (intercalate "\n") (Wasp.App.appHead $ getApp wasp) + [ "title" .= (AS.App.title (snd $ getApp spec) :: String), + "head" .= (maybe "" (intercalate "\n") (AS.App.head $ snd $ getApp spec) :: String) ] -- * Src dir @@ -132,15 +129,15 @@ srcDir = C.webAppSrcDirInWebAppRootDir -- | Generates api.js file which contains token management and configured api (e.g. axios) instance. genApi :: FileDraft -genApi = C.copyTmplAsIs (C.asTmplFile [relfile|src/api.js|]) +genApi = C.mkTmplFd (C.asTmplFile [relfile|src/api.js|]) -generateSrcDir :: Wasp -> [FileDraft] -generateSrcDir wasp = +generateSrcDir :: AppSpec -> [FileDraft] +generateSrcDir spec = generateLogo : - RouterGenerator.generateRouter wasp : + RouterGenerator.generateRouter spec : genApi : map - makeSimpleSrcTemplateFD + processSrcTmpl [ [relfile|index.js|], [relfile|index.css|], [relfile|serviceWorker.js|], @@ -148,16 +145,15 @@ generateSrcDir wasp = [relfile|queryCache.js|], [relfile|utils.js|] ] - ++ genOperations wasp - ++ AuthG.genAuth wasp + ++ genOperations spec + ++ AuthG.genAuth spec where generateLogo = - C.makeTemplateFD + C.mkTmplFdWithDstAndData (asTmplFile [relfile|src/logo.png|]) (srcDir asWebAppSrcFile [relfile|logo.png|]) Nothing - makeSimpleSrcTemplateFD path = - C.makeTemplateFD + processSrcTmpl path = + C.mkTmplFdWithDst (asTmplFile $ [reldir|src|] path) (srcDir asWebAppSrcFile path) - (Just $ toJSON wasp) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs index 50613ddc0..d7686452e 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs @@ -5,14 +5,17 @@ where import Data.Aeson (object, (.=)) import Data.Aeson.Types (Pair) +import Data.Maybe (fromMaybe) 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.WebAppGenerator.Common as C -import Wasp.Wasp (Wasp, getAuth) -import qualified Wasp.Wasp.Auth as Wasp.Auth -genAuth :: Wasp -> [FileDraft] -genAuth wasp = case maybeAuth of +genAuth :: AppSpec -> [FileDraft] +genAuth spec = case maybeAuth of Just auth -> [ genSignup, genLogin, @@ -23,54 +26,56 @@ genAuth wasp = case maybeAuth of ++ genAuthForms auth Nothing -> [] where - maybeAuth = getAuth wasp + maybeAuth = AS.App.auth $ snd $ AS.getApp spec -- | Generates file with signup function to be used by Wasp developer. 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. 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. 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. -genCreateAuthRequiredPage :: Wasp.Auth.Auth -> FileDraft +genCreateAuthRequiredPage :: AS.Auth.Auth -> FileDraft genCreateAuthRequiredPage auth = compileTmplToSamePath [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 -- access to the currently logged in user (and check whether user is logged in -- ot not). 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 = [ genLoginForm auth, genSignupForm auth ] -genLoginForm :: Wasp.Auth.Auth -> FileDraft +genLoginForm :: AS.Auth.Auth -> FileDraft 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 [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 = compileTmplToSamePath [relfile|auth/forms/Signup.js|] - ["onAuthSucceededRedirectTo" .= Wasp.Auth._onAuthSucceededRedirectTo auth] + ["onAuthSucceededRedirectTo" .= fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)] compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> FileDraft compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs = - C.makeTemplateFD + C.mkTmplFdWithDstAndData (asTmplFile $ [reldir|src|] tmplFileInTmplSrcDir) targetPath (Just templateData) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs index 88742946a..758bb967f 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Common.hs @@ -1,9 +1,10 @@ module Wasp.Generator.WebAppGenerator.Common ( webAppRootDirInProjectRootDir, webAppSrcDirInWebAppRootDir, - copyTmplAsIs, - makeSimpleTemplateFD, - makeTemplateFD, + mkTmplFd, + mkTmplFdWithDst, + mkTmplFdWithData, + mkTmplFdWithDstAndData, webAppSrcDirInProjectRootDir, webAppTemplatesDirInTemplatesDir, asTmplFile, @@ -21,7 +22,6 @@ import qualified StrongPath as SP import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft) import Wasp.Generator.Templates (TemplatesDir) -import Wasp.Wasp (Wasp) data WebAppRootDir @@ -57,18 +57,21 @@ webAppSrcDirInProjectRootDir = webAppRootDirInProjectRootDir webAppSrcDirInW webAppTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir WebAppTemplatesDir) webAppTemplatesDirInTemplatesDir = [reldir|react-app|] -copyTmplAsIs :: Path' (Rel WebAppTemplatesDir) File' -> FileDraft -copyTmplAsIs path = makeTemplateFD path (SP.castRel path) Nothing +mkTmplFd :: Path' (Rel WebAppTemplatesDir) File' -> FileDraft +mkTmplFd path = mkTmplFdWithDst path (SP.castRel path) -makeSimpleTemplateFD :: Path' (Rel WebAppTemplatesDir) File' -> Wasp -> FileDraft -makeSimpleTemplateFD path wasp = makeTemplateFD path (SP.castRel path) (Just $ Aeson.toJSON wasp) +mkTmplFdWithDst :: Path' (Rel WebAppTemplatesDir) File' -> Path' (Rel WebAppRootDir) File' -> FileDraft +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 WebAppRootDir) File' -> Maybe Aeson.Value -> FileDraft -makeTemplateFD srcPathInWebAppTemplatesDir dstPathInWebAppRootDir tmplData = +mkTmplFdWithDstAndData srcPathInWebAppTemplatesDir dstPathInWebAppRootDir tmplData = createTemplateFileDraft (webAppRootDirInProjectRootDir dstPathInWebAppRootDir) (webAppTemplatesDirInTemplatesDir srcPathInWebAppTemplatesDir) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs index b36846a33..2befa149e 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.WebAppGenerator.OperationsGenerator ( genOperations, ) @@ -10,44 +12,44 @@ import Data.Aeson import Data.List (intercalate) import Data.Maybe (fromJust) 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 qualified Wasp.Generator.ServerGenerator as ServerGenerator import qualified Wasp.Generator.ServerGenerator.OperationsRoutesG as ServerOperationsRoutesG import qualified Wasp.Generator.WebAppGenerator.Common as C 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 wasp = +genOperations :: AppSpec -> [FileDraft] +genOperations spec = concat - [ genQueries wasp, - genActions wasp, - [C.makeSimpleTemplateFD (C.asTmplFile [relfile|src/operations/index.js|]) wasp], - Resources.genResources wasp + [ genQueries spec, + genActions spec, + [C.mkTmplFd $ C.asTmplFile [relfile|src/operations/index.js|]], + Resources.genResources spec ] -genQueries :: Wasp -> [FileDraft] -genQueries wasp = - map (genQuery wasp) (Wasp.getQueries wasp) - ++ [C.makeSimpleTemplateFD (C.asTmplFile [relfile|src/queries/index.js|]) wasp] +genQueries :: AppSpec -> [FileDraft] +genQueries spec = + map (genQuery spec) (AS.getQueries spec) + ++ [C.mkTmplFd $ C.asTmplFile [relfile|src/queries/index.js|]] -genActions :: Wasp -> [FileDraft] -genActions wasp = - map (genAction wasp) (Wasp.getActions wasp) +genActions :: AppSpec -> [FileDraft] +genActions spec = + map (genAction spec) (AS.getActions spec) -genQuery :: Wasp -> Wasp.Query.Query -> FileDraft -genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genQuery :: AppSpec -> (String, AS.Query.Query) -> FileDraft +genQuery _ (queryName, query) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where tmplFile = C.asTmplFile [relfile|src/queries/_query.js|] dstFile = C.asWebAppFile $ [reldir|src/queries/|] fromJust (getOperationDstFileName operation) tmplData = object - [ "queryFnName" .= Wasp.Query._name query, + [ "queryFnName" .= (queryName :: String), "queryRoute" .= ( ServerGenerator.operationsRouteInRootRouter ++ "/" @@ -55,17 +57,17 @@ genQuery _ query = C.makeTemplateFD tmplFile dstFile (Just tmplData) ), "entitiesArray" .= makeJsArrayOfEntityNames operation ] - operation = Wasp.Operation.QueryOp query + operation = AS.Operation.QueryOp queryName query -genAction :: Wasp -> Wasp.Action.Action -> FileDraft -genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData) +genAction :: AppSpec -> (String, AS.Action.Action) -> FileDraft +genAction _ (actionName, action) = C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) where tmplFile = C.asTmplFile [relfile|src/actions/_action.js|] dstFile = C.asWebAppFile $ [reldir|src/actions/|] fromJust (getOperationDstFileName operation) tmplData = object - [ "actionFnName" .= Wasp.Action._name action, + [ "actionFnName" .= (actionName :: String), "actionRoute" .= ( ServerGenerator.operationsRouteInRootRouter ++ "/" @@ -73,14 +75,14 @@ genAction _ action = C.makeTemplateFD tmplFile dstFile (Just tmplData) ), "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. -- E.g. "['Task', 'Project']" -makeJsArrayOfEntityNames :: Wasp.Operation.Operation -> String +makeJsArrayOfEntityNames :: AS.Operation.Operation -> String makeJsArrayOfEntityNames operation = "[" ++ intercalate ", " entityStrings ++ "]" 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 operation = parseRelFile (Wasp.Operation.getName operation ++ ".js") +getOperationDstFileName :: AS.Operation.Operation -> Maybe (Path' Rel' File') +getOperationDstFileName operation = parseRelFile (AS.Operation.getName operation ++ ".js") diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator/ResourcesG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator/ResourcesG.hs index 3b5ac6706..3fd4bd190 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator/ResourcesG.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/OperationsGenerator/ResourcesG.hs @@ -5,12 +5,12 @@ where import Data.Aeson (object) import StrongPath (relfile) +import Wasp.AppSpec (AppSpec) import Wasp.Generator.FileDraft (FileDraft) import qualified Wasp.Generator.WebAppGenerator.Common as C -import Wasp.Wasp (Wasp) -genResources :: Wasp -> [FileDraft] -genResources _ = [C.makeTemplateFD tmplFile dstFile (Just tmplData)] +genResources :: AppSpec -> [FileDraft] +genResources _ = [C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)] where 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. diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs index 957980cec..72c9b8ad1 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module Wasp.Generator.WebAppGenerator.RouterGenerator ( generateRouter, ) @@ -5,17 +7,18 @@ where import Data.Aeson (ToJSON (..), object, (.=)) import Data.List (find) -import Data.Maybe (fromJust, fromMaybe, isJust) +import Data.Maybe (fromMaybe) import StrongPath (reldir, relfile, ()) 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.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile) 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 { _routes :: ![RouteTemplateData], @@ -56,47 +59,52 @@ instance ToJSON PageTemplateData where "importFrom" .= _importFrom pageTD ] -generateRouter :: Wasp -> FileDraft -generateRouter wasp = - C.makeTemplateFD +generateRouter :: AppSpec -> FileDraft +generateRouter spec = + C.mkTmplFdWithDstAndData (asTmplFile $ [reldir|src|] routerPath) targetPath (Just $ toJSON templateData) where routerPath = [relfile|router.js|] - templateData = createRouterTemplateData wasp + templateData = createRouterTemplateData spec targetPath = C.webAppSrcDirInWebAppRootDir asWebAppSrcFile routerPath -createRouterTemplateData :: Wasp -> RouterTemplateData -createRouterTemplateData wasp = +createRouterTemplateData :: AppSpec -> RouterTemplateData +createRouterTemplateData spec = RouterTemplateData { _routes = routes, _pagesToImport = pages, - _isAuthEnabled = isJust $ Wasp.getAuth wasp + _isAuthEnabled = AS.isAuthEnabled spec } where - routes = map (createRouteTemplateData wasp) $ Wasp.getRoutes wasp - pages = map createPageTemplateData $ Wasp.getPages wasp + routes = map (createRouteTemplateData spec) $ AS.getRoutes spec + pages = map createPageTemplateData $ AS.getPages spec -createRouteTemplateData :: Wasp -> Wasp.Route.Route -> RouteTemplateData -createRouteTemplateData wasp route = +createRouteTemplateData :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData +createRouteTemplateData spec namedRoute@(_, route) = RouteTemplateData - { _urlPath = Wasp.Route._urlPath route, - _targetComponent = determineRouteTargetComponent wasp route + { _urlPath = AS.Route.path route, + _targetComponent = determineRouteTargetComponent spec namedRoute } -determineRouteTargetComponent :: Wasp -> Wasp.Route.Route -> String -determineRouteTargetComponent wasp route = +determineRouteTargetComponent :: AppSpec -> (String, AS.Route.Route) -> String +determineRouteTargetComponent spec (_, route) = maybe targetPageName determineRouteTargetComponent' - (Wasp.Page._authRequired targetPage) + (AS.Page.authRequired $ snd targetPage) where - targetPageName = Wasp.Route._targetPage route + targetPageName = AS.refName (AS.Route.to route :: AS.Ref AS.Page.Page) targetPage = fromMaybe - (error $ "Can't find page with name '" ++ targetPageName ++ "', pointed to by route '" ++ Wasp.Route._urlPath route ++ "'") - (find ((==) targetPageName . Wasp.Page._name) (Wasp.getPages wasp)) + ( error $ + "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' authRequired = @@ -105,21 +113,20 @@ determineRouteTargetComponent wasp route = "createAuthRequiredPage(" ++ targetPageName ++ ")" else targetPageName -createPageTemplateData :: Wasp.Page.Page -> PageTemplateData +createPageTemplateData :: (String, AS.Page.Page) -> PageTemplateData createPageTemplateData page = PageTemplateData - { _importFrom = - relPathToExtSrcDir - ++ SP.fromRelFileP (fromJust $ SP.relFileToPosix $ Wasp.JsImport._from pageComponent), - _importWhat = case Wasp.JsImport._namedImports pageComponent of - -- 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." + { _importFrom = relPathToExtSrcDir FP. SP.fromRelFileP (AS.ExtImport.path pageComponent), + _importWhat = case AS.ExtImport.name pageComponent of + AS.ExtImport.ExtImportModule _ -> pageName + AS.ExtImport.ExtImportField identifier -> "{ " ++ identifier ++ " as " ++ pageName ++ " }" } where relPathToExtSrcDir :: FilePath relPathToExtSrcDir = "./ext-src/" - pageName = Wasp.Page._name page - pageComponent = Wasp.Page._component page + pageName :: String + pageName = fst page + + pageComponent :: AS.ExtImport.ExtImport + pageComponent = AS.Page.component $ snd page diff --git a/waspc/src/Wasp/Lib.hs b/waspc/src/Wasp/Lib.hs index 70fd90a9a..38b64a8e7 100644 --- a/waspc/src/Wasp/Lib.hs +++ b/waspc/src/Wasp/Lib.hs @@ -11,16 +11,17 @@ import Data.List (find, isSuffixOf) import StrongPath (Abs, Dir, File', Path', relfile) import qualified StrongPath as SP 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.CompileOptions (CompileOptions) import qualified Wasp.CompileOptions as CompileOptions +import Wasp.Error (showCompilerErrorForTerminal) import qualified Wasp.ExternalCode as ExternalCode import qualified Wasp.Generator as Generator import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Parser as Parser import qualified Wasp.Util.IO as Util.IO -import Wasp.Wasp (Wasp) -import qualified Wasp.Wasp as Wasp type CompileError = String @@ -30,34 +31,33 @@ compile :: CompileOptions -> IO (Either CompileError ()) compile waspDir outDir options = do - maybeWaspFile <- findWaspFile waspDir - case maybeWaspFile of + maybeWaspFilePath <- findWaspFile waspDir + case maybeWaspFilePath of Nothing -> return $ Left "Couldn't find a single *.wasp file." - Just waspFile -> do - waspStr <- readFile (SP.toFilePath waspFile) - - case Parser.parseWasp waspStr of - Left err -> return $ Left (show err) - Right wasp -> do + Just waspFilePath -> do + waspFileContent <- readFile (SP.fromAbsFile waspFilePath) + case Analyzer.analyze waspFileContent of + Left analyzeError -> + return $ + Left $ + showCompilerErrorForTerminal + (waspFilePath, waspFileContent) + (getErrorMessageAndCtx analyzeError) + Right decls -> do + externalCodeFiles <- + ExternalCode.readFiles (CompileOptions.externalCodeDirPath options) maybeDotEnvFile <- findDotEnvFile waspDir maybeMigrationsDir <- findMigrationsDir waspDir - ( wasp - `Wasp.setDotEnvFile` maybeDotEnvFile - `Wasp.setMigrationsDir` maybeMigrationsDir - `enrichWaspASTBasedOnCompileOptions` options - ) - >>= generateCode - where - generateCode wasp = Generator.writeWebAppCode wasp outDir options >> return (Right ()) - -enrichWaspASTBasedOnCompileOptions :: Wasp -> CompileOptions -> IO Wasp -enrichWaspASTBasedOnCompileOptions wasp options = do - externalCodeFiles <- ExternalCode.readFiles (CompileOptions.externalCodeDirPath options) - return - ( wasp - `Wasp.setExternalCodeFiles` externalCodeFiles - `Wasp.setIsBuild` CompileOptions.isBuild options - ) + let appSpec = + AS.AppSpec + { AS.decls = decls, + AS.externalCodeFiles = externalCodeFiles, + AS.externalCodeDirPath = CompileOptions.externalCodeDirPath options, + AS.migrationsDir = maybeMigrationsDir, + AS.dotEnvFile = maybeDotEnvFile, + AS.isBuild = CompileOptions.isBuild options + } + Right <$> Generator.writeWebAppCode appSpec outDir findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File')) findWaspFile waspDir = do @@ -74,7 +74,9 @@ findDotEnvFile waspDir = do dotEnvExists <- doesFileExist (SP.toFilePath dotEnvAbsPath) 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 let migrationsAbsPath = waspDir SP. dbMigrationsDirInWaspProjectDir migrationsExists <- doesDirectoryExist $ SP.fromAbsDir migrationsAbsPath diff --git a/waspc/src/Wasp/Parser.hs b/waspc/src/Wasp/Parser.hs deleted file mode 100644 index 95badd146..000000000 --- a/waspc/src/Wasp/Parser.hs +++ /dev/null @@ -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 diff --git a/waspc/src/Wasp/Parser/Action.hs b/waspc/src/Wasp/Parser/Action.hs deleted file mode 100644 index 025faf48a..000000000 --- a/waspc/src/Wasp/Parser/Action.hs +++ /dev/null @@ -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 - } diff --git a/waspc/src/Wasp/Parser/App.hs b/waspc/src/Wasp/Parser/App.hs deleted file mode 100644 index a011729be..000000000 --- a/waspc/src/Wasp/Parser/App.hs +++ /dev/null @@ -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. - } diff --git a/waspc/src/Wasp/Parser/Auth.hs b/waspc/src/Wasp/Parser/Auth.hs deleted file mode 100644 index e9b5154af..000000000 --- a/waspc/src/Wasp/Parser/Auth.hs +++ /dev/null @@ -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 diff --git a/waspc/src/Wasp/Parser/Common.hs b/waspc/src/Wasp/Parser/Common.hs deleted file mode 100644 index 328b311aa..000000000 --- a/waspc/src/Wasp/Parser/Common.hs +++ /dev/null @@ -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 ..." or "action ..." -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) diff --git a/waspc/src/Wasp/Parser/Db.hs b/waspc/src/Wasp/Parser/Db.hs deleted file mode 100644 index 2d2bf179b..000000000 --- a/waspc/src/Wasp/Parser/Db.hs +++ /dev/null @@ -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) diff --git a/waspc/src/Wasp/Parser/Entity.hs b/waspc/src/Wasp/Parser/Entity.hs deleted file mode 100644 index e46ef2e3b..000000000 --- a/waspc/src/Wasp/Parser/Entity.hs +++ /dev/null @@ -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 diff --git a/waspc/src/Wasp/Parser/ExternalCode.hs b/waspc/src/Wasp/Parser/ExternalCode.hs deleted file mode 100644 index ddba55015..000000000 --- a/waspc/src/Wasp/Parser/ExternalCode.hs +++ /dev/null @@ -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)) diff --git a/waspc/src/Wasp/Parser/JsCode.hs b/waspc/src/Wasp/Parser/JsCode.hs deleted file mode 100644 index d18defa5e..000000000 --- a/waspc/src/Wasp/Parser/JsCode.hs +++ /dev/null @@ -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" diff --git a/waspc/src/Wasp/Parser/JsImport.hs b/waspc/src/Wasp/Parser/JsImport.hs deleted file mode 100644 index 56535aaf5..000000000 --- a/waspc/src/Wasp/Parser/JsImport.hs +++ /dev/null @@ -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 from "@ext/..." --- import { } 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 - } diff --git a/waspc/src/Wasp/Parser/NpmDependencies.hs b/waspc/src/Wasp/Parser/NpmDependencies.hs deleted file mode 100644 index 47f88af6a..000000000 --- a/waspc/src/Wasp/Parser/NpmDependencies.hs +++ /dev/null @@ -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} diff --git a/waspc/src/Wasp/Parser/Operation.hs b/waspc/src/Wasp/Parser/Operation.hs deleted file mode 100644 index fe2979d69..000000000 --- a/waspc/src/Wasp/Parser/Operation.hs +++ /dev/null @@ -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] diff --git a/waspc/src/Wasp/Parser/Page.hs b/waspc/src/Wasp/Parser/Page.hs deleted file mode 100644 index 2432c2735..000000000 --- a/waspc/src/Wasp/Parser/Page.hs +++ /dev/null @@ -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 - } diff --git a/waspc/src/Wasp/Parser/Query.hs b/waspc/src/Wasp/Parser/Query.hs deleted file mode 100644 index 952422abd..000000000 --- a/waspc/src/Wasp/Parser/Query.hs +++ /dev/null @@ -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 - } diff --git a/waspc/src/Wasp/Parser/Route.hs b/waspc/src/Wasp/Parser/Route.hs deleted file mode 100644 index a495af94a..000000000 --- a/waspc/src/Wasp/Parser/Route.hs +++ /dev/null @@ -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 - } diff --git a/waspc/src/Wasp/Parser/Server.hs b/waspc/src/Wasp/Parser/Server.hs deleted file mode 100644 index eb96429b3..000000000 --- a/waspc/src/Wasp/Parser/Server.hs +++ /dev/null @@ -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] diff --git a/waspc/src/Wasp/Parser/Style.hs b/waspc/src/Wasp/Parser/Style.hs deleted file mode 100644 index 5511c8ada..000000000 --- a/waspc/src/Wasp/Parser/Style.hs +++ /dev/null @@ -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" diff --git a/waspc/src/Wasp/Util.hs b/waspc/src/Wasp/Util.hs index 659c002e7..b98ea0be6 100644 --- a/waspc/src/Wasp/Util.hs +++ b/waspc/src/Wasp/Util.hs @@ -8,6 +8,8 @@ module Wasp.Util indent, concatShortPrefixAndText, concatPrefixAndText, + insertAt, + leftPad, ) where @@ -102,3 +104,17 @@ concatShortPrefixAndText prefix text = concatPrefixAndText :: String -> String -> String concatPrefixAndText prefix 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 diff --git a/waspc/src/Wasp/Util/Terminal.hs b/waspc/src/Wasp/Util/Terminal.hs index d78f98d52..8373d522d 100644 --- a/waspc/src/Wasp/Util/Terminal.hs +++ b/waspc/src/Wasp/Util/Terminal.hs @@ -1,6 +1,9 @@ module Wasp.Util.Terminal ( Style (..), applyStyles, + styleCode, + escapeCode, + resetCode, ) where diff --git a/waspc/src/Wasp/Wasp.hs b/waspc/src/Wasp/Wasp.hs deleted file mode 100644 index b13a5f56f..000000000 --- a/waspc/src/Wasp/Wasp.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Action.hs b/waspc/src/Wasp/Wasp/Action.hs deleted file mode 100644 index 5616e49ad..000000000 --- a/waspc/src/Wasp/Wasp/Action.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/App.hs b/waspc/src/Wasp/Wasp/App.hs deleted file mode 100644 index 6e16fcd7b..000000000 --- a/waspc/src/Wasp/Wasp/App.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Auth.hs b/waspc/src/Wasp/Wasp/Auth.hs deleted file mode 100644 index ffdbd964c..000000000 --- a/waspc/src/Wasp/Wasp/Auth.hs +++ /dev/null @@ -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) diff --git a/waspc/src/Wasp/Wasp/Db.hs b/waspc/src/Wasp/Wasp/Db.hs deleted file mode 100644 index dbf67f18a..000000000 --- a/waspc/src/Wasp/Wasp/Db.hs +++ /dev/null @@ -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) diff --git a/waspc/src/Wasp/Wasp/Entity.hs b/waspc/src/Wasp/Wasp/Entity.hs deleted file mode 100644 index 4d12694f7..000000000 --- a/waspc/src/Wasp/Wasp/Entity.hs +++ /dev/null @@ -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) - ] diff --git a/waspc/src/Wasp/Wasp/JsCode.hs b/waspc/src/Wasp/Wasp/JsCode.hs deleted file mode 100644 index 77ef3e33b..000000000 --- a/waspc/src/Wasp/Wasp/JsCode.hs +++ /dev/null @@ -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 diff --git a/waspc/src/Wasp/Wasp/JsImport.hs b/waspc/src/Wasp/Wasp/JsImport.hs deleted file mode 100644 index cd6d31236..000000000 --- a/waspc/src/Wasp/Wasp/JsImport.hs +++ /dev/null @@ -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 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) - ] diff --git a/waspc/src/Wasp/Wasp/NpmDependencies.hs b/waspc/src/Wasp/Wasp/NpmDependencies.hs deleted file mode 100644 index ede1de43a..000000000 --- a/waspc/src/Wasp/Wasp/NpmDependencies.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Operation.hs b/waspc/src/Wasp/Wasp/Operation.hs deleted file mode 100644 index b491d5bdd..000000000 --- a/waspc/src/Wasp/Wasp/Operation.hs +++ /dev/null @@ -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 diff --git a/waspc/src/Wasp/Wasp/Page.hs b/waspc/src/Wasp/Wasp/Page.hs deleted file mode 100644 index 6d557adbb..000000000 --- a/waspc/src/Wasp/Wasp/Page.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Query.hs b/waspc/src/Wasp/Wasp/Query.hs deleted file mode 100644 index b7b185c8e..000000000 --- a/waspc/src/Wasp/Wasp/Query.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Route.hs b/waspc/src/Wasp/Wasp/Route.hs deleted file mode 100644 index bf67c11ce..000000000 --- a/waspc/src/Wasp/Wasp/Route.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Server.hs b/waspc/src/Wasp/Wasp/Server.hs deleted file mode 100644 index fd009a8ba..000000000 --- a/waspc/src/Wasp/Wasp/Server.hs +++ /dev/null @@ -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 - ] diff --git a/waspc/src/Wasp/Wasp/Style.hs b/waspc/src/Wasp/Wasp/Style.hs deleted file mode 100644 index 6150cc32e..000000000 --- a/waspc/src/Wasp/Wasp/Style.hs +++ /dev/null @@ -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 diff --git a/waspc/stack-snapshot.yaml b/waspc/stack-snapshot.yaml index 95776d683..be9831c4d 100644 --- a/waspc/stack-snapshot.yaml +++ b/waspc/stack-snapshot.yaml @@ -1 +1 @@ -resolver: lts-18.15 \ No newline at end of file +resolver: lts-18.21 diff --git a/waspc/stack.yaml b/waspc/stack.yaml index c268b1c62..0be48a15a 100644 --- a/waspc/stack.yaml +++ b/waspc/stack.yaml @@ -2,8 +2,8 @@ resolver: ./stack-snapshot.yaml packages: - . extra-deps: - - strong-path-1.1.0.0 - - path-0.9.0 + - strong-path-1.1.2.0 + - path-0.9.2 - path-io-1.6.3 # (Martin): I added this per instructions from haskell-language-server, in order to # enable type information and documentation on hover for dependencies. diff --git a/waspc/test/Analyzer/EvaluatorTest.hs b/waspc/test/Analyzer/EvaluatorTest.hs index bf1811622..5248163d5 100644 --- a/waspc/test/Analyzer/EvaluatorTest.hs +++ b/waspc/test/Analyzer/EvaluatorTest.hs @@ -8,6 +8,8 @@ module Analyzer.EvaluatorTest where import Data.Data (Data) import Data.List.Split (splitOn) +import Data.Maybe (fromJust) +import qualified StrongPath as SP import Test.Tasty.Hspec import Text.Read (readMaybe) import Wasp.Analyzer.Evaluator @@ -182,7 +184,7 @@ spec_Evaluator = do let typeDefs = TD.addDeclType @Special $ TD.empty let source = [ "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=}", "}" ] @@ -191,8 +193,8 @@ spec_Evaluator = do `shouldBe` Right [ ( "Test", Special - [ ExtImport (ExtImportField "field") "main.js", - ExtImport (ExtImportModule "main") "main.js" + [ ExtImport (ExtImportField "field") (fromJust $ SP.parseRelFileP "main.js"), + ExtImport (ExtImportModule "main") (fromJust $ SP.parseRelFileP "main.js") ] (JSON " \"key\": 1 ") ) diff --git a/waspc/test/Analyzer/ParserTest.hs b/waspc/test/Analyzer/ParserTest.hs index f5a270eaa..ad7eeb6f8 100644 --- a/waspc/test/Analyzer/ParserTest.hs +++ b/waspc/test/Analyzer/ParserTest.hs @@ -19,14 +19,15 @@ spec_Parser = do " yes: true,", " no: false,", " ident: Wasp,", + " // This is a comment", " innerDict: { innerDictReal: 2.17 }", "}" ] let ast = AST - [ wctx (1, 1) (10, 1) $ + [ wctx (1, 1) (11, 1) $ Decl "test" "Decl" $ - wctx (1, 11) (10, 1) $ + wctx (1, 11) (11, 1) $ Dict [ ("string", wctx (2, 11) (2, 25) $ StringLiteral "Hello Wasp =}"), ("escapedString", wctx (3, 18) (3, 29) $ StringLiteral "Look, a \""), @@ -36,15 +37,32 @@ spec_Parser = do ("no", wctx (7, 7) (7, 11) $ BoolLiteral False), ("ident", wctx (8, 10) (8, 13) $ Var "Wasp"), ( "innerDict", - wctx (9, 14) (9, 36) $ + wctx (10, 14) (10, 36) $ Dict - [ ("innerDictReal", wctx (9, 31) (9, 34) $ DoubleLiteral 2.17) + [ ("innerDictReal", wctx (10, 31) (10, 34) $ DoubleLiteral 2.17) ] ) ] ] 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 let source = unlines diff --git a/waspc/test/AnalyzerTest.hs b/waspc/test/AnalyzerTest.hs index a82cb6527..6d573545b 100644 --- a/waspc/test/AnalyzerTest.hs +++ b/waspc/test/AnalyzerTest.hs @@ -5,6 +5,8 @@ module AnalyzerTest where import Analyzer.TestUtil (ctx) import Data.Either (isRight) import Data.List (intercalate) +import Data.Maybe (fromJust) +import qualified StrongPath as SP import Test.Tasty.Hspec import Wasp.Analyzer import Wasp.Analyzer.Parser (Ctx) @@ -103,7 +105,11 @@ spec_Analyzer = do App.server = Just 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} } @@ -114,13 +120,19 @@ spec_Analyzer = do let expectedPages = [ ( "HomePage", Page.Page - { Page.component = ExtImport (ExtImportModule "Home") "@ext/pages/Main", + { Page.component = + ExtImport + (ExtImportModule "Home") + (fromJust $ SP.parseRelFileP "pages/Main"), Page.authRequired = Nothing } ), ( "ProfilePage", Page.Page - { Page.component = ExtImport (ExtImportField "profilePage") "@ext/pages/Profile", + { Page.component = + ExtImport + (ExtImportField "profilePage") + (fromJust $ SP.parseRelFileP "pages/Profile"), Page.authRequired = Just True } ) @@ -153,7 +165,10 @@ spec_Analyzer = do let expectedQueries = [ ( "getUsers", 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.auth = Nothing } @@ -164,7 +179,10 @@ spec_Analyzer = do let expectedAction = [ ( "updateUser", 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.auth = Just True } diff --git a/waspc/test/ErrorTest.hs b/waspc/test/ErrorTest.hs new file mode 100644 index 000000000..c3fc70349 --- /dev/null +++ b/waspc/test/ErrorTest.hs @@ -0,0 +1,79 @@ +module ErrorTest where + +import Data.List (intercalate) +import Data.Maybe (fromJust) +import Fixtures (systemSPRoot) +import qualified StrongPath as SP +import Test.Tasty.Hspec +import Wasp.Analyzer.Parser.Ctx (ctxFromRgn) +import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..)) +import Wasp.Error +import qualified Wasp.Util.Terminal as T + +spec_WaspError :: Spec +spec_WaspError = do + describe "showCompilerErrorForTerminal" $ do + describe "correctly shows simple error" $ do + let waspFilePath = systemSPRoot SP. fromJust (SP.parseRelFile "waspeteer/aproject/main.wasp") + let waspFileContent = + unlines + [ "app TestApp {", + " server:", + " { db: SQLite", + " },", + " title: \"Test App\"", + "}" + ] + let errMsg = "Whoops: a test error happened!" + it "when error spans multiple lines" $ do + let errCtx = ctxFromRgn (SourcePosition 2 3) (SourcePosition 4 5) + showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) + `shouldBe` intercalate + "\n" + [ SP.fromAbsFile waspFilePath ++ " @ 2:3 - 4:5", + " " ++ errMsg, + "", + " " ++ T.applyStyles [T.Yellow] " 1 | " ++ "app TestApp {", + " " ++ T.applyStyles [T.Yellow] " 2 | " ++ " " ++ T.applyStyles [T.Red] "server:", + " " ++ T.applyStyles [T.Yellow] " 3 | " ++ T.applyStyles [T.Red] " { db: SQLite", + " " ++ T.applyStyles [T.Yellow] " 4 | " ++ T.applyStyles [T.Red] " }" ++ ",", + " " ++ T.applyStyles [T.Yellow] " 5 | " ++ " title: \"Test App\"" + ] + it "when error spans a single line" $ do + let errCtx = ctxFromRgn (SourcePosition 5 10) (SourcePosition 5 19) + showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) + `shouldBe` intercalate + "\n" + [ SP.fromAbsFile waspFilePath ++ " @ 5:10-19", + " " ++ errMsg, + "", + " " ++ T.applyStyles [T.Yellow] " 4 | " ++ " },", + " " ++ T.applyStyles [T.Yellow] " 5 | " ++ " title: " ++ T.applyStyles [T.Red] "\"Test App\"", + " " ++ T.applyStyles [T.Yellow] " 6 | " ++ "}" + ] + it "when error spans a single character" $ do + let errCtx = ctxFromRgn (SourcePosition 3 11) (SourcePosition 3 11) + showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) + `shouldBe` intercalate + "\n" + [ SP.fromAbsFile waspFilePath ++ " @ 3:11", + " " ++ errMsg, + "", + " " ++ T.applyStyles [T.Yellow] " 2 | " ++ " server:", + " " ++ T.applyStyles [T.Yellow] " 3 | " ++ " { db: " ++ T.applyStyles [T.Red] "S" ++ "QLite", + " " ++ T.applyStyles [T.Yellow] " 4 | " ++ " }," + ] + it "when there is no context lines around the line with error" $ do + let waspFileContent' = "app $TestApp { title: \"Test App\" }" + let errCtx = ctxFromRgn (SourcePosition 1 5) (SourcePosition 1 12) + showCompilerErrorForTerminal (waspFilePath, waspFileContent') (errMsg, errCtx) + `shouldBe` intercalate + "\n" + [ SP.fromAbsFile waspFilePath ++ " @ 1:5-12", + " " ++ errMsg, + "", + " " ++ T.applyStyles [T.Yellow] " 1 | " + ++ "app " + ++ T.applyStyles [T.Red] "$TestApp" + ++ " { title: \"Test App\" }" + ] diff --git a/waspc/test/Fixtures.hs b/waspc/test/Fixtures.hs index 578a53793..c1ae5d180 100644 --- a/waspc/test/Fixtures.hs +++ b/waspc/test/Fixtures.hs @@ -4,29 +4,6 @@ import Data.Maybe (fromJust) import qualified Path as P import qualified StrongPath as SP import qualified System.FilePath as FP -import Wasp.Wasp -import qualified Wasp.Wasp.Route as RouteAST - -app :: App -app = - App - { appName = "test_app", - appTitle = "Hello World!", - appHead = Nothing - } - -routeHome :: RouteAST.Route -routeHome = - RouteAST.Route - { RouteAST._urlPath = "/home", - RouteAST._targetPage = "Home" - } - -wasp :: Wasp -wasp = - fromWaspElems - [ WaspElementApp app - ] systemSPRoot :: SP.Path' SP.Abs (SP.Dir d) systemSPRoot = fromJust $ SP.parseAbsDir systemFpRoot diff --git a/waspc/test/Generator/PackageJsonGeneratorTest.hs b/waspc/test/Generator/PackageJsonGeneratorTest.hs index 584a8ddca..754cbfde8 100644 --- a/waspc/test/Generator/PackageJsonGeneratorTest.hs +++ b/waspc/test/Generator/PackageJsonGeneratorTest.hs @@ -1,8 +1,8 @@ module Generator.PackageJsonGeneratorTest where import Test.Tasty.Hspec +import qualified Wasp.AppSpec.App.Dependency as D import Wasp.Generator.PackageJsonGenerator (resolveNpmDeps) -import qualified Wasp.NpmDependency as ND spec_resolveNpmDeps :: Spec spec_resolveNpmDeps = do @@ -16,21 +16,21 @@ spec_resolveNpmDeps = do [ ("foo", "bar"), ("foo2", "bar2") ] - resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps) - `shouldBe` Right (ND.fromList waspDeps, ND.fromList userDeps) + resolveNpmDeps (D.fromList waspDeps) (D.fromList userDeps) + `shouldBe` Right (D.fromList waspDeps, D.fromList userDeps) it "Does not repeat dep if it is both user and wasp dep." $ do let userDeps = [ ("axios", "^0.20.0"), ("foo", "bar") ] - resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps) - `shouldBe` Right (ND.fromList waspDeps, ND.fromList [("foo", "bar")]) + resolveNpmDeps (D.fromList waspDeps) (D.fromList userDeps) + `shouldBe` Right (D.fromList waspDeps, D.fromList [("foo", "bar")]) it "Reports error if user dep version does not match wasp dep version." $ do let userDeps = [ ("axios", "^1.20.0"), ("foo", "bar") ] - let Left conflicts = resolveNpmDeps (ND.fromList waspDeps) (ND.fromList userDeps) - map fst conflicts `shouldBe` ND.fromList [("axios", "^1.20.0")] + let Left conflicts = resolveNpmDeps (D.fromList waspDeps) (D.fromList userDeps) + map fst conflicts `shouldBe` D.fromList [("axios", "^1.20.0")] diff --git a/waspc/test/Generator/WebAppGeneratorTest.hs b/waspc/test/Generator/WebAppGeneratorTest.hs index 9e5c2e25b..7143997f7 100644 --- a/waspc/test/Generator/WebAppGeneratorTest.hs +++ b/waspc/test/Generator/WebAppGeneratorTest.hs @@ -4,7 +4,9 @@ import Fixtures (systemSPRoot) import qualified StrongPath as SP import System.FilePath (()) import Test.Tasty.Hspec -import qualified Wasp.CompileOptions as CompileOptions +import qualified Wasp.AppSpec as AS +import qualified Wasp.AppSpec.App as AS.App +import qualified Wasp.AppSpec.Core.Decl as AS.Decl import Wasp.Generator.FileDraft import qualified Wasp.Generator.FileDraft.CopyDirFileDraft as CopyDirFD import qualified Wasp.Generator.FileDraft.CopyFileDraft as CopyFD @@ -12,27 +14,39 @@ import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD import qualified Wasp.Generator.FileDraft.TextFileDraft as TextFD import Wasp.Generator.WebAppGenerator import qualified Wasp.Generator.WebAppGenerator.Common as Common -import Wasp.Wasp --- TODO(martin): We could define Arbitrary instance for Wasp, define properties over --- generator functions and then do property testing on them, that would be cool. +-- TODO(martin): We could maybe define Arbitrary instance for AppSpec, define properties +-- over generator functions and then do property testing on them, that would be cool. spec_WebAppGenerator :: Spec spec_WebAppGenerator = do - let testApp = App "TestApp" "Test App" Nothing - let testWasp = fromApp testApp - let testCompileOptions = - CompileOptions.CompileOptions - { CompileOptions.externalCodeDirPath = systemSPRoot SP. [SP.reldir|test/src|], - CompileOptions.isBuild = False + let testAppSpec = + AS.AppSpec + { AS.decls = + [ AS.Decl.makeDecl + "TestApp" + AS.App.App + { AS.App.title = "Test App", + AS.App.db = Nothing, + AS.App.server = Nothing, + AS.App.auth = Nothing, + AS.App.dependencies = Nothing, + AS.App.head = Nothing + } + ], + AS.externalCodeDirPath = systemSPRoot SP. [SP.reldir|test/src|], + AS.externalCodeFiles = [], + AS.isBuild = False, + AS.migrationsDir = Nothing, + AS.dotEnvFile = Nothing } describe "generateWebApp" $ do -- NOTE: This test does not (for now) check that content of files is correct or -- that they will successfully be written, it checks only that their -- destinations are correct. - it "Given a simple Wasp, creates file drafts at expected destinations" $ do - let fileDrafts = generateWebApp testWasp testCompileOptions + it "Given a simple AppSpec, creates file drafts at expected destinations" $ do + let fileDrafts = generateWebApp testAppSpec let expectedFileDraftDstPaths = map (SP.toFilePath Common.webAppRootDirInProjectRootDir ) $ concat diff --git a/waspc/test/Parser/ActionTest.hs b/waspc/test/Parser/ActionTest.hs deleted file mode 100644 index 1dfe2dd79..000000000 --- a/waspc/test/Parser/ActionTest.hs +++ /dev/null @@ -1,57 +0,0 @@ -module Parser.ActionTest where - -import Data.Char (toLower) -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Action (action) -import Wasp.Parser.Common (runWaspParser) -import qualified Wasp.Wasp.Action as Wasp.Action -import qualified Wasp.Wasp.JsImport as Wasp.JsImport - --- TODO: This file is mostly just duplication of Parser.QueryTest. --- We might want to look into generalizing the two, to avoid all this --- duplication -> well, we really shoud start with generalizing --- Parser.Query and Parser.Action. - -spec_parseAction :: Spec -spec_parseAction = - describe "Parsing action declaration" $ do - let parseAction = runWaspParser action - let testWhenAuth auth = - it ("When given a valid action declaration, returns correct AST(action.auth = " ++ show auth ++ ")") $ - parseAction (genActionCode auth) `shouldBe` Right (genActionAST auth) - testWhenAuth (Just True) - testWhenAuth (Just False) - testWhenAuth Nothing - it "When given action wasp declaration without 'fn' property, should return Left" $ do - isLeft (parseAction "action myAction { }") `shouldBe` True - where - genActionAST :: Maybe Bool -> Wasp.Action.Action - genActionAST aApplyAuth = - Wasp.Action.Action - { Wasp.Action._name = testActionName, - Wasp.Action._jsFunction = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Nothing, - Wasp.JsImport._namedImports = [testActionJsFunctionName], - Wasp.JsImport._from = testActionJsFunctionFrom - }, - Wasp.Action._entities = Nothing, - Wasp.Action._auth = aApplyAuth - } - genActionCode :: Maybe Bool -> String - genActionCode aApplyAuth = - "action " ++ testActionName ++ " {\n" - ++ " fn: import { " - ++ testActionJsFunctionName - ++ " } from \"@ext/some/path\"" - ++ authStr aApplyAuth - ++ "}" - - authStr :: Maybe Bool -> String - authStr (Just useAuth) = ",\n auth: " ++ map toLower (show useAuth) ++ "\n" - authStr _ = "\n" - testActionJsFunctionFrom = [SP.relfileP|some/path|] - testActionJsFunctionName = "myJsAction" - testActionName = "myAction" diff --git a/waspc/test/Parser/CommonTest.hs b/waspc/test/Parser/CommonTest.hs deleted file mode 100644 index 72fbc2134..000000000 --- a/waspc/test/Parser/CommonTest.hs +++ /dev/null @@ -1,112 +0,0 @@ -module Parser.CommonTest where - -import Data.Either -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Text.Parsec -import Wasp.Lexer -import qualified Wasp.Lexer as L -import Wasp.Parser.Common - -spec_parseWaspCommon :: Spec -spec_parseWaspCommon = do - describe "Parsing wasp element linked to an entity" $ do - it "When given a valid declaration, parses it correctly." $ do - runWaspParser - (waspElementLinkedToEntity "entity-form" (waspClosure whiteSpace)) - "entity-form TaskForm { }" - `shouldBe` Right ("Task", "TaskForm", ()) - - describe "Parsing wasp element name and properties" $ do - let parseWaspElementNameAndClosureContent elemKeyword p input = - runWaspParser (waspElementNameAndClosureContent elemKeyword p) input - - it - "When given valid wasp element declaration along with whitespace parser,\ - \ returns an expected result" - $ do - parseWaspElementNameAndClosureContent "app" whiteSpace "app someApp { }" - `shouldBe` Right ("someApp", ()) - - it - "When given valid wasp element declaration along with char parser, returns\ - \ an expected result" - $ do - parseWaspElementNameAndClosureContent "app" (char 'a') "app someApp {a}" - `shouldBe` Right ("someApp", 'a') - - it "When given wasp element declaration with invalid name, returns Left" $ do - isLeft (parseWaspElementNameAndClosureContent "app" whiteSpace "app 1someApp { }") - `shouldBe` True - - describe "Parsing wasp closure" $ do - it "Parses a closure with braces {}" $ do - runWaspParser (waspClosure (symbol "content")) "{ content }" - `shouldBe` Right "content" - - it "Does not parse a closure with brackets []" $ do - isLeft (runWaspParser (waspClosure (symbol "content")) "[ content ]") - `shouldBe` True - - describe "Parsing wasp property with a closure as a value" $ do - it "When given a string as a key and closure as a value, returns closure content." $ do - runWaspParser (waspPropertyClosure "someKey" (symbol "content")) "someKey: { content }" - `shouldBe` Right "content" - - describe "Parsing wasp property - string literal" $ do - let parseWaspPropertyStringLiteral key input = - runWaspParser (waspPropertyStringLiteral key) input - - it "When given key/value with int value, returns Left." $ do - isLeft (parseWaspPropertyStringLiteral "title" "title: 23") - `shouldBe` True - - it "When given key/value with string value, returns a parsed value." $ do - let appTitle = "my first app" - parseWaspPropertyStringLiteral "title" ("title: \"" ++ appTitle ++ "\"") - `shouldBe` Right appTitle - - describe "Parsing wasp property - jsx closure {=jsx...jsx=}" $ do - let parseWaspPropertyJsxClosure key input = - runWaspParser (waspPropertyJsxClosure key) input - - it "When given unexpected property key, returns Left." $ do - isLeft (parseWaspPropertyJsxClosure "content" "title: 23") - `shouldBe` True - - it "When given content within jsx closure, returns that content." $ do - parseWaspPropertyJsxClosure "content" "content: {=jsx some content jsx=}" - `shouldBe` Right "some content" - - describe "Parsing wasp jsx closure" $ do - let parseWaspJsxClosure input = runWaspParser waspJsxClosure input - let closureContent = "
hello world
" - - it "Returns the content of closure" $ do - parseWaspJsxClosure ("{=jsx " ++ closureContent ++ " jsx=}") - `shouldBe` Right closureContent - - it "Can parse braces {} within the closure" $ do - let closureContentWithBraces = "
hello world {task.length}
" - - parseWaspJsxClosure ("{=jsx " ++ closureContentWithBraces ++ " jsx=}") - `shouldBe` Right closureContentWithBraces - - it "Removes leading and trailing spaces" $ do - parseWaspJsxClosure ("{=jsx " ++ closureContent ++ " jsx=}") - `shouldBe` Right closureContent - - describe "Parsing relative file path string" $ do - it "Correctly parses relative path in double quotes" $ do - runWaspParser relFilePathString "\"foo/bar.txt\"" - `shouldBe` Right [SP.relfile|foo/bar.txt|] - - -- TODO: It is not passing on Windows, due to Path differently parsing paths on Windows. - -- Check out Path.Posix vs Path.Windows. - -- it "When path is not relative, returns Left" $ do - -- isLeft (runWaspParser relFilePathString "\"/foo/bar.txt\"") `shouldBe` True - - describe "Parsing wasp array" $ do - it "Correctly parses array of identifiers" $ do - runWaspParser (waspList L.identifier) "[ Task, Project ,User]" - `shouldBe` Right ["Task", "Project", "User"] diff --git a/waspc/test/Parser/DbTest.hs b/waspc/test/Parser/DbTest.hs deleted file mode 100644 index f6ea95d62..000000000 --- a/waspc/test/Parser/DbTest.hs +++ /dev/null @@ -1,21 +0,0 @@ -module Parser.DbTest where - -import Data.Either (isLeft) -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Db (db) -import qualified Wasp.Wasp.Db as Wasp.Db - -spec_parseDb :: Spec -spec_parseDb = - describe "Parsing db declaration" $ do - let parseDb input = runWaspParser db input - - it "When given a valid db declaration, returns correct AST" $ do - parseDb "db { system: PostgreSQL }" - `shouldBe` Right (Wasp.Db.Db {Wasp.Db._system = Wasp.Db.PostgreSQL}) - parseDb "db { system: SQLite }" - `shouldBe` Right (Wasp.Db.Db {Wasp.Db._system = Wasp.Db.SQLite}) - - it "When given db wasp declaration without 'db', should return Left" $ do - isLeft (parseDb "db { }") `shouldBe` True diff --git a/waspc/test/Parser/ExternalCodeTest.hs b/waspc/test/Parser/ExternalCodeTest.hs deleted file mode 100644 index af195e06f..000000000 --- a/waspc/test/Parser/ExternalCodeTest.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Parser.ExternalCodeTest where - -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.ExternalCode (extCodeFilePathString) - -spec_ParserExternalCode :: Spec -spec_ParserExternalCode = do - describe "Parsing external code file path string" $ do - it "Correctly parses external code path in double quotes" $ do - runWaspParser extCodeFilePathString "\"@ext/foo/bar.txt\"" - `shouldBe` Right [SP.relfileP|foo/bar.txt|] - - it "When path does not start with @ext/, returns Left" $ do - isLeft (runWaspParser extCodeFilePathString "\"@ext2/foo/bar.txt\"") `shouldBe` True diff --git a/waspc/test/Parser/JsImportTest.hs b/waspc/test/Parser/JsImportTest.hs deleted file mode 100644 index 71a058283..000000000 --- a/waspc/test/Parser/JsImportTest.hs +++ /dev/null @@ -1,44 +0,0 @@ -module Parser.JsImportTest where - -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.JsImport (jsImport) -import qualified Wasp.Wasp as Wasp - -spec_parseJsImport :: Spec -spec_parseJsImport = do - let someFilePath = [SP.relfileP|some/file.js|] - - it "Parses external code js import with default import correctly" $ do - runWaspParser jsImport "import something from \"@ext/some/file.js\"" - `shouldBe` Right (Wasp.JsImport (Just "something") [] someFilePath) - - it "Parses correctly when there is whitespace up front" $ do - runWaspParser jsImport " import something from \"@ext/some/file.js\"" - `shouldBe` Right (Wasp.JsImport (Just "something") [] someFilePath) - - it "Parses correctly when 'from' is part of WHAT part" $ do - runWaspParser jsImport "import somethingfrom from \"@ext/some/file.js\"" - `shouldBe` Right (Wasp.JsImport (Just "somethingfrom") [] someFilePath) - - it "Parses correctly when 'what' is a single named export" $ do - runWaspParser jsImport "import { something } from \"@ext/some/file.js\"" - `shouldBe` Right (Wasp.JsImport Nothing ["something"] someFilePath) - - it "For now we don't support multiple named exports in WHAT part" $ do - isLeft (runWaspParser jsImport "import { foo, bar } from \"@ext/some/file.js\"") - `shouldBe` True - - it "Throws error if there is no whitespace after import" $ do - isLeft (runWaspParser jsImport "importsomething from \"@ext/some/file.js\"") - `shouldBe` True - - it "Throws error if 'from' part is not referring to the external code" $ do - isLeft (runWaspParser jsImport "import something from \"some/file.js\"") - `shouldBe` True - - it "For now we don't support single quotes in FROM part (TODO: support them in the future!)" $ do - isLeft (runWaspParser jsImport "import something from '@ext/some/file.js'") - `shouldBe` True diff --git a/waspc/test/Parser/NpmDependenciesTest.hs b/waspc/test/Parser/NpmDependenciesTest.hs deleted file mode 100644 index 1d96629f4..000000000 --- a/waspc/test/Parser/NpmDependenciesTest.hs +++ /dev/null @@ -1,24 +0,0 @@ -module Parser.NpmDependenciesTest where - -import Data.Either (isLeft) -import Test.Tasty.Hspec -import qualified Wasp.NpmDependency as ND -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.NpmDependencies (npmDependencies) -import Wasp.Wasp.NpmDependencies - -spec_parseNpmDependencies :: Spec -spec_parseNpmDependencies = do - describe "Parsing npm dependencies" $ do - it "When given a valid declaration with valid json, parses it correctly" $ do - runWaspParser npmDependencies "dependencies {=json \"foo\": \"test1\", \"bar\": \"test2\" json=}" - `shouldBe` Right - NpmDependencies - { _dependencies = - [ ND.NpmDependency {ND._name = "foo", ND._version = "test1"}, - ND.NpmDependency {ND._name = "bar", ND._version = "test2"} - ] - } - it "When given invalid json, reports error" $ do - isLeft (runWaspParser npmDependencies "dependencies {=json foo: 42 json=}") - `shouldBe` True diff --git a/waspc/test/Parser/OperationTest.hs b/waspc/test/Parser/OperationTest.hs deleted file mode 100644 index 4c79a0bb2..000000000 --- a/waspc/test/Parser/OperationTest.hs +++ /dev/null @@ -1,34 +0,0 @@ -module Parser.OperationTest where - -import Data.List (intercalate) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Operation -import qualified Wasp.Wasp.JsImport as Wasp.JsImport - -spec_parseOperation :: Spec -spec_parseOperation = - describe "Parsing operation properties" $ do - let parseOperationProperties = runWaspParser properties - - it "When given a valid list of properties, correctly parses them" $ do - let testJsFnName = "myJsFn" - testJsFnFrom = [SP.relfileP|some/path|] - let testProps = - [ JsFunction $ - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Nothing, - Wasp.JsImport._namedImports = [testJsFnName], - Wasp.JsImport._from = testJsFnFrom - }, - Entities ["Task", "Project"] - ] - parseOperationProperties - ( intercalate - ",\n" - [ "fn: import { " ++ testJsFnName ++ " } from \"@ext/some/path\"", - "entities: [Task, Project]" - ] - ) - `shouldBe` Right testProps diff --git a/waspc/test/Parser/PageTest.hs b/waspc/test/Parser/PageTest.hs deleted file mode 100644 index 9cb8dc130..000000000 --- a/waspc/test/Parser/PageTest.hs +++ /dev/null @@ -1,57 +0,0 @@ -module Parser.PageTest where - -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Page (page) -import qualified Wasp.Wasp.JsImport as Wasp.JsImport -import qualified Wasp.Wasp.Page as Wasp.Page - -spec_parsePage :: Spec -spec_parsePage = - describe "Parsing page declaration" $ do - let parsePage input = runWaspParser page input - - let expectedPageComponentImport = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Just "Main", - Wasp.JsImport._namedImports = [], - Wasp.JsImport._from = [SP.relfileP|pages/Main|] - } - - it "When given a valid page declaration, returns correct AST" $ do - let testPageName = "Landing" - - parsePage - ( "page " ++ testPageName ++ " { " - ++ "component: import Main from \"@ext/pages/Main\"" - ++ "}" - ) - `shouldBe` Right - ( Wasp.Page.Page - { Wasp.Page._name = testPageName, - Wasp.Page._component = expectedPageComponentImport, - Wasp.Page._authRequired = Nothing - } - ) - - it "When given a valid page with authRequired property, returns correct AST" $ do - let testPageName = "Landing" - - parsePage - ( "page " ++ testPageName ++ " { " - ++ "component: import Main from \"@ext/pages/Main\"," - ++ "authRequired: true" - ++ "}" - ) - `shouldBe` Right - ( Wasp.Page.Page - { Wasp.Page._name = testPageName, - Wasp.Page._component = expectedPageComponentImport, - Wasp.Page._authRequired = Just True - } - ) - - it "When given page wasp declaration without 'page', should return Left" $ do - isLeft (parsePage "Landing { component: import Main from \"@ext/pages/Main\" }") `shouldBe` True diff --git a/waspc/test/Parser/ParserTest.hs b/waspc/test/Parser/ParserTest.hs deleted file mode 100644 index 53f95f9bc..000000000 --- a/waspc/test/Parser/ParserTest.hs +++ /dev/null @@ -1,125 +0,0 @@ -module Parser.ParserTest where - -import Data.Either -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.NpmDependency as ND -import Wasp.Parser -import Wasp.Parser.Common (runWaspParser) -import qualified Wasp.Psl.Parser.Model -import Wasp.Wasp -import qualified Wasp.Wasp.Auth as Wasp.Auth -import qualified Wasp.Wasp.Entity as Wasp.Entity -import qualified Wasp.Wasp.JsImport as Wasp.JsImport -import qualified Wasp.Wasp.NpmDependencies as Wasp.NpmDependencies -import qualified Wasp.Wasp.Page as Wasp.Page -import qualified Wasp.Wasp.Query as Wasp.Query -import qualified Wasp.Wasp.Route as R - -spec_parseWasp :: Spec -spec_parseWasp = - describe "Parsing wasp" $ do - it "When given wasp without app, should return Left" $ do - isLeft (parseWasp "hoho") `shouldBe` True - - before (readFile "test/Parser/valid.wasp") $ do - it "When given a valid wasp source, should return correct Wasp" $ \wasp -> - do - parseWasp wasp - `shouldBe` Right - ( fromWaspElems - [ WaspElementApp $ - App - { appName = "test_app", - appTitle = "Hello World!", - appHead = Nothing - }, - WaspElementAuth $ - Wasp.Auth.Auth - { Wasp.Auth._userEntity = "User", - Wasp.Auth._methods = [Wasp.Auth.EmailAndPassword], - Wasp.Auth._onAuthFailedRedirectTo = "/test", - Wasp.Auth._onAuthSucceededRedirectTo = "/" - }, - WaspElementRoute $ - R.Route - { R._urlPath = "/", - R._targetPage = "Landing" - }, - WaspElementPage $ - Wasp.Page.Page - { Wasp.Page._name = "Landing", - Wasp.Page._component = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Just "Landing", - Wasp.JsImport._namedImports = [], - Wasp.JsImport._from = [SP.relfileP|pages/Landing|] - }, - Wasp.Page._authRequired = Just False - }, - WaspElementRoute $ - R.Route - { R._urlPath = "/test", - R._targetPage = "TestPage" - }, - WaspElementPage $ - Wasp.Page.Page - { Wasp.Page._name = "TestPage", - Wasp.Page._component = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Just "Test", - Wasp.JsImport._namedImports = [], - Wasp.JsImport._from = [SP.relfileP|pages/Test|] - }, - Wasp.Page._authRequired = Nothing - }, - WaspElementEntity $ - Wasp.Entity.Entity - { Wasp.Entity._name = "Task", - Wasp.Entity._fields = - [ Wasp.Entity.Field - { Wasp.Entity._fieldName = "id", - Wasp.Entity._fieldType = Wasp.Entity.FieldTypeScalar Wasp.Entity.Int - }, - Wasp.Entity.Field - { Wasp.Entity._fieldName = "description", - Wasp.Entity._fieldType = Wasp.Entity.FieldTypeScalar Wasp.Entity.String - }, - Wasp.Entity.Field - { Wasp.Entity._fieldName = "isDone", - Wasp.Entity._fieldType = Wasp.Entity.FieldTypeScalar Wasp.Entity.Boolean - } - ], - Wasp.Entity._pslModelBody = - fromRight (error "failed to parse") $ - runWaspParser - Wasp.Psl.Parser.Model.body - "\ - \ id Int @id @default(autoincrement())\n\ - \ description String\n\ - \ isDone Boolean @default(false)" - }, - WaspElementQuery $ - Wasp.Query.Query - { Wasp.Query._name = "myQuery", - Wasp.Query._jsFunction = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Nothing, - Wasp.JsImport._namedImports = ["myJsQuery"], - Wasp.JsImport._from = [SP.relfileP|some/path|] - }, - Wasp.Query._entities = Nothing, - Wasp.Query._auth = Nothing - }, - WaspElementNpmDependencies $ - Wasp.NpmDependencies.NpmDependencies - { Wasp.NpmDependencies._dependencies = - [ ND.NpmDependency - { ND._name = "lodash", - ND._version = "^4.17.15" - } - ] - } - ] - `setJsImports` [JsImport (Just "something") [] [SP.relfileP|some/file|]] - ) diff --git a/waspc/test/Parser/QueryTest.hs b/waspc/test/Parser/QueryTest.hs deleted file mode 100644 index 6dedb85f6..000000000 --- a/waspc/test/Parser/QueryTest.hs +++ /dev/null @@ -1,53 +0,0 @@ -module Parser.QueryTest where - -import Data.Char (toLower) -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Query (query) -import qualified Wasp.Wasp.JsImport as Wasp.JsImport -import qualified Wasp.Wasp.Query as Wasp.Query - -spec_parseQuery :: Spec -spec_parseQuery = - describe "Parsing query declaration" $ do - let parseQuery = runWaspParser query - let testWhenAuth auth = - it ("When given a valid query declaration, returns correct AST(query.auth = " ++ show auth ++ ")") $ - parseQuery (genQueryCode auth) `shouldBe` Right (genQueryAST auth) - testWhenAuth (Just True) - testWhenAuth (Just False) - testWhenAuth Nothing - it "When given query wasp declaration without 'fn' property, should return Left" $ do - isLeft (parseQuery "query myQuery { }") `shouldBe` True - where - genQueryCode :: Maybe Bool -> String - genQueryCode qApplyAuth = - "query " ++ testQueryName ++ " {\n" - ++ " fn: import { " - ++ testQueryJsFunctionName - ++ " } from \"@ext/some/path\",\n" - ++ " entities: [Task, Project]" - ++ authStr qApplyAuth - ++ "}" - genQueryAST :: Maybe Bool -> Wasp.Query.Query - genQueryAST qApplyAuth = - Wasp.Query.Query - { Wasp.Query._name = testQueryName, - Wasp.Query._jsFunction = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Nothing, - Wasp.JsImport._namedImports = [testQueryJsFunctionName], - Wasp.JsImport._from = testQueryJsFunctionFrom - }, - Wasp.Query._entities = Just ["Task", "Project"], - Wasp.Query._auth = qApplyAuth - } - - authStr :: Maybe Bool -> String - authStr (Just useAuth) = ",\n auth: " ++ map toLower (show useAuth) ++ "\n" - authStr _ = "\n" - testQueryName = "myQuery" - testQueryJsFunctionName = "myJsQuery" - testQueryJsFunctionFrom = [SP.relfileP|some/path|] diff --git a/waspc/test/Parser/RouteTest.hs b/waspc/test/Parser/RouteTest.hs deleted file mode 100644 index 8cabbb89c..000000000 --- a/waspc/test/Parser/RouteTest.hs +++ /dev/null @@ -1,29 +0,0 @@ -module Parser.RouteTest where - -import Data.Either (isLeft) -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Route (route) -import qualified Wasp.Wasp.Route as RouteAST - -spec_parseRoute :: Spec -spec_parseRoute = - describe "Parsing route declaration" $ do - let parseRoute = runWaspParser route - - it "When given a valid route declaration, returns correct AST." $ do - let inputUrlPath = "/some/url/path" - let inputTargetPage = "somePage" - - parseRoute - ( "route " ++ "\"" ++ inputUrlPath ++ "\"" ++ " -> page " ++ inputTargetPage - ) - `shouldBe` Right - ( RouteAST.Route - { RouteAST._urlPath = inputUrlPath, - RouteAST._targetPage = inputTargetPage - } - ) - - it "When given a route declaration without 'page', should return Left" $ do - isLeft (parseRoute "route \"/url\" -> Home") `shouldBe` True diff --git a/waspc/test/Parser/ServerTest.hs b/waspc/test/Parser/ServerTest.hs deleted file mode 100644 index 0efda47a9..000000000 --- a/waspc/test/Parser/ServerTest.hs +++ /dev/null @@ -1,40 +0,0 @@ -module Parser.ServerTest where - -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Server (server) -import qualified Wasp.Wasp.JsImport as Wasp.JsImport -import qualified Wasp.Wasp.Server as Wasp.Server - -spec_parseServer :: Spec -spec_parseServer = - describe "Parsing server declaration" $ do - let parseServer = runWaspParser server - - it "When given a valid server declaration, returns correct AST" $ do - let setupFnName = "myServerSetup" - setupFnFrom = [SP.relfileP|some/path|] - let ast = - Wasp.Server.Server - { Wasp.Server._setupJsFunction = - Wasp.JsImport.JsImport - { Wasp.JsImport._defaultImport = Nothing, - Wasp.JsImport._namedImports = [setupFnName], - Wasp.JsImport._from = setupFnFrom - } - } - parseServer - ( "server {\n" - ++ " setupFn: import { " - ++ setupFnName - ++ " } from \"@ext/" - ++ SP.fromRelFileP setupFnFrom - ++ "\"\n" - ++ "}" - ) - `shouldBe` Right ast - - it "When given server wasp declaration without 'serverFn' property, should return Left" $ do - isLeft (parseServer "server { }") `shouldBe` True diff --git a/waspc/test/Parser/StyleTest.hs b/waspc/test/Parser/StyleTest.hs deleted file mode 100644 index 2934f1407..000000000 --- a/waspc/test/Parser/StyleTest.hs +++ /dev/null @@ -1,22 +0,0 @@ -module Parser.StyleTest where - -import Data.Either (isLeft) -import qualified StrongPath as SP -import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) -import Wasp.Parser.Style (style) -import qualified Wasp.Wasp.Style as Wasp.Style - -spec_parseStyle :: Spec -spec_parseStyle = do - it "Parses external code file path correctly" $ do - runWaspParser style "\"@ext/some/file.css\"" - `shouldBe` Right (Wasp.Style.ExtCodeCssFile [SP.relfileP|some/file.css|]) - - it "Parses css closure correctly" $ do - runWaspParser style "{=css Some css code css=}" - `shouldBe` Right (Wasp.Style.CssCode "Some css code") - - it "Throws error if path is not external code path." $ do - isLeft (runWaspParser style "\"some/file.css\"") - `shouldBe` True diff --git a/waspc/test/Parser/valid.wasp b/waspc/test/Parser/valid.wasp deleted file mode 100644 index e359ace38..000000000 --- a/waspc/test/Parser/valid.wasp +++ /dev/null @@ -1,40 +0,0 @@ -// Test .wasp file. - -import something from "@ext/some/file" - -// App definition. -app test_app { - // Title of the app. - title: "Hello World!" -} - -auth { - onAuthFailedRedirectTo: "/test", - userEntity: User, - methods: [ EmailAndPassword ] -} - -route "/" -> page Landing -page Landing { - component: import Landing from "@ext/pages/Landing", - authRequired: false -} - -route "/test" -> page TestPage -page TestPage { - component: import Test from "@ext/pages/Test" -} - -entity Task {=psl - id Int @id @default(autoincrement()) - description String - isDone Boolean @default(false) -psl=} - -query myQuery { - fn: import { myJsQuery } from "@ext/some/path" -} - -dependencies {=json - "lodash": "^4.17.15" -json=} diff --git a/waspc/test/Psl/Generator/ModelTest.hs b/waspc/test/Psl/Generator/ModelTest.hs index 80a509b30..fca350a31 100644 --- a/waspc/test/Psl/Generator/ModelTest.hs +++ b/waspc/test/Psl/Generator/ModelTest.hs @@ -6,7 +6,7 @@ module Psl.Generator.ModelTest where import Psl.Common.ModelTest (sampleBodyAst) import Test.Tasty.Hspec import Test.Tasty.QuickCheck -import Wasp.Parser.Common (runWaspParser) +import qualified Text.Parsec as Parsec import qualified Wasp.Psl.Ast.Model as AST import Wasp.Psl.Generator.Model (generateModel) import qualified Wasp.Psl.Parser.Model @@ -17,12 +17,12 @@ spec_generatePslModel = do let pslModelAst = AST.Model "User" sampleBodyAst it "parse(generate(sampleBodyAst)) == sampleBodyAst" $ do - runWaspParser Wasp.Psl.Parser.Model.model (generateModel pslModelAst) `shouldBe` Right pslModelAst + Parsec.parse Wasp.Psl.Parser.Model.model "" (generateModel pslModelAst) `shouldBe` Right pslModelAst prop_generatePslModel :: Property prop_generatePslModel = mapSize (const 100) $ \modelAst -> within 1000000 $ - runWaspParser Wasp.Psl.Parser.Model.model (generateModel modelAst) `shouldBe` Right modelAst + Parsec.parse Wasp.Psl.Parser.Model.model "" (generateModel modelAst) `shouldBe` Right modelAst instance Arbitrary AST.Model where arbitrary = AST.Model <$> arbitraryIdentifier <*> arbitrary diff --git a/waspc/test/Psl/Parser/ModelTest.hs b/waspc/test/Psl/Parser/ModelTest.hs index 043e72dbc..cf998aba5 100644 --- a/waspc/test/Psl/Parser/ModelTest.hs +++ b/waspc/test/Psl/Parser/ModelTest.hs @@ -3,7 +3,7 @@ module Psl.Parser.ModelTest where import Data.Either (isLeft) import Psl.Common.ModelTest (sampleBodyAst, sampleBodySchema) import Test.Tasty.Hspec -import Wasp.Parser.Common (runWaspParser) +import qualified Text.Parsec as Parsec import qualified Wasp.Psl.Ast.Model as AST import Wasp.Psl.Parser.Model (attrArgument, body, model) @@ -14,14 +14,14 @@ spec_parsePslModel = do expectedModelAst = AST.Model "User" sampleBodyAst it "Body parser correctly parses" $ do - runWaspParser body sampleBodySchema `shouldBe` Right sampleBodyAst + Parsec.parse body "" sampleBodySchema `shouldBe` Right sampleBodyAst it "Model parser correctly parses" $ do - runWaspParser model pslModel `shouldBe` Right expectedModelAst + Parsec.parse model "" pslModel `shouldBe` Right expectedModelAst describe "Body parser" $ do describe "Fails if input is not valid PSL" $ do - let runTest psl = it psl $ isLeft (runWaspParser body psl) `shouldBe` True + let runTest psl = it psl $ isLeft (Parsec.parse body "" psl) `shouldBe` True mapM_ runTest [ " noType", @@ -51,5 +51,5 @@ spec_parsePslModel = do ) ] let runTest (psl, expected) = - it ("correctly parses " ++ psl) $ runWaspParser attrArgument psl `shouldBe` Right expected + it ("correctly parses " ++ psl) $ Parsec.parse attrArgument "" psl `shouldBe` Right expected mapM_ runTest tests diff --git a/waspc/test/UtilTest.hs b/waspc/test/UtilTest.hs index 75ddf5bba..3eb92de9f 100644 --- a/waspc/test/UtilTest.hs +++ b/waspc/test/UtilTest.hs @@ -91,3 +91,25 @@ spec_concatPrefixAndText = do concatPrefixAndText "prefix: " "foo" `shouldBe` "prefix: foo" it "put all the text below the prefix, indented for 2 spaces, if text has multiple lines" $ do concatPrefixAndText "prefix: " "foo\nbar" `shouldBe` "prefix: \n foo\n bar" + +spec_leftPad :: Spec +spec_leftPad = do + describe "leftPad should" $ do + it "pad the list if it is shorter than desired length" $ do + leftPad ' ' 5 "hi" `shouldBe` " hi" + it "not modify the list if it is already long enough" $ do + leftPad ' ' 5 "hihih" `shouldBe` "hihih" + leftPad ' ' 5 "hihihi" `shouldBe` "hihihi" + +spec_insertAt :: Spec +spec_insertAt = do + describe "insertAt should" $ do + it "insert given list at the start of host list if index is 0 or negative" $ do + insertAt [0] 0 [1, 2, 3] `shouldBe` ([0, 1, 2, 3] :: [Int]) + insertAt [0] (-1) [1, 2, 3] `shouldBe` ([0, 1, 2, 3] :: [Int]) + it "insert given list in the host list at given index when index is in [1, host list length - 1]" $ do + insertAt [0] 1 [1, 2, 3] `shouldBe` ([1, 0, 2, 3] :: [Int]) + insertAt [0] 2 [1, 2, 3] `shouldBe` ([1, 2, 0, 3] :: [Int]) + it "insert given list at the end of host list if index is equal or bigger than host list length" $ do + insertAt [0] 3 [1, 2, 3] `shouldBe` ([1, 2, 3, 0] :: [Int]) + insertAt [0] 4 [1, 2, 3] `shouldBe` ([1, 2, 3, 0] :: [Int]) diff --git a/web/blog/2021-12-02-waspello.md b/web/blog/2021-12-02-waspello.md index 4e81f57bf..f2ebde948 100644 --- a/web/blog/2021-12-02-waspello.md +++ b/web/blog/2021-12-02-waspello.md @@ -138,7 +138,7 @@ page Main { } ``` -All pretty straightforward so far! As you can see here, Wasp also provides [authentication out-of-the-box](/docs/language/basic-elements#authentication--authorization). +All pretty straightforward so far! As you can see here, Wasp also provides [authentication out-of-the-box](/docs/language/features#authentication--authorization). Currently, the majority of the client logic of Waspello is contained in `ext/MainPage.js` (we should break it down a little 😅 - [you can help us!](https://github.com/wasp-lang/wasp/issues/334)). Just to give you an idea, here's a quick glimpse into it: @@ -162,7 +162,7 @@ const MainPage = ({ user }) => { ) } ``` -Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the `@wasp` prefix in the import path. `useQuery` ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it [here](/docs/language/basic-elements#usequery). +Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the `@wasp` prefix in the import path. `useQuery` ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it [here](/docs/language/features#usequery). This is pretty much it from the stuff that works 😄 ! I kinda rushed a bit through things here - for more details on all Wasp features and to build your first app with Wasp, check out our [docs](/docs/). diff --git a/web/docs/deploying.md b/web/docs/deploying.md index cda561033..99cb1c1f9 100644 --- a/web/docs/deploying.md +++ b/web/docs/deploying.md @@ -18,7 +18,7 @@ wasp build ``` generates deployable code for the whole app in the `.wasp/build/` directory. Next, we will deploy this code. -NOTE: You will not be able to build the app if you are using SQLite as a database (which is a default database) -> you will have to [switch to PostgreSQL](/docs/language/basic-elements#migrating-from-sqlite-to-postgresql). +NOTE: You will not be able to build the app if you are using SQLite as a database (which is a default database) -> you will have to [switch to PostgreSQL](/docs/language/features#migrating-from-sqlite-to-postgresql). # Deploying API server (backend) In `.wasp/build/`, there is a `Dockerfile` describing an image for building the server. diff --git a/web/docs/language/basic-elements.md b/web/docs/language/features.md similarity index 80% rename from web/docs/language/basic-elements.md rename to web/docs/language/features.md index 0d0bd62c6..83f26edf3 100644 --- a/web/docs/language/basic-elements.md +++ b/web/docs/language/features.md @@ -1,52 +1,68 @@ --- -title: Basic Elements +title: Features --- ## App -There can be only one `App` element per Wasp project. It serves as a starting point and defines global -properties of your app. Currently, it is very simple: +There can be only one declaration of `app` type per Wasp project. +It serves as a starting point and defines global properties of your app. ```css app todoApp { - title: "ToDo App", - head: [ - "" - ] + title: "ToDo App", + head: [ // optional + "" + ] } ``` -#### `app: identifier` -Name of your app. +### Fields -#### `title: string` +#### `title: string` (required) Title of your app. It will be displayed in the browser tab, next to the favicon. -#### `head: array of strings` +#### `head: [string]` (optional) Head of your HTML Document. Your app's metadata (styles, links, etc) can be added here. +#### `auth: dict` (optional) +Authentication and authorization configuration. +Check [`app.auth`](/docs/language/features#authentication--authorization) for more details. + +#### `db: dict` (optional) +Database configuration. +Check [`app.db`](/docs/language/features#database) for more details. + +#### `server: dict` (optional) +Server configuration. +Check [`app.server`](/docs/language/features#server) for more details. + +#### `dependencies: [(string, string)]` (optional) +List of dependencies (external libraries). +Check [`app.dependencies`](/docs/language/features#dependencies) for more details. ## Page -`Page` is the top-level layout abstraction. Your app can have multiple pages, and they are defined in Wasp -as follows: +`page` declaration is the top-level layout abstraction. Your app can have multiple pages. + ```css -page Main { - component: import Main from "@ext/pages/Main", - authRequired: false // optional property +page MainPage { + component: import Main from "@ext/pages/Main", + authRequired: false // optional } ``` -#### `page: identifier` -Name of the page. +Normally you will also want to associate `page` with a `route`, otherwise it won't be accessible in the app. -#### `component: js import statement` -Import statement of the page React element. See importing external code for details. +### Fields -`Page` also has to be associated with a `Route`, otherwise it won't be accessible in the app. +#### `component: ExtImport` (required) +Import statement of the React element that implements the page component. +See importing external code for details. -#### `authRequired: bool` -Optional property - can be specified only if [`auth`](/docs/language/basic-elements#authentication--authorization) is declared. If set to `true`, only authenticated users will be able to access this page. Unauthenticated users will be redirected to a route declared by `onAuthFailedRedirectTo` property within `auth`. +#### `authRequired: bool` (optional) +Can be specified only if [`app.auth`](/docs/language/features#authentication--authorization) is defined. + +If set to `true`, only authenticated users will be able to access this page. Unauthenticated users will be redirected to a route defined by `onAuthFailedRedirectTo` property within `app.auth`. If `authRequired` is set to `true`, the React component of a page (specified by `component` property) will be provided `user` object as a prop. @@ -54,21 +70,25 @@ Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/auth# ## Route -Using `Route` element is a way to implement routing functionality in Wasp: +`route` declaration provides top-level routing functionality in Wasp. + ```css -route "/about" -> page About +route AboutRoute { path: "/about", to: AboutPage } ``` -#### `route: string` -URL path of the route. Route string can be parametrised and follows the same conventions as +### Fields + +#### `path: string` (required) +URL path of the route. Route path can be parametrised and follows the same conventions as [React Router](https://reactrouter.com/web/). -#### `page: page identifier` -Page identifier of the route's target. Referenced page must be defined somewhere in `.wasp` file. +#### `to: page` (required) +Name of the `page` to which the path will lead. +Referenced page must be defined somewhere in `.wasp` file. ### Example - parametrised URL path ```css -route "/task/:id" -> page Task +route TaskRoute { path: "/task/:id", to: TaskPage } ``` For details on URL path format check [React Router](https://reactrouter.com/web/) documentation. @@ -81,9 +101,9 @@ started: ```c title="todoApp.wasp" // ... -route "/task/:id" -> page Task -page Task { - component: import Task from "@ext/pages/Task" +route TaskRoute { path: "/task/:id", to: TaskPage } +page TaskPage { + component: import Task from "@ext/pages/Task" } ``` @@ -107,9 +127,9 @@ Navigation can be performed from the React code via `` component, also us ```c title="todoApp.wasp" // ... -route "/home" -> page Home -page Home { - component: import Home from "@ext/pages/Home" +route HomeRoute { path: "/home", to: HomePage } +page HomePage { + component: import Home from "@ext/pages/Home" } ``` @@ -126,10 +146,10 @@ const OtherPage = (props) => { ## Entity -`Entity` element represents a database model. Wasp uses [Prisma](https://www.prisma.io/) to implement -database functionality and currently provides only a thin layer above it. +`entity` declaration represents a database model. +Wasp uses [Prisma](https://www.prisma.io/) to implement database functionality and currently provides only a thin layer above it. -Each `Entity` element corresponds 1-to-1 to Prisma data model and is defined in a following way: +Each `Entity` declaration corresponds 1-to-1 to Prisma data model and is defined in a following way: ```css entity Task {=psl @@ -138,10 +158,8 @@ entity Task {=psl isDone Boolean @default(false) psl=} ``` -#### `entity: identifier` -Name of the entity. -#### `{=psl ... psl=}: PSL` +### `{=psl ... psl=}: PSL` Definition of entity fields in *Prisma Schema Language* (PSL). See [here for intro and examples](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema) and [here for a more exhaustive language specification](https://github.com/prisma/specs/tree/master/schema). @@ -168,13 +186,18 @@ Queries are NodeJS functions that don't modify any state. Normally they fetch ce To create a Wasp Query, we need two things: declaration in Wasp and implementation in NodeJS: +1. `query` declaration in Wasp: ```c title="main.wasp" // ... query getTasks { fn: import { getAllTasks } from "@ext/foo.js" } ``` +`query` declaration type has two fields: +- `fn: ExtImport` (required) +- `entities: [Entity]` (optional) +2. Implemenation in NodeJS: ```js title="ext/foo.js" // ... export getAllTasks = async (args, context) => { @@ -291,13 +314,17 @@ More differences and action/query specific features will come in the future vers ## Dependencies -You can specify additional npm dependencies in following way, in your `*.wasp` file: +You can specify additional npm dependencies via `dependencies` field in `app` declaration, in following way: ```c -dependencies {=json - "redux": "^4.0.5", - "react-redux": "^7.1.3" -json=} +app MyApp { + title: "My app", + // ... + dependencies: [ + ("redux", "^4.0.5"), + ("react-redux", "^7.1.3") + ] +) ``` You will need to re-run `wasp start` after adding a dependency for Wasp to pick it up. @@ -310,29 +337,41 @@ In the future, we will add support for picking any version you like, but we have ## Authentication & Authorization -Wasp provides authentication and authorization support out-of-the-box. Enabling it for your app is optional and can be done by adding `auth` element to your `.wasp` file: +Wasp provides authentication and authorization support out-of-the-box. Enabling it for your app is optional and can be done by configuring `auth` field of the `app` declaration: ```css -auth { +app MyApp { + title: "My app", + // ... + auth: { userEntity: User, methods: [ EmailAndPassword ], onAuthFailedRedirectTo: "/someRoute" + } } ``` -`userEntity: entity` + +`app.auth` is a dictionary with following fields: + +#### `userEntity: entity` (required) Entity which represents the user (sometimes also referred to as *Principal*). -`methods: [AuthMethod]` +#### `methods: [AuthMethod]` (required) List of authentication methods that Wasp app supports. Currently supported methods are: * `EmailAndPassword`: Provides support for authentication with email address and a password. -`onAuthFailedRedirectTo: String` Name of the route where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page). +#### `onAuthFailedRedirectTo: String` (required) +Path where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page). Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/auth#updating-main-page-to-check-if-user-is-authenticated) to see an example of usage. +#### `onAuthSucceededRedirectTo: String` (optional) +Path where a successfully authenticated user will be sent upon successful login/signup. +Default value is "/". + ### Email and Password `EmailAndPassword` authentication method makes it possible to signup/login into the app by using email address and a password. -This method requires that `userEntity` specified in `auth` element contains `email: string` and `password: string` fields. +This method requires that `userEntity` specified in `auth` contains `email: string` and `password: string` fields. We provide basic validations out of the box, which you can customize as shown below. Default validations are: - `email`: non-empty @@ -550,23 +589,21 @@ should be denied access to it. ## Server configuration -```css -server { - ... +Via `server` field of `app` declaration, you can configure behaviour of the Node.js server (one that is executing wasp operations). + +```c +app MyApp { + title: "My app", + // ... + server: { + setupFn: import mySetupFunction from "@ext/myServerSetupCode.js" + } } ``` -With `server` declaration, you can configure behaviour of the Node.js server (one that is executing wasp operations). +`app.server` is a dictionary with following fields: -Currently `server` supports only one option, `setupFn`, but there will likely be more options added in the future. - -### setupFn - -```css -server { - setupFn: import mySetupFunction from "@ext/myServerSetupCode.js" -} -``` +#### `setupFn: ExtImport` (optional) `setupFn` declares a JS function that will be executed on server start. This function is expected to be async and will be awaited before server continues with its setup and starts serving any requests. @@ -630,26 +667,31 @@ console.log(process.env.DATABASE_URL) ## Database -You can specify database to be used by Wasp via `db` element (there can be only one such element per Wasp project): +Via `db` field of `app` declaration, you can configure the database used by Wasp. -```css -db { - system: PostgreSQL +```c +app MyApp { + title: "My app", + // ... + db: { + system: PostgreSQL + } } ``` -If you don't have `db` block, default database is used (which is `SQLite`). -If you create or modify `db` declaration, run `wasp db migrate-dev` to apply the changes. +`app.db` is a dictionary with following fields: -#### `system: identifier` -Database system that Wasp will use. It can be either `PostgreSQL` or `SQLite`. +#### `system: DbSystem` +Database system that Wasp will use. It can be either `PostgreSQL` or `SQLite`. +If not defined, or even if whole `db` field is not present, default value is `SQLite`. +If you add/remove/modify `db` field, run `wasp db migrate-dev` to apply the changes. ### SQLite Default database is `SQLite`, since it is great for getting started with a new project (needs no configuring), but it can be used only in development - once you want to deploy Wasp to production you will need to switch to `PostgreSQL` and stick with it. Check below for more details on how to migrate from SQLite to PostgreSQL. ### PostgreSQL -When using `PostgreSQL` (`db { system: PostgreSQL }`), you will need to spin up a postgres database on your own so it runs during development (when running `wasp start` or doing `wasp db ...` commands) and provide Wasp with `DATABASE_URL` environment variable that Wasp will use to connect to it. +When using `PostgreSQL` (`db: { system: PostgreSQL }`), you will need to spin up a postgres database on your own so it runs during development (when running `wasp start` or doing `wasp db ...` commands) and provide Wasp with `DATABASE_URL` environment variable that Wasp will use to connect to it. One of the easiest ways to do this is by spinning up postgres docker container when you need it with the shell command ``` @@ -668,6 +710,6 @@ to the `.env` file in the root directory of your Wasp project. ### Migrating from SQLite to PostgreSQL To run Wasp app in production, you will need to switch from `SQLite` to `PostgreSQL`. -1. Set `db.system` to `PostgreSQL` and set `DATABASE_URL` env var accordingly (as described [above](/docs/language/basic-elements#postgresql)). +1. Set `app.db.system` to `PostgreSQL` and set `DATABASE_URL` env var accordingly (as described [above](/docs/language/features#postgresql)). 2. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: `rm -r migrations/`. -3. Run `wasp db migrate-dev` to apply new changes and create new, initial migration. You will need to have your postgres database running while doing this (check [above](/docs/language/basic-elements#postgresql) for easy way to get it running). +3. Run `wasp db migrate-dev` to apply new changes and create new, initial migration. You will need to have your postgres database running while doing this (check [above](/docs/language/features#postgresql) for easy way to get it running). diff --git a/web/docs/language/overview.md b/web/docs/language/overview.md index 75db6d1ba..8a95ab565 100644 --- a/web/docs/language/overview.md +++ b/web/docs/language/overview.md @@ -3,7 +3,7 @@ title: Overview --- Wasp is a declarative language that recognizes web application-specific terms (e.g. *page* or *route*) as -words of the language. +words (types) of the language. The basic idea is that the higher-level overview of an app (e.g. pages, routes, database model, ...) is defined in `*.wasp` files (for now just one), while the specific parts (web components, back-end queries, ...) are implemented in specific non-wasp technologies (React, NodeJS, Prisma) and then referenced in the `*.wasp` files. @@ -29,8 +29,8 @@ app todoApp { title: "ToDo App" } -route "/" -> page Main -page Main { +route RootRoute { path: "/", to: MainPage } +page MainPage { component: import Main from "@ext/pages/Main" } @@ -53,4 +53,4 @@ psl=} You can check out a full working example [here](https://github.com/wasp-lang/wasp/tree/master/waspc/examples/todoApp). -In the following sections each of these basic language elements is explained. +In the following sections each of the basic language features is explained. diff --git a/web/docs/language/syntax.md b/web/docs/language/syntax.md new file mode 100644 index 000000000..e9a71c873 --- /dev/null +++ b/web/docs/language/syntax.md @@ -0,0 +1,76 @@ +--- +title: Syntax +--- + +Wasp is a declarative, statically typed, domain specific language (DSL). + +## Declarations + +The central point of Wasp language are **declarations**, and Wasp source is at the end just a bunch of declarations, each of them describing a part of your web app. + +```c +app MyApp { + title: "My app" +} + +route RootRoute { path: "/", to: DashboardPage } + +page DashboardPage { + component: import Dashboard from "@ext/Dashboard.js" +} +``` + +In the example above we described a web app via three declarations: `app MyApp { ... }`, `route RootRoute { ... }` and `page DashboardPage { ... }`. + +Syntax for writing a declaration is ` `, where: +- `` is one of the declaration types offered by Wasp (`app`, `route`, ...) +- `` is an identifier chosen by you to name this specific declaration +- `` is the value/definition of the declaration itself, which has to match the specific declaration body type determined by the chosen declaration type. + +So, for `app` declaration above, we have: +- declaration type `app` +- declaration name `MyApp` (we could have used any other identifier, like `foobar`, `foo_bar`, or `hi3Ho`) +- declaration body `{ title: "My app" }`, which is a dictionary with field `title` that has string value. + Type of this dictionary is in line with the declaration body type of the `app` declaration type. + If we provided something else, e.g. changed `title` to `little`, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for `app`. + +Each declaration has a meaning behind it that describes how your web app should behave and function. + +All the other types in Wasp language (primitive types (`string`, `number`), composite types (`dict`, `list`), enum types (`DbSystem`), ...) are used to define the declaration bodies. + +## Complete list of Wasp types +Wasp's type system can be divided into two main categories of types: **fundamental types** and **domain types**. + +While fundamental types are here to be basic building blocks of a language, and are very similar to what you would see in other popular lanuages, domain types are what makes Wasp special, as they model the concepts of a web app like `page`, `route` and similar. + +- Fundamental types ([source of truth](https://github.com/wasp-lang/wasp/blob/master/waspc/src/Wasp/Analyzer/Type.hs)) + - Primitive types + - **string** (`"foo"`, `"they said: \"hi\""`) + - **bool** (`true`, `false`) + - **number** (`12`, `14.5`) + - **declaration reference** (name of existing declaration: `TaskPage`, `updateTask`) + - **ExtImport** (external import) (`import Foo from "@ext/bar.js"`, `import { Smth } from "@ext/a/b.js"`) + - path has to be relative and start with "@ext". It is considered to be relative to the `ext/` directory. + - import has to be a default import `import Foo` or a single named import `import { Foo }`. + - **json** (`{=json { a: 5, b: ["hi"] } json=}`) + - **psl** (Prisma Schema Language) (`{=psl psl=}`) + - Check [Prisma docs](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model) for the syntax of psl data model. + - Composite types + - **dict** (dictionary) (`{ a: 5, b: "foo" }`) + - **list** (`[1, 2, 3]`) + - **tuple** (`(1, "bar")`, `(2, 4, true)`) + - Tuples can be of size 2, 3 and 4. +- Domain types ([source of truth](https://github.com/wasp-lang/wasp/blob/master/waspc/src/Wasp/Analyzer/StdTypeDefinitions.hs)) + - Declaration types + - **app** + - **page** + - **route** + - **query** + - **action** + - **entity** + - Enum types + - **AuthMethod** + - **DbSystem** + + +For more details about each of the domain types, both regarding their body types and what they mean, check the [Features](/language/features.md) section. diff --git a/web/docs/tutorials/todo-app/auth.md b/web/docs/tutorials/todo-app/auth.md index ba63ad492..f14a89306 100644 --- a/web/docs/tutorials/todo-app/auth.md +++ b/web/docs/tutorials/todo-app/auth.md @@ -33,18 +33,19 @@ wasp db migrate-dev ``` to propagate the schema change (we added User). -## Defining `auth` declaration -Next, we want to tell Wasp that we want full-stack [authentication](language/basic-elements.md#authentication--authorization) in our app, and that it should use entity `User` for it: +## Defining `app.auth` +Next, we want to tell Wasp that we want full-stack [authentication](language/features.md#authentication--authorization) in our app, and that it should use entity `User` for it: -```c title="main.wasp" -// ... +```c {4-9} title="main.wasp" +app TodoApp { + title: "Todo app", -auth { - // Expects entity User to have (email:String) and (password:String) fields. - userEntity: User, - methods: [ EmailAndPassword ], // More methods coming soon! - - onAuthFailedRedirectTo: "/login" // We'll see how this is used a bit later + auth: { + // Expects entity User to have (email:String) and (password:String) fields. + userEntity: User, + methods: [ EmailAndPassword ], // More methods coming soon! + onAuthFailedRedirectTo: "/login" // We'll see how this is used a bit later + } } ``` What this means for us is that Wasp now offers us: @@ -55,29 +56,29 @@ What this means for us is that Wasp now offers us: This is a very high-level API for auth which makes it very easy to get started quickly, but is not very flexible. If you require more control (e.g. want to execute some custom code on the server -during signup, check out [lower-level auth API](/docs/language/basic-elements#lower-level-api). +during signup, check out [lower-level auth API](/docs/language/features#lower-level-api). Ok, that was easy! -To recap, so far we have created: +To recap, so far we have defined: - `User` entity. -- `auth` declaration thanks to which Wasp gives us plenty of auth functionality. +- `app.auth` field, thanks to which Wasp gives us plenty of auth functionality. ## Adding Login and Signup pages -When we declared `auth` we got login and signup forms generated for us, but now we have to use them in their pages. In our `main.wasp` we'll add the following: +When we defined `app.auth` we got login and signup forms generated for us, but now we have to use them in their pages. In our `main.wasp` file we'll add the following: ```c title="main.wasp" // ... -route "/signup" -> page Signup -page Signup { - component: import Signup from "@ext/SignupPage" +route SignupRoute { path: "/signup", to: SignupPage } +page SignupPage { + component: import Signup from "@ext/SignupPage" } -route "/login" -> page Login -page Login { - component: import Login from "@ext/LoginPage" +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import Login from "@ext/LoginPage" } ``` @@ -128,22 +129,22 @@ export default SignupPage ``` -## Updating Main page to check if user is authenticated +## Updating `MainPage` page to check if user is authenticated -Now, let's see how are we going to handle the situation when user is not logged in. `Main` page is a private -page and we want users to be able to see it only if they are authenticated. +Now, let's see how are we going to handle the situation when user is not logged in. +`MainPage` page is a private page and we want users to be able to see it only if they are authenticated. There is a specific Wasp feature that allows us to achieve this in a simple way: ```c {3} title="main.wasp" // ... -page Main { +page MainPage { authRequired: true, component: import Main from "@ext/MainPage.js" } ``` -With `authRequired: true` we declared that page `Main` is accessible only to the authenticated users. -If an unauthenticated user tries to access route `/` where our page `Main` is, they will be redirected to `/login` as specified with `onAuthFailedRedirectTo` property in `auth`. +With `authRequired: true` we declared that page `MainPage` is accessible only to the authenticated users. +If an unauthenticated user tries to access route `/` where our page `MainPage` is, they will be redirected to `/login` as specified with `onAuthFailedRedirectTo` property in `app.auth`. Also, when `authRequired` is set to `true`, the React component of a page (specified by `component` property within `page`) will be provided `user` object as a prop. It can be accessed like this: diff --git a/web/docs/tutorials/todo-app/creating-new-project.md b/web/docs/tutorials/todo-app/creating-new-project.md index e7dc8f7ed..1c52163b8 100644 --- a/web/docs/tutorials/todo-app/creating-new-project.md +++ b/web/docs/tutorials/todo-app/creating-new-project.md @@ -45,18 +45,18 @@ TodoApp/ ``` Let's start with `main.wasp` file which introduces 3 new concepts: -[app](language/basic-elements.md#app), -[page](language/basic-elements.md#page) and -[route](language/basic-elements.md#route). +[app](language/features.md#app), +[page](language/features.md#page) and +[route](language/features.md#route). ```c title="main.wasp" app TodoApp { // Main declaration, defines a new web app. - title: "TodoApp" // Used as a browser tab title. + title: "Todo app" // Used as a browser tab title. } -route "/" -> page Main // Render page Main on url `/` (default url). +route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/` (default url). -page Main { +page MainPage { // We specify that ReactJS implementation of our page can be // found in `ext/MainPage.js` as a default export (uses standard // js import syntax). @@ -64,7 +64,7 @@ page Main { } ``` -And now to that React component we referenced in the `page Main { ... }` declaration in `main.wasp`: +And now to that React component we referenced in the `page MainPage { ... }` declaration in `main.wasp`: ```jsx title="ext/MainPage.js" import React from 'react' import waspLogo from './waspLogo.png' diff --git a/web/docs/tutorials/todo-app/dependencies.md b/web/docs/tutorials/todo-app/dependencies.md index e7d6117cd..1bf2f993f 100644 --- a/web/docs/tutorials/todo-app/dependencies.md +++ b/web/docs/tutorials/todo-app/dependencies.md @@ -6,15 +6,19 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; What is a Todo app without some clocks!? Well, still a Todo app, but certainly not as fun as one with the clocks! -So, let's add a couple of clocks to our app, to help us track time while we perform our tasks (and to demonstrate `dependencies` feature). +So, let's add a couple of clocks to our app, to help us track time while we perform our tasks (and to demonstrate `app.dependencies` feature). -For this, we will use `react-clock` library from NPM. We can add it to our project as a [dependency](language/basic-elements.md#dependencies) like this: -```c title="main.wasp" -// ... +For this, we will use `react-clock` library from NPM. We can add it to our project as a [dependency](language/features.md#dependencies) like this: +```c {6-8} title="main.wasp" +app TodoApp { + title: "Todo app", -dependencies {=json - "react-clock": "3.0.0" -json=} + // ... + + dependencies: [ + ("react-clock", "3.0.0") + ] +} ``` Run (if it is already running, stop it first and then run it again) diff --git a/web/docs/tutorials/todo-app/listing-tasks.md b/web/docs/tutorials/todo-app/listing-tasks.md index 0141ed8ef..2fa4c0999 100644 --- a/web/docs/tutorials/todo-app/listing-tasks.md +++ b/web/docs/tutorials/todo-app/listing-tasks.md @@ -8,7 +8,7 @@ We want to admire our tasks, so let's list them! ## Introducing operations (queries and actions) -The primary way of interacting with entities in Wasp is via [operations (queries and actions)](language/basic-elements.md#queries-and-actions-aka-operations). +The primary way of interacting with entities in Wasp is via [operations (queries and actions)](language/features.md#queries-and-actions-aka-operations). Queries are here when we need to fetch/read something, while actions are here when we need to change/update something. We will start with writing a query, since we are just listing tasks and not modifying anything for now. @@ -19,7 +19,7 @@ To list tasks, we will need two things: ## Wasp query -Let's implement `getTasks` [query](language/basic-elements.md#query). +Let's implement `getTasks` [query](language/features.md#query). It consists of a declaration in Wasp and implementation in JS (in `ext/` directory). ### Wasp declaration @@ -100,7 +100,7 @@ export default MainPage All of this is just regular React, except for the two special `@wasp` imports: - `import getTasks from '@wasp/queries/getTasks'`: provides us with our freshly defined Wasp query. - - `import { useQuery } from '@wasp/queries'`: provides us with Wasp's [useQuery](language/basic-elements.md#usequery) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query) [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp. + - `import { useQuery } from '@wasp/queries'`: provides us with Wasp's [useQuery](language/features.md#usequery) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query) [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp. While we could call query directly as `getTasks()`, calling it as `useQuery(getTasks)` gives us the reactivity (React component gets re-rendered if result of the query changes). diff --git a/web/docs/tutorials/todo-app/task-entity.md b/web/docs/tutorials/todo-app/task-entity.md index a751f65cb..1d08bf91e 100644 --- a/web/docs/tutorials/todo-app/task-entity.md +++ b/web/docs/tutorials/todo-app/task-entity.md @@ -4,7 +4,7 @@ title: "Task entity" import useBaseUrl from '@docusaurus/useBaseUrl'; -[Entities](language/basic-elements.md#entity) are one of the very central concepts in Wasp, and they mainly play the role of data models. +[Entities](language/features.md#entity) are one of the very central concepts in Wasp, and they mainly play the role of data models. Since our TodoApp is all about tasks, we will define Task entity in Wasp: ```c title="main.wasp" diff --git a/web/docusaurus.config.js b/web/docusaurus.config.js index 7c9c4e302..b4a80dbbe 100644 --- a/web/docusaurus.config.js +++ b/web/docusaurus.config.js @@ -71,7 +71,7 @@ module.exports = { }, { label: 'Reference', - to: 'docs/language/basic-elements' + to: 'docs/language/features' } ] }, diff --git a/web/sidebars.js b/web/sidebars.js index de5f71007..a8edf79e0 100644 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -46,7 +46,8 @@ module.exports = { collapsed: false, items: [ 'language/overview', - 'language/basic-elements' + 'language/syntax', + 'language/features' ] }, 'cli', diff --git a/web/src/pages/index.js b/web/src/pages/index.js index 54282c707..980879075 100644 --- a/web/src/pages/index.js +++ b/web/src/pages/index.js @@ -121,19 +121,19 @@ function HeroCodeExample() { const createAppWaspCode = `app todoApp { - title: "ToDo App" /* visible in tab */ + title: "ToDo App", /* visible in tab */ + + auth: { /* full-stack auth out-of-the-box */ + userEntity: User, + methods: [ EmailAndPassword ], + } } -route "/" -> page Main -page Main { +route RootRoute { path: "/", to: MainPage } +page MainPage { /* import your React code */ component: import Main from "@ext/Main.js" } - -auth { /* full-stack auth out-of-the-box */ - userEntity: User, - methods: [ EmailAndPassword ], -} ` return ( @@ -162,8 +162,8 @@ app todoApp { } /* routing */ -route "/" -> page Main -page Main { +route RootRoute { path: "/", to: MainPage } +page MainPage { component: import Main from "@ext/Main" /* import your React code */ } ` @@ -193,15 +193,19 @@ export default () => Hello World! ) } else if (currentCodeExample === CodeExample.ADD_AUTH) { const exampleCode = -`/* ... */ +`app todoApp { + /* ... */ -/* full-stack auth out-of-the-box */ -auth { - userEntity: User, - methods: [ EmailAndPassword ], /* more methods coming soon */ - onAuthFailedRedirectTo: "/login" + /* full-stack auth out-of-the-box */ + auth: { + userEntity: User, + methods: [ EmailAndPassword ], /* more methods coming soon */ + onAuthFailedRedirectTo: "/login" + } } +/* ... */ + /* email & password required because of the auth method above */ entity User {=psl id Int @id @default(autoincrement()) @@ -209,7 +213,7 @@ entity User {=psl password String psl=} -page Main { +page MainPage { authRequired: true, /* available only to logged in users */ component: import Main from "@ext/Main" } @@ -236,7 +240,7 @@ export default ({ user }) => {
To learn more about authentication & authorization in Wasp, check - the docs. + the docs.
) @@ -292,7 +296,7 @@ export default () => {
To learn more about working with data in Wasp, check - the docs. + the docs.
)