@demokit-ai/remix 0.0.1
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/fixtures-BP3SzTkj.d.cts +355 -0
- package/dist/fixtures-BP3SzTkj.d.ts +355 -0
- package/dist/index.cjs +385 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +195 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +346 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +307 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +162 -0
- package/dist/server.d.ts +162 -0
- package/dist/server.js +268 -0
- package/dist/server.js.map +1 -0
- package/package.json +78 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/server.ts
|
|
155
|
+
var DEFAULT_COOKIE_NAME = "demokit-demo-mode";
|
|
156
|
+
var DEFAULT_HEADER_NAME = "x-demokit-demo-mode";
|
|
157
|
+
var DEFAULT_ENV_VAR = "DEMOKIT_DEMO_MODE";
|
|
158
|
+
var DEFAULT_QUERY_PARAM = "demo";
|
|
159
|
+
function isDemoMode(request, options) {
|
|
160
|
+
const {
|
|
161
|
+
cookieName = DEFAULT_COOKIE_NAME,
|
|
162
|
+
headerName = DEFAULT_HEADER_NAME,
|
|
163
|
+
envVar = DEFAULT_ENV_VAR,
|
|
164
|
+
queryParam = DEFAULT_QUERY_PARAM
|
|
165
|
+
} = options ?? {};
|
|
166
|
+
const url = new URL(request.url);
|
|
167
|
+
const queryValue = url.searchParams.get(queryParam);
|
|
168
|
+
if (queryValue !== null) {
|
|
169
|
+
return queryValue === "true" || queryValue === "1" || queryValue === "";
|
|
170
|
+
}
|
|
171
|
+
const cookieHeader = request.headers.get("cookie");
|
|
172
|
+
if (cookieHeader) {
|
|
173
|
+
const cookies = parseCookies(cookieHeader);
|
|
174
|
+
const cookieValue = cookies[cookieName];
|
|
175
|
+
if (cookieValue !== void 0) {
|
|
176
|
+
return cookieValue === "true" || cookieValue === "1";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const headerValue = request.headers.get(headerName);
|
|
180
|
+
if (headerValue !== null) {
|
|
181
|
+
return headerValue === "true" || headerValue === "1";
|
|
182
|
+
}
|
|
183
|
+
if (typeof process !== "undefined" && process.env?.[envVar]) {
|
|
184
|
+
const envValue = process.env[envVar];
|
|
185
|
+
return envValue === "true" || envValue === "1";
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
function parseCookies(cookieHeader) {
|
|
190
|
+
const cookies = {};
|
|
191
|
+
for (const pair of cookieHeader.split(";")) {
|
|
192
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
193
|
+
if (name) {
|
|
194
|
+
cookies[name] = rest.join("=");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return cookies;
|
|
198
|
+
}
|
|
199
|
+
function createDemoModeChecker(options) {
|
|
200
|
+
return (request) => isDemoMode(request, options);
|
|
201
|
+
}
|
|
202
|
+
var globalFixtureStore = null;
|
|
203
|
+
function setupDemoFixtures(config) {
|
|
204
|
+
globalFixtureStore = createFixtureStore(config);
|
|
205
|
+
}
|
|
206
|
+
function getFixtureStore() {
|
|
207
|
+
return globalFixtureStore;
|
|
208
|
+
}
|
|
209
|
+
async function getDemoData(pattern, context) {
|
|
210
|
+
const store = getFixtureStore();
|
|
211
|
+
if (!store) return void 0;
|
|
212
|
+
const handler = store.getLoader(pattern);
|
|
213
|
+
if (!handler) return void 0;
|
|
214
|
+
return executeHandler(handler, context);
|
|
215
|
+
}
|
|
216
|
+
async function getDemoActionResult(pattern, context, method) {
|
|
217
|
+
const store = getFixtureStore();
|
|
218
|
+
if (!store) return void 0;
|
|
219
|
+
const handler = store.getAction(pattern, method);
|
|
220
|
+
if (!handler) return void 0;
|
|
221
|
+
return executeHandler(handler, context);
|
|
222
|
+
}
|
|
223
|
+
async function executeHandler(handler, context) {
|
|
224
|
+
if (typeof handler === "function") {
|
|
225
|
+
return handler(context);
|
|
226
|
+
}
|
|
227
|
+
return handler;
|
|
228
|
+
}
|
|
229
|
+
function createDemoModeCookie(enabled, options) {
|
|
230
|
+
const {
|
|
231
|
+
cookieName = DEFAULT_COOKIE_NAME,
|
|
232
|
+
maxAge = 60 * 60 * 24 * 7,
|
|
233
|
+
// 7 days
|
|
234
|
+
path = "/",
|
|
235
|
+
sameSite = "Lax",
|
|
236
|
+
secure = process.env.NODE_ENV === "production"
|
|
237
|
+
} = options ?? {};
|
|
238
|
+
const parts = [
|
|
239
|
+
`${cookieName}=${enabled ? "true" : "false"}`,
|
|
240
|
+
`Max-Age=${maxAge}`,
|
|
241
|
+
`Path=${path}`,
|
|
242
|
+
`SameSite=${sameSite}`
|
|
243
|
+
];
|
|
244
|
+
if (secure) {
|
|
245
|
+
parts.push("Secure");
|
|
246
|
+
}
|
|
247
|
+
return parts.join("; ");
|
|
248
|
+
}
|
|
249
|
+
function clearDemoModeCookie(options) {
|
|
250
|
+
const { cookieName = DEFAULT_COOKIE_NAME, path = "/" } = options ?? {};
|
|
251
|
+
return `${cookieName}=; Max-Age=0; Path=${path}`;
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
FixtureStore,
|
|
255
|
+
clearDemoModeCookie,
|
|
256
|
+
createDemoModeChecker,
|
|
257
|
+
createDemoModeCookie,
|
|
258
|
+
createFixtureStore,
|
|
259
|
+
defineRemixActionFixtures,
|
|
260
|
+
defineRemixFixtures,
|
|
261
|
+
defineRemixLoaderFixtures,
|
|
262
|
+
getDemoActionResult,
|
|
263
|
+
getDemoData,
|
|
264
|
+
getFixtureStore,
|
|
265
|
+
isDemoMode,
|
|
266
|
+
setupDemoFixtures
|
|
267
|
+
};
|
|
268
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@demokit-ai/remix",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Remix adapter for DemoKit - demo-aware loader and action mocking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/server.d.ts",
|
|
23
|
+
"default": "./dist/server.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/server.d.cts",
|
|
27
|
+
"default": "./dist/server.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@demokit-ai/core": "*",
|
|
42
|
+
"@demokit-ai/react": "*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@remix-run/node": "^2.15.0",
|
|
46
|
+
"@remix-run/react": "^2.15.0",
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"@types/react-dom": "^19.0.0",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"react-dom": "^19.0.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"typescript": "^5.7.2",
|
|
53
|
+
"vitest": "^2.1.8"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@remix-run/node": ">=2.0.0",
|
|
57
|
+
"@remix-run/react": ">=2.0.0",
|
|
58
|
+
"react": ">=17.0.0",
|
|
59
|
+
"react-dom": ">=17.0.0"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"demo",
|
|
63
|
+
"mock",
|
|
64
|
+
"remix",
|
|
65
|
+
"loader",
|
|
66
|
+
"action",
|
|
67
|
+
"fixtures",
|
|
68
|
+
"testing",
|
|
69
|
+
"server-side"
|
|
70
|
+
],
|
|
71
|
+
"license": "MIT",
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "https://github.com/your-org/demokit",
|
|
75
|
+
"directory": "packages/remix"
|
|
76
|
+
},
|
|
77
|
+
"sideEffects": false
|
|
78
|
+
}
|