Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ npx emulate --port 3000
# Use a seed config file
npx emulate --seed config.yaml

# Use a TypeScript config file with custom plugins
npx emulate --config emulate.config.ts

# Generate a starter config
npx emulate init

Expand All @@ -49,6 +52,7 @@ npx emulate list
|------|---------|-------------|
| `-p, --port` | `4000` | Base port (auto-increments per service) |
| `-s, --service` | all | Comma-separated services to enable |
| `--config` | auto-detect | Path to an `emulate.config.ts` / `.js` config file |
| `--seed` | auto-detect | Path to seed config (YAML or JSON) |
| `--base-url` | none | Override advertised base URL (supports `{service}` template) |
| `--portless` | off | Serve over HTTPS via portless (auto-registers aliases) |
Expand Down Expand Up @@ -156,7 +160,9 @@ afterAll(() => Promise.all([github.close(), vercel.close()]))

## Configuration

Configuration is optional. The CLI auto-detects config files in this order: `emulate.config.yaml` / `.yml`, `emulate.config.json`, `service-emulator.config.yaml` / `.yml`, `service-emulator.config.json`. Or pass `--seed <file>` explicitly. Run `npx emulate init` to generate a starter file.
Configuration is optional. The CLI auto-detects config files in this order: `emulate.config.ts`, `emulate.config.mts`, `emulate.config.js`, `emulate.config.mjs`, `emulate.config.yaml` / `.yml`, `emulate.config.json`, `service-emulator.config.yaml` / `.yml`, `service-emulator.config.json`. Pass `--config <file>` for executable config or `--seed <file>` for YAML or JSON seed data. Run `npx emulate init` to generate a starter seed file.

Use `emulate.config.ts` when you need custom plugins, custom ports, or local endpoint additions. Use YAML or JSON when you only need seed data.

```yaml
tokens:
Expand Down
29 changes: 28 additions & 1 deletion apps/web/app/docs/configuration/page.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration

Configuration is optional. All services start with sensible defaults. To customize seed data, create `emulate.config.yaml` in your project root (or pass `--seed`):
Configuration is optional. All services start with sensible defaults. To customize seed data, create `emulate.config.yaml` in your project root or pass `--seed`:

```yaml
tokens:
Expand All @@ -18,6 +18,33 @@ tokens:
- user
```

Use `emulate.config.ts` when you need custom plugins, custom ports, or local endpoint additions:

```typescript
import { defineConfig } from 'emulate'
import { createPlugin } from 'emulate/core'
import { githubPlugin } from 'emulate/plugins'

const github = createPlugin({
name: 'github',
register(app, store, webhooks, baseUrl, tokenMap) {
githubPlugin.register(app, store, webhooks, baseUrl, tokenMap)
app.get('/extra', (c) => c.json({ ok: true }))
},
seed: githubPlugin.seed,
})

export default defineConfig({
services: {
github: {
plugin: github,
port: 4001,
seed: { users: [{ login: 'octocat' }] },
},
},
})
```

## Vercel Seed Config

```yaml
Expand Down
27 changes: 27 additions & 0 deletions apps/web/app/docs/programmatic-api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,33 @@ const { app } = createServer(plugin, { baseUrl: 'http://localhost:4000' })
const server = serve({ fetch: app.fetch, port: 4000 })
```

The CLI can load the same style of custom plugin from `emulate.config.ts`:

```typescript
import { defineConfig } from 'emulate'
import { createPlugin } from 'emulate/core'
import { githubPlugin } from 'emulate/plugins'

const github = createPlugin({
name: 'github',
register(app, store, webhooks, baseUrl, tokenMap) {
githubPlugin.register(app, store, webhooks, baseUrl, tokenMap)
app.get('/extra', (c) => c.json({ ok: true }))
},
seed: githubPlugin.seed,
})

export default defineConfig({
services: {
github: {
plugin: github,
port: 4001,
seed: { users: [{ login: 'octocat' }] },
},
},
})
```

## Scoped packages

Each emulator is published as its own `@emulators/*` package. Install only the ones you need:
Expand Down
13 changes: 7 additions & 6 deletions packages/emulate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,23 @@
"dependencies": {
"@emulators/core": "workspace:*",
"commander": "^14",
"jiti": "^2.7.0",
"picocolors": "^1.1.1",
"yaml": "^2"
},
"devDependencies": {
"@emulators/github": "workspace:*",
"@emulators/apple": "workspace:*",
"@emulators/microsoft": "workspace:*",
"@emulators/okta": "workspace:*",
"@emulators/aws": "workspace:*",
"@emulators/clerk": "workspace:*",
"@emulators/github": "workspace:*",
"@emulators/google": "workspace:*",
"@emulators/microsoft": "workspace:*",
"@emulators/mongoatlas": "workspace:*",
"@emulators/slack": "workspace:*",
"@emulators/vercel": "workspace:*",
"@emulators/okta": "workspace:*",
"@emulators/resend": "workspace:*",
"@emulators/slack": "workspace:*",
"@emulators/stripe": "workspace:*",
"@emulators/clerk": "workspace:*",
"@emulators/vercel": "workspace:*",
"tsup": "^8",
"typescript": "^5.7"
}
Expand Down
50 changes: 50 additions & 0 deletions packages/emulate/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { mkdtempSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, describe, expect, it } from "vitest";
import { loadConfig } from "../config.js";

describe("loadConfig", () => {
let tempDir: string | null = null;

afterEach(() => {
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = null;
}
});

it("loads an emulate.config.ts file with a custom plugin", async () => {
tempDir = mkdtempSync(join(tmpdir(), "emulate-config-"));
const configPath = join(tempDir, "emulate.config.ts");
writeFileSync(
configPath,
`
export default {
port: 14100,
tokens: { test_token: { login: 'tester', scopes: ['read'] } },
services: {
custom: {
port: 14101,
seed: { message: 'hello' },
plugin: {
name: 'custom',
register(app) {
app.get('/ping', (c) => c.json({ ok: true }))
},
},
},
},
}
`,
"utf-8",
);

const loaded = await loadConfig({ configPath });

expect(loaded?.runtimeConfig?.port).toBe(14100);
expect(loaded?.runtimeConfig?.services?.custom?.port).toBe(14101);
expect(loaded?.runtimeConfig?.services?.custom?.plugin?.name).toBe("custom");
expect(loaded?.seedConfig.tokens?.test_token.login).toBe("tester");
});
});
3 changes: 2 additions & 1 deletion packages/emulate/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createServer, serve, type AppKeyResolver, type Store } from "@emulators/core";
import { createServer, serve, type AppKeyResolver } from "@emulators/core";
import { SERVICE_REGISTRY } from "./registry.js";
export type { ServiceName } from "./registry.js";
import type { ServiceName } from "./registry.js";
import { resolveBaseUrl } from "./base-url.js";
export { defineConfig, type EmulateConfig, type EmulateServiceConfig } from "./config.js";

export interface SeedConfig {
tokens?: Record<string, { login: string; scopes?: string[] }>;
Expand Down
Loading