cms-renderer 0.0.0 → 0.1.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.
Files changed (51) hide show
  1. package/dist/chunk-G22U6UHQ.js +45 -0
  2. package/dist/chunk-G22U6UHQ.js.map +1 -0
  3. package/dist/chunk-HVKFEZBT.js +116 -0
  4. package/dist/chunk-HVKFEZBT.js.map +1 -0
  5. package/dist/chunk-RPM73PQZ.js +17 -0
  6. package/dist/chunk-RPM73PQZ.js.map +1 -0
  7. package/dist/lib/block-renderer.d.ts +32 -0
  8. package/dist/lib/block-renderer.js +7 -0
  9. package/dist/lib/block-renderer.js.map +1 -0
  10. package/dist/lib/cms-api.d.ts +24 -0
  11. package/dist/lib/cms-api.js +7 -0
  12. package/dist/lib/cms-api.js.map +1 -0
  13. package/dist/lib/data-utils.d.ts +218 -0
  14. package/dist/lib/data-utils.js +247 -0
  15. package/dist/lib/data-utils.js.map +1 -0
  16. package/dist/lib/image/lazy-load.d.ts +75 -0
  17. package/dist/lib/image/lazy-load.js +83 -0
  18. package/dist/lib/image/lazy-load.js.map +1 -0
  19. package/dist/lib/markdown-utils.d.ts +172 -0
  20. package/dist/lib/markdown-utils.js +137 -0
  21. package/dist/lib/markdown-utils.js.map +1 -0
  22. package/dist/lib/renderer.d.ts +36 -0
  23. package/dist/lib/renderer.js +343 -0
  24. package/dist/lib/renderer.js.map +1 -0
  25. package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
  26. package/dist/lib/result.js +37 -0
  27. package/dist/lib/result.js.map +1 -0
  28. package/dist/lib/schema.d.ts +15 -0
  29. package/dist/lib/schema.js +35 -0
  30. package/dist/lib/schema.js.map +1 -0
  31. package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
  32. package/dist/lib/trpc.js +7 -0
  33. package/dist/lib/trpc.js.map +1 -0
  34. package/dist/lib/types.d.ts +163 -0
  35. package/dist/lib/types.js +1 -0
  36. package/dist/lib/types.js.map +1 -0
  37. package/package.json +50 -11
  38. package/.turbo/turbo-check-types.log +0 -2
  39. package/lib/__tests__/enrich-block-images.test.ts +0 -394
  40. package/lib/block-renderer.tsx +0 -60
  41. package/lib/cms-api.ts +0 -86
  42. package/lib/data-utils.ts +0 -572
  43. package/lib/image/lazy-load.ts +0 -209
  44. package/lib/markdown-utils.ts +0 -368
  45. package/lib/renderer.tsx +0 -189
  46. package/lib/schema.ts +0 -74
  47. package/lib/types.ts +0 -201
  48. package/next.config.ts +0 -39
  49. package/postcss.config.mjs +0 -5
  50. package/tsconfig.json +0 -12
  51. package/tsconfig.tsbuildinfo +0 -1
package/lib/data-utils.ts DELETED
@@ -1,572 +0,0 @@
1
- /**
2
- * Data Fetching Utilities
3
- *
4
- * Three implementations showing the progression from naive to production-ready:
5
- * - v1 (Naive): Direct await, crashes on any error
6
- * - v2 (Defensive): Try-catch with null fallback
7
- * - v3 (Robust): Result type, retry logic, timeout support
8
- *
9
- * The robust version (v3) is exported as the default `fetchPage`.
10
- */
11
-
12
- import { err, ok, type Result } from './result';
13
- import type { BlockData } from './types';
14
-
15
- // -----------------------------------------------------------------------------
16
- // Types
17
- // -----------------------------------------------------------------------------
18
-
19
- /**
20
- * Page structure returned by the tRPC API.
21
- */
22
- export interface Page {
23
- slug: string;
24
- title: string;
25
- blocks: BlockData[];
26
- }
27
-
28
- /**
29
- * Error codes for data fetching failures.
30
- */
31
- export const FetchErrorCode = {
32
- INVALID_SLUG: 'INVALID_SLUG',
33
- NOT_FOUND: 'NOT_FOUND',
34
- NETWORK_ERROR: 'NETWORK_ERROR',
35
- TIMEOUT: 'TIMEOUT',
36
- PARSE_ERROR: 'PARSE_ERROR',
37
- SERVER_ERROR: 'SERVER_ERROR',
38
- RETRY_EXHAUSTED: 'RETRY_EXHAUSTED',
39
- } as const;
40
-
41
- export type FetchErrorCode = (typeof FetchErrorCode)[keyof typeof FetchErrorCode];
42
-
43
- /**
44
- * Structured error for data fetching failures.
45
- */
46
- export interface FetchError {
47
- code: FetchErrorCode;
48
- message: string;
49
- cause?: Error;
50
- retryCount?: number;
51
- }
52
-
53
- /**
54
- * Options for robust data fetching.
55
- */
56
- export interface FetchOptions {
57
- /**
58
- * Timeout in milliseconds.
59
- * @default 10000 (10 seconds)
60
- */
61
- timeout?: number;
62
-
63
- /**
64
- * Number of retry attempts for transient failures.
65
- * @default 3
66
- */
67
- retries?: number;
68
-
69
- /**
70
- * Base delay between retries in milliseconds.
71
- * Uses exponential backoff: delay * 2^attempt
72
- * @default 1000 (1 second)
73
- */
74
- retryDelay?: number;
75
-
76
- /**
77
- * AbortSignal for cancellation support.
78
- */
79
- signal?: AbortSignal;
80
- }
81
-
82
- // -----------------------------------------------------------------------------
83
- // Constants
84
- // -----------------------------------------------------------------------------
85
-
86
- const DEFAULT_TIMEOUT = 10_000; // 10 seconds
87
- const DEFAULT_RETRIES = 3;
88
- const DEFAULT_RETRY_DELAY = 1_000; // 1 second
89
-
90
- /**
91
- * HTTP status codes that indicate transient failures (should retry).
92
- */
93
- const TRANSIENT_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
94
-
95
- // -----------------------------------------------------------------------------
96
- // Mock Data Store (for demonstration)
97
- // -----------------------------------------------------------------------------
98
-
99
- /**
100
- * Simulated data store.
101
- * In production, this would be replaced with actual tRPC/API calls.
102
- */
103
- const mockPages: Record<string, Page> = {
104
- demo: {
105
- slug: 'demo',
106
- title: 'Demo Page',
107
- blocks: [
108
- {
109
- id: 'mock-header-1',
110
- type: 'header',
111
- content: {
112
- headline: 'Welcome',
113
- alignment: 'center',
114
- },
115
- },
116
- {
117
- id: 'mock-article-1',
118
- type: 'article',
119
- content: {
120
- headline: 'Getting Started',
121
- body: '## Introduction\n\nThis is a demo article.',
122
- },
123
- },
124
- ],
125
- },
126
- about: {
127
- slug: 'about',
128
- title: 'About Us',
129
- blocks: [
130
- {
131
- id: 'mock-header-2',
132
- type: 'header',
133
- content: {
134
- headline: 'About Our Company',
135
- alignment: 'left',
136
- },
137
- },
138
- ],
139
- },
140
- };
141
-
142
- // -----------------------------------------------------------------------------
143
- // Helper Functions
144
- // -----------------------------------------------------------------------------
145
-
146
- /**
147
- * Creates a FetchError with the given code and message.
148
- */
149
- function createFetchError(
150
- code: FetchErrorCode,
151
- message: string,
152
- options: { cause?: Error; retryCount?: number } = {}
153
- ): FetchError {
154
- return { code, message, ...options };
155
- }
156
-
157
- /**
158
- * Delays execution for the specified duration.
159
- */
160
- function delay(ms: number): Promise<void> {
161
- return new Promise((resolve) => setTimeout(resolve, ms));
162
- }
163
-
164
- /**
165
- * Calculates exponential backoff delay.
166
- */
167
- function exponentialBackoff(baseDelay: number, attempt: number): number {
168
- // Add jitter to prevent thundering herd
169
- const jitter = Math.random() * 100;
170
- return baseDelay * 2 ** attempt + jitter;
171
- }
172
-
173
- /**
174
- * Checks if an error represents a transient failure that should be retried.
175
- */
176
- function isTransientError(error: unknown): boolean {
177
- if (error instanceof Error) {
178
- // Network errors
179
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
180
- return true;
181
- }
182
- // Check for HTTP status codes in error message
183
- for (const code of TRANSIENT_STATUS_CODES) {
184
- if (error.message.includes(String(code))) {
185
- return true;
186
- }
187
- }
188
- }
189
- return false;
190
- }
191
-
192
- /**
193
- * Simulates a network request with configurable behavior.
194
- * In production, this would be an actual fetch/tRPC call.
195
- */
196
- async function simulateFetch(slug: string, signal?: AbortSignal): Promise<Page> {
197
- // Simulate network latency
198
- await delay(50 + Math.random() * 100);
199
-
200
- // Check for cancellation
201
- if (signal?.aborted) {
202
- throw new Error('Request aborted');
203
- }
204
-
205
- const page = mockPages[slug];
206
- if (!page) {
207
- const error = new Error(`Page not found: ${slug}`);
208
- error.name = 'NotFoundError';
209
- throw error;
210
- }
211
-
212
- return page;
213
- }
214
-
215
- // -----------------------------------------------------------------------------
216
- // V1: Naive Implementation
217
- // -----------------------------------------------------------------------------
218
-
219
- /**
220
- * Naive data fetcher - crashes on any error.
221
- *
222
- * DO NOT USE IN PRODUCTION. This is for demonstration only.
223
- *
224
- * Problems:
225
- * - No input validation
226
- * - No error handling
227
- * - Will crash the entire app on network failure
228
- * - No timeout protection
229
- * - No retry logic for transient failures
230
- *
231
- * @example
232
- * ```ts
233
- * // This throws if slug is invalid or network fails
234
- * const page = await fetchPageV1('demo');
235
- * ```
236
- */
237
- export async function fetchPageV1(slug: string): Promise<Page> {
238
- // Direct await - any error crashes the caller
239
- return simulateFetch(slug);
240
- }
241
-
242
- // -----------------------------------------------------------------------------
243
- // V2: Defensive Implementation
244
- // -----------------------------------------------------------------------------
245
-
246
- /**
247
- * Defensive data fetcher - catches errors but loses context.
248
- *
249
- * Better than v1 but still problematic:
250
- * - Returns null on any error (loses error details)
251
- * - Caller can't distinguish between 404 and network failure
252
- * - No retry logic for transient failures
253
- * - No timeout protection
254
- *
255
- * @example
256
- * ```ts
257
- * const page = await fetchPageV2(slug);
258
- * if (!page) {
259
- * // What went wrong? 404? Network? Timeout? We don't know.
260
- * return <NotFound />;
261
- * }
262
- * ```
263
- */
264
- export async function fetchPageV2(slug: string): Promise<Page | null> {
265
- try {
266
- // Basic validation
267
- if (!slug || typeof slug !== 'string') {
268
- return null;
269
- }
270
-
271
- return await simulateFetch(slug);
272
- } catch {
273
- // All errors become null - we lose valuable context
274
- return null;
275
- }
276
- }
277
-
278
- // -----------------------------------------------------------------------------
279
- // V3: Robust Implementation
280
- // -----------------------------------------------------------------------------
281
-
282
- /**
283
- * Robust data fetcher - production-ready with full error context.
284
- *
285
- * Features:
286
- * - Input validation with specific error codes
287
- * - Result type for explicit error handling
288
- * - Automatic retry with exponential backoff
289
- * - Timeout support via AbortController
290
- * - Distinguishes between error types (404 vs network vs timeout)
291
- * - Preserves error context for debugging
292
- *
293
- * @param slug - Page slug to fetch
294
- * @param options - Fetch options (timeout, retries, etc.)
295
- * @returns Result containing the page data or a structured error
296
- *
297
- * @example
298
- * ```ts
299
- * const result = await fetchPageV3('demo', {
300
- * timeout: 5000,
301
- * retries: 3,
302
- * });
303
- *
304
- * if (!result.ok) {
305
- * switch (result.error.code) {
306
- * case 'NOT_FOUND':
307
- * return <NotFoundPage slug={slug} />;
308
- * case 'TIMEOUT':
309
- * return <TimeoutError onRetry={handleRetry} />;
310
- * case 'NETWORK_ERROR':
311
- * return <NetworkError message={result.error.message} />;
312
- * case 'RETRY_EXHAUSTED':
313
- * return <RetryExhausted attempts={result.error.retryCount} />;
314
- * default:
315
- * return <GenericError />;
316
- * }
317
- * }
318
- *
319
- * return <PageRenderer page={result.value} />;
320
- * ```
321
- */
322
- export async function fetchPageV3(
323
- slug: string,
324
- options: FetchOptions = {}
325
- ): Promise<Result<Page, FetchError>> {
326
- const {
327
- timeout = DEFAULT_TIMEOUT,
328
- retries = DEFAULT_RETRIES,
329
- retryDelay = DEFAULT_RETRY_DELAY,
330
- signal: externalSignal,
331
- } = options;
332
-
333
- // 1. Validate input
334
- if (slug === null || slug === undefined) {
335
- return err(
336
- createFetchError(
337
- FetchErrorCode.INVALID_SLUG,
338
- `Slug must be a string, received ${slug === null ? 'null' : 'undefined'}`
339
- )
340
- );
341
- }
342
-
343
- if (typeof slug !== 'string') {
344
- return err(
345
- createFetchError(
346
- FetchErrorCode.INVALID_SLUG,
347
- `Slug must be a string, received ${typeof slug}`
348
- )
349
- );
350
- }
351
-
352
- const trimmedSlug = slug.trim();
353
- if (trimmedSlug.length === 0) {
354
- return err(createFetchError(FetchErrorCode.INVALID_SLUG, 'Slug cannot be empty'));
355
- }
356
-
357
- // 2. Create abort controller for timeout
358
- const controller = new AbortController();
359
- const { signal } = controller;
360
-
361
- // Link external signal if provided
362
- if (externalSignal) {
363
- externalSignal.addEventListener('abort', () => controller.abort());
364
- }
365
-
366
- // 3. Set up timeout
367
- const timeoutId = setTimeout(() => controller.abort(), timeout);
368
-
369
- // 4. Attempt fetch with retry logic
370
- let lastError: Error | undefined;
371
- let attempt = 0;
372
-
373
- try {
374
- while (attempt <= retries) {
375
- try {
376
- const page = await simulateFetch(trimmedSlug, signal);
377
- clearTimeout(timeoutId);
378
- return ok(page);
379
- } catch (error) {
380
- lastError = error instanceof Error ? error : new Error(String(error));
381
-
382
- // Check for abort/timeout
383
- if (signal.aborted) {
384
- clearTimeout(timeoutId);
385
- return err(
386
- createFetchError(FetchErrorCode.TIMEOUT, `Request timed out after ${timeout}ms`, {
387
- cause: lastError,
388
- })
389
- );
390
- }
391
-
392
- // Check for 404 (not retryable)
393
- if (lastError.name === 'NotFoundError') {
394
- clearTimeout(timeoutId);
395
- return err(
396
- createFetchError(FetchErrorCode.NOT_FOUND, `Page "${trimmedSlug}" not found`, {
397
- cause: lastError,
398
- })
399
- );
400
- }
401
-
402
- // Check if we should retry
403
- if (attempt < retries && isTransientError(lastError)) {
404
- attempt++;
405
- const backoff = exponentialBackoff(retryDelay, attempt);
406
- if (process.env.NODE_ENV === 'development') {
407
- console.warn(
408
- `[fetchPageV3] Retry ${attempt}/${retries} for "${trimmedSlug}" after ${Math.round(backoff)}ms`
409
- );
410
- }
411
- await delay(backoff);
412
- continue;
413
- }
414
-
415
- // No more retries or non-transient error
416
- break;
417
- }
418
- }
419
-
420
- // 5. All retries exhausted
421
- clearTimeout(timeoutId);
422
-
423
- if (attempt >= retries) {
424
- return err(
425
- createFetchError(FetchErrorCode.RETRY_EXHAUSTED, `Failed after ${retries} retry attempts`, {
426
- cause: lastError,
427
- retryCount: attempt,
428
- })
429
- );
430
- }
431
-
432
- // Non-retryable error
433
- return err(
434
- createFetchError(
435
- FetchErrorCode.NETWORK_ERROR,
436
- lastError?.message ?? 'Unknown network error',
437
- { cause: lastError }
438
- )
439
- );
440
- } catch (error) {
441
- clearTimeout(timeoutId);
442
- const cause = error instanceof Error ? error : new Error(String(error));
443
- return err(
444
- createFetchError(FetchErrorCode.SERVER_ERROR, `Unexpected error: ${cause.message}`, { cause })
445
- );
446
- }
447
- }
448
-
449
- // -----------------------------------------------------------------------------
450
- // Default Export
451
- // -----------------------------------------------------------------------------
452
-
453
- /**
454
- * Production-ready page fetcher.
455
- *
456
- * This is an alias for `fetchPageV3` - the robust implementation
457
- * with validation, retry logic, and Result-based error handling.
458
- *
459
- * @example
460
- * ```ts
461
- * import { fetchPage } from '@/lib/data-utils';
462
- *
463
- * const result = await fetchPage('demo');
464
- * if (!result.ok) {
465
- * // Handle error with full context
466
- * console.error(`[${result.error.code}] ${result.error.message}`);
467
- * return null;
468
- * }
469
- * return result.value;
470
- * ```
471
- */
472
- export const fetchPage = fetchPageV3;
473
-
474
- // -----------------------------------------------------------------------------
475
- // Utility Functions
476
- // -----------------------------------------------------------------------------
477
-
478
- /**
479
- * Validates a page slug format.
480
- *
481
- * @param slug - Slug to validate
482
- * @returns true if slug is valid format
483
- */
484
- export function isValidSlug(slug: string): boolean {
485
- // Slugs should be lowercase, alphanumeric with hyphens
486
- return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
487
- }
488
-
489
- /**
490
- * Normalizes a slug (lowercase, trim, replace spaces with hyphens).
491
- *
492
- * @param input - Raw input to normalize
493
- * @returns Normalized slug
494
- */
495
- export function normalizeSlug(input: string): string {
496
- return input
497
- .toLowerCase()
498
- .trim()
499
- .replace(/\s+/g, '-')
500
- .replace(/[^a-z0-9-]/g, '')
501
- .replace(/-+/g, '-')
502
- .replace(/^-|-$/g, '');
503
- }
504
-
505
- /**
506
- * Prefetches multiple pages in parallel with Result types.
507
- *
508
- * @param slugs - Array of page slugs to fetch
509
- * @param options - Fetch options applied to all requests
510
- * @returns Array of Results for each page
511
- *
512
- * @example
513
- * ```ts
514
- * const results = await prefetchPages(['home', 'about', 'blog']);
515
- * const errors = results.filter(r => !r.ok);
516
- * if (errors.length > 0) {
517
- * console.warn(`${errors.length} pages failed to load`);
518
- * }
519
- * ```
520
- */
521
- export async function prefetchPages(
522
- slugs: string[],
523
- options?: FetchOptions
524
- ): Promise<Result<Page, FetchError>[]> {
525
- return Promise.all(slugs.map((slug) => fetchPageV3(slug, options)));
526
- }
527
-
528
- /**
529
- * Fetches a page with stale-while-revalidate semantics.
530
- * Returns cached data immediately if available, then updates in background.
531
- *
532
- * @param slug - Page slug to fetch
533
- * @param cache - Cache storage (e.g., Map, localStorage wrapper)
534
- * @param options - Fetch options
535
- * @returns Cached data or fresh fetch result
536
- */
537
- export async function fetchPageWithSWR(
538
- slug: string,
539
- cache: Map<string, { data: Page; timestamp: number }>,
540
- options: FetchOptions & { maxAge?: number } = {}
541
- ): Promise<Result<Page, FetchError>> {
542
- const { maxAge = 60_000 } = options; // 1 minute default
543
-
544
- const cached = cache.get(slug);
545
- const now = Date.now();
546
-
547
- // Return fresh cache immediately
548
- if (cached && now - cached.timestamp < maxAge) {
549
- return ok(cached.data);
550
- }
551
-
552
- // Fetch fresh data
553
- const result = await fetchPageV3(slug, options);
554
-
555
- // Update cache on success
556
- if (result.ok) {
557
- cache.set(slug, { data: result.value, timestamp: now });
558
- }
559
-
560
- // If fetch failed but we have stale cache, return it with warning
561
- if (!result.ok && cached) {
562
- if (process.env.NODE_ENV === 'development') {
563
- console.warn(
564
- `[fetchPageWithSWR] Returning stale cache for "${slug}" after fetch failure:`,
565
- result.error.message
566
- );
567
- }
568
- return ok(cached.data);
569
- }
570
-
571
- return result;
572
- }