Support for the client setup function (#595)

* Add clientSetup support to analyzer and tidy up generator

* Add clientSetup support to generator

* Add tests for client setup function and tidy up

* Address PR comments for client setup function

* Clean up server and web app generators

* Document react query invalidation on logout

* Update e2e tests after query client cache clean

* Fix syntax error in exmaple app

* Remove leftover todos

* Fix formatting

* Fix formatting

* Fix tests for client setup function

* Update e2e test

* Fix todos in templates

* Add docs for client setup

* Fix docs typo

* Fix docs typo

* Fix docs typo

* Fix docs typo

* Update e2e test
This commit is contained in:
Filip Sodić 2022-05-20 14:49:26 +02:00 committed by GitHub
parent a6f5a7d7b2
commit 59c714111e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 402 additions and 203 deletions

View File

@ -1,22 +1,25 @@
import config from '../config.js'
import { queryClient } from '../queryClient'
import { removeQueries } from '../operations/resources'
import api, { setAuthToken, handleApiError } from '../api.js'
const login = async (email, password) => {
export default async function login(email, password) {
try {
const args = { email, password }
const response = await api.post(config.apiUrl + '/auth/login', args)
setAuthToken(response.data.token)
// TODO(matija): Currently we are removing all the queries, but we should remove only
// non-public, user-dependent queries - public queries are expected not to change in respect
// to the currently logged in user.
queryClient.removeQueries()
// This isn't really neccessary because we remove all private queries after
// logout, but we do it to be extra safe.
//
// For example, in future versions, users might be able to get to an SPA
// login page while there's an active session. This code will prevent data
// leaks in such cases.
//
// TODO(filip): We are currently removing all the queries, but we should
// remove only non-public, user-dependent queries - public queries are
// expected not to change in respect to the currently logged in user.
removeQueries()
} catch (error) {
handleApiError(error)
}
}
export default login

View File

@ -1,17 +1,9 @@
import { clearLocalStorage } from '../api.js'
import { queryClient } from '../queryClient'
import { invalidateAndRemoveQueries } from '../operations/resources'
const logout = () => {
export default function logout() {
clearLocalStorage()
// TODO(matija): We are currently invalidating all the queries, but we should invalidate only the
// non-public, user-dependent ones.
queryClient.invalidateQueries()
// TODO(matija): We are currently clearing all the queries, but we should clear only the
// non-public, user-dependent ones.
queryClient.clear()
// TODO(filip): We are currently invalidating and removing all the queries, but
// we should remove only the non-public, user-dependent ones.
invalidateAndRemoveQueries()
}
export default logout

View File

@ -1,13 +1,10 @@
import config from '../config.js'
import api, { handleApiError } from '../api.js'
const signup = async (userFields) => {
export default async function signup(userFields) {
try {
await api.post(config.apiUrl + '/auth/signup', userFields)
} catch (error) {
handleApiError(error)
}
}
export default signup

View File

@ -2,7 +2,10 @@ import { useQuery } from '../queries'
import config from '../config.js'
import api, { handleApiError } from '../api.js'
const getMe = async () => {
export default function useAuth(queryFnArgs, config) {
return useQuery(getMe, queryFnArgs, config)
}
async function getMe() {
try {
const response = await api.get(config.apiUrl + '/auth/me')
@ -15,10 +18,5 @@ const getMe = async () => {
}
}
}
getMe.queryCacheKey = 'auth/me'
const useAuth = (queryFnArgs, config) => {
return useQuery(getMe, queryFnArgs, config)
}
export default useAuth

View File

@ -1,3 +1,4 @@
{{={= =}=}}
import React from 'react'
import ReactDOM from 'react-dom'
import { QueryClientProvider } from 'react-query'
@ -6,17 +7,32 @@ import router from './router'
import { queryClient } from './queryClient'
import * as serviceWorker from './serviceWorker'
{=# doesClientSetupFnExist =}
{=& clientSetupJsFnImportStatement =}
{=/ doesClientSetupFnExist =}
import './index.css'
startApp()
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
async function startApp() {
{=# doesClientSetupFnExist =}
await {= clientSetupJsFnIdentifier =}()
{=/ doesClientSetupFnExist =}
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
render()
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
}
function render() {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
}

View File

@ -13,7 +13,7 @@ const resourceToQueryCacheKeys = new Map()
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
export function addResourcesUsedByQuery(queryCacheKey, resources) {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
@ -28,7 +28,7 @@ export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
export function getQueriesUsingResource(resource) {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
@ -36,7 +36,7 @@ export const getQueriesUsingResource = (resource) => {
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
export function invalidateQueriesUsing(resources) {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
@ -45,3 +45,17 @@ export const invalidateQueriesUsing = (resources) => {
queryClient.invalidateQueries(queryCacheKey)
}
}
export function removeQueries() {
queryClient.removeQueries()
}
export function invalidateAndRemoveQueries() {
// If we don't invalidate the queries before removing them, Wasp will stay on
// the same page. The user would have to manually refresh the page to "finish"
// logging out.
queryClient.invalidateQueries()
// If we don't remove the queries after invalidating them, the old query data
// remains in the cache, casuing a potential privacy issue.
queryClient.removeQueries()
}

View File

@ -291,7 +291,7 @@
"file",
"web-app/src/index.js"
],
"030bfc5cd200d732eaca398b298057c148c3f9d9b8a3e7a24918db438b5d1861"
"a477184a6e6e7cb33e056febc8a4a1e1132ecc697763855fa2af07cf02c79908"
],
[
[
@ -312,7 +312,7 @@
"file",
"web-app/src/operations/resources.js"
],
"35f14c763398a5acdd12deea700cc124bca6393553696f2764c2ebfab0609ee1"
"4772a216b647ace3eedace9b02897922725b58131031d21be59187b1652ccade"
],
[
[

View File

@ -6,17 +6,26 @@ import router from './router'
import { queryClient } from './queryClient'
import * as serviceWorker from './serviceWorker'
import './index.css'
startApp()
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
async function startApp() {
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
render()
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
}
function render() {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
}

View File

@ -12,7 +12,7 @@ const resourceToQueryCacheKeys = new Map()
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
export function addResourcesUsedByQuery(queryCacheKey, resources) {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
@ -27,7 +27,7 @@ export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
export function getQueriesUsingResource(resource) {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
@ -35,7 +35,7 @@ export const getQueriesUsingResource = (resource) => {
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
export function invalidateQueriesUsing(resources) {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
@ -44,3 +44,17 @@ export const invalidateQueriesUsing = (resources) => {
queryClient.invalidateQueries(queryCacheKey)
}
}
export function removeQueries() {
queryClient.removeQueries()
}
export function invalidateAndRemoveQueries() {
// If we don't invalidate the queries before removing them, Wasp will stay on
// the same page. The user would have to manually refresh the page to "finish"
// logging out.
queryClient.invalidateQueries()
// If we don't remove the queries after invalidating them, the old query data
// remains in the cache, casuing a potential privacy issue.
queryClient.removeQueries()
}

View File

@ -291,7 +291,7 @@
"file",
"web-app/src/index.js"
],
"030bfc5cd200d732eaca398b298057c148c3f9d9b8a3e7a24918db438b5d1861"
"a477184a6e6e7cb33e056febc8a4a1e1132ecc697763855fa2af07cf02c79908"
],
[
[
@ -312,7 +312,7 @@
"file",
"web-app/src/operations/resources.js"
],
"35f14c763398a5acdd12deea700cc124bca6393553696f2764c2ebfab0609ee1"
"4772a216b647ace3eedace9b02897922725b58131031d21be59187b1652ccade"
],
[
[

View File

@ -6,17 +6,26 @@ import router from './router'
import { queryClient } from './queryClient'
import * as serviceWorker from './serviceWorker'
import './index.css'
startApp()
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
async function startApp() {
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
render()
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
}
function render() {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
}

View File

@ -12,7 +12,7 @@ const resourceToQueryCacheKeys = new Map()
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
export function addResourcesUsedByQuery(queryCacheKey, resources) {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
@ -27,7 +27,7 @@ export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
export function getQueriesUsingResource(resource) {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
@ -35,7 +35,7 @@ export const getQueriesUsingResource = (resource) => {
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
export function invalidateQueriesUsing(resources) {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
@ -44,3 +44,17 @@ export const invalidateQueriesUsing = (resources) => {
queryClient.invalidateQueries(queryCacheKey)
}
}
export function removeQueries() {
queryClient.removeQueries()
}
export function invalidateAndRemoveQueries() {
// If we don't invalidate the queries before removing them, Wasp will stay on
// the same page. The user would have to manually refresh the page to "finish"
// logging out.
queryClient.invalidateQueries()
// If we don't remove the queries after invalidating them, the old query data
// remains in the cache, casuing a potential privacy issue.
queryClient.removeQueries()
}

View File

@ -312,7 +312,7 @@
"file",
"web-app/src/index.js"
],
"030bfc5cd200d732eaca398b298057c148c3f9d9b8a3e7a24918db438b5d1861"
"a477184a6e6e7cb33e056febc8a4a1e1132ecc697763855fa2af07cf02c79908"
],
[
[
@ -333,7 +333,7 @@
"file",
"web-app/src/operations/resources.js"
],
"35f14c763398a5acdd12deea700cc124bca6393553696f2764c2ebfab0609ee1"
"4772a216b647ace3eedace9b02897922725b58131031d21be59187b1652ccade"
],
[
[

View File

@ -6,17 +6,26 @@ import router from './router'
import { queryClient } from './queryClient'
import * as serviceWorker from './serviceWorker'
import './index.css'
startApp()
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
async function startApp() {
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
render()
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
}
function render() {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
}

View File

@ -12,7 +12,7 @@ const resourceToQueryCacheKeys = new Map()
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
export function addResourcesUsedByQuery(queryCacheKey, resources) {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
@ -27,7 +27,7 @@ export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
export function getQueriesUsingResource(resource) {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
@ -35,7 +35,7 @@ export const getQueriesUsingResource = (resource) => {
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
export function invalidateQueriesUsing(resources) {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
@ -44,3 +44,17 @@ export const invalidateQueriesUsing = (resources) => {
queryClient.invalidateQueries(queryCacheKey)
}
}
export function removeQueries() {
queryClient.removeQueries()
}
export function invalidateAndRemoveQueries() {
// If we don't invalidate the queries before removing them, Wasp will stay on
// the same page. The user would have to manually refresh the page to "finish"
// logging out.
queryClient.invalidateQueries()
// If we don't remove the queries after invalidating them, the old query data
// remains in the cache, casuing a potential privacy issue.
queryClient.removeQueries()
}

View File

@ -291,7 +291,7 @@
"file",
"web-app/src/index.js"
],
"030bfc5cd200d732eaca398b298057c148c3f9d9b8a3e7a24918db438b5d1861"
"a477184a6e6e7cb33e056febc8a4a1e1132ecc697763855fa2af07cf02c79908"
],
[
[
@ -312,7 +312,7 @@
"file",
"web-app/src/operations/resources.js"
],
"35f14c763398a5acdd12deea700cc124bca6393553696f2764c2ebfab0609ee1"
"4772a216b647ace3eedace9b02897922725b58131031d21be59187b1652ccade"
],
[
[

View File

@ -6,17 +6,26 @@ import router from './router'
import { queryClient } from './queryClient'
import * as serviceWorker from './serviceWorker'
import './index.css'
startApp()
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
async function startApp() {
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
render()
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
}
function render() {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
{ router }
</QueryClientProvider>,
document.getElementById('root')
)
}

View File

@ -12,7 +12,7 @@ const resourceToQueryCacheKeys = new Map()
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
export function addResourcesUsedByQuery(queryCacheKey, resources) {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
@ -27,7 +27,7 @@ export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
export function getQueriesUsingResource(resource) {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
@ -35,7 +35,7 @@ export const getQueriesUsingResource = (resource) => {
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
export function invalidateQueriesUsing(resources) {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
@ -44,3 +44,17 @@ export const invalidateQueriesUsing = (resources) => {
queryClient.invalidateQueries(queryCacheKey)
}
}
export function removeQueries() {
queryClient.removeQueries()
}
export function invalidateAndRemoveQueries() {
// If we don't invalidate the queries before removing them, Wasp will stay on
// the same page. The user would have to manually refresh the page to "finish"
// logging out.
queryClient.invalidateQueries()
// If we don't remove the queries after invalidating them, the old query data
// remains in the cache, casuing a potential privacy issue.
queryClient.removeQueries()
}

View File

@ -0,0 +1,3 @@
export default function setup() {
console.log("This was called from the client setup function")
}

View File

@ -15,6 +15,9 @@ app todoApp {
server: {
setupFn: import setup from "@ext/serverSetup.js"
},
client: {
setupFn: import setup from "@ext/clientSetup.js"
},
db: {
system: PostgreSQL
}

View File

@ -4,6 +4,7 @@ module Wasp.AppSpec.App (App (..)) where
import Data.Data (Data)
import Wasp.AppSpec.App.Auth (Auth)
import Wasp.AppSpec.App.Client (Client)
import Wasp.AppSpec.App.Db (Db)
import Wasp.AppSpec.App.Dependency (Dependency)
import Wasp.AppSpec.App.Server (Server)
@ -14,6 +15,7 @@ data App = App
head :: Maybe [String],
auth :: Maybe Auth,
server :: Maybe Server,
client :: Maybe Client,
db :: Maybe Db,
dependencies :: Maybe [Dependency]
}

View File

@ -0,0 +1,14 @@
{-# LANGUAGE DeriveDataTypeable #-}
module Wasp.AppSpec.App.Client
( Client (..),
)
where
import Data.Data (Data)
import Wasp.AppSpec.ExtImport (ExtImport)
data Client = Client
{ setupFn :: Maybe ExtImport
}
deriving (Show, Eq, Data)

View File

@ -42,11 +42,6 @@ import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.ServerGenerator.AuthG (genAuth)
import Wasp.Generator.ServerGenerator.Common
( ServerSrcDir,
asServerFile,
asTmplFile,
)
import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
import qualified Wasp.Generator.ServerGenerator.ExternalCodeGenerator as ServerExternalCodeGenerator
@ -86,15 +81,15 @@ dotEnvInServerRootDir :: Path' (Rel C.ServerRootDir) File'
dotEnvInServerRootDir = [relfile|.env|]
genReadme :: Generator FileDraft
genReadme = return $ C.mkTmplFd (asTmplFile [relfile|README.md|])
genReadme = return $ C.mkTmplFd (C.asTmplFile [relfile|README.md|])
genPackageJson :: AppSpec -> N.NpmDepsForWasp -> Generator FileDraft
genPackageJson spec waspDependencies = do
combinedDependencies <- N.genNpmDepsForPackage spec waspDependencies
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|package.json|])
(asServerFile [relfile|package.json|])
(C.asTmplFile [relfile|package.json|])
(C.asServerFile [relfile|package.json|])
( Just $
object
[ "depsChunk" .= N.getDependenciesPackageJsonEntry combinedDependencies,
@ -137,16 +132,16 @@ genNpmrc :: Generator FileDraft
genNpmrc =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|npmrc|])
(asServerFile [relfile|.npmrc|])
(C.asTmplFile [relfile|npmrc|])
(C.asServerFile [relfile|.npmrc|])
Nothing
genNvmrc :: Generator FileDraft
genNvmrc =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|nvmrc|])
(asServerFile [relfile|.nvmrc|])
(C.asTmplFile [relfile|nvmrc|])
(C.asServerFile [relfile|.nvmrc|])
-- We want to specify only the major version here. If we specified the
-- entire version string (i.e., 16.0.0), our project would work only with
-- that exact version, which we don't want. Unfortunately, the nvmrc file
@ -158,17 +153,17 @@ genGitignore :: Generator FileDraft
genGitignore =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|gitignore|])
(asServerFile [relfile|.gitignore|])
(C.asTmplFile [relfile|gitignore|])
(C.asServerFile [relfile|.gitignore|])
Nothing
genSrcDir :: AppSpec -> Generator [FileDraft]
genSrcDir spec =
sequence
[ return $ C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|app.js|],
return $ C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|utils.js|],
return $ C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/AuthError.js|],
return $ C.mkSrcTmplFd $ C.asTmplSrcFile [relfile|core/HttpError.js|],
[ copyTmplFile [relfile|app.js|],
copyTmplFile [relfile|utils.js|],
copyTmplFile [relfile|core/AuthError.js|],
copyTmplFile [relfile|core/HttpError.js|],
genDbClient spec,
genConfigFile spec,
genServerJs spec
@ -177,6 +172,8 @@ genSrcDir spec =
<++> genOperationsRoutes spec
<++> genOperations spec
<++> genAuth spec
where
copyTmplFile = return . C.mkSrcTmplFd
genDbClient :: AppSpec -> Generator FileDraft
genDbClient spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
@ -200,8 +197,8 @@ genServerJs :: AppSpec -> Generator FileDraft
genServerJs spec =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/server.js|])
(asServerFile [relfile|src/server.js|])
(C.asTmplFile [relfile|src/server.js|])
(C.asServerFile [relfile|src/server.js|])
( Just $
object
[ "doesServerSetupFnExist" .= isJust maybeSetupJsFunction,
@ -217,7 +214,7 @@ genServerJs spec =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
-- | TODO: Make this not hardcoded!
relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir ServerSrcDir)) (Dir GeneratedExternalCodeDir)
relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir C.ServerSrcDir)) (Dir GeneratedExternalCodeDir)
relPosixPathFromSrcDirToExtSrcDir = [reldirP|./ext-src|]
genRoutesDir :: AppSpec -> Generator [FileDraft]
@ -226,8 +223,8 @@ genRoutesDir spec =
-- but I did not bother with it yet since it is used only here for now.
return
[ C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/routes/index.js|])
(asServerFile [relfile|src/routes/index.js|])
(C.asTmplFile [relfile|src/routes/index.js|])
(C.asServerFile [relfile|src/routes/index.js|])
( Just $
object
[ "operationsRouteInRootRouter" .= (operationsRouteInRootRouter :: String),

View File

@ -44,8 +44,6 @@ asServerFile = SP.castRel
asServerSrcFile :: Path' (Rel d) File' -> Path' (Rel ServerSrcDir) File'
asServerSrcFile = SP.castRel
-- * Paths
-- | Path where server root dir is generated.
serverRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir ServerRootDir)
serverRootDirInProjectRootDir = [reldir|server|]
@ -57,8 +55,6 @@ serverSrcDirInServerRootDir = [reldir|src|]
serverSrcDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir ServerSrcDir)
serverSrcDirInProjectRootDir = serverRootDirInProjectRootDir </> serverSrcDirInServerRootDir
-- * Templates
mkTmplFd :: Path' (Rel ServerTemplatesDir) File' -> FileDraft
mkTmplFd srcPath = mkTmplFdWithDstAndData srcPath dstPath Nothing
where

View File

@ -6,34 +6,35 @@ where
import Data.Aeson (object, (.=))
import Data.List (intercalate)
import Data.Maybe (fromMaybe, isJust)
import StrongPath
( Dir,
Path',
Path,
Posix,
Rel,
reldir,
relfile,
(</>),
)
import StrongPath.TH (reldirP)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import Wasp.AppSpec.App.Client as AS.App.Client
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.Common (nodeVersion, nodeVersionBounds, npmVersionBounds)
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.Generator.FileDraft
import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.NpmDependencies as N
import qualified Wasp.Generator.WebAppGenerator.AuthG as AuthG
import Wasp.Generator.WebAppGenerator.Common
( asTmplFile,
asWebAppFile,
asWebAppSrcFile,
)
import Wasp.Generator.WebAppGenerator.AuthG (genAuth)
import qualified Wasp.Generator.WebAppGenerator.Common as C
import qualified Wasp.Generator.WebAppGenerator.ExternalCodeGenerator as WebAppExternalCodeGenerator
import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations)
import qualified Wasp.Generator.WebAppGenerator.RouterGenerator as RouterGenerator
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
import qualified Wasp.SemanticVersion as SV
import Wasp.Util ((<++>))
@ -45,14 +46,14 @@ genWebApp spec = do
genNpmrc,
genNvmrc,
genGitignore,
return $ C.mkTmplFd $ asTmplFile [relfile|netlify.toml|]
return $ C.mkTmplFd $ C.asTmplFile [relfile|netlify.toml|]
]
<++> genPublicDir spec
<++> genSrcDir spec
<++> genExternalCodeDir WebAppExternalCodeGenerator.generatorStrategy (AS.externalCodeFiles spec)
genReadme :: Generator FileDraft
genReadme = return $ C.mkTmplFd $ asTmplFile [relfile|README.md|]
genReadme = return $ C.mkTmplFd $ C.asTmplFile [relfile|README.md|]
genPackageJson :: AppSpec -> N.NpmDepsForWasp -> Generator FileDraft
genPackageJson spec waspDependencies = do
@ -75,16 +76,16 @@ genNpmrc :: Generator FileDraft
genNpmrc =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|npmrc|])
(asWebAppFile [relfile|.npmrc|])
(C.asTmplFile [relfile|npmrc|])
(C.asWebAppFile [relfile|.npmrc|])
Nothing
genNvmrc :: Generator FileDraft
genNvmrc =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|nvmrc|])
(asWebAppFile [relfile|.nvmrc|])
(C.asTmplFile [relfile|nvmrc|])
(C.asWebAppFile [relfile|.nvmrc|])
-- We want to specify only the major version, check the comment in `ServerGenerator.hs` for details
(Just (object ["nodeVersion" .= show (SV.major nodeVersion)]))
@ -111,17 +112,17 @@ genGitignore :: Generator FileDraft
genGitignore =
return $
C.mkTmplFdWithDst
(asTmplFile [relfile|gitignore|])
(asWebAppFile [relfile|.gitignore|])
(C.asTmplFile [relfile|gitignore|])
(C.asWebAppFile [relfile|.gitignore|])
genPublicDir :: AppSpec -> Generator [FileDraft]
genPublicDir spec = do
publicIndexHtmlFd <- genPublicIndexHtml spec
return $
C.mkTmplFd (asTmplFile [relfile|public/favicon.ico|]) :
C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|]) :
publicIndexHtmlFd :
( let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
processPublicTmpl path = C.mkTmplFdWithData (asTmplFile $ [reldir|public|] </> path) tmplData
processPublicTmpl path = C.mkTmplFdWithData (C.asTmplFile $ [reldir|public|] </> path) tmplData
in processPublicTmpl
<$> [ [relfile|manifest.json|]
]
@ -131,7 +132,7 @@ genPublicIndexHtml :: AppSpec -> Generator FileDraft
genPublicIndexHtml spec =
return $
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|public/index.html|])
(C.asTmplFile [relfile|public/index.html|])
targetPath
(Just templateData)
where
@ -142,48 +143,51 @@ genPublicIndexHtml spec =
"head" .= (maybe "" (intercalate "\n") (AS.App.head $ snd $ getApp spec) :: String)
]
-- * Src dir
srcDir :: Path' (Rel C.WebAppRootDir) (Dir C.WebAppSrcDir)
srcDir = C.webAppSrcDirInWebAppRootDir
-- TODO(matija): Currently we also generate auth-specific parts in this file (e.g. token management),
-- although they are not used anywhere outside.
-- We could further "templatize" this file so only what is needed is generated.
--
-- | Generates api.js file which contains token management and configured api (e.g. axios) instance.
genApi :: FileDraft
genApi = C.mkTmplFd (C.asTmplFile [relfile|src/api.js|])
genSrcDir :: AppSpec -> Generator [FileDraft]
genSrcDir spec = do
routerFd <- RouterGenerator.generateRouter spec
operationsFds <- genOperations spec
authFds <- AuthG.genAuth spec
return $
generateLogo :
routerFd :
genApi :
map
processSrcTmpl
[ [relfile|index.js|],
[relfile|index.css|],
[relfile|serviceWorker.js|],
[relfile|config.js|],
[relfile|queryClient.js|],
[relfile|utils.js|]
]
++ operationsFds
++ authFds
genSrcDir spec =
sequence
[ copyTmplFile [relfile|index.css|],
copyTmplFile [relfile|logo.png|],
copyTmplFile [relfile|serviceWorker.js|],
copyTmplFile [relfile|config.js|],
copyTmplFile [relfile|queryClient.js|],
copyTmplFile [relfile|utils.js|],
genRouter spec,
genIndexJs spec,
genApi
]
<++> genOperations spec
<++> genAuth spec
where
generateLogo =
C.mkTmplFdWithDstAndData
(asTmplFile [relfile|src/logo.png|])
(srcDir </> asWebAppSrcFile [relfile|logo.png|])
Nothing
processSrcTmpl path =
C.mkTmplFdWithDst
(asTmplFile $ [reldir|src|] </> path)
(srcDir </> asWebAppSrcFile path)
copyTmplFile = return . C.mkSrcTmplFd
-- | Generates api.js file which contains token management and configured api (e.g. axios) instance.
genApi :: Generator FileDraft
genApi = return $ C.mkTmplFd (C.asTmplFile [relfile|src/api.js|])
genIndexJs :: AppSpec -> Generator FileDraft
genIndexJs spec =
return $
C.mkTmplFdWithDstAndData
(C.asTmplFile [relfile|src/index.js|])
(C.asWebAppFile [relfile|src/index.js|])
( Just $
object
[ "doesClientSetupFnExist" .= isJust maybeSetupJsFunction,
"clientSetupJsFnImportStatement" .= fromMaybe "" maybeSetupJsFnImportStmt,
"clientSetupJsFnIdentifier" .= fromMaybe "" maybeSetupJsFnImportIdentifier
]
)
where
maybeSetupJsFunction = AS.App.Client.setupFn =<< AS.App.client (snd $ getApp spec)
maybeSetupJsFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction
(maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
relPosixPathFromSrcDirToExtSrcDir :: Path Posix (Rel (Dir C.WebAppSrcDir)) (Dir GeneratedExternalCodeDir)
relPosixPathFromSrcDirToExtSrcDir = [reldirP|./ext-src|]

View File

@ -1,6 +1,7 @@
module Wasp.Generator.WebAppGenerator.Common
( webAppRootDirInProjectRootDir,
webAppSrcDirInWebAppRootDir,
mkSrcTmplFd,
mkTmplFd,
mkTmplFdWithDst,
mkTmplFdWithData,
@ -13,6 +14,7 @@ module Wasp.Generator.WebAppGenerator.Common
WebAppRootDir,
WebAppSrcDir,
WebAppTemplatesDir,
WebAppTemplatesSrcDir,
)
where
@ -29,6 +31,8 @@ data WebAppSrcDir
data WebAppTemplatesDir
data WebAppTemplatesSrcDir
asTmplFile :: Path' (Rel d) File' -> Path' (Rel WebAppTemplatesDir) File'
asTmplFile = SP.castRel
@ -38,8 +42,6 @@ asWebAppFile = SP.castRel
asWebAppSrcFile :: Path' (Rel d) File' -> Path' (Rel WebAppSrcDir) File'
asWebAppSrcFile = SP.castRel
-- * Paths
-- | Path where web app root dir is generated, relative to the root directory of the whole generated project.
webAppRootDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir WebAppRootDir)
webAppRootDirInProjectRootDir = [reldir|web-app|]
@ -48,15 +50,26 @@ webAppRootDirInProjectRootDir = [reldir|web-app|]
webAppSrcDirInWebAppRootDir :: Path' (Rel WebAppRootDir) (Dir WebAppSrcDir)
webAppSrcDirInWebAppRootDir = [reldir|src|]
-- | Path to generated web app src/ directory, relative to the root directory of the whole generated project.
webAppSrcDirInProjectRootDir :: Path' (Rel ProjectRootDir) (Dir WebAppSrcDir)
webAppSrcDirInProjectRootDir = webAppRootDirInProjectRootDir </> webAppSrcDirInWebAppRootDir
-- * Templates
-- | Path in templates directory where web app templates reside.
webAppTemplatesDirInTemplatesDir :: Path' (Rel TemplatesDir) (Dir WebAppTemplatesDir)
webAppTemplatesDirInTemplatesDir = [reldir|react-app|]
-- | Path to the web app templates src/ directory, relative to the web app templates directory.
srcDirInWebAppTemplatesDir :: Path' (Rel WebAppTemplatesDir) (Dir WebAppTemplatesSrcDir)
srcDirInWebAppTemplatesDir = [reldir|src|]
mkSrcTmplFd :: Path' (Rel WebAppTemplatesSrcDir) File' -> FileDraft
mkSrcTmplFd pathInTemplatesSrcDir = mkTmplFdWithDst srcPath dstPath
where
srcPath = srcDirInWebAppTemplatesDir </> pathInTemplatesSrcDir
dstPath =
webAppSrcDirInWebAppRootDir
</> (SP.castRel pathInTemplatesSrcDir :: Path' (Rel WebAppSrcDir) File')
mkTmplFd :: Path' (Rel WebAppTemplatesDir) File' -> FileDraft
mkTmplFd path = mkTmplFdWithDst path (SP.castRel path)

View File

@ -1,7 +1,7 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.Generator.WebAppGenerator.RouterGenerator
( generateRouter,
( genRouter,
)
where
@ -61,8 +61,8 @@ instance ToJSON PageTemplateData where
"importFrom" .= _importFrom pageTD
]
generateRouter :: AppSpec -> Generator FileDraft
generateRouter spec = do
genRouter :: AppSpec -> Generator FileDraft
genRouter spec = do
return $
C.mkTmplFdWithDstAndData
(asTmplFile $ [reldir|src|] </> routerPath)

View File

@ -15,6 +15,7 @@ import qualified Wasp.Analyzer.TypeChecker as TC
import qualified Wasp.AppSpec.Action as Action
import qualified Wasp.AppSpec.App as App
import qualified Wasp.AppSpec.App.Auth as Auth
import qualified Wasp.AppSpec.App.Client as Client
import qualified Wasp.AppSpec.App.Db as Db
import qualified Wasp.AppSpec.App.Dependency as Dependency
import qualified Wasp.AppSpec.App.Server as Server
@ -50,6 +51,9 @@ spec_Analyzer = do
" server: {",
" setupFn: import { setupServer } from \"@ext/bar.js\"",
" },",
" client: {",
" setupFn: import { setupClient } from \"@ext/baz.js\"",
" },",
" db: {",
" system: PostgreSQL",
" }",
@ -127,6 +131,13 @@ spec_Analyzer = do
(ExtImportField "setupServer")
(fromJust $ SP.parseRelFileP "bar.js")
},
App.client =
Just
Client.Client
{ Client.setupFn =
Just $
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js")
},
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL}
}
)

View File

@ -160,6 +160,7 @@ library
Wasp.AppSpec.Action
Wasp.AppSpec.App
Wasp.AppSpec.App.Auth
Wasp.AppSpec.App.Client
Wasp.AppSpec.App.Db
Wasp.AppSpec.App.Dependency
Wasp.AppSpec.App.Server

View File

@ -28,13 +28,17 @@ Head of your HTML Document. Your app's metadata (styles, links, etc) can be adde
Authentication and authorization configuration.
Check [`app.auth`](/docs/language/features#authentication--authorization) for more details.
#### `db: dict` (optional)
Database configuration.
Check [`app.db`](/docs/language/features#database) for more details.
#### `client: dict` (optional)
Client configuration.
Check [`app.client`](/docs/language/features#client-configuration) for more details.
#### `server: dict` (optional)
Server configuration.
Check [`app.server`](/docs/language/features#server) for more details.
Check [`app.server`](/docs/language/features#server-configuration) for more details.
#### `db: dict` (optional)
Database configuration.
Check [`app.db`](/docs/language/features#database-configuration) for more details.
#### `dependencies: [(string, string)]` (optional)
List of dependencies (external libraries).
@ -917,6 +921,45 @@ import AuthError from '@wasp/core/AuthError.js'
}
```
## Client configuration
You can configure the client using the `client` field inside the `app`
declaration,
```c
app MyApp {
title: "My app",
// ...
client: {
setupFn: import mySetupFunction from "@ext/myClientSetupCode.js"
}
}
```
`app.client` is a dictionary with the following fields:
#### `setupFn: ExtImport` (optional)
`setupFn` declares a JavaScript function that Wasp executes on the client
before everything else. It is expected to be asynchronous, and
Wasp will await its completion before rendering the page. The function takes no
arguments, and its return value is ignored.
You can use this function to perform any custom setup (e.g., setting up
client-side periodic jobs).
Here's a dummy example of such a function:
```js title="ext/myClientSetupCode.js"
async function mySetupFunction() {
let count = 1;
setInterval(
() => console.log(`You have been online for ${count++} hours.`),
1000 * 60 * 60,
)
}
```
## Server configuration
Via `server` field of `app` declaration, you can configure behaviour of the Node.js server (one that is executing wasp operations).
@ -995,7 +1038,7 @@ Any env vars defined in the `.env` will be forwarded to the server-side of your
console.log(process.env.DATABASE_URL)
```
## Database
## Database configuration
Via `db` field of `app` declaration, you can configure the database used by Wasp.