Merge branch 'filip-restructuring-prototype' into filip-project-install-step

This commit is contained in:
Filip Sodić 2024-01-29 12:41:00 +01:00
commit f85daea1fc
23 changed files with 488 additions and 117 deletions

View File

@ -383,6 +383,7 @@ If it happens just once every so it is probably nothing to worry about. If it ha
- Merge `release` back into `main` (`git merge release` while on the `main` branch), if needed.
- Publish new [docs](/web#deployment) from the `release` branch as well.
- Publish new [Mage](/mage#deployment) from the `release` branch as well, if needed.
- Publish new [Wasp VSCode extension](https://github.com/wasp-lang/vscode-wasp), if needed.
- Announce new release in Discord.
#### Determining next version

View File

@ -10,6 +10,7 @@
},
"exports": {
"./core/HttpError": "./core/HttpError.js",
"./core/AuthError": "./core/AuthError.js",
"./core/config": "./core/config.js",
"./core/stitches.config": "./core/stitches.config.js",
"./core/storage": "./core/storage.ts",

View File

@ -1,6 +1,6 @@
import express from 'express'
import HttpError from './core/HttpError.js'
import HttpError from 'wasp/core/HttpError'
import indexRouter from './routes/index.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that

View File

@ -13,7 +13,7 @@ import {
import { ensureValidEmail } from "../../validation.js";
import type { EmailFromField } from '../../../email/core/types.js';
import { GetPasswordResetEmailContentFn } from './types.js';
import HttpError from '../../../core/HttpError.js';
import HttpError from 'wasp/core/HttpError'
export function getRequestPasswordResetRoute({
fromField,

View File

@ -8,7 +8,7 @@ import {
} from "../../utils.js";
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
import { tokenVerificationErrors } from "./types.js";
import HttpError from '../../../core/HttpError.js';
import HttpError from 'wasp/core/HttpError';
export async function resetPassword(
req: Request<{ token: string; password: string; }>,

View File

@ -18,7 +18,7 @@ import {
import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js";
import { GetVerificationEmailContentFn } from './types.js';
import { validateAndGetUserFields } from '../../utils.js'
import HttpError from '../../../core/HttpError.js';
import HttpError from 'wasp/core/HttpError';
import { type UserSignupFields } from '../types.js';
export function getSignupRoute({

View File

@ -7,7 +7,7 @@ import {
deserializeAndSanitizeProviderData,
} from '../../utils.js';
import { tokenVerificationErrors } from './types.js';
import HttpError from '../../../core/HttpError.js';
import HttpError from 'wasp/core/HttpError';
export async function verifyEmail(

View File

@ -1,8 +1,8 @@
{{={= =}=}}
import { hashPassword } from './password.js'
import { verify } from './jwt.js'
import AuthError from '../core/AuthError.js'
import HttpError from '../core/HttpError.js'
import AuthError from 'wasp/core/AuthError'
import HttpError from 'wasp/core/HttpError'
import prisma from '../dbClient.js'
import { sleep } from '../utils.js'
import {

View File

@ -1,4 +1,4 @@
import HttpError from '../core/HttpError.js';
import HttpError from 'wasp/core/HttpError'
export const PASSWORD_FIELD = 'password';
const USERNAME_FIELD = 'username';

View File

@ -1,17 +0,0 @@
class AuthError extends Error {
constructor (message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AuthError)
}
this.name = this.constructor.name
if (data) {
this.data = data
}
}
}
export default AuthError

View File

@ -1,22 +0,0 @@
class HttpError extends Error {
constructor (statusCode, message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError)
}
this.name = this.constructor.name
if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) {
throw new Error('statusCode has to be integer in range [400, 600).')
}
this.statusCode = statusCode
if (data) {
this.data = data
}
}
}
export default HttpError

View File

@ -221,8 +221,6 @@ genSrcDir :: AppSpec -> Generator [FileDraft]
genSrcDir spec =
sequence
[ genFileCopy [relfile|app.js|],
genFileCopy [relfile|core/AuthError.js|],
genFileCopy [relfile|core/HttpError.js|],
genDbClient spec,
genConfigFile spec,
genServerJs spec,

View File

@ -71,7 +71,7 @@ After you defined the API, it should be implemented as a NodeJS function that ta
```ts title="src/server/apis.js"
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
};
```
@ -83,7 +83,7 @@ import { FooBar } from "@wasp/apis/types"; // This type is generated by Wasp bas
export const fooBar: FooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
};
```
@ -340,4 +340,4 @@ The `api` declaration has the following fields:
- `middlewareConfigFn: ServerImport`
The import statement to an Express middleware config function for this API. See more in [middleware section](../advanced/middleware-config) of the docs.
The import statement to an Express middleware config function for this API. See more in [middleware section](../advanced/middleware-config) of the docs.

View File

@ -71,10 +71,11 @@ This is how we can define our `webSocketFn` function:
```ts title=src/server/webSocket.js
import { v4 as uuidv4 } from 'uuid'
import { getFirstProviderUserId } from '@wasp/auth/user.js'
export const webSocketFn = (io, context) => {
io.on('connection', (socket) => {
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
console.log('a user connected: ', username)
socket.on('chatMessage', async (msg) => {
@ -92,10 +93,11 @@ export const webSocketFn = (io, context) => {
```ts title=src/server/webSocket.ts
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
import { v4 as uuidv4 } from 'uuid'
import { getFirstProviderUserId } from '@wasp/auth/user.js'
export const webSocketFn: WebSocketFn = (io, context) => {
io.on('connection', (socket) => {
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
console.log('a user connected: ', username)
socket.on('chatMessage', async (msg) => {
@ -332,4 +334,4 @@ The `webSocket` dict has the following fields:
- `autoConnect: bool`
Whether to automatically connect to the WebSocket server. Default: `true`.
Whether to automatically connect to the WebSocket server. Default: `true`.

View File

@ -0,0 +1,382 @@
---
title: Migration from 0.11.X to 0.12.X
---
Wasp made a big change in the way authentication works in version 0.12.0. This guide will help you migrate your app from 0.11.X to 0.12.X.
## What Changed?
### 0.11.X Auth
In 0.11.X, authentication was based on the `User` model which the developer needed to set up properly and take care of the auth fields like `email` or `password`.
```wasp title="main.wasp"
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
externalAuthAssociations SocialLogin[]
psl=}
entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
```
### New Auth
#### Auth Models
From 0.12.X onwards, authentication is based on the auth models which are automatically set up by Wasp. You don't need to take care of the auth fields anymore.
The `User` model is now just a business logic model and you use it for storing the data that is relevant for your app.
```wasp title="main.wasp"
app myApp {
wasp: {
version: "^0.12.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}
entity User {=psl
id Int @id @default(autoincrement())
psl=}
```
:::caution Regression Note: Multiple Auth Identities per User
With our old auth implementation, if you were using both Google and email auth methods, your users could sign up with Google first and then, later on, reset their password and therefore also enable logging in with their email and password. This was the only way in which a single user could have multiple login methods at the same time (Google and email).
This is not possible anymore. **The new auth system doesn't support multiple login methods per user at the moment**. We do plan to add this soon though, with the introduction of the [account merging feature](https://github.com/wasp-lang/wasp/issues/954).
If you have any users that have both Google and email login credentials at the same time, you will have to pick only one of those for that user to keep when migrating them.
:::
You can read more about the new auth system in the [Auth Entities](./entities) section.
## How to Migrate?
Migrating your existing app to the new auth system is a two-step process:
1. Migrate to the new auth system
1. Cleanup the old auth system
:::info Migrating a deployed app
While going through these steps, we will focus first on doing the changes locally and your local development database.
Once we confirm that everything works well, we will also apply those same changes to the deployed app.
**We'll put extra info for migrating a deployed app in a box like this one.**
:::
### 1. Migrate to the New Auth System
You can follow these steps to migrate to the new auth system:
1. Upgrade Wasp to the latest 0.12 version.
These instructions are for migrating the auth from Wasp `0.11.X` to Wasp `0.12.X`, which means that they work for example for both `0.12.0` and `0.12.5` versions. We suggest that you install the latest 0.12 version of Wasp. Find the available Wasp versions in the [Releases](https://github.com/wasp-lang/wasp/releases) section of our GitHub repo.
Then you can install that version with:
```bash
curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v 0.12.0
```
<small>
In the above command, replace `0.12.0` with the version you want to install.
</small>
1. Bump the version to `^0.12.0` in `main.wasp`.
1. Create the new auth tables in the database by running:
```bash
wasp db migrate-dev
```
You should see the new `Auth`, `AuthIdentity` and `Session` tables in your database. You can use the `wasp db studio` command to open the database in a GUI and verify that the tables are there.
1. Write your data migration function(s) in `src/server/migrateToNewAuth.ts`
- In the previous step, we migrated the schema, and now we need to prepare logic for migrating the data.
- Below you can find [examples of migration functions](#example-migration-scripts) for each of the auth methods. They should be fine to use as-is, meaning you can just copy them, but you can also modify them to your needs. You will want to have one function per each auth method that you use in your app.
1. Add the migration function(s) to the `db.seeds` config:
```wasp title="main.wasp"
app myApp {
wasp: {
version: "^0.12.0"
},
// ...
db: {
seeds: [
import { migrateEmailAuth } from "@server/migrateToNewAuth.js",
import { migrateGoogleAuth } from "@server/migrateToNewAuth.js",
]
},
}
1. Run the migration function(s) by running:
```bash
wasp db seed
```
If you added multiple migration functions, you can pick which one to run by selecting it from the list.
1. Verify that the auth still works by logging in with each of the auth methods.
1. Update your JS code to work correctly with the new auth entities.
You should use the new auth helper functions to get the `email` or `username` from a user object. Read more about the helpers in the [Auth Entities](./entities#accessing-the-auth-fields) section. The helpers you are most likely to use are the `getEmail` and `getUsername` helpers.
1. Finally, check that your app now works as it worked before. If the above steps were done correctly, everything should be working now.
:::info Migrating a deployed app
After successfully performing migration locally so far, and verifying the your app works as expected, it is time to also migrate our deployed app.
Before migrating your production (deployed) app, we advise you to back up your production database in case something goes wrong. Also, besides testing it in development, it's good to test the migration in a staging environment.
We will perform the production migration in 2 steps:
- Deploying the new code to production (client and server).
- Migrating the production database.
---
Between these two steps, so after deploying the new code to production and before migrating the production database, your app will not be working completely: new users will be able to sign up, but existing users won't be able to log in, and already logged in users will be logged out. Once you do the second step, migrating the production database, it will all be back to normal.
You will likely want to keep the time between the two steps as short as you can. Make sure you know exactly what each step means before doing them for real to eliminate any surprises.
---
- **First step:** deploy the new code (client and server), either via `wasp deploy` or manually.
Check our [Deployment docs](../advanced/deployment/overview.md) for more details.
- **Second step:** run the migration script on the production database with `wasp db seed` command.
We wrote instructions on how to do it for **Fly.io** deployments here: https://github.com/wasp-lang/wasp/issues/1464 . The instructions should be similar for other deployment providers: setting up some sort of an SSH tunnel from your local machine to the production database and running the migration script locally with `DATABASE_URL` pointing to the production database.
Your deployed app should be working normally now, with the new auth system.
:::
### 2. Cleanup the Old Auth System
Your app should be working correctly and using new auth, but to finish the migration, we need to clean up the old auth system:
1. Delete auth-related fields from `User` entity.
- This means any fields that were used for authentication, like `email`, `password`, `isEmailVerified`, `emailVerificationSentAt`, `passwordResetSentAt`, `username`, etc.
1. Remove the `externalAuthEntity` from the `auth` config and the `SocialLogin` entity if you used Google or GitHub auth.
1. Run `wasp db migrate-dev` again to remove the redundant fields from the database.
1. You can now delete the migration script and the `db.seeds` config.
:::info Migrating a deployed app
After doing the steps above successfully locally and making sure everything is working, it is time to push these changes to the deployed app again.
_Deploy the app again_, either via `wasp deploy` or manually. Check our [Deployment docs](../advanced/deployment/overview.md) for more details.
The database migrations will automatically run on successful deployment of the server and delete the now redundant auth-related `User` columns from the database.
Your app is now fully migrated to the new auth system.
:::
## Example Migration Functions
The migration functions provided below are written with the typical use cases in mind and you can use them as-is. If your setup requires additional logic, you can use them as a good starting point and modify them to your needs.
### Username & Password
```ts title="src/server/migrateToNewAuth.ts"
import { PrismaClient } from "@prisma/client";
import { ProviderName, UsernameProviderData } from "@wasp/auth/utils";
export async function migrateUsernameAuth(prismaClient: PrismaClient) {
const users = await prismaClient.user.findMany({
include: {
auth: true,
},
});
for (const user of users) {
if (user.auth) {
console.log("User was already migrated, skipping", user);
continue;
}
if (!user.username || !user.password) {
console.log("Missing username auth info, skipping user", user);
continue;
}
const providerData: UsernameProviderData = {
hashedPassword: user.password,
};
const providerName: ProviderName = "username";
await prismaClient.auth.create({
data: {
identities: {
create: {
providerName,
providerUserId: user.username.toLowerCase(),
providerData: JSON.stringify(providerData),
},
},
user: {
connect: {
id: user.id,
},
},
},
});
}
}
```
### Email
```ts title="src/server/migrateToNewAuth.ts"
import { PrismaClient } from "@prisma/client";
import { EmailProviderData, ProviderName } from "@wasp/auth/utils";
export async function migrateEmailAuth(prismaClient: PrismaClient) {
const users = await prismaClient.user.findMany({
include: {
auth: true,
},
});
for (const user of users) {
if (user.auth) {
console.log("User was already migrated, skipping", user);
continue;
}
if (!user.email || !user.password) {
console.log("Missing email auth info, skipping user", user);
continue;
}
const providerData: EmailProviderData = {
isEmailVerified: user.isEmailVerified,
emailVerificationSentAt:
user.emailVerificationSentAt?.toISOString() ?? null,
passwordResetSentAt: user.passwordResetSentAt?.toISOString() ?? null,
hashedPassword: user.password,
};
const providerName: ProviderName = "email";
await prismaClient.auth.create({
data: {
identities: {
create: {
providerName,
providerUserId: user.email,
providerData: JSON.stringify(providerData),
},
},
user: {
connect: {
id: user.id,
},
},
},
});
}
}
```
### Google & GitHub
```ts title="src/server/migrateToNewAuth.ts"
import { PrismaClient } from "@prisma/client";
import { ProviderName } from "@wasp/auth/utils";
export async function migrateGoogleAuth(prismaClient: PrismaClient) {
return createSocialLoginMigration(prismaClient, "google");
}
export async function migrateGitHubAuth(prismaClient: PrismaClient) {
return createSocialLoginMigration(prismaClient, "github");
}
async function createSocialLoginMigration(
prismaClient: PrismaClient,
providerName: "google" | "github"
) {
const users = await prismaClient.user.findMany({
include: {
auth: true,
externalAuthAssociations: true,
},
});
for (const user of users) {
if (user.auth) {
console.log("User was already migrated, skipping", user);
continue;
}
const provider = user.externalAuthAssociations.find(
(provider) => provider.provider === providerName
);
if (!provider) {
console.log(`Missing ${providerName} provider, skipping user`, user);
continue;
}
await prismaClient.auth.create({
data: {
identities: {
create: {
providerName,
providerUserId: provider.providerId,
providerData: JSON.stringify({}),
},
},
user: {
connect: {
id: user.id,
},
},
},
});
}
}
```

View File

@ -476,55 +476,6 @@ Again, annotating the Actions is optional, but greatly improves **full-stack typ
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
### Prisma Error Helpers
In your Operations, you may wish to handle general Prisma errors with HTTP-friendly responses.
Wasp exposes two helper functions, `isPrismaError`, and `prismaErrorToHttpError`, for this purpose. As of now, we convert two specific Prisma errors (which we will continue to expand), with the rest being `500`. See the [source here](https://github.com/wasp-lang/wasp/blob/main/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js).
Here's how you can import and use them:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
// ...
try {
await context.entities.Task.create({...})
} catch (e) {
if (isPrismaError(e)) {
throw prismaErrorToHttpError(e)
} else {
throw e
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```js
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
// ...
try {
await context.entities.Task.create({...})
} catch (e) {
if (isPrismaError(e)) {
throw prismaErrorToHttpError(e)
} else {
throw e
}
}
```
</TabItem>
</Tabs>
## Cache Invalidation
One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date.

View File

@ -35,7 +35,11 @@ Let's create and run our first Wasp app in 3 short steps:
That's it 🎉 You have successfully created and served a new full-stack web app at <http://localhost:3000> and Wasp is serving both frontend and backend for you.
:::note Something Unclear?
Check [More Details](#more-details) section below if anything went wrong with the installation, or if you have additional questions.
Check [More Details](#more-details) section below if anything went wrong with the installation, or if you have additional questions.
:::
:::tip Want an even faster start?
Try out [Wasp AI](../wasp-ai/creating-new-app.md) 🤖 to generate a new Wasp app in minutes just from a title and short description!
:::
:::tip Try Wasp Without Installing 🤔?

View File

@ -0,0 +1,47 @@
---
title: Creating New App with AI
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import ImgWithCaption from '@site/blog/components/ImgWithCaption'
Wasp comes with its own AI: Wasp AI, aka Mage (**M**agic web **A**pp **GE**nerator).
Wasp AI allows you to create a new Wasp app **from only a title and a short description** (using GPT in the background)!
There are two main ways to create a new Wasp app with Wasp AI:
1. Free, open-source online app [usemage.ai](https://usemage.ai).
2. Running `wasp new` on your machine and picking AI generation. For this you need to provide your own OpenAI API keys, but it allows for more flexibility (choosing GPT models).
They both use the same logic in the background, so both approaches are equally "smart", the difference is just in the UI / settings.
:::info
Wasp AI is an experimental feature. Apps that Wasp AI generates can have mistakes (proportional to their complexity), but even then they can often serve as a great starting point (once you fix the mistakes) or an interesting way to explore how to implement stuff in Wasp.
:::
## usemage.ai
<ImgWithCaption
source="img/gpt-wasp/how-it-works.gif"
caption="1. Describe your app 2. Pick the color 3. Generate your app 🚀"
/>
[Mage](https://usemage.ai) is an open-source app with which you can create new Wasp apps from just a short title and description.
It is completely free for you - it uses our OpenAI API keys and we take on the costs.
Once you provide an app title, app description, and choose some basic settings, your new Wasp app will be created for you in a matter of minutes and you will be able to download it to your machine and keep working on it!
If you want to know more, check this [blog post](/blog/2023/07/10/gpt-web-app-generator) for more details on how Mage works, or this [blog post](blog/2023/07/17/how-we-built-gpt-web-app-generator) for a high-level overview of how we implemented it.
## Wasp Cli
You can create a new Wasp app using Wasp AI by running `wasp new` in your terminal and picking AI generation.
If you don't have them set yet, `wasp` will ask you to provide (via ENV vars) your OpenAI API keys (which it will use to query GPT).
Then, after providing a title and description for your Wasp app, the new app will be generated on your disk!
![wasp-cli-ai-input](./wasp-ai-1.png)
![wasp-cli-ai-generation](./wasp-ai-2.png)

View File

@ -0,0 +1,9 @@
---
title: Developing Existing App with AI
---
import useBaseUrl from '@docusaurus/useBaseUrl';
While Wasp AI doesn't at the moment offer any additional help for developing your Wasp app with AI beyond initial generation, this is something we are exploring actively.
In the meantime, while waiting for Wasp AI to add support for this, we suggest checking out [aider](https://github.com/paul-gauthier/aider), which is an AI pair programming tool in your terminal. This is a third-party tool, not affiliated with Wasp in any way, but we and some of Wasp users have found that it can be helpful when working on Wasp apps.

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

View File

@ -105,7 +105,7 @@ module.exports = {
{
label: 'Todo app tutorial',
to: 'docs/tutorial/create',
}
},
],
},
{
@ -140,7 +140,7 @@ module.exports = {
// ContextualSearch is useful when you are doing versioning,
// it searches only in v1 docs if you are searching from v1 docs.
// Therefore we have it enabled, since we have multiple doc versions.
contextualSearch: true
contextualSearch: true,
},
image: 'img/wasp_twitter_cover.png',
metadata: [{ name: 'twitter:card', content: 'summary_large_image' }],
@ -163,6 +163,11 @@ module.exports = {
// "current" docs (under /docs) are in-progress docs, so we show them only in development.
includeCurrentVersion: process.env.NODE_ENV === 'development',
// In development, we want "current" docs to be the default docs (served at /docs),
// to make it easier for us a bit. Otherwise, by default, the latest versioned docs
// will be served under /docs.
lastVersion:
process.env.NODE_ENV === 'development' ? 'current' : undefined,
// Uncomment line below to build and show only current docs, for faster build times
// during development, if/when needed.
@ -171,15 +176,18 @@ module.exports = {
// "versions" option here enables us to customize each version of docs individually,
// and there are also other options if we ever need to customize versioned docs further.
versions: {
...(
(process.env.NODE_ENV === 'development') ? {
"current": {
path: "next", // Token used in the URL to address this version of docs: {baseUrl}/docs/{path}.
label: "Next", // Label shown in the documentation to address this version of docs.
noIndex: true, // these are un-released docs, we don't want search engines indexing them.
// We provide config for `current` only during development because otherwise
// we don't even build them (due to includeCurrentVersion above), so this config
// would cause error in that case.
...(process.env.NODE_ENV === 'development'
? {
current: {
label: 'Next', // Label shown in the documentation to address this version of docs.
noIndex: true, // these are un-released docs, we don't want search engines indexing them.
},
}
} : {}
),
: {}),
// Configuration example:
// "0.11.1": {
// path: "0.11.1", // default, but can be anything.
@ -187,10 +195,9 @@ module.exports = {
// banner: "unmaintained"
// // and more!
// },
}
},
// ------------------------------------------------------ //
},
blog: {
showReadingTime: true,

View File

@ -68,6 +68,7 @@ module.exports = {
],
},
'auth/entities/entities',
'auth/migrate-from-11',
],
},
{
@ -88,6 +89,13 @@ module.exports = {
'project/custom-vite-config',
],
},
{
type: 'category',
label: 'Wasp AI',
collapsed: false,
collapsible: false,
items: ['wasp-ai/creating-new-app', 'wasp-ai/developing-existing-app'],
},
{
type: 'category',
label: 'Advanced Features',