@demokit-ai/remix 0.2.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ FixtureStore,
3
+ createFixtureStore,
4
+ defineRemixActionFixtures,
5
+ defineRemixFixtures,
6
+ defineRemixLoaderFixtures
7
+ } from "./chunk-GUQM5HE6.js";
8
+
1
9
  // src/wrappers.ts
2
10
  function createDemoLoader(options) {
3
11
  const { loader, isEnabled = () => false, fixture, delay = 0, onDemo } = options;
@@ -77,159 +85,6 @@ function withDemoAction(action, options) {
77
85
  return createDemoAction({ ...options, action });
78
86
  }
79
87
 
80
- // src/fixtures.ts
81
- import { matchUrl } from "@demokit-ai/core";
82
- var FixtureStore = class {
83
- loaders = {};
84
- actions = {};
85
- constructor(config) {
86
- if (config?.loaders) {
87
- this.loaders = { ...config.loaders };
88
- }
89
- if (config?.actions) {
90
- this.actions = { ...config.actions };
91
- }
92
- }
93
- /**
94
- * Register a loader fixture
95
- */
96
- setLoader(path, handler) {
97
- this.loaders[path] = handler;
98
- return this;
99
- }
100
- /**
101
- * Register an action fixture
102
- */
103
- setAction(path, handler) {
104
- this.actions[path] = handler;
105
- return this;
106
- }
107
- /**
108
- * Get a loader fixture for a path
109
- */
110
- getLoader(path) {
111
- if (path in this.loaders) {
112
- return this.loaders[path];
113
- }
114
- for (const [pattern, handler] of Object.entries(this.loaders)) {
115
- const fullPattern = pattern.startsWith("GET ") ? pattern : `GET ${pattern}`;
116
- const result = matchUrl(fullPattern, "GET", path);
117
- if (result) {
118
- return handler;
119
- }
120
- }
121
- return void 0;
122
- }
123
- /**
124
- * Get an action fixture for a path and method
125
- */
126
- getAction(path, method) {
127
- if (path in this.actions) {
128
- return resolveActionHandler(this.actions[path], method);
129
- }
130
- for (const [pattern, fixture] of Object.entries(this.actions)) {
131
- const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(pattern) ? pattern : `${method} ${pattern}`;
132
- const result = matchUrl(fullPattern, method, path);
133
- if (result) {
134
- return resolveActionHandler(fixture, method);
135
- }
136
- }
137
- return void 0;
138
- }
139
- /**
140
- * Find loader fixture with match info
141
- */
142
- findLoader(path) {
143
- if (path in this.loaders) {
144
- return {
145
- handler: this.loaders[path],
146
- match: { matched: true, params: {} }
147
- };
148
- }
149
- for (const [pattern, handler] of Object.entries(this.loaders)) {
150
- const fullPattern = pattern.startsWith("GET ") ? pattern : `GET ${pattern}`;
151
- const result = matchUrl(fullPattern, "GET", path);
152
- if (result) {
153
- return { handler, match: result };
154
- }
155
- }
156
- return null;
157
- }
158
- /**
159
- * Find action fixture with match info
160
- */
161
- findAction(path, method) {
162
- if (path in this.actions) {
163
- const handler = resolveActionHandler(this.actions[path], method);
164
- if (handler) {
165
- return {
166
- handler,
167
- match: { matched: true, params: {} }
168
- };
169
- }
170
- }
171
- for (const [pattern, fixture] of Object.entries(this.actions)) {
172
- const fullPattern = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(pattern) ? pattern : `${method} ${pattern}`;
173
- const result = matchUrl(fullPattern, method, path);
174
- if (result) {
175
- const handler = resolveActionHandler(fixture, method);
176
- if (handler) {
177
- return { handler, match: result };
178
- }
179
- }
180
- }
181
- return null;
182
- }
183
- /**
184
- * Get all loader fixtures
185
- */
186
- getLoaders() {
187
- return { ...this.loaders };
188
- }
189
- /**
190
- * Get all action fixtures
191
- */
192
- getActions() {
193
- return { ...this.actions };
194
- }
195
- /**
196
- * Clear all fixtures
197
- */
198
- clear() {
199
- this.loaders = {};
200
- this.actions = {};
201
- }
202
- };
203
- function resolveActionHandler(fixture, method) {
204
- if (isMethodActionHandlers2(fixture)) {
205
- const upperMethod = method.toUpperCase();
206
- return fixture[upperMethod];
207
- }
208
- return fixture;
209
- }
210
- function isMethodActionHandlers2(fixture) {
211
- if (typeof fixture !== "object" || fixture === null) {
212
- return false;
213
- }
214
- const methods = ["POST", "PUT", "PATCH", "DELETE"];
215
- return methods.some((method) => method in fixture);
216
- }
217
- function createFixtureStore(config) {
218
- return new FixtureStore(config);
219
- }
220
- function defineRemixLoaderFixtures(fixtures) {
221
- return fixtures;
222
- }
223
- function defineRemixActionFixtures(fixtures) {
224
- return fixtures;
225
- }
226
- function defineRemixFixtures(config) {
227
- return {
228
- loaders: config.loaders ?? {},
229
- actions: config.actions ?? {}
230
- };
231
- }
232
-
233
88
  // src/provider.tsx
234
89
  import { createContext, useContext, useMemo, useState, useCallback } from "react";
235
90
  import { jsx } from "react/jsx-runtime";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/wrappers.ts","../src/fixtures.ts","../src/provider.tsx"],"sourcesContent":["import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'\nimport type {\n CreateDemoLoaderOptions,\n CreateDemoActionOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n MethodActionHandlers,\n} from './types'\n\n/**\n * Create a demo-aware loader function for Remix\n *\n * Returns a loader that intercepts requests when demo mode is enabled\n * and returns fixture data instead of calling the real loader.\n * Demo mode is checked server-side via cookies, headers, or request context.\n *\n * @example\n * // Basic usage with inline fixture\n * export const loader = createDemoLoader({\n * loader: async ({ params }) => {\n * const user = await db.users.findUnique({ where: { id: params.id } })\n * return json({ user })\n * },\n * isEnabled: (request) => request.headers.get('cookie')?.includes('demo=true') ?? false,\n * fixture: ({ params }) => ({\n * user: { id: params.id, name: 'Demo User', email: 'demo@example.com' }\n * }),\n * })\n *\n * @example\n * // With cookie-based detection\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export const loader = createDemoLoader({\n * loader: realLoader,\n * isEnabled: (request) => isDemoMode(request),\n * fixture: demoData,\n * delay: 200, // Simulate 200ms network latency\n * })\n */\nexport function createDemoLoader<T = unknown>(\n options: CreateDemoLoaderOptions<T>\n): (args: LoaderFunctionArgs) => Promise<T> {\n const { loader, isEnabled = () => false, fixture, delay = 0, onDemo } = options\n\n return async (args: LoaderFunctionArgs): Promise<T> => {\n // Check if demo mode is enabled (async to support cookie parsing)\n const enabled = await isEnabled(args.request)\n if (!enabled) {\n return loader(args)\n }\n\n // If no fixture provided, fall back to real loader\n if (fixture === undefined) {\n return loader(args)\n }\n\n // Build context for fixture handler\n const context: LoaderFixtureContext = {\n params: args.params,\n request: args.request,\n path: new URL(args.request.url).pathname,\n }\n\n // Notify callback\n onDemo?.(context)\n\n // Apply delay if configured\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n\n // Execute fixture\n return executeFixture(fixture, context)\n }\n}\n\n/**\n * Create a demo-aware action function for Remix\n *\n * Returns an action that intercepts requests when demo mode is enabled\n * and returns fixture data instead of calling the real action.\n *\n * @example\n * // Basic usage with inline fixture\n * export const action = createDemoAction({\n * action: async ({ request }) => {\n * const formData = await request.formData()\n * const user = await db.users.create({ data: Object.fromEntries(formData) })\n * return json({ user })\n * },\n * isEnabled: (request) => request.headers.get('cookie')?.includes('demo=true') ?? false,\n * fixture: async ({ formData }) => ({\n * user: { id: crypto.randomUUID(), ...Object.fromEntries(formData!) }\n * }),\n * })\n *\n * @example\n * // With method-specific fixtures\n * export const action = createDemoAction({\n * action: realAction,\n * isEnabled: isDemoMode,\n * fixture: {\n * POST: ({ formData }) => json({ created: true, id: crypto.randomUUID() }),\n * DELETE: ({ params }) => json({ deleted: true, id: params.id }),\n * },\n * })\n */\nexport function createDemoAction<T = unknown>(\n options: CreateDemoActionOptions<T>\n): (args: ActionFunctionArgs) => Promise<T> {\n const { action, isEnabled = () => false, fixture, delay = 0, onDemo } = options\n\n return async (args: ActionFunctionArgs): Promise<T> => {\n // Check if demo mode is enabled (async to support cookie parsing)\n const enabled = await isEnabled(args.request)\n if (!enabled) {\n return action(args)\n }\n\n // If no fixture provided, fall back to real action\n if (fixture === undefined) {\n return action(args)\n }\n\n // Parse form data for convenience\n let formData: FormData | undefined\n try {\n formData = await args.request.clone().formData()\n } catch {\n // Not form data, that's fine\n }\n\n // Build context for fixture handler\n const context: ActionFixtureContext = {\n params: args.params,\n request: args.request,\n path: new URL(args.request.url).pathname,\n formData,\n }\n\n // Notify callback\n onDemo?.(context)\n\n // Apply delay if configured\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n\n // Check if fixture is method-specific\n if (isMethodActionHandlers(fixture)) {\n const method = args.request.method.toUpperCase() as keyof MethodActionHandlers\n const handler = fixture[method]\n if (handler) {\n return executeFixture(handler, context) as Promise<T>\n }\n // No handler for this method, fall back to real action\n return action(args)\n }\n\n // Execute fixture\n return executeFixture(fixture as ActionFixtureHandler<T>, context)\n }\n}\n\n/**\n * Execute a fixture handler (static value or function)\n */\nasync function executeFixture<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n fixture: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof fixture === 'function') {\n return (fixture as (context: C) => T | Promise<T>)(context)\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 * Helper to create a demo loader with context from request\n *\n * Simplified version that uses request-based demo mode detection.\n *\n * @example\n * import { withDemoLoader, isDemoMode } from '@demokit-ai/remix'\n *\n * export const loader = withDemoLoader(\n * async ({ params }) => {\n * return json(await db.users.findUnique({ where: { id: params.id } }))\n * },\n * {\n * isEnabled: isDemoMode,\n * fixture: ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n * )\n */\nexport function withDemoLoader<T = unknown>(\n loader: (args: LoaderFunctionArgs) => T | Promise<T>,\n options: Omit<CreateDemoLoaderOptions<T>, 'loader'>\n): (args: LoaderFunctionArgs) => Promise<T> {\n return createDemoLoader({ ...options, loader })\n}\n\n/**\n * Helper to create a demo action with context from request\n *\n * Simplified version that uses request-based demo mode detection.\n *\n * @example\n * import { withDemoAction, isDemoMode } from '@demokit-ai/remix'\n *\n * export const action = withDemoAction(\n * async ({ request }) => {\n * const formData = await request.formData()\n * return json(await db.users.create({ data: Object.fromEntries(formData) }))\n * },\n * {\n * isEnabled: isDemoMode,\n * fixture: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * }\n * )\n */\nexport function withDemoAction<T = unknown>(\n action: (args: ActionFunctionArgs) => T | Promise<T>,\n options: Omit<CreateDemoActionOptions<T>, 'action'>\n): (args: ActionFunctionArgs) => Promise<T> {\n return createDemoAction({ ...options, action })\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","import { createContext, useContext, useMemo, useState, useCallback } from 'react'\nimport type {\n DemoRemixProviderProps,\n DemoRemixState,\n LoaderFixtureMap,\n ActionFixtureMap,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n} from './types'\n\n/**\n * Context value for demo remix state\n */\ninterface DemoRemixContextValue extends DemoRemixState {\n /** Enable demo mode */\n enable: () => void\n /** Disable demo mode */\n disable: () => void\n /** Toggle demo mode */\n toggle: () => boolean\n /** Set a loader fixture */\n setLoader: (path: string, handler: LoaderFixtureHandler) => void\n /** Remove a loader fixture */\n removeLoader: (path: string) => void\n /** Set an action fixture */\n setAction: (path: string, handler: ActionFixtureHandler) => void\n /** Remove an action fixture */\n removeAction: (path: string) => void\n /** Clear all fixtures */\n clearFixtures: () => void\n /** Get delay setting */\n delay: number\n}\n\nconst DemoRemixContext = createContext<DemoRemixContextValue | null>(null)\n\n/**\n * Convert object fixture map to Map\n */\nfunction toLoaderMap(obj?: LoaderFixtureMapObject): LoaderFixtureMap {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n}\n\n/**\n * Convert object fixture map to Map\n */\nfunction toActionMap(obj?: ActionFixtureMapObject): ActionFixtureMap {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n}\n\n/**\n * Provider for demo remix state (client-side)\n *\n * Wrap your Remix app with this provider to enable client-side fixture management\n * and demo mode toggle. For server-side demo mode detection, use the server utilities.\n *\n * @example\n * import { DemoRemixProvider } from '@demokit-ai/remix'\n *\n * const loaders = {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n *\n * const actions = {\n * '/users': ({ formData }) => ({\n * id: crypto.randomUUID(),\n * name: formData?.get('name'),\n * }),\n * }\n *\n * export default function App() {\n * return (\n * <DemoRemixProvider loaders={loaders} actions={actions} enabled>\n * <Outlet />\n * </DemoRemixProvider>\n * )\n * }\n */\nexport function DemoRemixProvider({\n children,\n enabled: initialEnabled = false,\n loaders: initialLoaders,\n actions: initialActions,\n delay = 0,\n}: DemoRemixProviderProps) {\n const [enabled, setEnabled] = useState(initialEnabled)\n const [loaders, setLoaders] = useState<LoaderFixtureMap>(() => toLoaderMap(initialLoaders))\n const [actions, setActions] = useState<ActionFixtureMap>(() => toActionMap(initialActions))\n\n const enable = useCallback(() => setEnabled(true), [])\n const disable = useCallback(() => setEnabled(false), [])\n const toggle = useCallback(() => {\n setEnabled((prev) => !prev)\n return !enabled\n }, [enabled])\n\n const setLoader = useCallback((path: string, handler: LoaderFixtureHandler) => {\n setLoaders((prev) => new Map(prev).set(path, handler))\n }, [])\n\n const removeLoader = useCallback((path: string) => {\n setLoaders((prev) => {\n const next = new Map(prev)\n next.delete(path)\n return next\n })\n }, [])\n\n const setAction = useCallback((path: string, handler: ActionFixtureHandler) => {\n setActions((prev) => new Map(prev).set(path, handler))\n }, [])\n\n const removeAction = useCallback((path: string) => {\n setActions((prev) => {\n const next = new Map(prev)\n next.delete(path)\n return next\n })\n }, [])\n\n const clearFixtures = useCallback(() => {\n setLoaders(new Map())\n setActions(new Map())\n }, [])\n\n const value = useMemo<DemoRemixContextValue>(\n () => ({\n enabled,\n loaders,\n actions,\n delay,\n enable,\n disable,\n toggle,\n setLoader,\n removeLoader,\n setAction,\n removeAction,\n clearFixtures,\n }),\n [\n enabled,\n loaders,\n actions,\n delay,\n enable,\n disable,\n toggle,\n setLoader,\n removeLoader,\n setAction,\n removeAction,\n clearFixtures,\n ]\n )\n\n return <DemoRemixContext.Provider value={value}>{children}</DemoRemixContext.Provider>\n}\n\n/**\n * Hook to access demo remix state and controls\n *\n * @example\n * function DemoModeToggle() {\n * const { enabled, toggle } = useDemoRemix()\n * return (\n * <button onClick={toggle}>\n * Demo Mode: {enabled ? 'ON' : 'OFF'}\n * </button>\n * )\n * }\n */\nexport function useDemoRemix(): DemoRemixContextValue {\n const context = useContext(DemoRemixContext)\n if (!context) {\n throw new Error('useDemoRemix must be used within a DemoRemixProvider')\n }\n return context\n}\n\n/**\n * Hook to check if demo mode is enabled\n *\n * @example\n * function DataDisplay() {\n * const isDemo = useIsDemoRemixMode()\n * return isDemo ? <DemoBanner /> : null\n * }\n */\nexport function useIsDemoRemixMode(): boolean {\n const context = useContext(DemoRemixContext)\n return context?.enabled ?? false\n}\n\n/**\n * Get the demo remix context (may be null if not in provider)\n *\n * Use this for optional demo mode integration.\n */\nexport function useDemoRemixOptional(): DemoRemixContextValue | null {\n return useContext(DemoRemixContext)\n}\n"],"mappings":";AA0CO,SAAS,iBACd,SAC0C;AAC1C,QAAM,EAAE,QAAQ,YAAY,MAAM,OAAO,SAAS,QAAQ,GAAG,OAAO,IAAI;AAExE,SAAO,OAAO,SAAyC;AAErD,UAAM,UAAU,MAAM,UAAU,KAAK,OAAO;AAC5C,QAAI,CAAC,SAAS;AACZ,aAAO,OAAO,IAAI;AAAA,IACpB;AAGA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,IAAI;AAAA,IACpB;AAGA,UAAM,UAAgC;AAAA,MACpC,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK;AAAA,MACd,MAAM,IAAI,IAAI,KAAK,QAAQ,GAAG,EAAE;AAAA,IAClC;AAGA,aAAS,OAAO;AAGhB,QAAI,QAAQ,GAAG;AACb,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAAA,IAC3D;AAGA,WAAO,eAAe,SAAS,OAAO;AAAA,EACxC;AACF;AAiCO,SAAS,iBACd,SAC0C;AAC1C,QAAM,EAAE,QAAQ,YAAY,MAAM,OAAO,SAAS,QAAQ,GAAG,OAAO,IAAI;AAExE,SAAO,OAAO,SAAyC;AAErD,UAAM,UAAU,MAAM,UAAU,KAAK,OAAO;AAC5C,QAAI,CAAC,SAAS;AACZ,aAAO,OAAO,IAAI;AAAA,IACpB;AAGA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,IAAI;AAAA,IACpB;AAGA,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,QAAQ,MAAM,EAAE,SAAS;AAAA,IACjD,QAAQ;AAAA,IAER;AAGA,UAAM,UAAgC;AAAA,MACpC,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK;AAAA,MACd,MAAM,IAAI,IAAI,KAAK,QAAQ,GAAG,EAAE;AAAA,MAChC;AAAA,IACF;AAGA,aAAS,OAAO;AAGhB,QAAI,QAAQ,GAAG;AACb,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAAA,IAC3D;AAGA,QAAI,uBAAuB,OAAO,GAAG;AACnC,YAAM,SAAS,KAAK,QAAQ,OAAO,YAAY;AAC/C,YAAM,UAAU,QAAQ,MAAM;AAC9B,UAAI,SAAS;AACX,eAAO,eAAe,SAAS,OAAO;AAAA,MACxC;AAEA,aAAO,OAAO,IAAI;AAAA,IACpB;AAGA,WAAO,eAAe,SAAoC,OAAO;AAAA,EACnE;AACF;AAKA,eAAe,eACb,SACA,SACY;AACZ,MAAI,OAAO,YAAY,YAAY;AACjC,WAAQ,QAA2C,OAAO;AAAA,EAC5D;AACA,SAAO;AACT;AAKA,SAAS,uBAAuB,SAAmD;AACjF,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,CAAC,QAAQ,OAAO,SAAS,QAAQ;AACjD,SAAO,QAAQ,KAAK,CAAC,WAAW,UAAU,OAAO;AACnD;AAoBO,SAAS,eACd,QACA,SAC0C;AAC1C,SAAO,iBAAiB,EAAE,GAAG,SAAS,OAAO,CAAC;AAChD;AAqBO,SAAS,eACd,QACA,SAC0C;AAC1C,SAAO,iBAAiB,EAAE,GAAG,SAAS,OAAO,CAAC;AAChD;;;AChPA,SAAS,gBAAkC;AAgBpC,IAAM,eAAN,MAAmB;AAAA,EAChB,UAAkC,CAAC;AAAA,EACnC,UAAkC,CAAC;AAAA,EAE3C,YAAY,QAA6B;AACvC,QAAI,QAAQ,SAAS;AACnB,WAAK,UAAU,EAAE,GAAG,OAAO,QAAQ;AAAA,IACrC;AACA,QAAI,QAAQ,SAAS;AACnB,WAAK,UAAU,EAAE,GAAG,OAAO,QAAQ;AAAA,IACrC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAc,SAAqC;AAC3D,SAAK,QAAQ,IAAI,IAAI;AACrB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAc,SAA4D;AAClF,SAAK,QAAQ,IAAI,IAAI;AACrB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAgD;AAExD,QAAI,QAAQ,KAAK,SAAS;AACxB,aAAO,KAAK,QAAQ,IAAI;AAAA,IAC1B;AAGA,eAAW,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAC7D,YAAM,cAAc,QAAQ,WAAW,MAAM,IAAI,UAAU,OAAO,OAAO;AACzE,YAAM,SAAS,SAAS,aAAa,OAAO,IAAI;AAChD,UAAI,QAAQ;AACV,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAc,QAAkD;AAExE,QAAI,QAAQ,KAAK,SAAS;AACxB,aAAO,qBAAqB,KAAK,QAAQ,IAAI,GAAG,MAAM;AAAA,IACxD;AAGA,eAAW,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAC7D,YAAM,cAAc,iCAAiC,KAAK,OAAO,IAC7D,UACA,GAAG,MAAM,IAAI,OAAO;AACxB,YAAM,SAAS,SAAS,aAAa,QAAQ,IAAI;AACjD,UAAI,QAAQ;AACV,eAAO,qBAAqB,SAAS,MAAM;AAAA,MAC7C;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAA4E;AAErF,QAAI,QAAQ,KAAK,SAAS;AACxB,aAAO;AAAA,QACL,SAAS,KAAK,QAAQ,IAAI;AAAA,QAC1B,OAAO,EAAE,SAAS,MAAM,QAAQ,CAAC,EAAE;AAAA,MACrC;AAAA,IACF;AAGA,eAAW,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAC7D,YAAM,cAAc,QAAQ,WAAW,MAAM,IAAI,UAAU,OAAO,OAAO;AACzE,YAAM,SAAS,SAAS,aAAa,OAAO,IAAI;AAChD,UAAI,QAAQ;AACV,eAAO,EAAE,SAAS,OAAO,OAAO;AAAA,MAClC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,WACE,MACA,QAC8D;AAE9D,QAAI,QAAQ,KAAK,SAAS;AACxB,YAAM,UAAU,qBAAqB,KAAK,QAAQ,IAAI,GAAG,MAAM;AAC/D,UAAI,SAAS;AACX,eAAO;AAAA,UACL;AAAA,UACA,OAAO,EAAE,SAAS,MAAM,QAAQ,CAAC,EAAE;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAGA,eAAW,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAC7D,YAAM,cAAc,iCAAiC,KAAK,OAAO,IAC7D,UACA,GAAG,MAAM,IAAI,OAAO;AACxB,YAAM,SAAS,SAAS,aAAa,QAAQ,IAAI;AACjD,UAAI,QAAQ;AACV,cAAM,UAAU,qBAAqB,SAAS,MAAM;AACpD,YAAI,SAAS;AACX,iBAAO,EAAE,SAAS,OAAO,OAAO;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqC;AACnC,WAAO,EAAE,GAAG,KAAK,QAAQ;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqC;AACnC,WAAO,EAAE,GAAG,KAAK,QAAQ;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,UAAU,CAAC;AAChB,SAAK,UAAU,CAAC;AAAA,EAClB;AACF;AAKA,SAAS,qBACP,SACA,QACkC;AAClC,MAAIA,wBAAuB,OAAO,GAAG;AACnC,UAAM,cAAc,OAAO,YAAY;AACvC,WAAO,QAAQ,WAAW;AAAA,EAC5B;AACA,SAAO;AACT;AAKA,SAASA,wBAAuB,SAAmD;AACjF,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,CAAC,QAAQ,OAAO,SAAS,QAAQ;AACjD,SAAO,QAAQ,KAAK,CAAC,WAAW,UAAU,OAAO;AACnD;AAwBO,SAAS,mBAAmB,QAA2C;AAC5E,SAAO,IAAI,aAAa,MAAM;AAChC;AAYO,SAAS,0BAA4D,UAAgB;AAC1F,SAAO;AACT;AAgBO,SAAS,0BAA4D,UAAgB;AAC1F,SAAO;AACT;AAeO,SAAS,oBAGd,QAAkE;AAClE,SAAO;AAAA,IACL,SAAU,OAAO,WAAW,CAAC;AAAA,IAC7B,SAAU,OAAO,WAAW,CAAC;AAAA,EAC/B;AACF;;;ACnRA,SAAS,eAAe,YAAY,SAAS,UAAU,mBAAmB;AAiKjE;AA7HT,IAAM,mBAAmB,cAA4C,IAAI;AAKzE,SAAS,YAAY,KAAgD;AACnE,MAAI,CAAC,IAAK,QAAO,oBAAI,IAAI;AACzB,SAAO,IAAI,IAAI,OAAO,QAAQ,GAAG,CAAC;AACpC;AAKA,SAAS,YAAY,KAAgD;AACnE,MAAI,CAAC,IAAK,QAAO,oBAAI,IAAI;AACzB,SAAO,IAAI,IAAI,OAAO,QAAQ,GAAG,CAAC;AACpC;AA+BO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,SAAS,iBAAiB;AAAA,EAC1B,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV,GAA2B;AACzB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,cAAc;AACrD,QAAM,CAAC,SAAS,UAAU,IAAI,SAA2B,MAAM,YAAY,cAAc,CAAC;AAC1F,QAAM,CAAC,SAAS,UAAU,IAAI,SAA2B,MAAM,YAAY,cAAc,CAAC;AAE1F,QAAM,SAAS,YAAY,MAAM,WAAW,IAAI,GAAG,CAAC,CAAC;AACrD,QAAM,UAAU,YAAY,MAAM,WAAW,KAAK,GAAG,CAAC,CAAC;AACvD,QAAM,SAAS,YAAY,MAAM;AAC/B,eAAW,CAAC,SAAS,CAAC,IAAI;AAC1B,WAAO,CAAC;AAAA,EACV,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,YAAY,YAAY,CAAC,MAAc,YAAkC;AAC7E,eAAW,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,EACvD,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,YAAY,CAAC,SAAiB;AACjD,eAAW,CAAC,SAAS;AACnB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,WAAK,OAAO,IAAI;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,YAAY,CAAC,MAAc,YAAkC;AAC7E,eAAW,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,EACvD,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,YAAY,CAAC,SAAiB;AACjD,eAAW,CAAC,SAAS;AACnB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,WAAK,OAAO,IAAI;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB,YAAY,MAAM;AACtC,eAAW,oBAAI,IAAI,CAAC;AACpB,eAAW,oBAAI,IAAI,CAAC;AAAA,EACtB,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,oBAAC,iBAAiB,UAAjB,EAA0B,OAAe,UAAS;AAC5D;AAeO,SAAS,eAAsC;AACpD,QAAM,UAAU,WAAW,gBAAgB;AAC3C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AACA,SAAO;AACT;AAWO,SAAS,qBAA8B;AAC5C,QAAM,UAAU,WAAW,gBAAgB;AAC3C,SAAO,SAAS,WAAW;AAC7B;AAOO,SAAS,uBAAqD;AACnE,SAAO,WAAW,gBAAgB;AACpC;","names":["isMethodActionHandlers"]}
1
+ {"version":3,"sources":["../src/wrappers.ts","../src/provider.tsx"],"sourcesContent":["import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'\nimport type {\n CreateDemoLoaderOptions,\n CreateDemoActionOptions,\n LoaderFixtureContext,\n ActionFixtureContext,\n ActionFixtureHandler,\n MethodActionHandlers,\n} from './types'\n\n/**\n * Create a demo-aware loader function for Remix\n *\n * Returns a loader that intercepts requests when demo mode is enabled\n * and returns fixture data instead of calling the real loader.\n * Demo mode is checked server-side via cookies, headers, or request context.\n *\n * @example\n * // Basic usage with inline fixture\n * export const loader = createDemoLoader({\n * loader: async ({ params }) => {\n * const user = await db.users.findUnique({ where: { id: params.id } })\n * return json({ user })\n * },\n * isEnabled: (request) => request.headers.get('cookie')?.includes('demo=true') ?? false,\n * fixture: ({ params }) => ({\n * user: { id: params.id, name: 'Demo User', email: 'demo@example.com' }\n * }),\n * })\n *\n * @example\n * // With cookie-based detection\n * import { isDemoMode } from '@demokit-ai/remix/server'\n *\n * export const loader = createDemoLoader({\n * loader: realLoader,\n * isEnabled: (request) => isDemoMode(request),\n * fixture: demoData,\n * delay: 200, // Simulate 200ms network latency\n * })\n */\nexport function createDemoLoader<T = unknown>(\n options: CreateDemoLoaderOptions<T>\n): (args: LoaderFunctionArgs) => Promise<T> {\n const { loader, isEnabled = () => false, fixture, delay = 0, onDemo } = options\n\n return async (args: LoaderFunctionArgs): Promise<T> => {\n // Check if demo mode is enabled (async to support cookie parsing)\n const enabled = await isEnabled(args.request)\n if (!enabled) {\n return loader(args)\n }\n\n // If no fixture provided, fall back to real loader\n if (fixture === undefined) {\n return loader(args)\n }\n\n // Build context for fixture handler\n const context: LoaderFixtureContext = {\n params: args.params,\n request: args.request,\n path: new URL(args.request.url).pathname,\n }\n\n // Notify callback\n onDemo?.(context)\n\n // Apply delay if configured\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n\n // Execute fixture\n return executeFixture(fixture, context)\n }\n}\n\n/**\n * Create a demo-aware action function for Remix\n *\n * Returns an action that intercepts requests when demo mode is enabled\n * and returns fixture data instead of calling the real action.\n *\n * @example\n * // Basic usage with inline fixture\n * export const action = createDemoAction({\n * action: async ({ request }) => {\n * const formData = await request.formData()\n * const user = await db.users.create({ data: Object.fromEntries(formData) })\n * return json({ user })\n * },\n * isEnabled: (request) => request.headers.get('cookie')?.includes('demo=true') ?? false,\n * fixture: async ({ formData }) => ({\n * user: { id: crypto.randomUUID(), ...Object.fromEntries(formData!) }\n * }),\n * })\n *\n * @example\n * // With method-specific fixtures\n * export const action = createDemoAction({\n * action: realAction,\n * isEnabled: isDemoMode,\n * fixture: {\n * POST: ({ formData }) => json({ created: true, id: crypto.randomUUID() }),\n * DELETE: ({ params }) => json({ deleted: true, id: params.id }),\n * },\n * })\n */\nexport function createDemoAction<T = unknown>(\n options: CreateDemoActionOptions<T>\n): (args: ActionFunctionArgs) => Promise<T> {\n const { action, isEnabled = () => false, fixture, delay = 0, onDemo } = options\n\n return async (args: ActionFunctionArgs): Promise<T> => {\n // Check if demo mode is enabled (async to support cookie parsing)\n const enabled = await isEnabled(args.request)\n if (!enabled) {\n return action(args)\n }\n\n // If no fixture provided, fall back to real action\n if (fixture === undefined) {\n return action(args)\n }\n\n // Parse form data for convenience\n let formData: FormData | undefined\n try {\n formData = await args.request.clone().formData()\n } catch {\n // Not form data, that's fine\n }\n\n // Build context for fixture handler\n const context: ActionFixtureContext = {\n params: args.params,\n request: args.request,\n path: new URL(args.request.url).pathname,\n formData,\n }\n\n // Notify callback\n onDemo?.(context)\n\n // Apply delay if configured\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n\n // Check if fixture is method-specific\n if (isMethodActionHandlers(fixture)) {\n const method = args.request.method.toUpperCase() as keyof MethodActionHandlers\n const handler = fixture[method]\n if (handler) {\n return executeFixture(handler, context) as Promise<T>\n }\n // No handler for this method, fall back to real action\n return action(args)\n }\n\n // Execute fixture\n return executeFixture(fixture as ActionFixtureHandler<T>, context)\n }\n}\n\n/**\n * Execute a fixture handler (static value or function)\n */\nasync function executeFixture<T, C extends LoaderFixtureContext | ActionFixtureContext>(\n fixture: T | ((context: C) => T | Promise<T>),\n context: C\n): Promise<T> {\n if (typeof fixture === 'function') {\n return (fixture as (context: C) => T | Promise<T>)(context)\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 * Helper to create a demo loader with context from request\n *\n * Simplified version that uses request-based demo mode detection.\n *\n * @example\n * import { withDemoLoader, isDemoMode } from '@demokit-ai/remix'\n *\n * export const loader = withDemoLoader(\n * async ({ params }) => {\n * return json(await db.users.findUnique({ where: { id: params.id } }))\n * },\n * {\n * isEnabled: isDemoMode,\n * fixture: ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n * )\n */\nexport function withDemoLoader<T = unknown>(\n loader: (args: LoaderFunctionArgs) => T | Promise<T>,\n options: Omit<CreateDemoLoaderOptions<T>, 'loader'>\n): (args: LoaderFunctionArgs) => Promise<T> {\n return createDemoLoader({ ...options, loader })\n}\n\n/**\n * Helper to create a demo action with context from request\n *\n * Simplified version that uses request-based demo mode detection.\n *\n * @example\n * import { withDemoAction, isDemoMode } from '@demokit-ai/remix'\n *\n * export const action = withDemoAction(\n * async ({ request }) => {\n * const formData = await request.formData()\n * return json(await db.users.create({ data: Object.fromEntries(formData) }))\n * },\n * {\n * isEnabled: isDemoMode,\n * fixture: ({ formData }) => ({ id: crypto.randomUUID(), name: formData?.get('name') }),\n * }\n * )\n */\nexport function withDemoAction<T = unknown>(\n action: (args: ActionFunctionArgs) => T | Promise<T>,\n options: Omit<CreateDemoActionOptions<T>, 'action'>\n): (args: ActionFunctionArgs) => Promise<T> {\n return createDemoAction({ ...options, action })\n}\n","import { createContext, useContext, useMemo, useState, useCallback } from 'react'\nimport type {\n DemoRemixProviderProps,\n DemoRemixState,\n LoaderFixtureMap,\n ActionFixtureMap,\n LoaderFixtureMapObject,\n ActionFixtureMapObject,\n LoaderFixtureHandler,\n ActionFixtureHandler,\n} from './types'\n\n/**\n * Context value for demo remix state\n */\ninterface DemoRemixContextValue extends DemoRemixState {\n /** Enable demo mode */\n enable: () => void\n /** Disable demo mode */\n disable: () => void\n /** Toggle demo mode */\n toggle: () => boolean\n /** Set a loader fixture */\n setLoader: (path: string, handler: LoaderFixtureHandler) => void\n /** Remove a loader fixture */\n removeLoader: (path: string) => void\n /** Set an action fixture */\n setAction: (path: string, handler: ActionFixtureHandler) => void\n /** Remove an action fixture */\n removeAction: (path: string) => void\n /** Clear all fixtures */\n clearFixtures: () => void\n /** Get delay setting */\n delay: number\n}\n\nconst DemoRemixContext = createContext<DemoRemixContextValue | null>(null)\n\n/**\n * Convert object fixture map to Map\n */\nfunction toLoaderMap(obj?: LoaderFixtureMapObject): LoaderFixtureMap {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n}\n\n/**\n * Convert object fixture map to Map\n */\nfunction toActionMap(obj?: ActionFixtureMapObject): ActionFixtureMap {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n}\n\n/**\n * Provider for demo remix state (client-side)\n *\n * Wrap your Remix app with this provider to enable client-side fixture management\n * and demo mode toggle. For server-side demo mode detection, use the server utilities.\n *\n * @example\n * import { DemoRemixProvider } from '@demokit-ai/remix'\n *\n * const loaders = {\n * '/users': [{ id: '1', name: 'Demo User' }],\n * '/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n *\n * const actions = {\n * '/users': ({ formData }) => ({\n * id: crypto.randomUUID(),\n * name: formData?.get('name'),\n * }),\n * }\n *\n * export default function App() {\n * return (\n * <DemoRemixProvider loaders={loaders} actions={actions} enabled>\n * <Outlet />\n * </DemoRemixProvider>\n * )\n * }\n */\nexport function DemoRemixProvider({\n children,\n enabled: initialEnabled = false,\n loaders: initialLoaders,\n actions: initialActions,\n delay = 0,\n}: DemoRemixProviderProps) {\n const [enabled, setEnabled] = useState(initialEnabled)\n const [loaders, setLoaders] = useState<LoaderFixtureMap>(() => toLoaderMap(initialLoaders))\n const [actions, setActions] = useState<ActionFixtureMap>(() => toActionMap(initialActions))\n\n const enable = useCallback(() => setEnabled(true), [])\n const disable = useCallback(() => setEnabled(false), [])\n const toggle = useCallback(() => {\n setEnabled((prev) => !prev)\n return !enabled\n }, [enabled])\n\n const setLoader = useCallback((path: string, handler: LoaderFixtureHandler) => {\n setLoaders((prev) => new Map(prev).set(path, handler))\n }, [])\n\n const removeLoader = useCallback((path: string) => {\n setLoaders((prev) => {\n const next = new Map(prev)\n next.delete(path)\n return next\n })\n }, [])\n\n const setAction = useCallback((path: string, handler: ActionFixtureHandler) => {\n setActions((prev) => new Map(prev).set(path, handler))\n }, [])\n\n const removeAction = useCallback((path: string) => {\n setActions((prev) => {\n const next = new Map(prev)\n next.delete(path)\n return next\n })\n }, [])\n\n const clearFixtures = useCallback(() => {\n setLoaders(new Map())\n setActions(new Map())\n }, [])\n\n const value = useMemo<DemoRemixContextValue>(\n () => ({\n enabled,\n loaders,\n actions,\n delay,\n enable,\n disable,\n toggle,\n setLoader,\n removeLoader,\n setAction,\n removeAction,\n clearFixtures,\n }),\n [\n enabled,\n loaders,\n actions,\n delay,\n enable,\n disable,\n toggle,\n setLoader,\n removeLoader,\n setAction,\n removeAction,\n clearFixtures,\n ]\n )\n\n return <DemoRemixContext.Provider value={value}>{children}</DemoRemixContext.Provider>\n}\n\n/**\n * Hook to access demo remix state and controls\n *\n * @example\n * function DemoModeToggle() {\n * const { enabled, toggle } = useDemoRemix()\n * return (\n * <button onClick={toggle}>\n * Demo Mode: {enabled ? 'ON' : 'OFF'}\n * </button>\n * )\n * }\n */\nexport function useDemoRemix(): DemoRemixContextValue {\n const context = useContext(DemoRemixContext)\n if (!context) {\n throw new Error('useDemoRemix must be used within a DemoRemixProvider')\n }\n return context\n}\n\n/**\n * Hook to check if demo mode is enabled\n *\n * @example\n * function DataDisplay() {\n * const isDemo = useIsDemoRemixMode()\n * return isDemo ? <DemoBanner /> : null\n * }\n */\nexport function useIsDemoRemixMode(): boolean {\n const context = useContext(DemoRemixContext)\n return context?.enabled ?? false\n}\n\n/**\n * Get the demo remix context (may be null if not in provider)\n *\n * Use this for optional demo mode integration.\n */\nexport function useDemoRemixOptional(): DemoRemixContextValue | null {\n return useContext(DemoRemixContext)\n}\n"],"mappings":"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","names":[]}
package/dist/server.cjs CHANGED
@@ -39,9 +39,9 @@ module.exports = __toCommonJS(server_exports);
39
39
  // src/fixtures.ts
40
40
  var import_core = require("@demokit-ai/core");
41
41
  var FixtureStore = class {
42
- loaders = {};
43
- actions = {};
44
42
  constructor(config) {
43
+ this.loaders = {};
44
+ this.actions = {};
45
45
  if (config?.loaders) {
46
46
  this.loaders = { ...config.loaders };
47
47
  }
@@ -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.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":[]}