Updated Wasp AI to work with wasp 0.12. (#1726)

This commit is contained in:
Martin Šošić 2024-02-09 11:38:16 +01:00 committed by GitHub
parent 814b82bd58
commit 03d234fd7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 97 additions and 272 deletions

View File

@ -26,7 +26,7 @@ import Wasp.AI.GenerateNewProject.Operation
) )
import Wasp.AI.GenerateNewProject.OperationsJsFile (fixOperationsJsFile) import Wasp.AI.GenerateNewProject.OperationsJsFile (fixOperationsJsFile)
import Wasp.AI.GenerateNewProject.Page (generateAndWritePage, getPageComponentPath) import Wasp.AI.GenerateNewProject.Page (generateAndWritePage, getPageComponentPath)
import Wasp.AI.GenerateNewProject.PageComponentFile (fixImportsInPageComponentFile, fixPageComponent) import Wasp.AI.GenerateNewProject.PageComponentFile (fixPageComponent)
import Wasp.AI.GenerateNewProject.Plan (generatePlan) import Wasp.AI.GenerateNewProject.Plan (generatePlan)
import qualified Wasp.AI.GenerateNewProject.Plan as Plan import qualified Wasp.AI.GenerateNewProject.Plan as Plan
import Wasp.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeletonAndPresetFiles) import Wasp.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeletonAndPresetFiles)
@ -99,12 +99,6 @@ generateNewProject newProjectDetails waspProjectSkeletonFiles = do
writeToLog $ "Fixed '" <> fromString pageFp <> "' page." writeToLog $ "Fixed '" <> fromString pageFp <> "' page."
writeToLog "Pages fixed." writeToLog "Pages fixed."
writeToLogFixing "import mistakes in pages..."
forM_ (getPageComponentPath <$> pages) $ \pageFp -> do
fixImportsInPageComponentFile pageFp queries actions
writeToLog $ "Fixed '" <> fromString pageFp <> "' page."
writeToLog "Imports in pages fixed."
(promptTokensUsed, completionTokensUsed) <- getTotalTokensUsage (promptTokensUsed, completionTokensUsed) <- getTotalTokensUsage
writeToLog $ writeToLog $
"\nTotal tokens usage: " "\nTotal tokens usage: "

View File

@ -74,7 +74,7 @@ waspFileExample =
onAuthFailedRedirectTo: "/login" onAuthFailedRedirectTo: "/login"
}, },
client: { client: {
rootComponent: import { Layout } from "@client/Layout.jsx", rootComponent: import { Layout } from "@src/Layout.jsx",
}, },
db: { db: {
prisma: { prisma: {
@ -85,24 +85,22 @@ waspFileExample =
route SignupRoute { path: "/signup", to: SignupPage } route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage { page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx" component: import Signup from "@src/pages/auth/Signup.jsx"
} }
route LoginRoute { path: "/login", to: LoginPage } route LoginRoute { path: "/login", to: LoginPage }
page LoginPage { page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx" component: import Login from "@src/pages/auth/Login.jsx"
} }
route DashboardRoute { path: "/", to: DashboardPage } route DashboardRoute { path: "/", to: DashboardPage }
page DashboardPage { page DashboardPage {
authRequired: true, authRequired: true,
component: import Dashboard from "@client/pages/Dashboard.jsx" component: import Dashboard from "@src/pages/Dashboard.jsx"
} }
entity User {=psl entity User {=psl
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[] tasks Task[]
psl=} psl=}
@ -115,22 +113,22 @@ waspFileExample =
psl=} psl=}
query getUser { query getUser {
fn: import { getUser } from "@server/queries.js", fn: import { getUser } from "@src/queries.js",
entities: [User] // Entities that this query operates on. entities: [User] // Entities that this query operates on.
} }
query getTasks { query getTasks {
fn: import { getTasks } from "@server/queries.js", fn: import { getTasks } from "@src/queries.js",
entities: [Task] entities: [Task]
} }
action createTask { action createTask {
fn: import { createTask } from "@server/actions.js", fn: import { createTask } from "@src/actions.js",
entities: [Task] entities: [Task]
} }
action updateTask { action updateTask {
fn: import { updateTask } from "@server/actions.js", fn: import { updateTask } from "@src/actions.js",
entities: [Task] entities: [Task]
} }
``` ```

View File

@ -97,7 +97,7 @@ generateOperation operationType newProjectDetails entityPlans operationPlan = do
Example of response: Example of response:
{ "opWaspDecl": "${operationTypeText} ${operationName} {\n fn: import { ${operationName} } from \"${operationFnPath}\",\n entities: [Task]\n}", { "opWaspDecl": "${operationTypeText} ${operationName} {\n fn: import { ${operationName} } from \"${operationFnPath}\",\n entities: [Task]\n}",
"opJsImpl": "export const {$operationName} = async (args, context) => { ... }", "opJsImpl": "export const {$operationName} = async (args, context) => { ... }",
"opJsImports": "import HttpError from '@wasp/core/HttpError.js'" "opJsImports": "import { HttpError } from 'wasp/server'"
} }
"opWaspDecl" and "opJsImpl" are required, "opJsImports" you can skip if none are needed. "opWaspDecl" and "opJsImpl" are required, "opJsImports" you can skip if none are needed.
There should be no other text in the response, just valid JSON. There should be no other text in the response, just valid JSON.
@ -108,7 +108,7 @@ generateOperation operationType newProjectDetails entityPlans operationPlan = do
Instead, always write real implementation! Instead, always write real implementation!
- Don't import prisma client in the JS imports, it is not needed. - Don't import prisma client in the JS imports, it is not needed.
- In wasp declaration (`opWaspDecl`), make sure to use ',' before `entities:`. - In wasp declaration (`opWaspDecl`), make sure to use ',' before `entities:`.
Also, make sure to use full import statement for `fn:`: `import { getTasks } from "@server/actions.js"`, Also, make sure to use full import statement for `fn:`: `import { getTasks } from "@src/actions.js"`,
don't provide just the file path. don't provide just the file path.
- In NodeJS implementation, you will typically want to check if user is authenticated, by doing `if (!context.user) { throw new HttpError(401) }` at the start of the operation. - In NodeJS implementation, you will typically want to check if user is authenticated, by doing `if (!context.user) { throw new HttpError(401) }` at the start of the operation.
@ -160,14 +160,14 @@ actionDocPrompt =
- Wasp declaration: - Wasp declaration:
```wasp ```wasp
action updateTask { action updateTask {
fn: import { updateTask } from "@server/taskActions.js", fn: import { updateTask } from "@src/taskActions.js",
entities: [Task] // Entities that action mutates. entities: [Task] // Entities that action mutates.
} }
``` ```
- NodeJS implementation (with imports): - NodeJS implementation (with imports):
```js ```js
import HttpError from '@wasp/core/HttpError.js' import { HttpError } from 'wasp/server'
export const updateTask = async (args, context) => { export const updateTask = async (args, context) => {
if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated. if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated.
@ -189,14 +189,14 @@ actionDocPrompt =
```wasp ```wasp
action deleteList { action deleteList {
fn: import { deleteList } from "@server/actions.js", fn: import { deleteList } from "@src/actions.js",
entities: [List, Card] entities: [List, Card]
} }
``` ```
- NodeJS implementation (with imports): - NodeJS implementation (with imports):
```js ```js
import HttpError from '@wasp/core/HttpError.js' import { HttpError } from 'wasp/server'
export const deleteList = async ({ listId }, context) => { export const deleteList = async ({ listId }, context) => {
if (!context.user) { throw new HttpError(401) }; if (!context.user) { throw new HttpError(401) };
@ -232,14 +232,14 @@ queryDocPrompt =
- Wasp declaration: - Wasp declaration:
```wasp ```wasp
query fetchFilteredTasks { query fetchFilteredTasks {
fn: import { getFilteredTasks } from "@server/taskQueries.js", fn: import { getFilteredTasks } from "@src/taskQueries.js",
entities: [Task] // Entities that query uses. entities: [Task] // Entities that query uses.
} }
``` ```
- NodeJS implementation (with imports): - NodeJS implementation (with imports):
```js ```js
import HttpError from '@wasp/core/HttpError.js' import { HttpError } from 'wasp/server'
export const getFilteredTasks = async (args, context) => { export const getFilteredTasks = async (args, context) => {
if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated. if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated.
@ -259,29 +259,28 @@ queryDocPrompt =
- Wasp declaration: - Wasp declaration:
```wasp ```wasp
query getAuthor { query getAuthor {
fn: import { getAuthor } from "@server/author/queries.js", fn: import { getAuthor } from "@src/author/queries.js",
entities: [Author] entities: [Author]
} }
``` ```
- NodeJS implementation (with imports): - NodeJS implementation (with imports):
```js ```js
import HttpError from '@wasp/core/HttpError.js' import { HttpError } from 'wasp/server'
export const getAuthor = async ({ username }, context) => { export const getAuthor = async ({ id }, context) => {
// Here we don't check if user is authenticated as this query is public. // Here we don't check if user is authenticated as this query is public.
const author = await context.entities.Author.findUnique({ const author = await context.entities.Author.findUnique({
where: { username }, where: { id },
select: { select: {
username: true,
id: true, id: true,
bio: true, bio: true,
profilePictureUrl: true profilePictureUrl: true
} }
}); });
if (!author) throw new HttpError(404, 'No author with username ' + username); if (!author) throw new HttpError(404, 'No author with id ' + id);
return author; return author;
} }
@ -355,9 +354,8 @@ writeOperationToJsFile operation =
getOperationJsFilePath :: Operation -> FilePath getOperationJsFilePath :: Operation -> FilePath
getOperationJsFilePath operation = resolvePath $ Plan.opFnPath $ opPlan operation getOperationJsFilePath operation = resolvePath $ Plan.opFnPath $ opPlan operation
where where
pathPrefix = "@server/" resolvePath p | "@src/" `isPrefixOf` p = drop (length ("@" :: String)) p
resolvePath p | pathPrefix `isPrefixOf` p = "src/" <> drop (length ("@" :: String)) p resolvePath _ = error "path incorrectly formatted, should start with @src/ ."
resolvePath _ = error "path incorrectly formatted, should start with " <> pathPrefix <> "."
writeOperationToWaspFile :: FilePath -> Operation -> CodeAgent () writeOperationToWaspFile :: FilePath -> Operation -> CodeAgent ()
writeOperationToWaspFile waspFilePath operation = writeOperationToWaspFile waspFilePath operation =

View File

@ -109,7 +109,7 @@ generatePage newProjectDetails entityPlans queries actions pPlan = do
Example of such JSON: Example of such JSON:
{ {
"pageWaspDecl": "route ExampleRoute { path: \"/\", to: ExamplePage }\npage ExamplePage {\n component: import ExamplePage from \"@client/ExamplePage.jsx\",\n authRequired: true\n}", "pageWaspDecl": "route ExampleRoute { path: \"/\", to: ExamplePage }\npage ExamplePage {\n component: import ExamplePage from \"@src/ExamplePage.jsx\",\n authRequired: true\n}",
"pageJsImpl": "JS imports + React component implementing the page.", "pageJsImpl": "JS imports + React component implementing the page.",
} }
There should be no other text in the response. There should be no other text in the response.
@ -134,7 +134,7 @@ makePageDocPrompt =
```wasp ```wasp
route TasksRoute { path: "/", to: ExamplePage } route TasksRoute { path: "/", to: ExamplePage }
page TasksPage { page TasksPage {
component: import Tasks from "@client/Tasks.jsx", component: import Tasks from "@src/Tasks.jsx",
authRequired: true authRequired: true
} }
``` ```
@ -143,11 +143,13 @@ makePageDocPrompt =
```jsx ```jsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useQuery } from '@wasp/queries'; // A thin wrapper around react-query's useQuery import {
import { useAction } from '@wasp/actions'; // A thin wrapper around react-query's useMutation useQuery, // A thin wrapper around react-query's useQuery
import getTasks from '@wasp/queries/getTasks'; useAction, // A thin wrapper around react-query's useMutation
import createTask from '@wasp/actions/createTask'; getTasks, // query
import toggleTask from '@wasp/actions/toggleTask'; createTask, // action
toggleTask // action
} from 'wasp/client/operations';
const Tasks = () => { const Tasks = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks); const { data: tasks, isLoading, error } = useQuery(getTasks);
@ -208,7 +210,7 @@ makePageDocPrompt =
```wasp ```wasp
route DashboardRoute { path: "/dashboard", to: DashboardPage } route DashboardRoute { path: "/dashboard", to: DashboardPage }
page DashboardPage { page DashboardPage {
component: import Dashboard from "@client/Dashboard.jsx", component: import Dashboard from "@src/Dashboard.jsx",
authRequired: true authRequired: true
} }
``` ```
@ -218,10 +220,7 @@ makePageDocPrompt =
```jsx ```jsx
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useQuery } from '@wasp/queries'; import { useQuery, useAction, getUsers, deleteUser } from 'wasp/client/operations';
import { useAction } from '@wasp/actions';
import getUsers from '@wasp/queries/getUsers';
import deleteUser from '@wasp/actions/deleteUser';
const DashboardPage = () => { const DashboardPage = () => {
const { data: users, isLoading, error } = useQuery(getUsers); const { data: users, isLoading, error } = useQuery(getUsers);
@ -266,15 +265,11 @@ makePageDocPrompt =
Here are the rules for importing actions and queries. Here are the rules for importing actions and queries.
If a query is called "myQuery", its import MUST BE `import myQuery from '@wasp/queries/myQuery';`. If a query is called "myQuery", its import MUST BE `import { myQuery } from 'wasp/client/operations';`.
More generally, a query import MUST BE a default import, and name of the file is the same as name of the query.
The hook for wrapping queries is called `useQuery`. The hook for wrapping queries is called `useQuery`.
Use a single import statement per query.
If an action is called "myAction", its import MUST BE `import myAction from '@wasp/actions/myAction';`. If an action is called "myAction", its import MUST BE `import { myAction } from 'wasp/client/operations';`.
More generally, an action import MUST BE a default import, and name of the file is the same as name of the action.
The hook for wrapping actions is called `useAction`. The hook for wrapping actions is called `useAction`.
Use a single import statement per action.
Note: There is no `useMutation` hook in Wasp. Note: There is no `useMutation` hook in Wasp.
|] |]
@ -303,9 +298,8 @@ getPageComponentPath :: Page -> String
getPageComponentPath page = path getPageComponentPath page = path
where where
path = resolvePath $ Plan.componentPath $ pagePlan page path = resolvePath $ Plan.componentPath $ pagePlan page
pathPrefix = "@client/" resolvePath p | "@src/" `isPrefixOf` p = drop (length ("@" :: String)) p
resolvePath p | pathPrefix `isPrefixOf` p = "src/" <> drop (length ("@" :: String)) p resolvePath _ = error "path incorrectly formatted, should start with @src/."
resolvePath _ = error "path incorrectly formatted, should start with " <> pathPrefix <> "."
writePageToWaspFile :: FilePath -> Page -> CodeAgent () writePageToWaspFile :: FilePath -> Page -> CodeAgent ()
writePageToWaspFile waspFilePath page = writePageToWaspFile waspFilePath page =

View File

@ -2,27 +2,15 @@
module Wasp.AI.GenerateNewProject.PageComponentFile module Wasp.AI.GenerateNewProject.PageComponentFile
( fixPageComponent, ( fixPageComponent,
fixImportsInPageComponentFile,
-- NOTE: Exports below are exported only for testing!
getPageComponentFileContentWithFixedImports,
partitionComponentFileByImports,
getImportedNamesFromImport,
getAllPossibleWaspJsClientImports,
) )
where where
import Control.Arrow (first)
import Data.Aeson (FromJSON) import Data.Aeson (FromJSON)
import Data.List (intercalate, isInfixOf, isPrefixOf, partition, stripPrefix) import Data.Maybe (fromMaybe)
import Data.List.Extra (nub)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromJust, fromMaybe, mapMaybe)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import GHC.Generics (Generic) import GHC.Generics (Generic)
import NeatInterpolation (trimming) import NeatInterpolation (trimming)
import Text.Printf (printf)
import Wasp.AI.CodeAgent (getFile, writeToFile) import Wasp.AI.CodeAgent (getFile, writeToFile)
import Wasp.AI.GenerateNewProject.Common import Wasp.AI.GenerateNewProject.Common
( CodeAgent, ( CodeAgent,
@ -33,93 +21,8 @@ import Wasp.AI.GenerateNewProject.Common
) )
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock) import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts
import Wasp.AI.GenerateNewProject.Operation (Operation)
import qualified Wasp.AI.GenerateNewProject.Operation as Operation
import Wasp.AI.GenerateNewProject.Page (makePageDocPrompt) import Wasp.AI.GenerateNewProject.Page (makePageDocPrompt)
import qualified Wasp.AI.GenerateNewProject.Plan as Plan
import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..)) import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
import Wasp.Util (trim)
fixImportsInPageComponentFile :: FilePath -> [Operation] -> [Operation] -> CodeAgent ()
fixImportsInPageComponentFile pageComponentPath queries actions = do
currentPageComponentContent <- fromMaybe (error "couldn't find page file to fix") <$> getFile pageComponentPath
let fixedComponentContent = getPageComponentFileContentWithFixedImports currentPageComponentContent allPossibleWaspImports
writeToFile pageComponentPath (const fixedComponentContent)
where
allPossibleWaspImports = getAllPossibleWaspJsClientImports $ queries ++ actions
getPageComponentFileContentWithFixedImports :: Text -> Map String String -> Text
getPageComponentFileContentWithFixedImports pageComponentContent allPossibleWaspImports =
T.intercalate "\n" [nonWaspImportsText, fixedWaspImportsText, remainingCodeText]
where
fixedWaspImportsText = T.pack $ intercalate "\n" $ mapMaybe (`M.lookup` allPossibleWaspImports) importedNames
nonWaspImportsText = T.pack $ intercalate "\n" nonWaspImports
remainingCodeText = T.pack $ intercalate "\n" remainingCode
importedNames = nub $ concatMap getImportedNamesFromImport waspImports
(waspImports, nonWaspImports, remainingCode) = partitionComponentFileByImports pageComponentContent
-- NOTE: Doesn't work correctly for imports that use `as` keyword!
getImportedNamesFromImport :: String -> [String]
getImportedNamesFromImport =
nub
. words
. map convertSpecialCharToSpace
. trim
. removeSuffix "from"
. trim
. removePrefix "import"
. trim
. takeWhile (not . (`elem` ['"', '\'']))
where
convertSpecialCharToSpace char
| char `elem` [',', '}', '{'] = ' '
| otherwise = char
removePrefix prefix = fromJust . stripPrefix prefix
removeSuffix suffix = reverse . removePrefix (reverse suffix) . reverse
partitionComponentFileByImports :: Text -> ([String], [String], [String])
partitionComponentFileByImports componentContent = (waspImportLines, nonWaspImportLines, "" : remainingCodeLines)
where
(waspImportLines, nonWaspImportLines) = partition isWaspImportLine importLines
(importLines, remainingCodeLines) =
first cleanUpImportLines $
span isImportLineOrEmpty $ lines $ T.unpack componentContent
isImportLineOrEmpty l = let l' = trim l in "import" `isPrefixOf` l' || null l'
isWaspImportLine = ("@wasp" `isInfixOf`)
cleanUpImportLines = filter (not . null) . fmap trim
-- | Given a list of all operations in the app, it returns a list of all possible @@wasp imports
-- that a Page could import. Those are imports for the specified operations, but also some general
-- imports like login/logouts, hooks, ... .
-- Each entry in the returned map is one possible @@wasp import, where key is imported symbol
-- while import statement is the value.
getAllPossibleWaspJsClientImports :: [Operation] -> M.Map String String
getAllPossibleWaspJsClientImports operations = M.fromList $ possibleUnchangingImports ++ map makeOperationImport operations
where
possibleUnchangingImports :: [(String, String)]
possibleUnchangingImports =
[ ("logout", "import logout from '@wasp/auth/logout';"),
("useAuth", "import useAuth from '@wasp/auth/useAuth';"),
("useQuery", "import { useQuery } from '@wasp/queries';"),
("useAction", "import { useAction } from '@wasp/actions';")
]
makeOperationImport :: Operation -> (String, String)
makeOperationImport operation = (opName, opImport)
where
opImport :: String
opImport = printf "import %s from '@wasp/%s/%s';" opName opType opName
opName :: String
opName = Plan.opName $ Operation.opPlan operation
opType :: String
opType = case Operation.opType operation of
Operation.Action -> "actions"
Operation.Query -> "queries"
fixPageComponent :: NewProjectDetails -> FilePath -> FilePath -> CodeAgent () fixPageComponent :: NewProjectDetails -> FilePath -> FilePath -> CodeAgent ()
fixPageComponent newProjectDetails waspFilePath pageComponentPath = do fixPageComponent newProjectDetails waspFilePath pageComponentPath = do
@ -169,6 +72,10 @@ fixPageComponent newProjectDetails waspFilePath pageComponentPath = do
- If there are any js imports of local modules (`from "./`, `from "../`), - If there are any js imports of local modules (`from "./`, `from "../`),
remove them and instead add the needed implementation directly in the file we are fixing right now. remove them and instead add the needed implementation directly in the file we are fixing right now.
- Remove redundant imports, but don't change any of the remaining ones. - Remove redundant imports, but don't change any of the remaining ones.
- Feel free to merge together imports from the same path.
e.g. if you have `import { useQuery } from 'wasp/client/operations';` and
`import { useAction } from 'wasp/client/operations';`, you can merge those into
`import { useQuery, useAction } from 'wasp/client/operations';`.
- Make sure that the component is exported as a default export. - Make sure that the component is exported as a default export.
With this in mind, generate a new, fixed React component (${pageComponentPathText}). With this in mind, generate a new, fixed React component (${pageComponentPathText}).

View File

@ -87,21 +87,21 @@ generatePlan newProjectDetails planRules = do
{ {
"entities": [{ "entities": [{
"entityName": "User", "entityName": "User",
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]" "entityBodyPsl": " id Int @id @default(autoincrement())\n tasks Task[]"
}], }],
"actions": [{ "actions": [{
"opName": "createTask", "opName": "createTask",
"opFnPath": "@server/actions.js", "opFnPath": "@src/actions.js",
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task." "opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
}], }],
"queries": [{ "queries": [{
"opName": "getTask", "opName": "getTask",
"opFnPath": "@server/queries.js", "opFnPath": "@src/queries.js",
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them." "opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
}], }],
"pages": [{ "pages": [{
"pageName": "TaskPage", "pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx", "componentPath": "@src/pages/Task.jsx",
"routeName: "TaskRoute", "routeName: "TaskRoute",
"routePath": "/task/:taskId", "routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.", "pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
@ -273,10 +273,10 @@ checkPlanForOperationIssues opType plan =
else [] else []
checkOperationFnPath op = checkOperationFnPath op =
if not ("@server" `isPrefixOf` opFnPath op) if not ("@src" `isPrefixOf` opFnPath op)
then then
[ "fn path of " <> caseOnOpType "query" "action" <> " '" <> opName op [ "fn path of " <> caseOnOpType "query" "action" <> " '" <> opName op
<> "' must start with '@server'." <> "' must start with '@src'."
] ]
else [] else []
@ -308,9 +308,9 @@ checkPlanForPageIssues plan =
else [] else []
checkPageComponentPath page = checkPageComponentPath page =
if not ("@client" `isPrefixOf` componentPath page) if not ("@src" `isPrefixOf` componentPath page)
then then
[ "component path of page '" <> pageName page <> "' must start with '@client'." [ "component path of page '" <> pageName page <> "' must start with '@src'."
] ]
else [] else []

View File

@ -35,6 +35,8 @@ generateAndWriteProjectSkeletonAndPresetFiles newProjectDetails waspProjectSkele
let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
writeNewFile waspFile writeNewFile waspFile
writeNewFile $ generatePackageJson newProjectDetails
case getProjectAuth newProjectDetails of case getProjectAuth newProjectDetails of
UsernameAndPassword -> do UsernameAndPassword -> do
writeNewFile generateLoginJsPage writeNewFile generateLoginJsPage
@ -72,10 +74,8 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
[ "App uses username and password authentication.", [ "App uses username and password authentication.",
T.unpack T.unpack
[trimming| [trimming|
App MUST have a 'User' entity, with following fields required: App MUST have a 'User' entity, with following field(s) required:
- `id Int @id @default(autoincrement())` - `id Int @id @default(autoincrement())`
- `username String @unique`
- `password String`
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`. It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
|], |],
"One of the pages in the app must have a route path \"/\"." "One of the pages in the app must have a route path \"/\"."
@ -92,7 +92,7 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
}, },
title: "${appTitle}", title: "${appTitle}",
client: { client: {
rootComponent: import { Layout } from "@client/Layout.jsx", rootComponent: import { Layout } from "@src/Layout.jsx",
}, },
db: { db: {
prisma: { prisma: {
@ -104,21 +104,47 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
route LoginRoute { path: "/login", to: LoginPage } route LoginRoute { path: "/login", to: LoginPage }
page LoginPage { page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx" component: import Login from "@src/pages/auth/Login.jsx"
} }
route SignupRoute { path: "/signup", to: SignupPage } route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage { page SignupPage {
component: import Signup from "@client/pages/auth/Signup.jsx" component: import Signup from "@src/pages/auth/Signup.jsx"
} }
|] |]
-- TODO: We have duplication here, since package.json is already defined
-- in `basic` templates file. We should find a way to reuse that, so we don't
-- have to keep stuff in sync here.
generatePackageJson :: NewProjectDetails -> File
generatePackageJson newProjectDetails =
( "package.json",
[trimming|
{
"name": "${appName}",
"dependencies": {
"wasp": "file:.wasp/out/sdk/wasp",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.1.0",
"vite": "^4.3.9",
"@types/react": "^18.0.37",
"prisma": "4.16.2"
}
}
|]
)
where
appName = T.pack $ _projectAppName newProjectDetails
generateLoginJsPage :: File generateLoginJsPage :: File
generateLoginJsPage = generateLoginJsPage =
( "src/client/pages/auth/Login.jsx", ( "src/pages/auth/Login.jsx",
[trimming| [trimming|
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { LoginForm } from "@wasp/auth/forms/Login"; import { LoginForm } from "wasp/client/auth";
export default function Login() { export default function Login() {
return ( return (
@ -153,11 +179,11 @@ generateLoginJsPage =
generateSignupJsPage :: File generateSignupJsPage :: File
generateSignupJsPage = generateSignupJsPage =
( "src/client/pages/auth/Signup.jsx", ( "src/pages/auth/Signup.jsx",
[trimming| [trimming|
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { SignupForm } from "@wasp/auth/forms/Signup"; import { SignupForm } from "wasp/client/auth";
export default function Signup() { export default function Signup() {
return ( return (
@ -201,7 +227,7 @@ generateDotEnvServerFile =
generateMainCssFile :: File generateMainCssFile :: File
generateMainCssFile = generateMainCssFile =
( "src/client/Main.css", ( "src/Main.css",
[trimming| [trimming|
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@ -217,11 +243,11 @@ generateMainCssFile =
generateLayoutComponent :: NewProjectDetails -> File generateLayoutComponent :: NewProjectDetails -> File
generateLayoutComponent newProjectDetails = generateLayoutComponent newProjectDetails =
( "src/client/Layout.jsx", ( "src/Layout.jsx",
[trimming| [trimming|
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import useAuth from '@wasp/auth/useAuth'; import { useAuth, logout } from "wasp/client/auth";
import logout from '@wasp/auth/logout'; import { getUsername } from "wasp/auth";
import "./Main.css"; import "./Main.css";
export const Layout = ({ children }) => { export const Layout = ({ children }) => {
@ -236,7 +262,7 @@ generateLayoutComponent newProjectDetails =
</Link> </Link>
{ user ? ( { user ? (
<span> <span>
Hi, {user.username}!{' '} Hi, {getUsername(user)}!{' '}
<button onClick={logout} className="text-xl2 underline"> <button onClick={logout} className="text-xl2 underline">
(Log out) (Log out)
</button> </button>

View File

@ -103,7 +103,7 @@ fixWaspFile newProjectDetails waspFilePath plan = do
For example, the following is missing ',' after the component field: For example, the following is missing ',' after the component field:
```wasp ```wasp
page ExamplePage { page ExamplePage {
component: import ExamplePage from "@client/pages/ExamplePage.jsx" // <- missing ',' component: import ExamplePage from "@src/pages/ExamplePage.jsx" // <- missing ','
authRequired: true authRequired: true
} }
``` ```
@ -111,8 +111,8 @@ fixWaspFile newProjectDetails waspFilePath plan = do
Fix these by replacing them with actual implementation. Fix these by replacing them with actual implementation.
- Strings in Wasp must use double quotes, not single quotes. - Strings in Wasp must use double quotes, not single quotes.
- Value of `fn:` field in `query` or `action` not having correct import syntax, - Value of `fn:` field in `query` or `action` not having correct import syntax,
for example it might have invalid syntax, e.g. `fn: @server/actions.js`. for example it might have invalid syntax, e.g. `fn: @src/actions.js`.
Fix these by replacing it with correct syntax, e.g. `fn: import { actionName } from "@server/actions.js"`. Fix these by replacing it with correct syntax, e.g. `fn: import { actionName } from "@src/actions.js"`.
- If two entities are in a relation, make sure that they both have a field that references the other entity. - If two entities are in a relation, make sure that they both have a field that references the other entity.
- If an entity has a field that references another entity (e.g. location), make sure to include @relation directive on that field. - If an entity has a field that references another entity (e.g. location), make sure to include @relation directive on that field.
- If an entity references another entity, make sure the ID field (e.g. locationId) of the referenced entity is also included. - If an entity references another entity, make sure the ID field (e.g. locationId) of the referenced entity is also included.

View File

@ -1,92 +0,0 @@
module AI.GenerateNewProject.PageComponentFileTest where
import qualified Data.Map as M
import NeatInterpolation (trimming)
import Test.Tasty.Hspec
import Wasp.AI.GenerateNewProject.PageComponentFile
spec_PageComponentFileTest :: Spec
spec_PageComponentFileTest = do
describe "getPageComponentFileContentWithFixedImports" $ do
let mockAllPossibleWaspClientImports =
M.fromList
[ ("useQuery", "import { useQuery } from '@wasp/queries';"),
("useAction", "import { useAction } from '@wasp/actions';"),
("useAuth", "import useAuth from '@wasp/auth/useAuth';"),
("someAction", "import someAction from '@wasp/actions/someAction';"),
("someQuery", "import someQuery from '@wasp/queries/someQuery';")
]
it "should fix incorrect @wasp imports while keeping non-@wasp imports and removing made up @wasp ones." $ do
let mockPageComponentFileContent =
[trimming|
import React from 'react';
import { useAuth } from '@wasp/authorization/useAuth';
import { useQuery, useAction } from '@wasp/queries_and_actions';
import { Link } from 'react-router';
import madeUpThingy from '@wasp/madeup';
import { someAction } from '@wasp/actions';
function HomePage () {
...
}
|]
getPageComponentFileContentWithFixedImports
mockPageComponentFileContent
mockAllPossibleWaspClientImports
`shouldBe` [trimming|
import React from 'react';
import { Link } from 'react-router';
import useAuth from '@wasp/auth/useAuth';
import { useQuery } from '@wasp/queries';
import { useAction } from '@wasp/actions';
import someAction from '@wasp/actions/someAction';
function HomePage () {
...
}
|]
describe "getImportedNameFromImport" $ do
it "should correctly pick up names from various imports statements" $ do
getImportedNamesFromImport "import { foo, barBar} from 'some/path'"
`shouldBe` ["foo", "barBar"]
getImportedNamesFromImport " import fooFoo from \"../\" "
`shouldBe` ["fooFoo"]
describe "partitionComponentFileByImports" $ do
it "should partition file content to wasp imports, non-wasp imports, and the rest of the file." $ do
let fileContent =
"\n"
<> [trimming|
import React from 'react';
import { useAuth } from '@wasp/useAuth';
import { useQuery } from '@wasp/queries';
import { someAction } from '@wasp/actions';
import { Link } from 'react-router';
function importStuff () {
importStuffNow("@wasp");
}
const a = 5;
|]
<> "\n\n"
partitionComponentFileByImports fileContent
`shouldBe` ( [ "import { useAuth } from '@wasp/useAuth';",
"import { useQuery } from '@wasp/queries';",
"import { someAction } from '@wasp/actions';"
],
[ "import React from 'react';",
"import { Link } from 'react-router';"
],
[ "",
"function importStuff () {",
" importStuffNow(\"@wasp\");",
"}",
"",
"const a = 5;",
""
]
)

View File

@ -52,6 +52,7 @@ data-files:
Cli/templates/basic/main.wasp Cli/templates/basic/main.wasp
Cli/templates/skeleton/.gitignore Cli/templates/skeleton/.gitignore
Cli/templates/skeleton/.wasproot Cli/templates/skeleton/.wasproot
Cli/templates/skeleton/public/.gitignore
Cli/templates/skeleton/src/.waspignore Cli/templates/skeleton/src/.waspignore
Lsp/templates/**/*.js Lsp/templates/**/*.js
Lsp/templates/**/*.ts Lsp/templates/**/*.ts
@ -573,7 +574,6 @@ test-suite waspc-test
, tasty-quickcheck ^>= 0.10 , tasty-quickcheck ^>= 0.10
, tasty-golden ^>= 2.3.5 , tasty-golden ^>= 2.3.5
other-modules: other-modules:
AI.GenerateNewProject.PageComponentFileTest
AI.GenerateNewProject.LogMsgTest AI.GenerateNewProject.LogMsgTest
Analyzer.Evaluation.EvaluationErrorTest Analyzer.Evaluation.EvaluationErrorTest
Analyzer.EvaluatorTest Analyzer.EvaluatorTest