mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-11 07:16:09 +03:00
Updated Wasp AI to work with wasp 0.12. (#1726)
This commit is contained in:
parent
814b82bd58
commit
03d234fd7d
@ -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: "
|
||||
|
@ -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]
|
||||
}
|
||||
```
|
||||
|
@ -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 =
|
||||
|
@ -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 =
|
||||
|
@ -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}).
|
||||
|
@ -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 []
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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;",
|
||||
""
|
||||
]
|
||||
)
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user