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.Page (generateAndWritePage, getPageComponentPath)
import Wasp.AI.GenerateNewProject.PageComponentFile (fixImportsInPageComponentFile, fixPageComponent)
import Wasp.AI.GenerateNewProject.PageComponentFile (fixPageComponent)
import Wasp.AI.GenerateNewProject.Plan (generatePlan)
import qualified Wasp.AI.GenerateNewProject.Plan as Plan
import Wasp.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeletonAndPresetFiles)
@ -99,12 +99,6 @@ generateNewProject newProjectDetails waspProjectSkeletonFiles = do
writeToLog $ "Fixed '" <> fromString pageFp <> "' page."
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
writeToLog $
"\nTotal tokens usage: "

View File

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

View File

@ -97,7 +97,7 @@ generateOperation operationType newProjectDetails entityPlans operationPlan = do
Example of response:
{ "opWaspDecl": "${operationTypeText} ${operationName} {\n fn: import { ${operationName} } from \"${operationFnPath}\",\n entities: [Task]\n}",
"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.
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!
- Don't import prisma client in the JS imports, it is not needed.
- 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.
- 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
action updateTask {
fn: import { updateTask } from "@server/taskActions.js",
fn: import { updateTask } from "@src/taskActions.js",
entities: [Task] // Entities that action mutates.
}
```
- NodeJS implementation (with imports):
```js
import HttpError from '@wasp/core/HttpError.js'
import { HttpError } from 'wasp/server'
export const updateTask = async (args, context) => {
if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated.
@ -189,14 +189,14 @@ actionDocPrompt =
```wasp
action deleteList {
fn: import { deleteList } from "@server/actions.js",
fn: import { deleteList } from "@src/actions.js",
entities: [List, Card]
}
```
- NodeJS implementation (with imports):
```js
import HttpError from '@wasp/core/HttpError.js'
import { HttpError } from 'wasp/server'
export const deleteList = async ({ listId }, context) => {
if (!context.user) { throw new HttpError(401) };
@ -232,14 +232,14 @@ queryDocPrompt =
- Wasp declaration:
```wasp
query fetchFilteredTasks {
fn: import { getFilteredTasks } from "@server/taskQueries.js",
fn: import { getFilteredTasks } from "@src/taskQueries.js",
entities: [Task] // Entities that query uses.
}
```
- NodeJS implementation (with imports):
```js
import HttpError from '@wasp/core/HttpError.js'
import { HttpError } from 'wasp/server'
export const getFilteredTasks = async (args, context) => {
if (!context.user) { throw new HttpError(401) }; // If user needs to be authenticated.
@ -259,29 +259,28 @@ queryDocPrompt =
- Wasp declaration:
```wasp
query getAuthor {
fn: import { getAuthor } from "@server/author/queries.js",
fn: import { getAuthor } from "@src/author/queries.js",
entities: [Author]
}
```
- NodeJS implementation (with imports):
```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.
const author = await context.entities.Author.findUnique({
where: { username },
where: { id },
select: {
username: true,
id: true,
bio: 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;
}
@ -355,9 +354,8 @@ writeOperationToJsFile operation =
getOperationJsFilePath :: Operation -> FilePath
getOperationJsFilePath operation = resolvePath $ Plan.opFnPath $ opPlan operation
where
pathPrefix = "@server/"
resolvePath p | pathPrefix `isPrefixOf` p = "src/" <> drop (length ("@" :: String)) p
resolvePath _ = error "path incorrectly formatted, should start with " <> pathPrefix <> "."
resolvePath p | "@src/" `isPrefixOf` p = drop (length ("@" :: String)) p
resolvePath _ = error "path incorrectly formatted, should start with @src/ ."
writeOperationToWaspFile :: FilePath -> Operation -> CodeAgent ()
writeOperationToWaspFile waspFilePath operation =

View File

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

View File

@ -2,27 +2,15 @@
module Wasp.AI.GenerateNewProject.PageComponentFile
( fixPageComponent,
fixImportsInPageComponentFile,
-- NOTE: Exports below are exported only for testing!
getPageComponentFileContentWithFixedImports,
partitionComponentFileByImports,
getImportedNamesFromImport,
getAllPossibleWaspJsClientImports,
)
where
import Control.Arrow (first)
import Data.Aeson (FromJSON)
import Data.List (intercalate, isInfixOf, isPrefixOf, partition, stripPrefix)
import Data.List.Extra (nub)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromJust, fromMaybe, mapMaybe)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
import NeatInterpolation (trimming)
import Text.Printf (printf)
import Wasp.AI.CodeAgent (getFile, writeToFile)
import Wasp.AI.GenerateNewProject.Common
( CodeAgent,
@ -33,93 +21,8 @@ import Wasp.AI.GenerateNewProject.Common
)
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
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 qualified Wasp.AI.GenerateNewProject.Plan as Plan
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 waspFilePath pageComponentPath = do
@ -169,6 +72,10 @@ fixPageComponent newProjectDetails waspFilePath pageComponentPath = do
- 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 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.
With this in mind, generate a new, fixed React component (${pageComponentPathText}).

View File

@ -87,21 +87,21 @@ generatePlan newProjectDetails planRules = do
{
"entities": [{
"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": [{
"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."
}],
"queries": [{
"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."
}],
"pages": [{
"pageName": "TaskPage",
"componentPath": "@client/pages/Task.jsx",
"componentPath": "@src/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"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 []
checkOperationFnPath op =
if not ("@server" `isPrefixOf` opFnPath op)
if not ("@src" `isPrefixOf` opFnPath op)
then
[ "fn path of " <> caseOnOpType "query" "action" <> " '" <> opName op
<> "' must start with '@server'."
<> "' must start with '@src'."
]
else []
@ -308,9 +308,9 @@ checkPlanForPageIssues plan =
else []
checkPageComponentPath page =
if not ("@client" `isPrefixOf` componentPath page)
if not ("@src" `isPrefixOf` componentPath page)
then
[ "component path of page '" <> pageName page <> "' must start with '@client'."
[ "component path of page '" <> pageName page <> "' must start with '@src'."
]
else []

View File

@ -35,6 +35,8 @@ generateAndWriteProjectSkeletonAndPresetFiles newProjectDetails waspProjectSkele
let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
writeNewFile waspFile
writeNewFile $ generatePackageJson newProjectDetails
case getProjectAuth newProjectDetails of
UsernameAndPassword -> do
writeNewFile generateLoginJsPage
@ -72,10 +74,8 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
[ "App uses username and password authentication.",
T.unpack
[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())`
- `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[]`.
|],
"One of the pages in the app must have a route path \"/\"."
@ -92,7 +92,7 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
},
title: "${appTitle}",
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
rootComponent: import { Layout } from "@src/Layout.jsx",
},
db: {
prisma: {
@ -104,21 +104,47 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
route LoginRoute { path: "/login", to: 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 }
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 =
( "src/client/pages/auth/Login.jsx",
( "src/pages/auth/Login.jsx",
[trimming|
import React from "react";
import { Link } from "react-router-dom";
import { LoginForm } from "@wasp/auth/forms/Login";
import { LoginForm } from "wasp/client/auth";
export default function Login() {
return (
@ -153,11 +179,11 @@ generateLoginJsPage =
generateSignupJsPage :: File
generateSignupJsPage =
( "src/client/pages/auth/Signup.jsx",
( "src/pages/auth/Signup.jsx",
[trimming|
import React from "react";
import { Link } from "react-router-dom";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { SignupForm } from "wasp/client/auth";
export default function Signup() {
return (
@ -201,7 +227,7 @@ generateDotEnvServerFile =
generateMainCssFile :: File
generateMainCssFile =
( "src/client/Main.css",
( "src/Main.css",
[trimming|
@tailwind base;
@tailwind components;
@ -217,11 +243,11 @@ generateMainCssFile =
generateLayoutComponent :: NewProjectDetails -> File
generateLayoutComponent newProjectDetails =
( "src/client/Layout.jsx",
( "src/Layout.jsx",
[trimming|
import { Link } from "react-router-dom";
import useAuth from '@wasp/auth/useAuth';
import logout from '@wasp/auth/logout';
import { useAuth, logout } from "wasp/client/auth";
import { getUsername } from "wasp/auth";
import "./Main.css";
export const Layout = ({ children }) => {
@ -236,7 +262,7 @@ generateLayoutComponent newProjectDetails =
</Link>
{ user ? (
<span>
Hi, {user.username}!{' '}
Hi, {getUsername(user)}!{' '}
<button onClick={logout} className="text-xl2 underline">
(Log out)
</button>

View File

@ -103,7 +103,7 @@ fixWaspFile newProjectDetails waspFilePath plan = do
For example, the following is missing ',' after the component field:
```wasp
page ExamplePage {
component: import ExamplePage from "@client/pages/ExamplePage.jsx" // <- missing ','
component: import ExamplePage from "@src/pages/ExamplePage.jsx" // <- missing ','
authRequired: true
}
```
@ -111,8 +111,8 @@ fixWaspFile newProjectDetails waspFilePath plan = do
Fix these by replacing them with actual implementation.
- Strings in Wasp must use double quotes, not single quotes.
- 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`.
Fix these by replacing it with correct syntax, e.g. `fn: import { actionName } from "@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 "@src/actions.js"`.
- 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 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/skeleton/.gitignore
Cli/templates/skeleton/.wasproot
Cli/templates/skeleton/public/.gitignore
Cli/templates/skeleton/src/.waspignore
Lsp/templates/**/*.js
Lsp/templates/**/*.ts
@ -573,7 +574,6 @@ test-suite waspc-test
, tasty-quickcheck ^>= 0.10
, tasty-golden ^>= 2.3.5
other-modules:
AI.GenerateNewProject.PageComponentFileTest
AI.GenerateNewProject.LogMsgTest
Analyzer.Evaluation.EvaluationErrorTest
Analyzer.EvaluatorTest