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

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

View File

@ -38,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 {

View File

@ -3,55 +3,64 @@ app Conduit {
head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
],
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=}

View File

@ -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=}
psl=}

View File

@ -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=}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,12 +136,12 @@ CLI is actually `wasp` executable, and it uses the library, where most of the lo
Wasp compiler takes .wasp files + everything in the `ext/` dir (JS, HTML, ...) and generates a web app that consists of client, server and database.
Wasp compiler 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`).

View File

@ -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\"",
"}"
]

View File

@ -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)

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
app todoApp {
title: "ToDo App",
head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
]
}
dependencies {=json
"@material-ui/core": "4.11.3"
json=}
auth {
title: "ToDo App",
head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
],
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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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})

View File

@ -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

View File

@ -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

View File

@ -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'),

View File

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

View File

@ -10,7 +10,6 @@ import Wasp.AppSpec.Core.Decl (IsDecl)
import Wasp.AppSpec.Core.Ref (Ref)
import Wasp.AppSpec.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.

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

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

View File

@ -12,7 +12,7 @@ import qualified Data.Version
import qualified Paths_waspc
import 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.

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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 ++ " }")

View File

@ -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"

View File

@ -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)
]
)
]

View File

@ -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)
]

View File

@ -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 =

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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.

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@ module Wasp.Util
indent,
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ resolver: ./stack-snapshot.yaml
packages:
- .
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.

View File

@ -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 ")
)

View File

@ -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

View File

@ -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
}

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