@demokit-ai/remix 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/fixtures.ts"],"sourcesContent":["/**\n * @demokit-ai/remix/server\n *\n * Server-side utilities for Remix demo mode detection and fixture management.\n * Import from '@demokit-ai/remix/server' in your loader/action files.\n */\n\nimport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n} from './types'\nimport { FixtureStore, createFixtureStore, defineRemixFixtures } from './fixtures'\n\n/** Default demo mode cookie name */\nconst DEFAULT_COOKIE_NAME = 'demokit-demo-mode'\n\n/** Default demo mode header name */\nconst DEFAULT_HEADER_NAME = 'x-demokit-demo-mode'\n\n/** Default environment variable name */\nconst DEFAULT_ENV_VAR = 'DEMOKIT_DEMO_MODE'\n\n/** Default query parameter name */\nconst DEFAULT_QUERY_PARAM = 'demo'\n\n/**\n * Check if demo mode is enabled for a request\n *\n * Checks in order:\n * 1. Query parameter (for easy testing)\n * 2. Cookie (persistent across requests)\n * 3. Header (for API requests)\n * 4. Environment variable (global override)\n *\n * @example\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json({ user: { id: '1', name: 'Demo User' } })\n * }\n * return json({ user: await db.users.findFirst() })\n * }\n */\nexport function isDemoMode(request: Request, options?: DemoModeOptions): boolean {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n headerName = DEFAULT_HEADER_NAME,\n envVar = DEFAULT_ENV_VAR,\n queryParam = DEFAULT_QUERY_PARAM,\n } = options ?? {}\n\n // Check query parameter first (highest priority for easy testing)\n const url = new URL(request.url)\n const queryValue = url.searchParams.get(queryParam)\n if (queryValue !== null) {\n return queryValue === 'true' || queryValue === '1' || queryValue === ''\n }\n\n // Check cookie\n const cookieHeader = request.headers.get('cookie')\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader)\n const cookieValue = cookies[cookieName]\n if (cookieValue !== undefined) {\n return cookieValue === 'true' || cookieValue === '1'\n }\n }\n\n // Check header\n const headerValue = request.headers.get(headerName)\n if (headerValue !== null) {\n return headerValue === 'true' || headerValue === '1'\n }\n\n // Check environment variable (global fallback)\n if (typeof process !== 'undefined' && process.env?.[envVar]) {\n const envValue = process.env[envVar]\n return envValue === 'true' || envValue === '1'\n }\n\n return false\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {}\n for (const pair of cookieHeader.split(';')) {\n const [name, ...rest] = pair.trim().split('=')\n if (name) {\n cookies[name] = rest.join('=')\n }\n }\n return cookies\n}\n\n/**\n * Create a demo mode checker function with preset options\n *\n * @example\n * import { createDemoModeChecker } from '@demokit-ai/remix/server'\n *\n * const isDemoMode = createDemoModeChecker({\n * cookieName: 'my-demo-cookie',\n * envVar: 'MY_DEMO_MODE',\n * })\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json(demoData)\n * }\n * return json(await fetchRealData())\n * }\n */\nexport function createDemoModeChecker(\n options?: DemoModeOptions\n): (request: Request) => boolean {\n return (request: Request) => isDemoMode(request, options)\n}\n\n// Global fixture store instance (for simple usage)\nlet globalFixtureStore: FixtureStore | null = null\n\n/**\n * Set up the global fixture store\n *\n * Call this once at app startup to configure demo fixtures.\n *\n * @example\n * // In your entry.server.tsx or root loader\n * import { setupDemoFixtures } from '@demokit-ai/remix/server'\n *\n * setupDemoFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function setupDemoFixtures(config: {\n loaders?: LoaderFixtureMapObject\n actions?: ActionFixtureMapObject\n}): void {\n globalFixtureStore = createFixtureStore(config)\n}\n\n/**\n * Get the global fixture store\n */\nexport function getFixtureStore(): FixtureStore | null {\n return globalFixtureStore\n}\n\n/**\n * Get demo data for a loader path\n *\n * Returns the fixture data if available, otherwise undefined.\n * Use with isDemoMode() for conditional demo data.\n *\n * @example\n * import { isDemoMode, getDemoData } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request, params }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * const demoData = await getDemoData('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * })\n * if (demoData) return json(demoData)\n * }\n * return json(await db.users.findFirst({ where: { id: params.id } }))\n * }\n */\nexport async function getDemoData<T = unknown>(\n pattern: string,\n context: LoaderFixtureContext\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getLoader(pattern)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Get demo action result for an action path\n *\n * @example\n * import { isDemoMode, getDemoActionResult } from '@demokit-ai/remix/server'\n *\n * export async function action({ request, params }: ActionFunctionArgs) {\n * if (isDemoMode(request)) {\n * const formData = await request.clone().formData()\n * const demoResult = await getDemoActionResult('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * formData,\n * }, request.method)\n * if (demoResult) return json(demoResult)\n * }\n * // Real action...\n * }\n */\nexport async function getDemoActionResult<T = unknown>(\n pattern: string,\n context: ActionFixtureContext,\n method: string\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getAction(pattern, method)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Execute a fixture handler\n */\nasync function executeHandler<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n handler: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof handler === 'function') {\n return (handler as (context: C) => T | Promise<T>)(context)\n }\n return handler\n}\n\n/**\n * Create a cookie value to enable demo mode\n *\n * @example\n * import { createDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': createDemoModeCookie(true),\n * },\n * })\n * }\n */\nexport function createDemoModeCookie(\n enabled: boolean,\n options?: {\n cookieName?: string\n maxAge?: number\n path?: string\n sameSite?: 'Strict' | 'Lax' | 'None'\n secure?: boolean\n }\n): string {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n maxAge = 60 * 60 * 24 * 7, // 7 days\n path = '/',\n sameSite = 'Lax',\n secure = process.env.NODE_ENV === 'production',\n } = options ?? {}\n\n const parts = [\n `${cookieName}=${enabled ? 'true' : 'false'}`,\n `Max-Age=${maxAge}`,\n `Path=${path}`,\n `SameSite=${sameSite}`,\n ]\n\n if (secure) {\n parts.push('Secure')\n }\n\n return parts.join('; ')\n}\n\n/**\n * Create a cookie value to clear demo mode\n *\n * @example\n * import { clearDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': clearDemoModeCookie(),\n * },\n * })\n * }\n */\nexport function clearDemoModeCookie(options?: { cookieName?: string; path?: string }): string {\n const { cookieName = DEFAULT_COOKIE_NAME, path = '/' } = options ?? {}\n return `${cookieName}=; Max-Age=0; Path=${path}`\n}\n\n// Re-export fixture utilities for convenience\nexport { FixtureStore, createFixtureStore, defineRemixFixtures }\nexport {\n defineRemixLoaderFixtures,\n defineRemixActionFixtures,\n} from './fixtures'\n\n// Re-export types\nexport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n}\n","import { matchUrl, type MatchResult } from '@demokit-ai/core'\nimport type {\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n FixtureStoreConfig,\n} from './types'\n\n/**\n * Server-side fixture store for managing demo data\n *\n * Use this to centralize fixture definitions for your Remix app.\n * Works with the demo mode detection utilities.\n */\nexport class FixtureStore {\n private loaders: LoaderFixtureMapObject = {}\n private actions: ActionFixtureMapObject = {}\n\n constructor(config?: FixtureStoreConfig) {\n if (config?.loaders) {\n this.loaders = { ...config.loaders }\n }\n if (config?.actions) {\n this.actions = { ...config.actions }\n }\n }\n\n /**\n * Register a loader fixture\n */\n setLoader(path: string, handler: LoaderFixtureHandler): this {\n this.loaders[path] = handler\n return this\n }\n\n /**\n * Register an action fixture\n */\n setAction(path: string, handler: ActionFixtureHandler | MethodActionHandlers): this {\n this.actions[path] = handler\n return this\n }\n\n /**\n * Get a loader fixture for a path\n */\n getLoader(path: string): LoaderFixtureHandler | undefined {\n // Try exact match first\n if (path in this.loaders) {\n return this.loaders[path]\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return handler\n }\n }\n\n return undefined\n }\n\n /**\n * Get an action fixture for a path and method\n */\n getAction(path: string, method: string): ActionFixtureHandler | undefined {\n // Try exact match first\n if (path in this.actions) {\n return resolveActionHandler(this.actions[path], method)\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n return resolveActionHandler(fixture, method)\n }\n }\n\n return undefined\n }\n\n /**\n * Find loader fixture with match info\n */\n findLoader(path: string): { handler: LoaderFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.loaders) {\n return {\n handler: this.loaders[path],\n match: { matched: true, params: {} },\n }\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return { handler, match: result }\n }\n }\n\n return null\n }\n\n /**\n * Find action fixture with match info\n */\n findAction(\n path: string,\n method: string\n ): { handler: ActionFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.actions) {\n const handler = resolveActionHandler(this.actions[path], method)\n if (handler) {\n return {\n handler,\n match: { matched: true, params: {} },\n }\n }\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n const handler = resolveActionHandler(fixture, method)\n if (handler) {\n return { handler, match: result }\n }\n }\n }\n\n return null\n }\n\n /**\n * Get all loader fixtures\n */\n getLoaders(): LoaderFixtureMapObject {\n return { ...this.loaders }\n }\n\n /**\n * Get all action fixtures\n */\n getActions(): ActionFixtureMapObject {\n return { ...this.actions }\n }\n\n /**\n * Clear all fixtures\n */\n clear(): void {\n this.loaders = {}\n this.actions = {}\n }\n}\n\n/**\n * Resolve action handler based on method\n */\nfunction resolveActionHandler(\n fixture: ActionFixtureHandler | MethodActionHandlers,\n method: string\n): ActionFixtureHandler | undefined {\n if (isMethodActionHandlers(fixture)) {\n const upperMethod = method.toUpperCase() as keyof MethodActionHandlers\n return fixture[upperMethod]\n }\n return fixture\n}\n\n/**\n * Type guard for method action handlers\n */\nfunction isMethodActionHandlers(fixture: unknown): fixture is MethodActionHandlers {\n if (typeof fixture !== 'object' || fixture === null) {\n return false\n }\n const methods = ['POST', 'PUT', 'PATCH', 'DELETE']\n return methods.some((method) => method in fixture)\n}\n\n/**\n * Create a fixture store with initial fixtures\n *\n * @example\n * import { createFixtureStore } from '@demokit-ai/remix/server'\n *\n * export const fixtures = createFixtureStore({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * },\n * })\n */\nexport function createFixtureStore(config?: FixtureStoreConfig): FixtureStore {\n return new FixtureStore(config)\n}\n\n/**\n * Define loader fixtures with type inference\n *\n * @example\n * const loaders = defineRemixLoaderFixtures({\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * '/projects': async () => fetchDemoProjects(),\n * })\n */\nexport function defineRemixLoaderFixtures<T extends LoaderFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define action fixtures with type inference\n *\n * @example\n * const actions = defineRemixActionFixtures({\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * })\n */\nexport function defineRemixActionFixtures<T extends ActionFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define complete Remix fixtures (loaders + actions)\n *\n * @example\n * const { loaders, actions } = defineRemixFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function defineRemixFixtures<\n L extends LoaderFixtureMapObject,\n A extends ActionFixtureMapObject,\n>(config: { loaders?: L; actions?: A }): { loaders: L; actions: A } {\n return {\n loaders: (config.loaders ?? {}) as L,\n actions: (config.actions ?? {}) as A,\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/fixtures.ts"],"sourcesContent":["/**\n * @demokit-ai/remix/server\n *\n * Server-side utilities for Remix demo mode detection and fixture management.\n * Import from '@demokit-ai/remix/server' in your loader/action files.\n */\n\nimport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n} from './types'\nimport { FixtureStore, createFixtureStore, defineRemixFixtures } from './fixtures'\n\n/** Default demo mode cookie name */\nconst DEFAULT_COOKIE_NAME = 'demokit-demo-mode'\n\n/** Default demo mode header name */\nconst DEFAULT_HEADER_NAME = 'x-demokit-demo-mode'\n\n/** Default environment variable name */\nconst DEFAULT_ENV_VAR = 'DEMOKIT_DEMO_MODE'\n\n/** Default query parameter name */\nconst DEFAULT_QUERY_PARAM = 'demo'\n\n/**\n * Check if demo mode is enabled for a request\n *\n * Checks in order:\n * 1. Query parameter (for easy testing)\n * 2. Cookie (persistent across requests)\n * 3. Header (for API requests)\n * 4. Environment variable (global override)\n *\n * @example\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json({ user: { id: '1', name: 'Demo User' } })\n * }\n * return json({ user: await db.users.findFirst() })\n * }\n */\nexport function isDemoMode(request: Request, options?: DemoModeOptions): boolean {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n headerName = DEFAULT_HEADER_NAME,\n envVar = DEFAULT_ENV_VAR,\n queryParam = DEFAULT_QUERY_PARAM,\n } = options ?? {}\n\n // Check query parameter first (highest priority for easy testing)\n const url = new URL(request.url)\n const queryValue = url.searchParams.get(queryParam)\n if (queryValue !== null) {\n return queryValue === 'true' || queryValue === '1' || queryValue === ''\n }\n\n // Check cookie\n const cookieHeader = request.headers.get('cookie')\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader)\n const cookieValue = cookies[cookieName]\n if (cookieValue !== undefined) {\n return cookieValue === 'true' || cookieValue === '1'\n }\n }\n\n // Check header\n const headerValue = request.headers.get(headerName)\n if (headerValue !== null) {\n return headerValue === 'true' || headerValue === '1'\n }\n\n // Check environment variable (global fallback)\n if (typeof process !== 'undefined' && process.env?.[envVar]) {\n const envValue = process.env[envVar]\n return envValue === 'true' || envValue === '1'\n }\n\n return false\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {}\n for (const pair of cookieHeader.split(';')) {\n const [name, ...rest] = pair.trim().split('=')\n if (name) {\n cookies[name] = rest.join('=')\n }\n }\n return cookies\n}\n\n/**\n * Create a demo mode checker function with preset options\n *\n * @example\n * import { createDemoModeChecker } from '@demokit-ai/remix/server'\n *\n * const isDemoMode = createDemoModeChecker({\n * cookieName: 'my-demo-cookie',\n * envVar: 'MY_DEMO_MODE',\n * })\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json(demoData)\n * }\n * return json(await fetchRealData())\n * }\n */\nexport function createDemoModeChecker(\n options?: DemoModeOptions\n): (request: Request) => boolean {\n return (request: Request) => isDemoMode(request, options)\n}\n\n// Global fixture store instance (for simple usage)\nlet globalFixtureStore: FixtureStore | null = null\n\n/**\n * Set up the global fixture store\n *\n * Call this once at app startup to configure demo fixtures.\n *\n * @example\n * // In your entry.server.tsx or root loader\n * import { setupDemoFixtures } from '@demokit-ai/remix/server'\n *\n * setupDemoFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function setupDemoFixtures(config: {\n loaders?: LoaderFixtureMapObject\n actions?: ActionFixtureMapObject\n}): void {\n globalFixtureStore = createFixtureStore(config)\n}\n\n/**\n * Get the global fixture store\n */\nexport function getFixtureStore(): FixtureStore | null {\n return globalFixtureStore\n}\n\n/**\n * Get demo data for a loader path\n *\n * Returns the fixture data if available, otherwise undefined.\n * Use with isDemoMode() for conditional demo data.\n *\n * @example\n * import { isDemoMode, getDemoData } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request, params }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * const demoData = await getDemoData('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * })\n * if (demoData) return json(demoData)\n * }\n * return json(await db.users.findFirst({ where: { id: params.id } }))\n * }\n */\nexport async function getDemoData<T = unknown>(\n pattern: string,\n context: LoaderFixtureContext\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getLoader(pattern)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Get demo action result for an action path\n *\n * @example\n * import { isDemoMode, getDemoActionResult } from '@demokit-ai/remix/server'\n *\n * export async function action({ request, params }: ActionFunctionArgs) {\n * if (isDemoMode(request)) {\n * const formData = await request.clone().formData()\n * const demoResult = await getDemoActionResult('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * formData,\n * }, request.method)\n * if (demoResult) return json(demoResult)\n * }\n * // Real action...\n * }\n */\nexport async function getDemoActionResult<T = unknown>(\n pattern: string,\n context: ActionFixtureContext,\n method: string\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getAction(pattern, method)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Execute a fixture handler\n */\nasync function executeHandler<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n handler: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof handler === 'function') {\n return (handler as (context: C) => T | Promise<T>)(context)\n }\n return handler\n}\n\n/**\n * Create a cookie value to enable demo mode\n *\n * @example\n * import { createDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': createDemoModeCookie(true),\n * },\n * })\n * }\n */\nexport function createDemoModeCookie(\n enabled: boolean,\n options?: {\n cookieName?: string\n maxAge?: number\n path?: string\n sameSite?: 'Strict' | 'Lax' | 'None'\n secure?: boolean\n }\n): string {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n maxAge = 60 * 60 * 24 * 7, // 7 days\n path = '/',\n sameSite = 'Lax',\n secure = process.env.NODE_ENV === 'production',\n } = options ?? {}\n\n const parts = [\n `${cookieName}=${enabled ? 'true' : 'false'}`,\n `Max-Age=${maxAge}`,\n `Path=${path}`,\n `SameSite=${sameSite}`,\n ]\n\n if (secure) {\n parts.push('Secure')\n }\n\n return parts.join('; ')\n}\n\n/**\n * Create a cookie value to clear demo mode\n *\n * @example\n * import { clearDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': clearDemoModeCookie(),\n * },\n * })\n * }\n */\nexport function clearDemoModeCookie(options?: { cookieName?: string; path?: string }): string {\n const { cookieName = DEFAULT_COOKIE_NAME, path = '/' } = options ?? {}\n return `${cookieName}=; Max-Age=0; Path=${path}`\n}\n\n// Re-export fixture utilities for convenience\nexport { FixtureStore, createFixtureStore, defineRemixFixtures }\nexport {\n defineRemixLoaderFixtures,\n defineRemixActionFixtures,\n} from './fixtures'\n\n// Re-export types\nexport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n}\n","import { matchUrl, type MatchResult } from '@demokit-ai/core'\nimport type {\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n FixtureStoreConfig,\n} from './types'\n\n/**\n * Server-side fixture store for managing demo data\n *\n * Use this to centralize fixture definitions for your Remix app.\n * Works with the demo mode detection utilities.\n */\nexport class FixtureStore {\n private loaders: LoaderFixtureMapObject = {}\n private actions: ActionFixtureMapObject = {}\n\n constructor(config?: FixtureStoreConfig) {\n if (config?.loaders) {\n this.loaders = { ...config.loaders }\n }\n if (config?.actions) {\n this.actions = { ...config.actions }\n }\n }\n\n /**\n * Register a loader fixture\n */\n setLoader(path: string, handler: LoaderFixtureHandler): this {\n this.loaders[path] = handler\n return this\n }\n\n /**\n * Register an action fixture\n */\n setAction(path: string, handler: ActionFixtureHandler | MethodActionHandlers): this {\n this.actions[path] = handler\n return this\n }\n\n /**\n * Get a loader fixture for a path\n */\n getLoader(path: string): LoaderFixtureHandler | undefined {\n // Try exact match first\n if (path in this.loaders) {\n return this.loaders[path]\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return handler\n }\n }\n\n return undefined\n }\n\n /**\n * Get an action fixture for a path and method\n */\n getAction(path: string, method: string): ActionFixtureHandler | undefined {\n // Try exact match first\n if (path in this.actions) {\n return resolveActionHandler(this.actions[path], method)\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n return resolveActionHandler(fixture, method)\n }\n }\n\n return undefined\n }\n\n /**\n * Find loader fixture with match info\n */\n findLoader(path: string): { handler: LoaderFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.loaders) {\n return {\n handler: this.loaders[path],\n match: { matched: true, params: {} },\n }\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return { handler, match: result }\n }\n }\n\n return null\n }\n\n /**\n * Find action fixture with match info\n */\n findAction(\n path: string,\n method: string\n ): { handler: ActionFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.actions) {\n const handler = resolveActionHandler(this.actions[path], method)\n if (handler) {\n return {\n handler,\n match: { matched: true, params: {} },\n }\n }\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n const handler = resolveActionHandler(fixture, method)\n if (handler) {\n return { handler, match: result }\n }\n }\n }\n\n return null\n }\n\n /**\n * Get all loader fixtures\n */\n getLoaders(): LoaderFixtureMapObject {\n return { ...this.loaders }\n }\n\n /**\n * Get all action fixtures\n */\n getActions(): ActionFixtureMapObject {\n return { ...this.actions }\n }\n\n /**\n * Clear all fixtures\n */\n clear(): void {\n this.loaders = {}\n this.actions = {}\n }\n}\n\n/**\n * Resolve action handler based on method\n */\nfunction resolveActionHandler(\n fixture: ActionFixtureHandler | MethodActionHandlers,\n method: string\n): ActionFixtureHandler | undefined {\n if (isMethodActionHandlers(fixture)) {\n const upperMethod = method.toUpperCase() as keyof MethodActionHandlers\n return fixture[upperMethod]\n }\n return fixture\n}\n\n/**\n * Type guard for method action handlers\n */\nfunction isMethodActionHandlers(fixture: unknown): fixture is MethodActionHandlers {\n if (typeof fixture !== 'object' || fixture === null) {\n return false\n }\n const methods = ['POST', 'PUT', 'PATCH', 'DELETE']\n return methods.some((method) => method in fixture)\n}\n\n/**\n * Create a fixture store with initial fixtures\n *\n * @example\n * import { createFixtureStore } from '@demokit-ai/remix/server'\n *\n * export const fixtures = createFixtureStore({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * },\n * })\n */\nexport function createFixtureStore(config?: FixtureStoreConfig): FixtureStore {\n return new FixtureStore(config)\n}\n\n/**\n * Define loader fixtures with type inference\n *\n * @example\n * const loaders = defineRemixLoaderFixtures({\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * '/projects': async () => fetchDemoProjects(),\n * })\n */\nexport function defineRemixLoaderFixtures<T extends LoaderFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define action fixtures with type inference\n *\n * @example\n * const actions = defineRemixActionFixtures({\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * })\n */\nexport function defineRemixActionFixtures<T extends ActionFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define complete Remix fixtures (loaders + actions)\n *\n * @example\n * const { loaders, actions } = defineRemixFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function defineRemixFixtures<\n L extends LoaderFixtureMapObject,\n A extends ActionFixtureMapObject,\n>(config: { loaders?: L; actions?: A }): { loaders: L; actions: A } {\n return {\n loaders: (config.loaders ?? {}) as L,\n actions: (config.actions ?? {}) as A,\n }\n}\n"],"mappings":"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","names":[]}
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as DemoModeOptions, j as LoaderFixtureMapObject, l as ActionFixtureMapObject, F as FixtureStore, g as LoaderFixtureContext, h as ActionFixtureContext } from './fixtures-BP3SzTkj.cjs';
2
- export { A as ActionFixtureHandler, L as LoaderFixtureHandler, M as MethodActionHandlers, c as createFixtureStore, e as defineRemixActionFixtures, f as defineRemixFixtures, d as defineRemixLoaderFixtures } from './fixtures-BP3SzTkj.cjs';
1
+ import { o as DemoModeOptions, j as LoaderFixtureMapObject, l as ActionFixtureMapObject, F as FixtureStore, g as LoaderFixtureContext, h as ActionFixtureContext } from './fixtures-BUUj5QdP.cjs';
2
+ export { A as ActionFixtureHandler, L as LoaderFixtureHandler, M as MethodActionHandlers, c as createFixtureStore, e as defineRemixActionFixtures, f as defineRemixFixtures, d as defineRemixLoaderFixtures } from './fixtures-BUUj5QdP.cjs';
3
3
  import '@demokit-ai/core';
4
4
  import '@remix-run/node';
5
5
 
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as DemoModeOptions, j as LoaderFixtureMapObject, l as ActionFixtureMapObject, F as FixtureStore, g as LoaderFixtureContext, h as ActionFixtureContext } from './fixtures-BP3SzTkj.js';
2
- export { A as ActionFixtureHandler, L as LoaderFixtureHandler, M as MethodActionHandlers, c as createFixtureStore, e as defineRemixActionFixtures, f as defineRemixFixtures, d as defineRemixLoaderFixtures } from './fixtures-BP3SzTkj.js';
1
+ import { o as DemoModeOptions, j as LoaderFixtureMapObject, l as ActionFixtureMapObject, F as FixtureStore, g as LoaderFixtureContext, h as ActionFixtureContext } from './fixtures-BUUj5QdP.js';
2
+ export { A as ActionFixtureHandler, L as LoaderFixtureHandler, M as MethodActionHandlers, c as createFixtureStore, e as defineRemixActionFixtures, f as defineRemixFixtures, d as defineRemixLoaderFixtures } from './fixtures-BUUj5QdP.js';
3
3
  import '@demokit-ai/core';
4
4
  import '@remix-run/node';
5
5
 
package/dist/server.js CHANGED
@@ -1,155 +1,10 @@
1
- // src/fixtures.ts
2
- import { matchUrl } from "@demokit-ai/core";
3
- var FixtureStore = class {
4
- loaders = {};
5
- actions = {};
6
- constructor(config) {
7
- if (config?.loaders) {
8
- this.loaders = { ...config.loaders };
9
- }
10
- if (config?.actions) {
11
- this.actions = { ...config.actions };
12
- }
13
- }
14
- /**
15
- * Register a loader fixture
16
- */
17
- setLoader(path, handler) {
18
- this.loaders[path] = handler;
19
- return this;
20
- }
21
- /**
22
- * Register an action fixture
23
- */
24
- setAction(path, handler) {
25
- this.actions[path] = handler;
26
- return this;
27
- }
28
- /**
29
- * Get a loader fixture for a path
30
- */
31
- getLoader(path) {
32
- if (path in this.loaders) {
33
- return this.loaders[path];
34
- }
35
- for (const [pattern, handler] of Object.entries(this.loaders)) {
36
- const fullPattern = pattern.startsWith("GET ") ? pattern : `GET ${pattern}`;
37
- const result = matchUrl(fullPattern, "GET", path);
38
- if (result) {
39
- return handler;
40
- }
41
- }
42
- return void 0;
43
- }
44
- /**
45
- * Get an action fixture for a path and method
46
- */
47
- getAction(path, method) {
48
- if (path in this.actions) {
49
- return resolveActionHandler(this.actions[path], method);
50
- }
51
- for (const [pattern, fixture] of Object.entries(this.actions)) {
52
- const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(pattern) ? pattern : `${method} ${pattern}`;
53
- const result = matchUrl(fullPattern, method, path);
54
- if (result) {
55
- return resolveActionHandler(fixture, method);
56
- }
57
- }
58
- return void 0;
59
- }
60
- /**
61
- * Find loader fixture with match info
62
- */
63
- findLoader(path) {
64
- if (path in this.loaders) {
65
- return {
66
- handler: this.loaders[path],
67
- match: { matched: true, params: {} }
68
- };
69
- }
70
- for (const [pattern, handler] of Object.entries(this.loaders)) {
71
- const fullPattern = pattern.startsWith("GET ") ? pattern : `GET ${pattern}`;
72
- const result = matchUrl(fullPattern, "GET", path);
73
- if (result) {
74
- return { handler, match: result };
75
- }
76
- }
77
- return null;
78
- }
79
- /**
80
- * Find action fixture with match info
81
- */
82
- findAction(path, method) {
83
- if (path in this.actions) {
84
- const handler = resolveActionHandler(this.actions[path], method);
85
- if (handler) {
86
- return {
87
- handler,
88
- match: { matched: true, params: {} }
89
- };
90
- }
91
- }
92
- for (const [pattern, fixture] of Object.entries(this.actions)) {
93
- const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(pattern) ? pattern : `${method} ${pattern}`;
94
- const result = matchUrl(fullPattern, method, path);
95
- if (result) {
96
- const handler = resolveActionHandler(fixture, method);
97
- if (handler) {
98
- return { handler, match: result };
99
- }
100
- }
101
- }
102
- return null;
103
- }
104
- /**
105
- * Get all loader fixtures
106
- */
107
- getLoaders() {
108
- return { ...this.loaders };
109
- }
110
- /**
111
- * Get all action fixtures
112
- */
113
- getActions() {
114
- return { ...this.actions };
115
- }
116
- /**
117
- * Clear all fixtures
118
- */
119
- clear() {
120
- this.loaders = {};
121
- this.actions = {};
122
- }
123
- };
124
- function resolveActionHandler(fixture, method) {
125
- if (isMethodActionHandlers(fixture)) {
126
- const upperMethod = method.toUpperCase();
127
- return fixture[upperMethod];
128
- }
129
- return fixture;
130
- }
131
- function isMethodActionHandlers(fixture) {
132
- if (typeof fixture !== "object" || fixture === null) {
133
- return false;
134
- }
135
- const methods = ["POST", "PUT", "PATCH", "DELETE"];
136
- return methods.some((method) => method in fixture);
137
- }
138
- function createFixtureStore(config) {
139
- return new FixtureStore(config);
140
- }
141
- function defineRemixLoaderFixtures(fixtures) {
142
- return fixtures;
143
- }
144
- function defineRemixActionFixtures(fixtures) {
145
- return fixtures;
146
- }
147
- function defineRemixFixtures(config) {
148
- return {
149
- loaders: config.loaders ?? {},
150
- actions: config.actions ?? {}
151
- };
152
- }
1
+ import {
2
+ FixtureStore,
3
+ createFixtureStore,
4
+ defineRemixActionFixtures,
5
+ defineRemixFixtures,
6
+ defineRemixLoaderFixtures
7
+ } from "./chunk-GUQM5HE6.js";
153
8
 
154
9
  // src/server.ts
155
10
  var DEFAULT_COOKIE_NAME = "demokit-demo-mode";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fixtures.ts","../src/server.ts"],"sourcesContent":["import { matchUrl, type MatchResult } from '@demokit-ai/core'\nimport type {\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n FixtureStoreConfig,\n} from './types'\n\n/**\n * Server-side fixture store for managing demo data\n *\n * Use this to centralize fixture definitions for your Remix app.\n * Works with the demo mode detection utilities.\n */\nexport class FixtureStore {\n private loaders: LoaderFixtureMapObject = {}\n private actions: ActionFixtureMapObject = {}\n\n constructor(config?: FixtureStoreConfig) {\n if (config?.loaders) {\n this.loaders = { ...config.loaders }\n }\n if (config?.actions) {\n this.actions = { ...config.actions }\n }\n }\n\n /**\n * Register a loader fixture\n */\n setLoader(path: string, handler: LoaderFixtureHandler): this {\n this.loaders[path] = handler\n return this\n }\n\n /**\n * Register an action fixture\n */\n setAction(path: string, handler: ActionFixtureHandler | MethodActionHandlers): this {\n this.actions[path] = handler\n return this\n }\n\n /**\n * Get a loader fixture for a path\n */\n getLoader(path: string): LoaderFixtureHandler | undefined {\n // Try exact match first\n if (path in this.loaders) {\n return this.loaders[path]\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return handler\n }\n }\n\n return undefined\n }\n\n /**\n * Get an action fixture for a path and method\n */\n getAction(path: string, method: string): ActionFixtureHandler | undefined {\n // Try exact match first\n if (path in this.actions) {\n return resolveActionHandler(this.actions[path], method)\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n return resolveActionHandler(fixture, method)\n }\n }\n\n return undefined\n }\n\n /**\n * Find loader fixture with match info\n */\n findLoader(path: string): { handler: LoaderFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.loaders) {\n return {\n handler: this.loaders[path],\n match: { matched: true, params: {} },\n }\n }\n\n // Try pattern matching\n for (const [pattern, handler] of Object.entries(this.loaders)) {\n const fullPattern = pattern.startsWith('GET ') ? pattern : `GET ${pattern}`\n const result = matchUrl(fullPattern, 'GET', path)\n if (result) {\n return { handler, match: result }\n }\n }\n\n return null\n }\n\n /**\n * Find action fixture with match info\n */\n findAction(\n path: string,\n method: string\n ): { handler: ActionFixtureHandler; match: MatchResult } | null {\n // Try exact match first\n if (path in this.actions) {\n const handler = resolveActionHandler(this.actions[path], method)\n if (handler) {\n return {\n handler,\n match: { matched: true, params: {} },\n }\n }\n }\n\n // Try pattern matching\n for (const [pattern, fixture] of Object.entries(this.actions)) {\n const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(pattern)\n ? pattern\n : `${method} ${pattern}`\n const result = matchUrl(fullPattern, method, path)\n if (result) {\n const handler = resolveActionHandler(fixture, method)\n if (handler) {\n return { handler, match: result }\n }\n }\n }\n\n return null\n }\n\n /**\n * Get all loader fixtures\n */\n getLoaders(): LoaderFixtureMapObject {\n return { ...this.loaders }\n }\n\n /**\n * Get all action fixtures\n */\n getActions(): ActionFixtureMapObject {\n return { ...this.actions }\n }\n\n /**\n * Clear all fixtures\n */\n clear(): void {\n this.loaders = {}\n this.actions = {}\n }\n}\n\n/**\n * Resolve action handler based on method\n */\nfunction resolveActionHandler(\n fixture: ActionFixtureHandler | MethodActionHandlers,\n method: string\n): ActionFixtureHandler | undefined {\n if (isMethodActionHandlers(fixture)) {\n const upperMethod = method.toUpperCase() as keyof MethodActionHandlers\n return fixture[upperMethod]\n }\n return fixture\n}\n\n/**\n * Type guard for method action handlers\n */\nfunction isMethodActionHandlers(fixture: unknown): fixture is MethodActionHandlers {\n if (typeof fixture !== 'object' || fixture === null) {\n return false\n }\n const methods = ['POST', 'PUT', 'PATCH', 'DELETE']\n return methods.some((method) => method in fixture)\n}\n\n/**\n * Create a fixture store with initial fixtures\n *\n * @example\n * import { createFixtureStore } from '@demokit-ai/remix/server'\n *\n * export const fixtures = createFixtureStore({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * },\n * })\n */\nexport function createFixtureStore(config?: FixtureStoreConfig): FixtureStore {\n return new FixtureStore(config)\n}\n\n/**\n * Define loader fixtures with type inference\n *\n * @example\n * const loaders = defineRemixLoaderFixtures({\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * '/projects': async () => fetchDemoProjects(),\n * })\n */\nexport function defineRemixLoaderFixtures<T extends LoaderFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define action fixtures with type inference\n *\n * @example\n * const actions = defineRemixActionFixtures({\n * '/users': {\n * POST: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * },\n * '/users/:id': {\n * PUT: ({ formData }) => ({ updated: true }),\n * DELETE: ({ params }) => ({ deleted: true, id: params.id }),\n * },\n * })\n */\nexport function defineRemixActionFixtures<T extends ActionFixtureMapObject>(fixtures: T): T {\n return fixtures\n}\n\n/**\n * Define complete Remix fixtures (loaders + actions)\n *\n * @example\n * const { loaders, actions } = defineRemixFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function defineRemixFixtures<\n L extends LoaderFixtureMapObject,\n A extends ActionFixtureMapObject,\n>(config: { loaders?: L; actions?: A }): { loaders: L; actions: A } {\n return {\n loaders: (config.loaders ?? {}) as L,\n actions: (config.actions ?? {}) as A,\n }\n}\n","/**\n * @demokit-ai/remix/server\n *\n * Server-side utilities for Remix demo mode detection and fixture management.\n * Import from '@demokit-ai/remix/server' in your loader/action files.\n */\n\nimport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n} from './types'\nimport { FixtureStore, createFixtureStore, defineRemixFixtures } from './fixtures'\n\n/** Default demo mode cookie name */\nconst DEFAULT_COOKIE_NAME = 'demokit-demo-mode'\n\n/** Default demo mode header name */\nconst DEFAULT_HEADER_NAME = 'x-demokit-demo-mode'\n\n/** Default environment variable name */\nconst DEFAULT_ENV_VAR = 'DEMOKIT_DEMO_MODE'\n\n/** Default query parameter name */\nconst DEFAULT_QUERY_PARAM = 'demo'\n\n/**\n * Check if demo mode is enabled for a request\n *\n * Checks in order:\n * 1. Query parameter (for easy testing)\n * 2. Cookie (persistent across requests)\n * 3. Header (for API requests)\n * 4. Environment variable (global override)\n *\n * @example\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json({ user: { id: '1', name: 'Demo User' } })\n * }\n * return json({ user: await db.users.findFirst() })\n * }\n */\nexport function isDemoMode(request: Request, options?: DemoModeOptions): boolean {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n headerName = DEFAULT_HEADER_NAME,\n envVar = DEFAULT_ENV_VAR,\n queryParam = DEFAULT_QUERY_PARAM,\n } = options ?? {}\n\n // Check query parameter first (highest priority for easy testing)\n const url = new URL(request.url)\n const queryValue = url.searchParams.get(queryParam)\n if (queryValue !== null) {\n return queryValue === 'true' || queryValue === '1' || queryValue === ''\n }\n\n // Check cookie\n const cookieHeader = request.headers.get('cookie')\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader)\n const cookieValue = cookies[cookieName]\n if (cookieValue !== undefined) {\n return cookieValue === 'true' || cookieValue === '1'\n }\n }\n\n // Check header\n const headerValue = request.headers.get(headerName)\n if (headerValue !== null) {\n return headerValue === 'true' || headerValue === '1'\n }\n\n // Check environment variable (global fallback)\n if (typeof process !== 'undefined' && process.env?.[envVar]) {\n const envValue = process.env[envVar]\n return envValue === 'true' || envValue === '1'\n }\n\n return false\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {}\n for (const pair of cookieHeader.split(';')) {\n const [name, ...rest] = pair.trim().split('=')\n if (name) {\n cookies[name] = rest.join('=')\n }\n }\n return cookies\n}\n\n/**\n * Create a demo mode checker function with preset options\n *\n * @example\n * import { createDemoModeChecker } from '@demokit-ai/remix/server'\n *\n * const isDemoMode = createDemoModeChecker({\n * cookieName: 'my-demo-cookie',\n * envVar: 'MY_DEMO_MODE',\n * })\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json(demoData)\n * }\n * return json(await fetchRealData())\n * }\n */\nexport function createDemoModeChecker(\n options?: DemoModeOptions\n): (request: Request) => boolean {\n return (request: Request) => isDemoMode(request, options)\n}\n\n// Global fixture store instance (for simple usage)\nlet globalFixtureStore: FixtureStore | null = null\n\n/**\n * Set up the global fixture store\n *\n * Call this once at app startup to configure demo fixtures.\n *\n * @example\n * // In your entry.server.tsx or root loader\n * import { setupDemoFixtures } from '@demokit-ai/remix/server'\n *\n * setupDemoFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function setupDemoFixtures(config: {\n loaders?: LoaderFixtureMapObject\n actions?: ActionFixtureMapObject\n}): void {\n globalFixtureStore = createFixtureStore(config)\n}\n\n/**\n * Get the global fixture store\n */\nexport function getFixtureStore(): FixtureStore | null {\n return globalFixtureStore\n}\n\n/**\n * Get demo data for a loader path\n *\n * Returns the fixture data if available, otherwise undefined.\n * Use with isDemoMode() for conditional demo data.\n *\n * @example\n * import { isDemoMode, getDemoData } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request, params }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * const demoData = await getDemoData('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * })\n * if (demoData) return json(demoData)\n * }\n * return json(await db.users.findFirst({ where: { id: params.id } }))\n * }\n */\nexport async function getDemoData<T = unknown>(\n pattern: string,\n context: LoaderFixtureContext\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getLoader(pattern)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Get demo action result for an action path\n *\n * @example\n * import { isDemoMode, getDemoActionResult } from '@demokit-ai/remix/server'\n *\n * export async function action({ request, params }: ActionFunctionArgs) {\n * if (isDemoMode(request)) {\n * const formData = await request.clone().formData()\n * const demoResult = await getDemoActionResult('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * formData,\n * }, request.method)\n * if (demoResult) return json(demoResult)\n * }\n * // Real action...\n * }\n */\nexport async function getDemoActionResult<T = unknown>(\n pattern: string,\n context: ActionFixtureContext,\n method: string\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getAction(pattern, method)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Execute a fixture handler\n */\nasync function executeHandler<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n handler: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof handler === 'function') {\n return (handler as (context: C) => T | Promise<T>)(context)\n }\n return handler\n}\n\n/**\n * Create a cookie value to enable demo mode\n *\n * @example\n * import { createDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': createDemoModeCookie(true),\n * },\n * })\n * }\n */\nexport function createDemoModeCookie(\n enabled: boolean,\n options?: {\n cookieName?: string\n maxAge?: number\n path?: string\n sameSite?: 'Strict' | 'Lax' | 'None'\n secure?: boolean\n }\n): string {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n maxAge = 60 * 60 * 24 * 7, // 7 days\n path = '/',\n sameSite = 'Lax',\n secure = process.env.NODE_ENV === 'production',\n } = options ?? {}\n\n const parts = [\n `${cookieName}=${enabled ? 'true' : 'false'}`,\n `Max-Age=${maxAge}`,\n `Path=${path}`,\n `SameSite=${sameSite}`,\n ]\n\n if (secure) {\n parts.push('Secure')\n }\n\n return parts.join('; ')\n}\n\n/**\n * Create a cookie value to clear demo mode\n *\n * @example\n * import { clearDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': clearDemoModeCookie(),\n * },\n * })\n * }\n */\nexport function clearDemoModeCookie(options?: { cookieName?: string; path?: string }): string {\n const { cookieName = DEFAULT_COOKIE_NAME, path = '/' } = options ?? {}\n return `${cookieName}=; Max-Age=0; Path=${path}`\n}\n\n// Re-export fixture utilities for convenience\nexport { FixtureStore, createFixtureStore, defineRemixFixtures }\nexport {\n defineRemixLoaderFixtures,\n defineRemixActionFixtures,\n} from './fixtures'\n\n// Re-export types\nexport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * @demokit-ai/remix/server\n *\n * Server-side utilities for Remix demo mode detection and fixture management.\n * Import from '@demokit-ai/remix/server' in your loader/action files.\n */\n\nimport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n} from './types'\nimport { FixtureStore, createFixtureStore, defineRemixFixtures } from './fixtures'\n\n/** Default demo mode cookie name */\nconst DEFAULT_COOKIE_NAME = 'demokit-demo-mode'\n\n/** Default demo mode header name */\nconst DEFAULT_HEADER_NAME = 'x-demokit-demo-mode'\n\n/** Default environment variable name */\nconst DEFAULT_ENV_VAR = 'DEMOKIT_DEMO_MODE'\n\n/** Default query parameter name */\nconst DEFAULT_QUERY_PARAM = 'demo'\n\n/**\n * Check if demo mode is enabled for a request\n *\n * Checks in order:\n * 1. Query parameter (for easy testing)\n * 2. Cookie (persistent across requests)\n * 3. Header (for API requests)\n * 4. Environment variable (global override)\n *\n * @example\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json({ user: { id: '1', name: 'Demo User' } })\n * }\n * return json({ user: await db.users.findFirst() })\n * }\n */\nexport function isDemoMode(request: Request, options?: DemoModeOptions): boolean {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n headerName = DEFAULT_HEADER_NAME,\n envVar = DEFAULT_ENV_VAR,\n queryParam = DEFAULT_QUERY_PARAM,\n } = options ?? {}\n\n // Check query parameter first (highest priority for easy testing)\n const url = new URL(request.url)\n const queryValue = url.searchParams.get(queryParam)\n if (queryValue !== null) {\n return queryValue === 'true' || queryValue === '1' || queryValue === ''\n }\n\n // Check cookie\n const cookieHeader = request.headers.get('cookie')\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader)\n const cookieValue = cookies[cookieName]\n if (cookieValue !== undefined) {\n return cookieValue === 'true' || cookieValue === '1'\n }\n }\n\n // Check header\n const headerValue = request.headers.get(headerName)\n if (headerValue !== null) {\n return headerValue === 'true' || headerValue === '1'\n }\n\n // Check environment variable (global fallback)\n if (typeof process !== 'undefined' && process.env?.[envVar]) {\n const envValue = process.env[envVar]\n return envValue === 'true' || envValue === '1'\n }\n\n return false\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {}\n for (const pair of cookieHeader.split(';')) {\n const [name, ...rest] = pair.trim().split('=')\n if (name) {\n cookies[name] = rest.join('=')\n }\n }\n return cookies\n}\n\n/**\n * Create a demo mode checker function with preset options\n *\n * @example\n * import { createDemoModeChecker } from '@demokit-ai/remix/server'\n *\n * const isDemoMode = createDemoModeChecker({\n * cookieName: 'my-demo-cookie',\n * envVar: 'MY_DEMO_MODE',\n * })\n *\n * export async function loader({ request }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * return json(demoData)\n * }\n * return json(await fetchRealData())\n * }\n */\nexport function createDemoModeChecker(\n options?: DemoModeOptions\n): (request: Request) => boolean {\n return (request: Request) => isDemoMode(request, options)\n}\n\n// Global fixture store instance (for simple usage)\nlet globalFixtureStore: FixtureStore | null = null\n\n/**\n * Set up the global fixture store\n *\n * Call this once at app startup to configure demo fixtures.\n *\n * @example\n * // In your entry.server.tsx or root loader\n * import { setupDemoFixtures } from '@demokit-ai/remix/server'\n *\n * setupDemoFixtures({\n * loaders: {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * },\n * actions: {\n * '/users': ({ formData }) => ({ created: true }),\n * },\n * })\n */\nexport function setupDemoFixtures(config: {\n loaders?: LoaderFixtureMapObject\n actions?: ActionFixtureMapObject\n}): void {\n globalFixtureStore = createFixtureStore(config)\n}\n\n/**\n * Get the global fixture store\n */\nexport function getFixtureStore(): FixtureStore | null {\n return globalFixtureStore\n}\n\n/**\n * Get demo data for a loader path\n *\n * Returns the fixture data if available, otherwise undefined.\n * Use with isDemoMode() for conditional demo data.\n *\n * @example\n * import { isDemoMode, getDemoData } from '@demokit-ai/remix/server'\n *\n * export async function loader({ request, params }: LoaderFunctionArgs) {\n * if (isDemoMode(request)) {\n * const demoData = await getDemoData('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * })\n * if (demoData) return json(demoData)\n * }\n * return json(await db.users.findFirst({ where: { id: params.id } }))\n * }\n */\nexport async function getDemoData<T = unknown>(\n pattern: string,\n context: LoaderFixtureContext\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getLoader(pattern)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Get demo action result for an action path\n *\n * @example\n * import { isDemoMode, getDemoActionResult } from '@demokit-ai/remix/server'\n *\n * export async function action({ request, params }: ActionFunctionArgs) {\n * if (isDemoMode(request)) {\n * const formData = await request.clone().formData()\n * const demoResult = await getDemoActionResult('/users/:id', {\n * params,\n * request,\n * path: '/users/' + params.id,\n * formData,\n * }, request.method)\n * if (demoResult) return json(demoResult)\n * }\n * // Real action...\n * }\n */\nexport async function getDemoActionResult<T = unknown>(\n pattern: string,\n context: ActionFixtureContext,\n method: string\n): Promise<T | undefined> {\n const store = getFixtureStore()\n if (!store) return undefined\n\n const handler = store.getAction(pattern, method)\n if (!handler) return undefined\n\n return executeHandler(handler, context) as Promise<T>\n}\n\n/**\n * Execute a fixture handler\n */\nasync function executeHandler<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n handler: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof handler === 'function') {\n return (handler as (context: C) => T | Promise<T>)(context)\n }\n return handler\n}\n\n/**\n * Create a cookie value to enable demo mode\n *\n * @example\n * import { createDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': createDemoModeCookie(true),\n * },\n * })\n * }\n */\nexport function createDemoModeCookie(\n enabled: boolean,\n options?: {\n cookieName?: string\n maxAge?: number\n path?: string\n sameSite?: 'Strict' | 'Lax' | 'None'\n secure?: boolean\n }\n): string {\n const {\n cookieName = DEFAULT_COOKIE_NAME,\n maxAge = 60 * 60 * 24 * 7, // 7 days\n path = '/',\n sameSite = 'Lax',\n secure = process.env.NODE_ENV === 'production',\n } = options ?? {}\n\n const parts = [\n `${cookieName}=${enabled ? 'true' : 'false'}`,\n `Max-Age=${maxAge}`,\n `Path=${path}`,\n `SameSite=${sameSite}`,\n ]\n\n if (secure) {\n parts.push('Secure')\n }\n\n return parts.join('; ')\n}\n\n/**\n * Create a cookie value to clear demo mode\n *\n * @example\n * import { clearDemoModeCookie } from '@demokit-ai/remix/server'\n *\n * export async function action({ request }: ActionFunctionArgs) {\n * return redirect('/', {\n * headers: {\n * 'Set-Cookie': clearDemoModeCookie(),\n * },\n * })\n * }\n */\nexport function clearDemoModeCookie(options?: { cookieName?: string; path?: string }): string {\n const { cookieName = DEFAULT_COOKIE_NAME, path = '/' } = options ?? {}\n return `${cookieName}=; Max-Age=0; Path=${path}`\n}\n\n// Re-export fixture utilities for convenience\nexport { FixtureStore, createFixtureStore, defineRemixFixtures }\nexport {\n defineRemixLoaderFixtures,\n defineRemixActionFixtures,\n} from './fixtures'\n\n// Re-export types\nexport type {\n DemoModeOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demokit-ai/remix",
3
- "version": "0.2.0",
4
- "description": "Remix adapter for DemoKit - demo-aware loader and action mocking",
3
+ "version": "0.4.0",
4
+ "description": "Remix adapter for DemoKit - loader and action mocking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -31,48 +31,32 @@
31
31
  "files": [
32
32
  "dist"
33
33
  ],
34
- "scripts": {
35
- "build": "tsup",
36
- "dev": "tsup --watch",
37
- "test": "vitest",
38
- "typecheck": "tsc --noEmit"
39
- },
40
34
  "dependencies": {
41
- "@demokit-ai/core": "*",
42
- "@demokit-ai/react": "*"
35
+ "@demokit-ai/core": "0.4.0",
36
+ "@demokit-ai/react": "0.4.0"
43
37
  },
44
38
  "devDependencies": {
45
- "@remix-run/node": "^2.15.0",
46
- "@remix-run/react": "^2.15.0",
39
+ "@remix-run/node": "^2.17.2",
40
+ "@remix-run/react": "^2.17.2",
41
+ "@types/node": "^22.0.0",
47
42
  "@types/react": "^19.0.0",
48
- "@types/react-dom": "^19.0.0",
49
- "react": "^19.0.0",
50
- "react-dom": "^19.0.0",
51
43
  "tsup": "^8.3.5",
52
- "typescript": "^5.7.2",
53
- "vitest": "^2.1.8"
44
+ "typescript": "^5.7.2"
54
45
  },
55
46
  "peerDependencies": {
56
47
  "@remix-run/node": ">=2.0.0",
57
48
  "@remix-run/react": ">=2.0.0",
58
- "react": ">=17.0.0",
59
- "react-dom": ">=17.0.0"
49
+ "react": ">=17.0.0"
60
50
  },
61
- "keywords": [
62
- "demo",
63
- "mock",
64
- "remix",
65
- "loader",
66
- "action",
67
- "fixtures",
68
- "testing",
69
- "server-side"
70
- ],
71
- "license": "MIT",
72
- "repository": {
73
- "type": "git",
74
- "url": "https://github.com/your-org/demokit",
75
- "directory": "packages/remix"
51
+ "engines": {
52
+ "node": ">=18"
76
53
  },
77
- "sideEffects": false
54
+ "license": "Apache-2.0",
55
+ "sideEffects": false,
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsup --watch --no-dts",
59
+ "typecheck": "tsc --noEmit",
60
+ "clean": "rm -rf dist"
61
+ }
78
62
  }
package/README.md DELETED
@@ -1,203 +0,0 @@
1
- # @demokit-ai/remix
2
-
3
- ![Tests](https://img.shields.io/badge/tests-119%20passing-brightgreen)
4
- ![Coverage](https://img.shields.io/badge/coverage-17%25-red)
5
-
6
- Remix adapter for DemoKit - demo-aware loader and action mocking for Remix applications.
7
-
8
- ## Installation
9
-
10
- ```bash
11
- npm install @demokit-ai/remix @demokit-ai/core @demokit-ai/react
12
- ```
13
-
14
- ## Features
15
-
16
- - **Demo Loaders** - Wrap loaders to return fixtures in demo mode
17
- - **Demo Actions** - Wrap actions with method-specific fixtures
18
- - **Route Configuration** - Apply demo mode to entire route trees
19
- - **Provider Pattern** - Centralized fixture management
20
- - **Server-Side Support** - Works with Remix server rendering
21
- - Full TypeScript support
22
-
23
- ## Usage
24
-
25
- ### Wrap Individual Loaders
26
-
27
- ```ts
28
- // app/routes/users.tsx
29
- import { createDemoLoader } from '@demokit-ai/remix'
30
-
31
- export const loader = createDemoLoader({
32
- loader: async () => {
33
- return db.users.findMany()
34
- },
35
- isEnabled: () => process.env.DEMO_MODE === 'true',
36
- fixture: [
37
- { id: '1', name: 'Demo User', email: 'demo@example.com' },
38
- { id: '2', name: 'Another User', email: 'another@example.com' },
39
- ],
40
- delay: 100,
41
- })
42
- ```
43
-
44
- ### Wrap Individual Actions
45
-
46
- ```ts
47
- // app/routes/users.$id.tsx
48
- import { createDemoAction } from '@demokit-ai/remix'
49
-
50
- export const action = createDemoAction({
51
- action: async ({ request, params }) => {
52
- const formData = await request.formData()
53
- return db.users.update({
54
- where: { id: params.id },
55
- data: Object.fromEntries(formData),
56
- })
57
- },
58
- isEnabled: () => process.env.DEMO_MODE === 'true',
59
- fixture: {
60
- PUT: ({ params, formData }) => ({
61
- id: params.id,
62
- ...Object.fromEntries(formData!),
63
- updated: true,
64
- }),
65
- DELETE: ({ params }) => ({
66
- id: params.id,
67
- deleted: true,
68
- }),
69
- },
70
- })
71
- ```
72
-
73
- ### Route Configuration Wrapping
74
-
75
- ```ts
76
- // app/root.tsx
77
- import { createDemoRoutes, defineLoaderFixtures, defineActionFixtures } from '@demokit-ai/remix'
78
-
79
- const loaders = defineLoaderFixtures({
80
- '/users': [
81
- { id: '1', name: 'Demo User' },
82
- { id: '2', name: 'Another User' },
83
- ],
84
- '/users/:id': ({ params }) => ({
85
- id: params.id,
86
- name: 'Demo User',
87
- email: 'demo@example.com',
88
- }),
89
- })
90
-
91
- const actions = defineActionFixtures({
92
- '/users/:id': {
93
- PUT: ({ formData }) => ({ updated: true }),
94
- DELETE: ({ params }) => ({ deleted: true, id: params.id }),
95
- },
96
- })
97
-
98
- // Apply to routes
99
- const routes = createDemoRoutes(originalRoutes, {
100
- isEnabled: () => process.env.DEMO_MODE === 'true',
101
- loaders,
102
- actions,
103
- delay: 100,
104
- })
105
- ```
106
-
107
- ### Provider Pattern
108
-
109
- ```tsx
110
- // app/root.tsx
111
- import { DemoRemixProvider, useDemoRemix } from '@demokit-ai/remix'
112
-
113
- export default function App() {
114
- return (
115
- <DemoRemixProvider
116
- enabled={process.env.DEMO_MODE === 'true'}
117
- loaders={{
118
- '/users': [{ id: '1', name: 'Demo User' }],
119
- }}
120
- actions={{
121
- '/users': ({ formData }) => ({ id: '1', ...Object.fromEntries(formData!) }),
122
- }}
123
- >
124
- <Outlet />
125
- </DemoRemixProvider>
126
- )
127
- }
128
-
129
- function DemoControls() {
130
- const { enabled, toggle, setLoader } = useDemoRemix()
131
-
132
- return (
133
- <div>
134
- <button onClick={toggle}>Demo: {enabled ? 'ON' : 'OFF'}</button>
135
- <button onClick={() => setLoader('/custom', () => ({ custom: true }))}>
136
- Add Custom
137
- </button>
138
- </div>
139
- )
140
- }
141
- ```
142
-
143
- ### Server-Side Demo Detection
144
-
145
- ```ts
146
- // app/routes/api.users.tsx
147
- import { isDemoRequest, getDemoScenario } from '@demokit-ai/remix/server'
148
-
149
- export async function loader({ request }: LoaderFunctionArgs) {
150
- if (isDemoRequest(request)) {
151
- const scenario = getDemoScenario(request)
152
- return json(demoFixtures[scenario])
153
- }
154
-
155
- return json(await db.users.findMany())
156
- }
157
- ```
158
-
159
- ## API Reference
160
-
161
- ### `createDemoLoader(options)`
162
-
163
- Create a demo-aware loader function.
164
-
165
- Options:
166
- - `loader` - The real loader function
167
- - `isEnabled` - Function to check if demo mode is enabled
168
- - `fixture` - Demo fixture (static value or function)
169
- - `delay` - Simulated delay in milliseconds
170
- - `onDemo` - Callback when demo mode is used
171
-
172
- ### `createDemoAction(options)`
173
-
174
- Create a demo-aware action function.
175
-
176
- Options:
177
- - `action` - The real action function
178
- - `isEnabled` - Function to check if demo mode is enabled
179
- - `fixture` - Demo fixture (static, function, or method-specific handlers)
180
- - `delay` - Simulated delay in milliseconds
181
- - `onDemo` - Callback when demo mode is used
182
-
183
- ### `DemoRemixProvider`
184
-
185
- Props:
186
- - `enabled` - Whether demo mode is enabled
187
- - `loaders` - Loader fixtures by path
188
- - `actions` - Action fixtures by path
189
- - `delay` - Default delay in milliseconds
190
-
191
- ### `useDemoRemix()`
192
-
193
- Returns:
194
- - `enabled` - Whether demo mode is enabled
195
- - `toggle()` - Toggle demo mode
196
- - `setLoader(path, handler)` - Set a loader fixture
197
- - `removeLoader(path)` - Remove a loader fixture
198
- - `setAction(path, handler)` - Set an action fixture
199
- - `removeAction(path)` - Remove an action fixture
200
-
201
- ## License
202
-
203
- MIT