Adds support for root component (#1009)

This commit is contained in:
Mihovil Ilakovac 2023-02-20 11:15:54 +01:00 committed by GitHub
parent bfbb57ecd0
commit c2a3e04283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 198 additions and 73 deletions

View File

@ -1,4 +1,5 @@
# Changelog
## v0.8.2
### Non-breaking Changes
@ -7,6 +8,9 @@
### Bug fixes
- Fixes a file lock error that kills CLI when changing entities with `wasp start` running on newer Macs.
### Support for defining the web app's root component
You can now define a root component for your client app. This is useful if you want to wrap your app in a provider or have a common layout. You can define it in `app.client.rootComponent` in your `.wasp` file.
### `wasp deploy` CLI command added
We have made it much easier to deploy your Wasp apps via a new CLI command, `wasp deploy`. 🚀 This release adds support for Fly.io, but we hope to add more hosting providers soon!

View File

@ -4,22 +4,25 @@ import ReactDOM from 'react-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import {
import {
initializeQueryClient,
queryClientInitialized,
} from './queryClient'
import * as serviceWorker from './serviceWorker'
{=# doesClientSetupFnExist =}
{=& clientSetupJsFnImportStatement =}
{=/ doesClientSetupFnExist =}
{=# setupFn.isDefined =}
{=& setupFn.importStatement =}
{=/ setupFn.isDefined =}
{=# rootComponent.isDefined =}
{=& rootComponent.importStatement =}
{=/ rootComponent.isDefined =}
startApp()
async function startApp() {
{=# doesClientSetupFnExist =}
await {= clientSetupJsFnIdentifier =}()
{=/ doesClientSetupFnExist =}
{=# setupFn.isDefined =}
await {= setupFn.importIdentifier =}()
{=/ setupFn.isDefined =}
initializeQueryClient()
await render()
@ -34,7 +37,13 @@ async function render() {
const queryClient = await queryClientInitialized
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
{=# rootComponent.isDefined =}
<{= rootComponent.importIdentifier =}>
{=/ rootComponent.isDefined =}
{router}
{=# rootComponent.isDefined =}
</{= rootComponent.importIdentifier =}>
{=/ rootComponent.isDefined =}
</QueryClientProvider>,
document.getElementById('root')
)

View File

@ -333,7 +333,7 @@
"file",
"web-app/src/index.js"
],
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
],
[
[

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import {
import {
initializeQueryClient,
queryClientInitialized,
} from './queryClient'
@ -27,7 +27,7 @@ async function render() {
const queryClient = await queryClientInitialized
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
{router}
</QueryClientProvider>,
document.getElementById('root')
)

View File

@ -333,7 +333,7 @@
"file",
"web-app/src/index.js"
],
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
],
[
[

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import {
import {
initializeQueryClient,
queryClientInitialized,
} from './queryClient'
@ -27,7 +27,7 @@ async function render() {
const queryClient = await queryClientInitialized
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
{router}
</QueryClientProvider>,
document.getElementById('root')
)

View File

@ -347,7 +347,7 @@
"file",
"web-app/src/index.js"
],
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
],
[
[

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import {
import {
initializeQueryClient,
queryClientInitialized,
} from './queryClient'
@ -27,7 +27,7 @@ async function render() {
const queryClient = await queryClientInitialized
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
{router}
</QueryClientProvider>,
document.getElementById('root')
)

View File

@ -333,7 +333,7 @@
"file",
"web-app/src/index.js"
],
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
],
[
[

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
import { QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import {
import {
initializeQueryClient,
queryClientInitialized,
} from './queryClient'
@ -27,7 +27,7 @@ async function render() {
const queryClient = await queryClientInitialized
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
{router}
</QueryClientProvider>,
document.getElementById('root')
)

View File

@ -0,0 +1,10 @@
export function App({ children }) {
return (
<div className="p-6">
<header className="mb-6">
<h1 className="text-3xl font-bold">ToDo App</h1>
</header>
{children}
</div>
);
}

View File

@ -29,6 +29,7 @@ app todoApp {
setupFn: import setup from "@server/serverSetup.js"
},
client: {
rootComponent: import { App } from "@client/App.jsx",
setupFn: import setup from "@client/clientSetup"
},
db: {

View File

@ -9,6 +9,7 @@ import Data.Data (Data)
import Wasp.AppSpec.ExtImport (ExtImport)
data Client = Client
{ setupFn :: Maybe ExtImport
{ setupFn :: Maybe ExtImport,
rootComponent :: Maybe ExtImport
}
deriving (Show, Eq, Data)

View File

@ -1,9 +1,11 @@
module Wasp.Generator.JsImport
( extImportToJsImport,
ImportLocation,
jsImportToImportJson,
)
where
import Data.Aeson (KeyValue ((.=)), object)
import qualified Data.Aeson as Aeson
import StrongPath (Dir, File', Path, Posix, Rel, (</>))
import qualified StrongPath as SP
import qualified Wasp.AppSpec.ExtImport as EI
@ -12,15 +14,14 @@ import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.JsImport
( JsImport,
JsImportName (JsImportField, JsImportModule),
getJsImportStmtAndIdentifier,
makeJsImport,
)
type ImportLocation = ()
extImportToJsImport ::
GeneratedSrcDir d =>
Path Posix (Rel d) (Dir GeneratedExternalCodeDir) ->
Path Posix (Rel ImportLocation) (Dir d) ->
Path Posix (Rel importLocation) (Dir d) ->
EI.ExtImport ->
JsImport
extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir extImport = makeJsImport importPath importName
@ -32,3 +33,17 @@ extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir ex
extImportNameToJsImportName :: EI.ExtImportName -> JsImportName
extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name
extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name
jsImportToImportJson :: Maybe JsImport -> Aeson.Value
jsImportToImportJson maybeJsImport = maybe notDefinedValue mkTmplData maybeJsImport
where
notDefinedValue = object ["isDefined" .= False]
mkTmplData :: JsImport -> Aeson.Value
mkTmplData jsImport =
let (jsImportStmt, jsImportIdentifier) = getJsImportStmtAndIdentifier jsImport
in object
[ "isDefined" .= True,
"importStatement" .= jsImportStmt,
"importIdentifier" .= jsImportIdentifier
]

View File

@ -222,7 +222,7 @@ genServerJs spec =
(maybeSetupJsFnImportStmt, maybeSetupJsFnImportIdentifier) =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
relPathToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathToServerSrcDir = [reldirP|./|]
genRoutesDir :: AppSpec -> Generator [FileDraft]

View File

@ -142,7 +142,7 @@ getTmplDataForAuthMethodConfig auth authMethod =
maybeOnSignInFnImportDetails = getJsImportStmtAndIdentifier' <$> maybeGetUserFieldsFn
(maybeOnSignInFnImportStmt, maybeOnSignInFnImportIdentifier) = (fst <$> maybeOnSignInFnImportDetails, snd <$> maybeOnSignInFnImportDetails)
relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathFromAuthConfigToServerSrcDir = [reldirP|../../../../|]
depsRequiredByPassport :: AppSpec -> [App.Dependency.Dependency]

View File

@ -78,7 +78,7 @@ genJob (jobName, job) =
]
maybeJobSchedule = jobScheduleTmplData <$> J.schedule job
relPathFromJobsDirToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
relPathFromJobsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathFromJobsDirToServerSrcDir = [reldirP|../|]
-- Creates a file that is imported on the server to ensure all job JS modules are loaded

View File

@ -4,7 +4,6 @@ import Data.Maybe (fromJust)
import StrongPath (Dir, Path, Posix, Rel)
import qualified StrongPath as SP
import qualified Wasp.AppSpec.ExtImport as EI
import Wasp.Generator.JsImport (ImportLocation)
import qualified Wasp.Generator.JsImport as GJI
import Wasp.Generator.ServerGenerator.Common (ServerSrcDir)
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir)
@ -16,13 +15,13 @@ import Wasp.JsImport
import qualified Wasp.JsImport as JI
getJsImportStmtAndIdentifier ::
Path Posix (Rel ImportLocation) (Dir ServerSrcDir) ->
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
EI.ExtImport ->
(JsImportStatement, JsImportIdentifier)
getJsImportStmtAndIdentifier pathFromImportLocationToExtCodeDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToExtCodeDir
extImportToJsImport ::
Path Posix (Rel ImportLocation) (Dir ServerSrcDir) ->
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
EI.ExtImport ->
JsImport
extImportToJsImport = GJI.extImportToJsImport serverExtDir

View File

@ -134,5 +134,5 @@ operationTmplData operation =
where
(importStmt, importIdentifier) = getJsImportStmtAndIdentifier relPathFromOperationsDirToServerSrcDir (AS.Operation.getFn operation)
relPathFromOperationsDirToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
relPathFromOperationsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathFromOperationsDirToServerSrcDir = [reldirP|../|]

View File

@ -8,7 +8,6 @@ where
import Data.Aeson (object, (.=))
import Data.List (intercalate)
import Data.Maybe (fromMaybe, isJust)
import StrongPath
( Dir,
File',
@ -46,7 +45,7 @@ import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
( extClientCodeGeneratorStrategy,
extSharedCodeGeneratorStrategy,
)
import Wasp.Generator.WebAppGenerator.JsImport (getJsImportStmtAndIdentifier)
import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson)
import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations)
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
import Wasp.Util ((<++>))
@ -246,16 +245,13 @@ genIndexJs spec =
(C.asWebAppFile [relfile|src/index.js|])
( Just $
object
[ "doesClientSetupFnExist" .= isJust maybeSetupJsFunction,
"clientSetupJsFnImportStatement" .= fromMaybe "" maybeSetupJsFnImportStmt,
"clientSetupJsFnIdentifier" .= fromMaybe "" maybeSetupJsFnImportIdentifier
[ "setupFn" .= extImportToImportJson relPathToWebAppSrcDir maybeSetupJsFunction,
"rootComponent" .= extImportToImportJson relPathToWebAppSrcDir maybeRootComponent
]
)
where
maybeSetupJsFunction = AS.App.Client.setupFn =<< AS.App.client (snd $ getApp spec)
maybeSetupJsFnImportDetails = getJsImportStmtAndIdentifier relPathToWebAppSrcDir <$> maybeSetupJsFunction
(maybeSetupJsFnImportStmt, maybeSetupJsFnImportIdentifier) =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
maybeRootComponent = AS.App.Client.rootComponent =<< AS.App.client (snd $ getApp spec)
relPathToWebAppSrcDir :: Path Posix (Rel ()) (Dir C.WebAppSrcDir)
relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir)
relPathToWebAppSrcDir = [reldirP|./|]

View File

@ -1,10 +1,14 @@
module Wasp.Generator.WebAppGenerator.JsImport where
import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust)
import StrongPath (Dir, Path, Posix, Rel)
import qualified StrongPath as SP
import Wasp.AppSpec.ExtImport (ExtImport)
import qualified Wasp.AppSpec.ExtImport as EI
import Wasp.Generator.JsImport (ImportLocation)
import Wasp.Generator.JsImport
( jsImportToImportJson,
)
import qualified Wasp.Generator.JsImport as GJI
import Wasp.Generator.WebAppGenerator.Common (WebAppSrcDir)
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir)
@ -15,14 +19,22 @@ import Wasp.JsImport
)
import qualified Wasp.JsImport as JI
extImportToImportJson ::
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
Maybe ExtImport ->
Aeson.Value
extImportToImportJson pathFromImportLocationToSrcDir maybeExtImport = jsImportToImportJson jsImport
where
jsImport = extImportToJsImport pathFromImportLocationToSrcDir <$> maybeExtImport
getJsImportStmtAndIdentifier ::
Path Posix (Rel ImportLocation) (Dir WebAppSrcDir) ->
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
EI.ExtImport ->
(JsImportStatement, JsImportIdentifier)
getJsImportStmtAndIdentifier pathFromImportLocationToSrcDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToSrcDir
extImportToJsImport ::
Path Posix (Rel ImportLocation) (Dir WebAppSrcDir) ->
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
EI.ExtImport ->
JsImport
extImportToJsImport = GJI.extImportToJsImport webAppExtDir

View File

@ -167,7 +167,7 @@ createPageTemplateData page =
importStmt :: String
(importStmt, _) = getJsImportStmtAndIdentifier $ applyJsImportAlias (Just importAlias) $ extImportToJsImport relPathToWebAppSrcDir pageComponent
relPathToWebAppSrcDir :: Path Posix (Rel ()) (Dir C.WebAppSrcDir)
relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir)
relPathToWebAppSrcDir = [reldirP|./|]
pageComponent :: AS.ExtImport.ExtImport

View File

@ -57,6 +57,7 @@ spec_Analyzer = do
" setupFn: import { setupServer } from \"@server/bar.js\"",
" },",
" client: {",
" rootComponent: import { App } from \"@client/App.jsx\",",
" setupFn: import { setupClient } from \"@client/baz.js\"",
" },",
" db: {",
@ -148,7 +149,10 @@ spec_Analyzer = do
Client.Client
{ Client.setupFn =
Just $
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js")
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js"),
Client.rootComponent =
Just $
ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx")
},
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL}
}

View File

@ -485,7 +485,7 @@ In other words, the hook returns a function whose API matches the original Actio
The `useAction` hook accepts two arguments:
- `actionFn` (required) - The Wasp Action (i.e., the client-side query function generated by Wasp based on a query declaration) you wish to enhance.
- `actionOptions` (optional) - An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:
- `actionOptions` (optional) - An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:
- `optimisticUpdates` (optional) - An array of objects where each object defines an [optimistic update](https://stackoverflow.com/a/33009713) to perform on the query cache. To define an optimistic update, you must specify the following properties:
- `getQuerySpecifier` (required) - A function returning the query specifier (i.e., a value used to address the query you want to update). A query specifier is an array specifying the query function and arguments. For example, to optimistically update the query used with `useQuery(fetchFilteredTasks, {isDone: true }]`, your `getQuerySpecifier` function would have to return the array `[fetchFilteredTasks, { isDone: true}]`. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/change item to address the query).
- `updateQuery` (required) - The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:
@ -494,7 +494,7 @@ The `useAction` hook accepts two arguments:
**NOTE:** The `updateQuery` function must be a pure function. It must return the desired cache value identified by the `getQuerySpecifier` function and _must not_ perform any side effects. Also, make sure you only update the query caches affected by your action causing the optimistic update (Wasp cannot yet verify this). Finally, your implementation of the `updateQuery` function should work correctly regardless of the state of `oldData` (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use _react-query_'s lower-level API (read more about it [here](#advanced-usage)).
Here's an example showing how to configure the Action from the previous example to perform an optimistic update:
Here's an example showing how to configure the Action from the previous example to perform an optimistic update:
```jsx {3,9,10,11,12,13,14,15,16,27} title=src/client/pages/Task.js
import React from 'react'
import { useQuery } from '@wasp/queries'
@ -532,7 +532,7 @@ const TaskPage = ({ id }) => {
)
}
export default TaskPage
export default TaskPage
```
#### Advanced usage
The `useAction` hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.
@ -590,8 +590,8 @@ If you have server tasks that you do not want to handle as part of the normal re
* persist between server restarts
* can be retried if they fail
* can be delayed until the future
* can have a recurring schedule!
* can have a recurring schedule!
Some examples where you may want to use a `job` on the server include sending an email, making an HTTP request to some external API, or doing some nightly calculations.
### Job Executors
@ -606,7 +606,7 @@ Currently, Wasp supports only one type of job executor, which is `PgBoss`, but i
We have selected [pg-boss](https://github.com/timgit/pg-boss/) as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/)) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.
:::info
:::info
Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.
<details>
@ -654,9 +654,9 @@ console.log(await submittedJob.pgBoss.details())
await mySpecialJob.delay(10).submit({ job: "args" })
```
And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`.
And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`.
Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function.
Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function.
### Recurring jobs
@ -715,7 +715,7 @@ job mySpecialJob {
// Can reference context.entities.Task, for example.
}
```
- ##### `executorOptions: dict` (optional)
Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with `submit()` or in a `schedule`.
@ -723,15 +723,15 @@ job mySpecialJob {
See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.0.0/docs/readme.md#sendname-data-options).
#### `schedule: dict` (optional)
- ##### `cron: string` (required)
A 5-placeholder format cron expression string. See rationale for minute-level precision [here](https://github.com/timgit/pg-boss/blob/8.0.0/docs/readme.md#scheduling).
_If you need help building cron expressions, Check out_ <em>[Crontab guru](https://crontab.guru/#0_*_*_*_*).</em>
- ##### `args: JSON` (optional)
The arguments to pass to the `perform.fn` function when invoked.
- ##### `executorOptions: dict` (optional)
Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The `perform.executorOptions` are the default options, and `schedule.executorOptions` can override/extend those.
@ -814,7 +814,7 @@ In the future, we will add support for picking any version you like, but we have
Wasp provides authentication and authorization support out-of-the-box. Enabling it for your app is optional and can be done by configuring the `auth` field of the `app` declaration:
```c
```c
app MyApp {
title: "My app",
//...
@ -855,7 +855,7 @@ Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/06-au
Path where a successfully authenticated user will be sent upon successful login/signup.
Default value is "/".
:::note
:::note
Automatic redirect on successful login only works when using the Wasp provided [`Signup` and `Login` forms](#high-level-api)
:::
@ -864,7 +864,7 @@ Automatic redirect on successful login only works when using the Wasp provided [
`usernameAndPassword` authentication method makes it possible to signup/login into the app by using a username and password.
This method requires that `userEntity` specified in `auth` contains `username: string` and `password: string` fields:
```c
```c
app MyApp {
title: "My app",
//...
@ -916,10 +916,10 @@ export const signUp = async (args, context) => {
// ...
const newUser = context.entities.User.create({
data: {
username: args.username,
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
}
}
})
// Your custom code after sign-up.
@ -937,8 +937,8 @@ You don't need to worry about hashing the password yourself! Even when you are u
To disable/enable default validations, or add your own, you can modify your custom signUp function like so:
```js
const newUser = context.entities.User.create({
data: {
username: args.username,
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
},
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
@ -1133,7 +1133,7 @@ import AuthError from '@wasp/core/AuthError.js'
```
## Social Login Providers (OAuth 2.0)
Wasp allows you to easily add social login providers to your app.
Wasp allows you to easily add social login providers to your app.
The following is a list of links to guides that will help you get started with the currently supported providers:
- [GitHub](/docs/integrations/github)
@ -1150,7 +1150,7 @@ When using Social Login Providers, Wasp gives you the following options:
<Tabs>
<TabItem value="google" label="Google" default>
```c
```c
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
@ -1159,7 +1159,7 @@ When using Social Login Providers, Wasp gives you the following options:
},
}
```
<p>Add <code>google: &#123;&#125;</code> to your <code>auth.methods</code> dictionary to use it with default settings</p>
<p>By default, Wasp expects you to set two environment variables in order to use Google authentication:</p>
<ul>
@ -1266,7 +1266,7 @@ If you would like to allow the user to select their own username, or some other
Alternatively, you could add a `displayName` property to your User entity and assign it using the details of their provider account. Below is an example of how to do this by using:
- the `getUserFieldsFn` function to configure the user's `username` or `displayName` from their provider account
We also show you how to customize the configuration of the Provider's settings using:
We also show you how to customize the configuration of the Provider's settings using:
- the `configFn` function
```c title=main.wasp {9,10,13,14,26}
@ -1304,7 +1304,7 @@ psl=}
```
#### `configFn`
#### `configFn`
This function should return an object with the following shape:
<Tabs>
@ -1377,13 +1377,14 @@ This function should return the user fields to use when creating a new user upon
## Client configuration
You can configure the client using the `client` field inside the `app`
declaration,
declaration,
```c
app MyApp {
title: "My app",
// ...
client: {
rootComponent: import Root from "@client/Root.jsx",
setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
}
}
@ -1391,6 +1392,79 @@ app MyApp {
`app.client` is a dictionary with the following fields:
#### `rootComponent: ClientImport` (optional)
`rootComponent` defines the root component of your client application. It is
expected to be a React component, and Wasp will use it to wrap your entire app.
It must render its children, which are the actual pages of your application.
You can use it to define a common layout for your application:
```jsx title="src/client/Root.jsx"
export default async function Root({ children }) {
return (
<div>
<header>
<h1>My App</h1>
</header>
{children}
<footer>
<p>My App footer</p>
</footer>
</div>
)
}
```
You can use it to set up various providers that your application needs:
```jsx title="src/client/Root.jsx"
import store from './store'
import { Provider } from 'react-redux'
export default async function Root({ children }) {
return (
<Provider store={store}>
{children}
</Provider>
)
}
```
As long as you render the children, you can do whatever you want in your root
component. Here's an example of a root component both sets up a provider and
renders a custom layout:
```jsx title="src/client/Root.jsx"
import store from './store'
import { Provider } from 'react-redux'
export default function Root({ children }) {
return (
<Provider store={store}>
<Layout>
{children}
</Layout>
</Provider>
)
}
function Layout({ children }) {
return (
<div>
<header>
<h1>My App</h1>
</header>
{children}
<footer>
<p>My App footer</p>
</footer>
</div>
)
}
```
#### `setupFn: ClientImport` (optional)
`setupFn` declares a JavaScript function that Wasp executes on the client