diff --git a/apps/web/app/docs/programmatic-api/page.mdx b/apps/web/app/docs/programmatic-api/page.mdx index 1f4d65e1..10f02c79 100644 --- a/apps/web/app/docs/programmatic-api/page.mdx +++ b/apps/web/app/docs/programmatic-api/page.mdx @@ -104,6 +104,27 @@ afterAll(() => Promise.all([github.close(), vercel.close()])) +### 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: diff --git a/packages/@emulators/core/src/index.ts b/packages/@emulators/core/src/index.ts index 25f0fd6d..c2aa7522 100644 --- a/packages/@emulators/core/src/index.ts +++ b/packages/@emulators/core/src/index.ts @@ -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, diff --git a/packages/@emulators/core/src/plugin.ts b/packages/@emulators/core/src/plugin.ts index a42f1778..bda88eb1 100644 --- a/packages/@emulators/core/src/plugin.ts +++ b/packages/@emulators/core/src/plugin.ts @@ -16,3 +16,7 @@ export interface ServicePlugin { register(app: Hono, store: Store, webhooks: WebhookDispatcher, baseUrl: string, tokenMap?: TokenMap): void; seed?(store: Store, baseUrl: string): void; } + +export function createPlugin(plugin: T): T { + return plugin; +} diff --git a/packages/emulate/package.json b/packages/emulate/package.json index b48ac8ed..9d9dd392 100644 --- a/packages/emulate/package.json +++ b/packages/emulate/package.json @@ -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" } @@ -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:*", diff --git a/packages/emulate/src/__tests__/exports.test.ts b/packages/emulate/src/__tests__/exports.test.ts new file mode 100644 index 00000000..5ee615d3 --- /dev/null +++ b/packages/emulate/src/__tests__/exports.test.ts @@ -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"); + }); +}); diff --git a/packages/emulate/src/core.ts b/packages/emulate/src/core.ts new file mode 100644 index 00000000..96b8008c --- /dev/null +++ b/packages/emulate/src/core.ts @@ -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"; diff --git a/packages/emulate/src/plugins.ts b/packages/emulate/src/plugins.ts new file mode 100644 index 00000000..99027a65 --- /dev/null +++ b/packages/emulate/src/plugins.ts @@ -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; diff --git a/packages/emulate/tsup.config.ts b/packages/emulate/tsup.config.ts index db718e14..103729a7 100644 --- a/packages/emulate/tsup.config.ts +++ b/packages/emulate/tsup.config.ts @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e4e036d..591ea1de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -685,6 +685,9 @@ importers: packages/emulate: dependencies: + '@emulators/core': + specifier: workspace:* + version: link:../@emulators/core commander: specifier: ^14 version: 14.0.3 @@ -704,9 +707,6 @@ importers: '@emulators/clerk': specifier: workspace:* version: link:../@emulators/clerk - '@emulators/core': - specifier: workspace:* - version: link:../@emulators/core '@emulators/github': specifier: workspace:* version: link:../@emulators/github diff --git a/skills/emulate/SKILL.md b/skills/emulate/SKILL.md index bc640979..fe1a85b1 100644 --- a/skills/emulate/SKILL.md +++ b/skills/emulate/SKILL.md @@ -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