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
21 changes: 21 additions & 0 deletions apps/web/app/docs/programmatic-api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ afterAll(() => Promise.all([github.close(), vercel.close()]))
</tbody>
</table>

### Custom composition

The `emulate` package also exposes core runtime primitives and built-in service plugins for custom emulators or local endpoint additions without direct `@emulators/*` installs:

```typescript
import { createPlugin, createServer, serve } from 'emulate/core'
import { githubPlugin } from 'emulate/plugins'

const plugin = 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,
})

const { app } = createServer(plugin, { baseUrl: 'http://localhost:4000' })
const server = serve({ fetch: app.fetch, port: 4000 })
```

## Scoped packages

Each emulator is published as its own `@emulators/*` package. Install only the ones you need:
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export {
type Next,
type ServeOptions,
} from "./http.js";
export { type ServicePlugin, type RouteContext } from "./plugin.js";
export { createPlugin, type ServicePlugin, type RouteContext } from "./plugin.js";
export { WebhookDispatcher, type WebhookSubscription, type WebhookDelivery } from "./webhooks.js";
export {
errorHandler,
Expand Down
4 changes: 4 additions & 0 deletions packages/@emulators/core/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export interface ServicePlugin {
register(app: Hono<AppEnv>, store: Store, webhooks: WebhookDispatcher, baseUrl: string, tokenMap?: TokenMap): void;
seed?(store: Store, baseUrl: string): void;
}

export function createPlugin<const T extends ServicePlugin>(plugin: T): T {
return plugin;
}
10 changes: 9 additions & 1 deletion packages/emulate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
"import": "./dist/api.js",
"types": "./dist/api.d.ts"
},
"./core": {
"import": "./dist/core.js",
"types": "./dist/core.d.ts"
},
"./plugins": {
"import": "./dist/plugins.js",
"types": "./dist/plugins.d.ts"
},
"./cli": {
"import": "./dist/index.js"
}
Expand Down Expand Up @@ -52,12 +60,12 @@
"lint": "eslint src"
},
"dependencies": {
"@emulators/core": "workspace:*",
"commander": "^14",
"picocolors": "^1.1.1",
"yaml": "^2"
},
"devDependencies": {
"@emulators/core": "workspace:*",
"@emulators/github": "workspace:*",
"@emulators/apple": "workspace:*",
"@emulators/microsoft": "workspace:*",
Expand Down
21 changes: 21 additions & 0 deletions packages/emulate/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { createPlugin, createServer } from "../core.js";
import { githubPlugin } from "../plugins.js";

describe("composition exports", () => {
it("re-exports core server primitives and built-in plugins", async () => {
const plugin = createPlugin({
name: "custom",
register(app) {
app.get("/ping", (c) => c.json({ ok: true }));
},
});

const { app } = createServer(plugin, { baseUrl: "http://localhost:4000" });
const res = await app.request("/ping");

expect(res.status).toBe(200);
expect(await res.json()).toEqual({ ok: true });
expect(githubPlugin.name).toBe("github");
});
});
82 changes: 82 additions & 0 deletions packages/emulate/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export {
ApiError,
Collection,
Context,
Hono,
HonoRequest,
Store,
WebhookDispatcher,
authMiddleware,
bodyStr,
constantTimeSecretEqual,
cors,
createApiErrorHandler,
createErrorHandler,
createPlugin,
createServer,
debug,
deserializeValue,
errorHandler,
escapeAttr,
escapeHtml,
filePersistence,
forbidden,
matchesRedirectUri,
normalizeUri,
notFound,
parseCookies,
parseJsonBody,
parsePagination,
registerFontRoutes,
renderCardPage,
renderCheckoutPage,
renderErrorPage,
renderFormPostPage,
renderInspectorPage,
renderSettingsPage,
renderUserButton,
requireAppAuth,
requireAuth,
restoreTokenMap,
serve,
serializeTokenMap,
serializeValue,
setLinkHeader,
unauthorized,
validationError,
type AppEnv,
type AppKeyResolver,
type AuthApp,
type AuthFallback,
type AuthInstallation,
type AuthUser,
type CheckoutLineItem,
type CheckoutPageOptions,
type CollectionSnapshot,
type ContentfulStatusCode,
type CorsOptions,
type Entity,
type ErrorHandler,
type FetchHandler,
type FilterFn,
type Handler,
type InsertInput,
type InspectorTab,
type MiddlewareHandler,
type Next,
type PaginatedResult,
type PaginationParams,
type PersistenceAdapter,
type QueryOptions,
type RouteContext,
type ServeOptions,
type ServerOptions,
type ServicePlugin,
type SortFn,
type StoreSnapshot,
type TokenEntry,
type TokenMap,
type UserButtonOptions,
type WebhookDelivery,
type WebhookSubscription,
} from "@emulators/core";
26 changes: 26 additions & 0 deletions packages/emulate/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { applePlugin as baseApplePlugin } from "@emulators/apple";
import { awsPlugin as baseAwsPlugin } from "@emulators/aws";
import { clerkPlugin as baseClerkPlugin } from "@emulators/clerk";
import { githubPlugin as baseGithubPlugin } from "@emulators/github";
import { googlePlugin as baseGooglePlugin } from "@emulators/google";
import { microsoftPlugin as baseMicrosoftPlugin } from "@emulators/microsoft";
import { mongoatlasPlugin as baseMongoatlasPlugin } from "@emulators/mongoatlas";
import { oktaPlugin as baseOktaPlugin } from "@emulators/okta";
import { resendPlugin as baseResendPlugin } from "@emulators/resend";
import { slackPlugin as baseSlackPlugin } from "@emulators/slack";
import { stripePlugin as baseStripePlugin } from "@emulators/stripe";
import { vercelPlugin as baseVercelPlugin } from "@emulators/vercel";
import type { ServicePlugin } from "@emulators/core";

export const applePlugin: ServicePlugin = baseApplePlugin;
export const awsPlugin: ServicePlugin = baseAwsPlugin;
export const clerkPlugin: ServicePlugin = baseClerkPlugin;
export const githubPlugin: ServicePlugin = baseGithubPlugin;
export const googlePlugin: ServicePlugin = baseGooglePlugin;
export const microsoftPlugin: ServicePlugin = baseMicrosoftPlugin;
export const mongoatlasPlugin: ServicePlugin = baseMongoatlasPlugin;
export const oktaPlugin: ServicePlugin = baseOktaPlugin;
export const resendPlugin: ServicePlugin = baseResendPlugin;
export const slackPlugin: ServicePlugin = baseSlackPlugin;
export const stripePlugin: ServicePlugin = baseStripePlugin;
export const vercelPlugin: ServicePlugin = baseVercelPlugin;
2 changes: 1 addition & 1 deletion packages/emulate/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default defineConfig([
},
{
...shared,
entry: ["src/api.ts"],
entry: ["src/api.ts", "src/core.ts", "src/plugins.ts"],
format: ["esm"],
dts: true,
clean: false,
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions skills/emulate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ await vercel.close()
| `reset()` | Wipe the store and replay seed data |
| `close()` | Shut down the HTTP server, returns a Promise |

### Custom composition

Use `emulate/core` and `emulate/plugins` to compose custom emulators or add local endpoints without installing `@emulators/*` packages directly:

```typescript
import { createPlugin, createServer, serve } from 'emulate/core'
import { githubPlugin } from 'emulate/plugins'

const plugin = 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,
})

const { app } = createServer(plugin, { baseUrl: 'http://localhost:4000' })
const server = serve({ fetch: app.fetch, port: 4000 })
```

## Vitest / Jest Setup

```typescript
Expand Down
Loading