Adds support for client subdir (#1516)

This commit is contained in:
Mihovil Ilakovac 2023-10-27 15:12:47 +02:00 committed by GitHub
parent 9f31b3229e
commit 520c35af5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 150 additions and 41 deletions

View File

@ -1,5 +1,22 @@
# Changelog
## 0.11.8
### 🎉 [New Feature] Serving the Client From a Subdirectory
You can now serve the client from a subdirectory. This is useful if you want to serve the client from a subdirectory of your domain, e.g. `https://example.com/my-app/`.
To do this, you need to add the `client.baseDir` property to your `.wasp` file:
```wasp
app todoApp {
// ...
client: {
baseDir: "/my-app",
},
}
```
## 0.11.7
### 🐞 Bug fixes / 🔧 small improvements

View File

@ -47,7 +47,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="{= baseDir =}">
{=# rootComponent.isDefined =}
<{= rootComponent.importIdentifier =}>
{=/ rootComponent.isDefined =}

View File

@ -12,9 +12,10 @@ const _waspUserProvidedConfig = {};
{=/ customViteConfig.isDefined =}
const defaultViteConfig = {
base: "{= baseDir =}",
plugins: [react()],
server: {
port: 3000,
port: {= defaultClientPort =},
host: "0.0.0.0",
open: true,
},

View File

@ -32,7 +32,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || '{= defaultClientUrl =}');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -109,7 +109,7 @@
"file",
"server/src/config.js"
],
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
"d135535e045e5f5852e0b6d8bd49360e7231021cd38b540f419f5f44c6158dc2"
],
[
[
@ -459,7 +459,7 @@
"file",
"web-app/src/router.tsx"
],
"79192ef1e636c2573815ca7151cf8e3a76ebb00a0b0ee3e804cbb60cba5f7197"
"067478c4990bbe966fa1984cd9db91aba9aaa68196c5858eab787eb376ab48b9"
],
[
[
@ -564,6 +564,6 @@
"file",
"web-app/vite.config.ts"
],
"ba22ae0b9027a2a4d3cd2689e9a9e1caff526b96dfab5e7f0f58f194dff830d9"
"08962d79f2d71eb470ee85dee03db6deca7ede28df9d41542bbaea752db0eeed"
]
]

View File

@ -26,7 +26,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000/');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -24,7 +24,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="/">
<Switch>
{Object.entries(routes).map(([routeKey, route]) => (
<Route

View File

@ -6,6 +6,7 @@ import customViteConfig from './src/ext-src/vite.config'
const _waspUserProvidedConfig = customViteConfig
const defaultViteConfig = {
base: "/",
plugins: [react()],
server: {
port: 3000,

View File

@ -116,7 +116,7 @@
"file",
"server/src/config.js"
],
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
"d135535e045e5f5852e0b6d8bd49360e7231021cd38b540f419f5f44c6158dc2"
],
[
[
@ -473,7 +473,7 @@
"file",
"web-app/src/router.tsx"
],
"79192ef1e636c2573815ca7151cf8e3a76ebb00a0b0ee3e804cbb60cba5f7197"
"067478c4990bbe966fa1984cd9db91aba9aaa68196c5858eab787eb376ab48b9"
],
[
[
@ -578,6 +578,6 @@
"file",
"web-app/vite.config.ts"
],
"ba22ae0b9027a2a4d3cd2689e9a9e1caff526b96dfab5e7f0f58f194dff830d9"
"08962d79f2d71eb470ee85dee03db6deca7ede28df9d41542bbaea752db0eeed"
]
]

View File

@ -26,7 +26,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000/');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -24,7 +24,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="/">
<Switch>
{Object.entries(routes).map(([routeKey, route]) => (
<Route

View File

@ -6,6 +6,7 @@ import customViteConfig from './src/ext-src/vite.config'
const _waspUserProvidedConfig = customViteConfig
const defaultViteConfig = {
base: "/",
plugins: [react()],
server: {
port: 3000,

View File

@ -193,7 +193,7 @@
"file",
"server/src/config.js"
],
"b5e2d31201460b018e781532b7bb6348cf3f7eb45851a81711a18584c0f4f5ac"
"e4b647166ce3a74d1260f3505ce6366b52d195d6d1dcb75929b3fef30ea9ea4d"
],
[
[
@ -914,7 +914,7 @@
"file",
"web-app/src/router.tsx"
],
"1b167573635d206d53a8598ae39a81b140cd017c6c71ce13bf57c9a04b5b8160"
"16209b667488c40c3a7a80be224d6d1ebb8df75bbaf8f2c6398eff07a92618ca"
],
[
[
@ -1026,6 +1026,6 @@
"file",
"web-app/vite.config.ts"
],
"ba22ae0b9027a2a4d3cd2689e9a9e1caff526b96dfab5e7f0f58f194dff830d9"
"08962d79f2d71eb470ee85dee03db6deca7ede28df9d41542bbaea752db0eeed"
]
]

View File

@ -29,7 +29,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000/');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -27,7 +27,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="/">
<App>
<Switch>
{Object.entries(routes).map(([routeKey, route]) => (

View File

@ -6,6 +6,7 @@ import customViteConfig from './src/ext-src/vite.config'
const _waspUserProvidedConfig = customViteConfig
const defaultViteConfig = {
base: "/",
plugins: [react()],
server: {
port: 3000,

View File

@ -116,7 +116,7 @@
"file",
"server/src/config.js"
],
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
"d135535e045e5f5852e0b6d8bd49360e7231021cd38b540f419f5f44c6158dc2"
],
[
[
@ -515,7 +515,7 @@
"file",
"web-app/src/router.tsx"
],
"79192ef1e636c2573815ca7151cf8e3a76ebb00a0b0ee3e804cbb60cba5f7197"
"067478c4990bbe966fa1984cd9db91aba9aaa68196c5858eab787eb376ab48b9"
],
[
[
@ -620,6 +620,6 @@
"file",
"web-app/vite.config.ts"
],
"ba22ae0b9027a2a4d3cd2689e9a9e1caff526b96dfab5e7f0f58f194dff830d9"
"08962d79f2d71eb470ee85dee03db6deca7ede28df9d41542bbaea752db0eeed"
]
]

View File

@ -26,7 +26,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000/');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -24,7 +24,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="/">
<Switch>
{Object.entries(routes).map(([routeKey, route]) => (
<Route

View File

@ -6,6 +6,7 @@ import customViteConfig from './src/ext-src/vite.config'
const _waspUserProvidedConfig = customViteConfig
const defaultViteConfig = {
base: "/",
plugins: [react()],
server: {
port: 3000,

View File

@ -116,7 +116,7 @@
"file",
"server/src/config.js"
],
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
"d135535e045e5f5852e0b6d8bd49360e7231021cd38b540f419f5f44c6158dc2"
],
[
[
@ -473,7 +473,7 @@
"file",
"web-app/src/router.tsx"
],
"79192ef1e636c2573815ca7151cf8e3a76ebb00a0b0ee3e804cbb60cba5f7197"
"067478c4990bbe966fa1984cd9db91aba9aaa68196c5858eab787eb376ab48b9"
],
[
[
@ -578,6 +578,6 @@
"file",
"web-app/vite.config.ts"
],
"ba22ae0b9027a2a4d3cd2689e9a9e1caff526b96dfab5e7f0f58f194dff830d9"
"08962d79f2d71eb470ee85dee03db6deca7ede28df9d41542bbaea752db0eeed"
]
]

View File

@ -26,7 +26,7 @@ const resolvedConfig = merge(config.all, config[env])
export default resolvedConfig
function getDevelopmentConfig() {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL) || 'http://localhost:3000';
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000/');
return {
frontendUrl,
allowedCORSOrigins: '*',

View File

@ -24,7 +24,7 @@ export const routes = {
export type Routes = RouteDefinitionsToRoutes<typeof routes>
const router = (
<Router>
<Router basename="/">
<Switch>
{Object.entries(routes).map(([routeKey, route]) => (
<Route

View File

@ -6,6 +6,7 @@ import customViteConfig from './src/ext-src/vite.config'
const _waspUserProvidedConfig = customViteConfig
const defaultViteConfig = {
base: "/",
plugins: [react()],
server: {
port: 3000,

View File

@ -10,6 +10,8 @@ import Wasp.AppSpec.ExtImport (ExtImport)
data Client = Client
{ setupFn :: Maybe ExtImport,
rootComponent :: Maybe ExtImport
rootComponent :: Maybe ExtImport,
-- We expect the base dir to start with a slash e.g. /client
baseDir :: Maybe String
}
deriving (Show, Eq, Data)

View File

@ -23,6 +23,7 @@ import Wasp.AppSpec.App (App)
import qualified Wasp.AppSpec.App as AS.App
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 AS.Db
import qualified Wasp.AppSpec.App.Wasp as Wasp
import Wasp.AppSpec.Core.Decl (takeDecls)
@ -61,7 +62,8 @@ validateAppSpec spec =
validateApiRoutesAreUnique spec,
validateApiNamespacePathsAreUnique spec,
validateCrudOperations spec,
validatePrismaOptions spec
validatePrismaOptions spec,
validateWebAppBaseDir spec
]
validateExactlyOneAppExists :: AppSpec -> Maybe ValidationError
@ -364,6 +366,19 @@ validatePrismaOptions spec =
prismaClientPreviewFeatures = AS.Db.clientPreviewFeatures =<< prismaOptions
prismaDbExtensions = AS.Db.dbExtensions =<< prismaOptions
validateWebAppBaseDir :: AppSpec -> [ValidationError]
validateWebAppBaseDir spec = case maybeBaseDir of
Just baseDir
| not (startsWithSlash baseDir) ->
[GenericValidationError "The app.client.baseDir should start with a slash e.g. \"/test\""]
_anyOtherCase -> []
where
maybeBaseDir = Client.baseDir =<< AS.App.client (snd $ getApp spec)
startsWithSlash :: String -> Bool
startsWithSlash ('/' : _) = True
startsWithSlash _ = False
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
-- Check https://github.com/wasp-lang/wasp/pull/455 for considerations on this and analysis of different approaches.

View File

@ -1,6 +1,5 @@
module Wasp.Generator.ServerGenerator.ConfigG
( genConfigFile,
configFileInSrcDir,
)
where
@ -12,6 +11,7 @@ import Wasp.AppSpec.Valid (isAuthEnabled)
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C
import qualified Wasp.Generator.WebAppGenerator.Common as WebApp
import Wasp.Project.Db (databaseUrlEnvVarName)
genConfigFile :: AppSpec -> Generator FileDraft
@ -22,7 +22,8 @@ genConfigFile spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tm
tmplData =
object
[ "isAuthEnabled" .= isAuthEnabled spec,
"databaseUrlEnvVarName" .= databaseUrlEnvVarName
"databaseUrlEnvVarName" .= databaseUrlEnvVarName,
"defaultClientUrl" .= WebApp.getDefaultClientUrl spec
]
configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'

View File

@ -339,7 +339,11 @@ genViteConfig spec = return $ C.mkTmplFdWithData tmplFile tmplData
where
tmplFile = C.asTmplFile [relfile|vite.config.ts|]
tmplData =
object ["customViteConfig" .= jsImportToImportJson (makeCustomViteConfigJsImport <$> AS.customViteConfigPath spec)]
object
[ "customViteConfig" .= jsImportToImportJson (makeCustomViteConfigJsImport <$> AS.customViteConfigPath spec),
"baseDir" .= SP.fromAbsDirP (C.getBaseDir spec),
"defaultClientPort" .= C.defaultClientPort
]
makeCustomViteConfigJsImport :: Path' (Rel SourceExternalCodeDir) File' -> JsImport
makeCustomViteConfigJsImport pathToConfig = makeJsImport importPath importName

View File

@ -20,14 +20,21 @@ module Wasp.Generator.WebAppGenerator.Common
toViteImportPath,
staticAssetsDirInWebAppDir,
WebAppStaticAssetsDir,
getBaseDir,
getDefaultClientUrl,
defaultClientPort,
)
where
import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust)
import StrongPath (Dir, File, File', Path, Path', Posix, Rel, reldir, (</>))
import Data.Maybe (fromJust, fromMaybe)
import StrongPath (Abs, Dir, File, File', Path, Path', Posix, Rel, absdirP, reldir, (</>))
import qualified StrongPath as SP
import System.FilePath (splitExtension)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Client as AS.App.Client
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.Common
( GeneratedSrcDir,
ProjectRootDir,
@ -123,3 +130,14 @@ toViteImportPath :: Path Posix (Rel r) (File f) -> Path Posix (Rel r) (File f)
toViteImportPath = fromJust . SP.parseRelFileP . dropExtension . SP.fromRelFileP
where
dropExtension = fst . splitExtension
getBaseDir :: AppSpec -> Path Posix Abs (Dir ())
getBaseDir spec = fromMaybe [absdirP|/|] maybeBaseDir
where
maybeBaseDir = SP.parseAbsDirP =<< (AS.App.Client.baseDir =<< AS.App.client (snd $ getApp spec))
defaultClientPort :: Int
defaultClientPort = 3000
getDefaultClientUrl :: AppSpec -> String
getDefaultClientUrl spec = "http://localhost:" ++ show defaultClientPort ++ SP.fromAbsDirP (getBaseDir spec)

View File

@ -10,6 +10,7 @@ import qualified Data.Aeson as Aeson
import Data.List (find)
import Data.Maybe (fromMaybe)
import StrongPath (Dir, Path, Rel, reldir, reldirP, relfile, (</>))
import qualified StrongPath as SP
import StrongPath.Types (Posix)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
@ -36,7 +37,8 @@ data RouterTemplateData = RouterTemplateData
_isAuthEnabled :: Bool,
_isExternalAuthEnabled :: Bool,
_externalAuthProviders :: ![ExternalAuthProviderTemplateData],
_rootComponent :: Aeson.Value
_rootComponent :: Aeson.Value,
_baseDir :: String
}
instance ToJSON RouterTemplateData where
@ -47,7 +49,8 @@ instance ToJSON RouterTemplateData where
"isAuthEnabled" .= _isAuthEnabled routerTD,
"isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD,
"externalAuthProviders" .= _externalAuthProviders routerTD,
"rootComponent" .= _rootComponent routerTD
"rootComponent" .= _rootComponent routerTD,
"baseDir" .= _baseDir routerTD
]
data RouteTemplateData = RouteTemplateData
@ -124,7 +127,8 @@ createRouterTemplateData spec =
_isAuthEnabled = isAuthEnabled spec,
_isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True,
_externalAuthProviders = externalAuthProviders,
_rootComponent = extImportToImportJson relPathToWebAppSrcDir maybeRootComponent
_rootComponent = extImportToImportJson relPathToWebAppSrcDir maybeRootComponent,
_baseDir = SP.fromAbsDirP $ C.getBaseDir spec
}
where
routes = map (createRouteTemplateData spec) $ AS.getRoutes spec

View File

@ -62,7 +62,8 @@ spec_Analyzer = do
" },",
" client: {",
" rootComponent: import { App } from \"@client/App.jsx\",",
" setupFn: import { setupClient } from \"@client/baz.js\"",
" setupFn: import { setupClient } from \"@client/baz.js\",",
" baseDir: \"/\"",
" },",
" db: {",
" system: PostgreSQL,",
@ -176,7 +177,8 @@ spec_Analyzer = do
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js"),
Client.rootComponent =
Just $
ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx")
ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx"),
Client.baseDir = Just "/"
},
App.db =
Just

View File

@ -0,0 +1,6 @@
:::caution Setting the correct env variable
If you set the `baseDir` option, make sure that the `WASP_WEB_CLIENT_URL` env variable also includes that base directory.
For example, if you are serving your app from `https://example.com/my-app`, the `WASP_WEB_CLIENT_URL` should be also set to `https://example.com/my-app`, and not just `https://example.com`.
:::

View File

@ -2,6 +2,8 @@
title: Client Config
---
import BaseDirEnvNote from './_baseDirEnvNote.md'
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'
You can configure the client using the `client` field inside the `app` declaration:
@ -265,6 +267,26 @@ explained in
Read more about the setup function in the [API Reference](#setupfn-clientimport).
## Base Directory
If you need to serve the client from a subdirectory, you can use the `baseDir` option:
```wasp title="main.wasp"
app MyApp {
title: "My app",
// ...
client: {
baseDir: "/my-app",
}
}
```
This means that if you serve your app from `https://example.com/my-app`, the
router will work correctly, and all the assets will be served from
`https://example.com/my-app`.
<BaseDirEnvNote />
## API Reference
<Tabs groupId="js-ts">
@ -290,7 +312,8 @@ app MyApp {
// ...
client: {
rootComponent: import Root from "@client/Root.tsx",
setupFn: import mySetupFunction from "@client/myClientSetupCode.ts"
setupFn: import mySetupFunction from "@client/myClientSetupCode.ts",
baseDir: "/my-app",
}
}
```
@ -413,3 +436,14 @@ Client has the following options:
</TabItem>
</Tabs>
- #### `baseDir: String`
If you need to serve the client from a subdirectory, you can use the `baseDir` option.
If you set `baseDir` to `/my-app` for example, that will make Wasp set the `basename` prop of the `Router` to
`/my-app`. It will also set the `base` option of the Vite config to `/my-app`.
This means that if you serve your app from `https://example.com/my-app`, the router will work correctly, and all the assets will be served from `https://example.com/my-app`.
<BaseDirEnvNote />