|
| 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 |
0 commit comments