Skip to content

Commit 7ae8829

Browse files
authored
Add Nuxt emulator adapter (#188)
* Add Nuxt emulator adapter * Add @emulators/adapter-nuxt with Nuxt server route handling, persistence, response rewriting, and Nitro tracing support. * Document Nuxt setup across README, docs site, and agent skills. * Add Nuxt embedded example * Add examples/nuxt-embedded demonstrating embedded emulators in a Nuxt app via @emulators/adapter-nuxt: same-origin OAuth flow (GitHub + Google), catch-all emulate server route, and cookie-based sessions. * Fix withEmulate to ship the emulator UI fonts into production builds. It set a non-existent nitro.traceDeps option, so builds 500'd on missing font files. Replace with a Nitro `compiled` hook that copies @emulators/core's fonts into the server output. * Export ./package.json from @emulators/core so the adapter can resolve the fonts directory via require.resolve. * Add @parcel/watcher and esbuild build-script approvals pulled in by Nuxt so pnpm install exits cleanly. * Fix Nuxt example install in CI * Ignore generated Nuxt files in Prettier * Fix Nuxt adapter persistence and forwarded hosts * Address Nuxt adapter review feedback
1 parent 830fbd7 commit 7ae8829

41 files changed

Lines changed: 13453 additions & 4372 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
dist
22
.next
3+
.nuxt
34
node_modules
45
pnpm-lock.yaml

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,99 @@ persistence: filePersistence('.emulate/state.json'),
10941094

10951095
The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
10961096

1097+
## Nuxt Integration
1098+
1099+
Embed emulators directly in your Nuxt app so they run on the same origin. This gives OAuth flows stable callback URLs in local and preview deployments.
1100+
1101+
### Install
1102+
1103+
```bash
1104+
npm install @emulators/adapter-nuxt @emulators/github @emulators/google
1105+
```
1106+
1107+
Only install the emulators you need. Each `@emulators/*` package is published independently.
1108+
1109+
### Server route
1110+
1111+
Create a named catch-all route that serves emulator traffic:
1112+
1113+
```typescript
1114+
// server/routes/emulate/[...path].ts
1115+
import { createEmulateHandler } from '@emulators/adapter-nuxt'
1116+
import * as github from '@emulators/github'
1117+
import * as google from '@emulators/google'
1118+
1119+
export default defineEventHandler(createEmulateHandler({
1120+
services: {
1121+
github: {
1122+
emulator: github,
1123+
seed: {
1124+
users: [{ login: 'octocat', name: 'The Octocat' }],
1125+
repos: [{ owner: 'octocat', name: 'hello-world', auto_init: true }],
1126+
},
1127+
},
1128+
google: {
1129+
emulator: google,
1130+
seed: {
1131+
users: [{ email: 'test@example.com', name: 'Test User' }],
1132+
},
1133+
},
1134+
},
1135+
}))
1136+
```
1137+
1138+
### Nuxt config
1139+
1140+
Emulator UI pages use bundled fonts. Wrap your Nuxt config so Nitro traces the core package assets into production builds:
1141+
1142+
```typescript
1143+
// nuxt.config.ts
1144+
import { withEmulate } from '@emulators/adapter-nuxt'
1145+
1146+
export default defineNuxtConfig(withEmulate({
1147+
// your normal Nuxt config
1148+
}))
1149+
```
1150+
1151+
### OAuth configuration
1152+
1153+
Point your OAuth provider at the emulator paths on the same origin:
1154+
1155+
```typescript
1156+
const baseUrl = process.env.NUXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
1157+
1158+
export const githubOAuth = {
1159+
clientId: 'any-value',
1160+
clientSecret: 'any-value',
1161+
authorizationUrl: `${baseUrl}/emulate/github/login/oauth/authorize`,
1162+
tokenUrl: `${baseUrl}/emulate/github/login/oauth/access_token`,
1163+
userInfoUrl: `${baseUrl}/emulate/github/user`,
1164+
}
1165+
```
1166+
1167+
No `oauth_apps` need to be seeded. When none are configured, the emulator skips `client_id`, `client_secret`, and `redirect_uri` validation.
1168+
1169+
### Persistence
1170+
1171+
By default, emulator state is in-memory and resets on every cold start. To persist state across restarts, pass a `persistence` adapter:
1172+
1173+
```typescript
1174+
import { createEmulateHandler } from '@emulators/adapter-nuxt'
1175+
import * as github from '@emulators/github'
1176+
1177+
const storageAdapter = {
1178+
async load() { return await useStorage('emulate').getItem<string>('state') },
1179+
async save(data: string) { await useStorage('emulate').setItem('state', data) },
1180+
}
1181+
1182+
export default defineEventHandler(createEmulateHandler({
1183+
services: { github: { emulator: github } },
1184+
persistence: storageAdapter,
1185+
}))
1186+
```
1187+
1188+
The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
1189+
10971190
## Architecture
10981191

10991192
```
@@ -1102,6 +1195,7 @@ packages/
11021195
@emulators/
11031196
core/ # HTTP server, in-memory store, plugin interface, middleware
11041197
adapter-next/ # Next.js App Router integration
1198+
adapter-nuxt/ # Nuxt server route integration
11051199
vercel/ # Vercel API service
11061200
github/ # GitHub API service
11071201
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, Drive

apps/web/app/api/docs-chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
1414

1515
const SYSTEM_PROMPT = `You are a helpful documentation assistant for emulate, a local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, Okta, AWS, Resend, Stripe, MongoDB Atlas, Clerk, Linear, and Twilio APIs used in CI and no-network sandboxes.
1616
17-
emulate provides fully stateful, production-fidelity API emulation, not mocks. The CLI is installed as the "emulate" npm package and run via "npx emulate". It also supports a programmatic API via createEmulator and a Next.js adapter (@emulators/adapter-next) for embedding emulators in your app.
17+
emulate provides fully stateful, production-fidelity API emulation, not mocks. The CLI is installed as the "emulate" npm package and run via "npx emulate". It also supports a programmatic API via createEmulator, a Next.js adapter (@emulators/adapter-next), and a Nuxt adapter (@emulators/adapter-nuxt) for embedding emulators in your app.
1818
1919
You have access to the full emulate documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
2020

apps/web/app/docs/architecture/page.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ packages/
66
@emulators/
77
core/ # HTTP server, in-memory store, plugin interface, middleware
88
adapter-next/ # Next.js App Router integration
9+
adapter-nuxt/ # Nuxt server route integration
910
vercel/ # Vercel API service
1011
github/ # GitHub API service
1112
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, Drive

apps/web/app/docs/nuxt/layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { pageMetadata } from "@/lib/page-metadata";
2+
3+
export const metadata = pageMetadata("nuxt");
4+
5+
export default function Layout({ children }: { children: React.ReactNode }) {
6+
return children;
7+
}

apps/web/app/docs/nuxt/page.mdx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Nuxt Integration
2+
3+
The `@emulators/adapter-nuxt` package embeds emulators directly into a Nuxt app, running them on the same origin. This is useful for preview deployments where OAuth callback URLs change with every deployment.
4+
5+
## Install
6+
7+
```bash
8+
npm install @emulators/adapter-nuxt @emulators/github @emulators/google
9+
```
10+
11+
Only install the emulators you need. Each `@emulators/*` package is published independently, keeping your server bundles small.
12+
13+
## Server Route
14+
15+
Create a named catch-all route that serves emulator traffic:
16+
17+
```typescript
18+
// server/routes/emulate/[...path].ts
19+
import { createEmulateHandler } from '@emulators/adapter-nuxt'
20+
import * as github from '@emulators/github'
21+
import * as google from '@emulators/google'
22+
23+
export default defineEventHandler(createEmulateHandler({
24+
services: {
25+
github: {
26+
emulator: github,
27+
seed: {
28+
users: [{ login: 'octocat', name: 'The Octocat' }],
29+
repos: [{ owner: 'octocat', name: 'hello-world', auto_init: true }],
30+
},
31+
},
32+
google: {
33+
emulator: google,
34+
seed: {
35+
users: [{ email: 'test@example.com', name: 'Test User' }],
36+
},
37+
},
38+
},
39+
}))
40+
```
41+
42+
This creates these routes:
43+
44+
- `/emulate/github/**` serves the GitHub emulator
45+
- `/emulate/google/**` serves the Google emulator
46+
47+
## Nuxt Config
48+
49+
Emulator UI pages use bundled fonts. Wrap your Nuxt config so Nitro traces the core package assets into production builds:
50+
51+
```typescript
52+
// nuxt.config.ts
53+
import { withEmulate } from '@emulators/adapter-nuxt'
54+
55+
export default defineNuxtConfig(withEmulate({
56+
// your normal Nuxt config
57+
}))
58+
```
59+
60+
## OAuth Configuration
61+
62+
Point your OAuth provider at the emulator paths on the same origin:
63+
64+
```typescript
65+
const baseUrl = process.env.NUXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
66+
67+
export const githubOAuth = {
68+
clientId: 'any-value',
69+
clientSecret: 'any-value',
70+
authorizationUrl: `${baseUrl}/emulate/github/login/oauth/authorize`,
71+
tokenUrl: `${baseUrl}/emulate/github/login/oauth/access_token`,
72+
userInfoUrl: `${baseUrl}/emulate/github/user`,
73+
}
74+
```
75+
76+
No `oauth_apps` need to be seeded. When none are configured, the emulator skips `client_id`, `client_secret`, and `redirect_uri` validation.
77+
78+
## Persistence
79+
80+
By default, emulator state is in-memory and resets on every cold start. To persist state across restarts, pass a `persistence` adapter.
81+
82+
### Nitro Storage
83+
84+
```typescript
85+
import { createEmulateHandler } from '@emulators/adapter-nuxt'
86+
import * as github from '@emulators/github'
87+
88+
const storageAdapter = {
89+
async load() { return await useStorage('emulate').getItem<string>('state') },
90+
async save(data: string) { await useStorage('emulate').setItem('state', data) },
91+
}
92+
93+
export default defineEventHandler(createEmulateHandler({
94+
services: { github: { emulator: github } },
95+
persistence: storageAdapter,
96+
}))
97+
```
98+
99+
### File Persistence
100+
101+
For local development, `@emulators/core` ships a file-based adapter:
102+
103+
```typescript
104+
import { filePersistence } from '@emulators/core'
105+
106+
persistence: filePersistence('.emulate/state.json'),
107+
```
108+
109+
### How It Works
110+
111+
- **Cold start**: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map. If not found, it seeds from config and saves the initial state.
112+
- **After mutating requests** (POST, PUT, PATCH, DELETE): State is saved. Saves are serialized via an internal queue to prevent race conditions.
113+
- **No persistence configured**: Falls back to pure in-memory. Seed data re-initializes on every cold start.
114+
115+
## Custom Mounts
116+
117+
The adapter reads the `path` param from `server/routes/emulate/[...path].ts`. If you use a different catch-all name, pass it as the second argument:
118+
119+
```typescript
120+
export default defineEventHandler(createEmulateHandler(config, { param: 'slug' }))
121+
```
122+
123+
If the mount path cannot be detected from the URL, pass `routePrefix`:
124+
125+
```typescript
126+
export default defineEventHandler(createEmulateHandler(config, { routePrefix: '/api/emulate' }))
127+
```
128+
129+
## How It Works
130+
131+
1. **Incoming request**: `/emulate/github/login/oauth/authorize?client_id=...`
132+
2. **Parse**: service = `github`, rest = `/login/oauth/authorize`
133+
3. **Strip prefix**: A new `Request` is created with the stripped path and forwarded to the GitHub service app
134+
4. **Rewrite response**: HTML `action` and `href` attributes, CSS `url()` font references, and `Location` headers get the service prefix prepended
135+
5. **Persist**: After mutating requests, state is saved via the persistence adapter
136+
137+
## Limitations
138+
139+
- Requires a Node-compatible Nuxt server runtime since emulators use Node APIs
140+
- Concurrent serverless instances writing to the same persistence adapter use last write wins semantics, which is acceptable for dev and preview traffic

apps/web/app/docs/page.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@ The port can also be set via `EMULATE_PORT` or `PORT` environment variables.
8484

8585
You can also use emulate as a library in your tests. See the [Programmatic API](/docs/programmatic-api) page for `createEmulator`, Vitest/Jest setup, and instance methods.
8686

87-
## Next.js Integration
87+
## Framework Integration
8888

89-
Embed emulators directly in your Next.js app so they run on the same origin. See the [Next.js Integration](/docs/nextjs) page for setup instructions.
89+
Embed emulators directly in your Next.js or Nuxt app so they run on the same origin. See the [Next.js Integration](/docs/nextjs) and [Nuxt Integration](/docs/nuxt) pages for setup instructions.

apps/web/app/docs/programmatic-api/page.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ npm install @emulators/github @emulators/google @emulators/linear
137137
<tr><td><code>@emulators/clerk</code></td><td>Clerk Backend API and OAuth</td></tr>
138138
<tr><td><code>@emulators/core</code></td><td>Shared store, middleware, and utilities</td></tr>
139139
<tr><td><code>@emulators/adapter-next</code></td><td>Next.js App Router integration</td></tr>
140+
<tr><td><code>@emulators/adapter-nuxt</code></td><td>Nuxt server route integration</td></tr>
140141
</tbody>
141142
</table>
142143

@@ -149,4 +150,4 @@ import * as github from '@emulators/github'
149150
import * as stripe from '@emulators/stripe'
150151
```
151152

152-
These are the same modules used internally by the CLI and the [Next.js adapter](/docs/nextjs). See the Next.js Integration page for a full example of using scoped packages with `createEmulateHandler`.
153+
These are the same modules used internally by the CLI and the [Next.js adapter](/docs/nextjs) or [Nuxt adapter](/docs/nuxt). See the framework integration pages for full examples of using scoped packages with `createEmulateHandler`.

apps/web/app/page.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,17 @@ afterAll(() => github.close())`}</code>
151151
</div>
152152
</div>
153153
<div className="rounded-lg border border-neutral-200 p-4 dark:border-neutral-800">
154-
<div className="mb-1 text-sm font-medium text-neutral-900 dark:text-neutral-100">Next.js adapter</div>
154+
<div className="mb-1 text-sm font-medium text-neutral-900 dark:text-neutral-100">Framework adapters</div>
155155
<p className="mb-3 text-sm text-neutral-600 dark:text-neutral-400">
156-
Embed in your app. Same origin, no CORS issues, works on Vercel preview deployments.
156+
Embed in Next.js or Nuxt. Same origin, no CORS issues, works on preview deployments.
157157
</p>
158-
<div className="overflow-x-auto rounded-md bg-neutral-100 px-3 py-2 font-mono text-xs text-neutral-700 dark:bg-neutral-900 dark:text-neutral-300">
159-
npm install @emulators/adapter-next @emulators/github
158+
<div className="space-y-2">
159+
<div className="overflow-x-auto rounded-md bg-neutral-100 px-3 py-2 font-mono text-xs text-neutral-700 dark:bg-neutral-900 dark:text-neutral-300">
160+
npm install @emulators/adapter-next @emulators/github
161+
</div>
162+
<div className="overflow-x-auto rounded-md bg-neutral-100 px-3 py-2 font-mono text-xs text-neutral-700 dark:bg-neutral-900 dark:text-neutral-300">
163+
npm install @emulators/adapter-nuxt @emulators/github
164+
</div>
160165
</div>
161166
</div>
162167
</div>

apps/web/components/docs-mobile-nav.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const sections: NavSection[] = [
1717
{ href: "/docs/programmatic-api", label: "Programmatic API" },
1818
{ href: "/docs/configuration", label: "Configuration" },
1919
{ href: "/docs/nextjs", label: "Next.js Integration" },
20+
{ href: "/docs/nuxt", label: "Nuxt Integration" },
2021
],
2122
},
2223
{

0 commit comments

Comments
 (0)