polen 0.10.0-next.13 → 0.10.0-next.14

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 (104) hide show
  1. package/build/api/vite/plugins/build.d.ts.map +1 -1
  2. package/build/api/vite/plugins/build.js +11 -3
  3. package/build/api/vite/plugins/build.js.map +1 -1
  4. package/build/api/vite/plugins/core.d.ts.map +1 -1
  5. package/build/api/vite/plugins/core.js +12 -10
  6. package/build/api/vite/plugins/core.js.map +1 -1
  7. package/build/api/vite/plugins/pages.d.ts.map +1 -1
  8. package/build/api/vite/plugins/pages.js +6 -7
  9. package/build/api/vite/plugins/pages.js.map +1 -1
  10. package/build/api/vite/plugins/serve.d.ts.map +1 -1
  11. package/build/api/vite/plugins/serve.js +47 -7
  12. package/build/api/vite/plugins/serve.js.map +1 -1
  13. package/build/lib/file-router/diagnostic-reporter.js +2 -2
  14. package/build/lib/file-router/diagnostic-reporter.js.map +1 -1
  15. package/build/lib/graphql-document/components/GraphQLDocument.d.ts.map +1 -1
  16. package/build/lib/graphql-document/components/GraphQLDocument.js +23 -11
  17. package/build/lib/graphql-document/components/GraphQLDocument.js.map +1 -1
  18. package/build/lib/graphql-document/positioning-simple.d.ts +0 -5
  19. package/build/lib/graphql-document/positioning-simple.d.ts.map +1 -1
  20. package/build/lib/graphql-document/positioning-simple.js +78 -90
  21. package/build/lib/graphql-document/positioning-simple.js.map +1 -1
  22. package/build/lib/kit-temp.d.ts +103 -0
  23. package/build/lib/kit-temp.d.ts.map +1 -1
  24. package/build/lib/kit-temp.js +236 -2
  25. package/build/lib/kit-temp.js.map +1 -1
  26. package/build/lib/vite-plugin-reactive-data/vite-plugin-reactive-data.d.ts +1 -8
  27. package/build/lib/vite-plugin-reactive-data/vite-plugin-reactive-data.d.ts.map +1 -1
  28. package/build/lib/vite-plugin-reactive-data/vite-plugin-reactive-data.js +48 -53
  29. package/build/lib/vite-plugin-reactive-data/vite-plugin-reactive-data.js.map +1 -1
  30. package/build/package-paths.js +3 -3
  31. package/build/package-paths.js.map +1 -1
  32. package/build/template/components/Link.d.ts +1 -1
  33. package/build/template/components/Link.d.ts.map +1 -1
  34. package/build/template/components/Link.js +14 -5
  35. package/build/template/components/Link.js.map +1 -1
  36. package/build/template/components/content/GraphQLDocumentWithSchema.d.ts.map +1 -1
  37. package/build/template/components/content/GraphQLDocumentWithSchema.js +0 -3
  38. package/build/template/components/content/GraphQLDocumentWithSchema.js.map +1 -1
  39. package/build/template/components/content/GraphQLDocumentWrapper.d.ts.map +1 -1
  40. package/build/template/components/content/GraphQLDocumentWrapper.js +8 -7
  41. package/build/template/components/content/GraphQLDocumentWrapper.js.map +1 -1
  42. package/build/template/components/sidebar/SidebarItem.js +2 -2
  43. package/build/template/entry.client.d.ts.map +1 -1
  44. package/build/template/entry.client.js +0 -3
  45. package/build/template/entry.client.js.map +1 -1
  46. package/build/template/hooks/useClientOnly.d.ts +9 -0
  47. package/build/template/hooks/useClientOnly.d.ts.map +1 -0
  48. package/build/template/hooks/useClientOnly.js +16 -0
  49. package/build/template/hooks/useClientOnly.js.map +1 -0
  50. package/build/template/routes/root.d.ts.map +1 -1
  51. package/build/template/routes/root.js +2 -150
  52. package/build/template/routes/root.js.map +1 -1
  53. package/build/template/server/app.d.ts +8 -1
  54. package/build/template/server/app.d.ts.map +1 -1
  55. package/build/template/server/app.js +21 -21
  56. package/build/template/server/app.js.map +1 -1
  57. package/build/template/server/create-page-html-response.d.ts +7 -0
  58. package/build/template/server/create-page-html-response.d.ts.map +1 -0
  59. package/build/template/server/{render-page.js → create-page-html-response.js} +11 -16
  60. package/build/template/server/create-page-html-response.js.map +1 -0
  61. package/build/template/server/main.js +2 -1
  62. package/build/template/server/main.js.map +1 -1
  63. package/build/template/server/middleware/page.d.ts +4 -0
  64. package/build/template/server/middleware/page.d.ts.map +1 -0
  65. package/build/template/server/middleware/page.js +15 -0
  66. package/build/template/server/middleware/page.js.map +1 -0
  67. package/build/template/server/middleware/unsupported-assets.d.ts +10 -0
  68. package/build/template/server/middleware/unsupported-assets.d.ts.map +1 -0
  69. package/build/template/server/middleware/unsupported-assets.js +21 -0
  70. package/build/template/server/middleware/unsupported-assets.js.map +1 -0
  71. package/build/template/server/ssg/generate.d.ts.map +1 -1
  72. package/build/template/server/ssg/generate.js +33 -34
  73. package/build/template/server/ssg/generate.js.map +1 -1
  74. package/build/template/styles/code-block.css +218 -0
  75. package/package.json +3 -2
  76. package/src/api/singletons/markdown/markdown.test.ts +1 -1
  77. package/src/api/vite/plugins/build.ts +97 -89
  78. package/src/api/vite/plugins/core.ts +15 -10
  79. package/src/api/vite/plugins/pages.ts +9 -7
  80. package/src/api/vite/plugins/serve.ts +62 -9
  81. package/src/lib/file-router/diagnostic-reporter.ts +2 -2
  82. package/src/lib/graphql-document/components/GraphQLDocument.tsx +23 -11
  83. package/src/lib/graphql-document/positioning-simple.test.ts +18 -22
  84. package/src/lib/graphql-document/positioning-simple.ts +97 -108
  85. package/src/lib/kit-temp.test.ts +15 -3
  86. package/src/lib/kit-temp.ts +304 -4
  87. package/src/lib/vite-plugin-reactive-data/vite-plugin-reactive-data.ts +52 -58
  88. package/src/package-paths.ts +3 -3
  89. package/src/template/components/Link.tsx +20 -12
  90. package/src/template/components/content/GraphQLDocumentWithSchema.tsx +0 -5
  91. package/src/template/components/content/GraphQLDocumentWrapper.tsx +14 -7
  92. package/src/template/components/sidebar/SidebarItem.tsx +2 -2
  93. package/src/template/entry.client.tsx +0 -3
  94. package/src/template/hooks/useClientOnly.ts +21 -0
  95. package/src/template/routes/root.tsx +0 -159
  96. package/src/template/server/app.ts +33 -23
  97. package/src/template/server/{render-page.tsx → create-page-html-response.ts} +19 -16
  98. package/src/template/server/main.ts +2 -1
  99. package/src/template/server/middleware/page.ts +19 -0
  100. package/src/template/server/middleware/unsupported-assets.ts +25 -0
  101. package/src/template/server/ssg/generate.ts +68 -72
  102. package/build/template/server/render-page.d.ts +0 -3
  103. package/build/template/server/render-page.d.ts.map +0 -1
  104. package/build/template/server/render-page.js.map +0 -1
@@ -12,10 +12,8 @@
12
12
  //
13
13
  //
14
14
 
15
- import { Arr, Err, Fs, Http, Path, type Ts, Undefined } from '@wollybeard/kit'
16
- import { never } from '@wollybeard/kit/language'
15
+ import { Arr, Err, Fs, Http, Path, Undefined } from '@wollybeard/kit'
17
16
  import type { ResolveHookContext } from 'node:module'
18
- import type { IsNever } from 'type-fest'
19
17
 
20
18
  export const arrayEquals = (a: any[], b: any[]) => {
21
19
  if (a.length !== b.length) return false
@@ -109,7 +107,7 @@ export const objPolicyFilter = <
109
107
  if (mode === 'allow') {
110
108
  // For allow mode, only add specified keys
111
109
  for (const key of keys) {
112
- if (key in obj) {
110
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
113
111
  // @ts-expect-error
114
112
  result[key] = obj[key]
115
113
  }
@@ -220,3 +218,305 @@ export type ExtendsExact<$Input, $Constraint> =
220
218
  ? $Input
221
219
  : never
222
220
  : never
221
+
222
+ /**
223
+ * Split an array into chunks of specified size
224
+ *
225
+ * @param array - The array to chunk
226
+ * @param size - The size of each chunk
227
+ * @returns Array of chunks
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
232
+ * chunk(['a', 'b', 'c'], 3) // [['a', 'b', 'c']]
233
+ * ```
234
+ */
235
+ export const chunk = <T>(array: readonly T[], size: number): T[][] => {
236
+ if (size <= 0) throw new Error('Chunk size must be greater than 0')
237
+ if (array.length === 0) return []
238
+
239
+ const chunks: T[][] = []
240
+ for (let i = 0; i < array.length; i += size) {
241
+ chunks.push(array.slice(i, i + size))
242
+ }
243
+ return chunks
244
+ }
245
+
246
+ export interface AsyncParallelOptions {
247
+ /**
248
+ * Maximum number of items to process concurrently
249
+ * @default 10
250
+ */
251
+ concurrency?: number
252
+
253
+ /**
254
+ * If true, stops processing on first error
255
+ * If false, continues processing all items even if some fail
256
+ * @default false
257
+ */
258
+ failFast?: boolean
259
+
260
+ /**
261
+ * Size of batches to process items in
262
+ * If not specified, all items are processed with the specified concurrency
263
+ */
264
+ batchSize?: number
265
+ }
266
+
267
+ export interface AsyncParallelResult<T, R> {
268
+ /** Successfully processed results */
269
+ results: R[]
270
+ /** Errors that occurred during processing */
271
+ errors: (Error & { item: T })[]
272
+ /** Whether all items were processed successfully */
273
+ success: boolean
274
+ }
275
+
276
+ /**
277
+ * Process items in parallel with configurable options
278
+ *
279
+ * @param items - Items to process
280
+ * @param operation - Async function to apply to each item (with optional index)
281
+ * @param options - Configuration options
282
+ * @returns Results and errors from processing
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * const items = [1, 2, 3, 4, 5]
287
+ * const result = await asyncParallel(items, async (n, index) => n * 2, {
288
+ * concurrency: 2,
289
+ * batchSize: 3,
290
+ * failFast: false
291
+ * })
292
+ * // result.results: [2, 4, 6, 8, 10]
293
+ * // result.errors: []
294
+ * // result.success: true
295
+ * ```
296
+ */
297
+ export const asyncParallel = async <T, R>(
298
+ items: readonly T[],
299
+ operation: (item: T, index: number) => Promise<R>,
300
+ options: AsyncParallelOptions = {},
301
+ ): Promise<AsyncParallelResult<T, R>> => {
302
+ const { concurrency = 10, failFast = false, batchSize } = options
303
+
304
+ if (items.length === 0) {
305
+ return { results: [], errors: [], success: true }
306
+ }
307
+
308
+ const allResults: R[] = []
309
+ const allErrors: (Error & { item: T })[] = []
310
+
311
+ // If batchSize is specified, process in batches
312
+ if (batchSize !== undefined) {
313
+ const batches = chunk(items, batchSize)
314
+ let globalIndex = 0
315
+
316
+ for (const batch of batches) {
317
+ const batchResult = await processBatch(batch, operation, concurrency, failFast, globalIndex)
318
+ allResults.push(...batchResult.results)
319
+ allErrors.push(...batchResult.errors)
320
+ globalIndex += batch.length
321
+
322
+ if (failFast && batchResult.errors.length > 0) {
323
+ break
324
+ }
325
+ }
326
+ } else {
327
+ // Process all items with specified concurrency
328
+ const result = await processBatch(items, operation, concurrency, failFast, 0)
329
+ allResults.push(...result.results)
330
+ allErrors.push(...result.errors)
331
+ }
332
+
333
+ return {
334
+ results: allResults,
335
+ errors: allErrors,
336
+ success: allErrors.length === 0,
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Process a batch of items with limited concurrency
342
+ */
343
+ const processBatch = async <T, R>(
344
+ items: readonly T[],
345
+ operation: (item: T, index: number) => Promise<R>,
346
+ concurrency: number,
347
+ failFast: boolean,
348
+ startIndex: number = 0,
349
+ ): Promise<AsyncParallelResult<T, R>> => {
350
+ const results: R[] = []
351
+ const errors: (Error & { item: T })[] = []
352
+
353
+ // Process items in chunks based on concurrency limit
354
+ const chunks = chunk(items, concurrency)
355
+ let currentIndex = startIndex
356
+
357
+ for (const chunkItems of chunks) {
358
+ const promises = chunkItems.map(async (item, chunkIndex) => {
359
+ const globalIndex = currentIndex + chunkIndex
360
+ try {
361
+ const result = await operation(item, globalIndex)
362
+ return { success: true, result, item }
363
+ } catch (error) {
364
+ const enhancedError = error instanceof Error ? error : new Error(String(error))
365
+ Object.assign(enhancedError, { item })
366
+ return { success: false, error: enhancedError as Error & { item: T }, item }
367
+ }
368
+ })
369
+
370
+ currentIndex += chunkItems.length
371
+
372
+ const chunkResults = await Promise.allSettled(promises)
373
+
374
+ for (const promiseResult of chunkResults) {
375
+ if (promiseResult.status === 'fulfilled') {
376
+ const { success, result, error, item } = promiseResult.value
377
+ if (success) {
378
+ results.push(result!)
379
+ } else {
380
+ errors.push(error!)
381
+ if (failFast) {
382
+ return { results, errors, success: false }
383
+ }
384
+ }
385
+ } else {
386
+ // This shouldn't happen since we're catching errors above
387
+ // But handle it just in case
388
+ const error = new Error('Unexpected promise rejection') as Error & { item: any }
389
+ errors.push(error)
390
+ if (failFast) {
391
+ return { results, errors, success: false }
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ return { results, errors, success: errors.length === 0 }
398
+ }
399
+
400
+ // /**
401
+ // * Reduce an array asynchronously, processing each item in sequence
402
+ // *
403
+ // * @param items - Array of items to process
404
+ // * @param reducer - Async function that takes accumulator and current item
405
+ // * @param initial - Initial value for the accumulator
406
+ // * @returns Final accumulated value
407
+ // *
408
+ // * @example
409
+ // * ```ts
410
+ // * const numbers = [1, 2, 3, 4]
411
+ // * const sum = await asyncReduce(numbers, async (acc, n) => acc + n, 0)
412
+ // * // sum: 10
413
+ // *
414
+ // * const transforms = [addHeader, addFooter, minify]
415
+ // * const html = await asyncReduce(transforms, async (html, transform) => transform(html), initialHtml)
416
+ // * ```
417
+ // */
418
+ // export const asyncReduce = async <T, R>(
419
+ // items: readonly T[],
420
+ // reducer: (accumulator: R, current: T, index: number) => Promise<R> | R,
421
+ // initial: R,
422
+ // ): Promise<R> => {
423
+ // let result = initial
424
+ // for (let i = 0; i < items.length; i++) {
425
+ // const item = items[i]!
426
+ // result = await reducer(result, item, i)
427
+ // }
428
+ // return result
429
+ // }
430
+
431
+ // /**
432
+ // * Curried version of asyncReduce for functions that transform a value
433
+ // *
434
+ // * @param transformers - Array of transformer functions
435
+ // * @returns A function that takes an initial value and applies all transformers
436
+ // *
437
+ // * @example
438
+ // * ```ts
439
+ // * const transformers = [addHeader, addFooter, minify]
440
+ // * const applyTransforms = asyncReduceWith(transformers)
441
+ // * const finalHtml = await applyTransforms(initialHtml)
442
+ // *
443
+ // * // For simple pipelines where each function transforms the same type
444
+ // * const htmlPipeline = asyncReduceWith([
445
+ // * (html) => html.replace('foo', 'bar'),
446
+ // * async (html) => await prettify(html),
447
+ // * (html) => html.trim()
448
+ // * ])
449
+ // * ```
450
+ // */
451
+ // export const asyncReduceWith = <T>(
452
+ // transformers: readonly ((value: T) => Promise<T> | T)[],
453
+ // ) => {
454
+ // return async (initial: T): Promise<T> => {
455
+ // return asyncReduce(transformers, (value, transform) => transform(value), initial)
456
+ // }
457
+ // }
458
+
459
+ /**
460
+ * Reduce an array asynchronously with context, processing each item in sequence
461
+ *
462
+ * @param items - Array of items to process
463
+ * @param reducer - Async function that takes accumulator, current item, and context
464
+ * @param initial - Initial value for the accumulator
465
+ * @param context - Context object passed to each reducer call
466
+ * @returns Final accumulated value
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * const transformers = [transformer1, transformer2]
471
+ * const ctx = { request: req, response: res }
472
+ * const result = await asyncReduceWithContext(
473
+ * transformers,
474
+ * async (html, transformer) => transformer(html, ctx),
475
+ * initialHtml,
476
+ * ctx
477
+ * )
478
+ * ```
479
+ */
480
+ export const asyncReduce = async <T, R, C>(
481
+ items: readonly T[],
482
+ reducer: (accumulator: R, current: T, context: C, index: number) => Promise<R> | R,
483
+ initial: R,
484
+ context: C,
485
+ ): Promise<R> => {
486
+ let result = initial
487
+ for (let i = 0; i < items.length; i++) {
488
+ const item = items[i]!
489
+ result = await reducer(result, item, context, i)
490
+ }
491
+ return result
492
+ }
493
+
494
+ /**
495
+ * Curried version of asyncReduceWithContext for functions that transform a value with context
496
+ *
497
+ * @param transformers - Array of transformer functions that take value and context
498
+ * @returns A function that takes an initial value and context, and applies all transformers
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * const transformers = [
503
+ * (html, ctx) => html.replace('{{url}}', ctx.req.url),
504
+ * async (html, ctx) => await ctx.minify(html),
505
+ * ]
506
+ * const applyTransforms = asyncReduceWithContextWith(transformers)
507
+ * const finalHtml = await applyTransforms(initialHtml, ctx)
508
+ * ```
509
+ */
510
+ export const asyncReduceWith = <T, C>(
511
+ transformers: readonly ((value: T, context: C) => Promise<T> | T)[],
512
+ context: C,
513
+ ) => {
514
+ return async (initial: T): Promise<T> => {
515
+ return asyncReduce(
516
+ transformers,
517
+ (value, transform, ctx) => transform(value, ctx),
518
+ initial,
519
+ context,
520
+ )
521
+ }
522
+ }
@@ -1,8 +1,6 @@
1
- import { ensureEnd } from '#lib/kit-temp'
2
1
  import { VitePluginJson } from '#lib/vite-plugin-json/index'
3
- import { superjson } from '#singletons/superjson'
2
+ import { debugPolen } from '#singletons/debug'
4
3
  import { type ComputedRef, effect, isRef } from '@vue/reactivity'
5
- import { Debug } from '@wollybeard/kit'
6
4
  import type { Plugin, ViteDevServer } from 'vite'
7
5
 
8
6
  interface ReactiveDataOptions {
@@ -19,11 +17,9 @@ interface ReactiveDataOptions {
19
17
  * - A reactive value directly
20
18
  */
21
19
  data: ComputedRef<object | unknown[]> | (() => object | unknown[]) | object | unknown[]
22
- /** Debounce updates (ms). If not set, uses process.nextTick for batching */
23
- debounce?: number
24
20
  /**
25
21
  * JSON codec to use (e.g., superjson)
26
- * Default: superjson
22
+ * Default: JSON
27
23
  * Only used when includeJsonPlugin is true
28
24
  */
29
25
  codec?: VitePluginJson.Codec
@@ -32,47 +28,46 @@ interface ReactiveDataOptions {
32
28
  @default 'reactive-data'
33
29
  */
34
30
  name?: string
35
- /**
36
- * Module type to return. Default: 'json'
37
- * Use 'superjson' to avoid conflicts with built-in JSON plugin
38
- */
39
- moduleType?: string
40
31
  }
41
32
 
42
- const debug = Debug.create('vite-plugin-reactive-data')
33
+ const pluginDebug = debugPolen.sub('vite-reactive-data')
43
34
 
44
35
  export const create = (options: ReactiveDataOptions): Plugin => {
45
- const codec = options.codec ?? superjson
46
- const moduleType = options.moduleType ?? 'json'
47
- const moduleId = ensureEnd(options.moduleId, `.${moduleType}`)
36
+ const codec = options.codec ?? JSON
37
+ const moduleId = options.moduleId
48
38
  const name = options.name ?? `reactive-data`
49
39
 
50
- let server: ViteDevServer
51
- let updateTimer: NodeJS.Timeout | undefined
52
- let updateScheduled = false
40
+ const debug = pluginDebug.sub(name)
41
+ debug('constructor', { moduleId })
53
42
 
54
- const doUpdate = () => {
55
- debug('update')
56
- updateTimer = undefined
57
- updateScheduled = false
58
- if (!server) return
59
- const moduleNode = server.moduleGraph.getModuleById(moduleId)
43
+ let $server: ViteDevServer
44
+ let $invalidationScheduled = false
45
+
46
+ const tryInvalidate = () => {
47
+ $invalidationScheduled = false
48
+ // updateTimer = undefined
49
+ if (!$server) throw new Error('Server not available yet - this should be impossible')
50
+ const moduleNode = $server.moduleGraph.getModuleById(moduleId)
60
51
  if (moduleNode) {
61
- server.moduleGraph.invalidateModule(moduleNode)
52
+ debug('invalidate', { id: moduleNode.id })
53
+ $server.moduleGraph.invalidateModule(moduleNode)
54
+ } else {
55
+ debug('cannot invalidate', {
56
+ reason: 'notInModuleGraph',
57
+ moduleId,
58
+ hint: 'maybe it was not loaded yet',
59
+ })
62
60
  }
63
61
  }
64
62
 
65
- const scheduleUpdate = () => {
66
- if (options.debounce) {
67
- // User wants actual debouncing for rapid updates
68
- if (updateTimer) clearTimeout(updateTimer)
69
- updateTimer = setTimeout(doUpdate, options.debounce)
70
- } else {
71
- // Just batch synchronous updates using nextTick
72
- if (updateScheduled) return
73
- updateScheduled = true
74
- process.nextTick(doUpdate)
75
- }
63
+ const scheduleInvalidate = () => {
64
+ if ($invalidationScheduled) return // already scheduled
65
+
66
+ $invalidationScheduled = true
67
+
68
+ if (!$server) return // server will flush when ready
69
+
70
+ tryInvalidate()
76
71
  }
77
72
 
78
73
  // Helper to get the current data value
@@ -90,20 +85,21 @@ export const create = (options: ReactiveDataOptions): Plugin => {
90
85
  effect(() => {
91
86
  // Access data to track dependencies
92
87
  const data = getData()
93
- debug('data changed:', data)
94
- // Trigger update only if server is available
95
- if (server) {
96
- scheduleUpdate()
97
- }
88
+ debug('effect triggered', { data })
89
+
90
+ scheduleInvalidate()
98
91
  })
99
92
 
100
93
  return {
101
94
  name,
102
95
 
103
96
  configureServer(_server) {
104
- server = _server
105
- // Trigger initial update since server is now available
106
- scheduleUpdate()
97
+ debug('hook configureServer')
98
+ $server = _server
99
+ if ($invalidationScheduled) {
100
+ debug('try invalidate scheduled before server was ready')
101
+ tryInvalidate()
102
+ }
107
103
  },
108
104
 
109
105
  resolveId(id) {
@@ -112,20 +108,18 @@ export const create = (options: ReactiveDataOptions): Plugin => {
112
108
  }
113
109
  },
114
110
 
115
- load: {
116
- // todo: doesn't work for some reason, prefer over handler
117
- // filter: {
118
- // id: {
119
- // include: moduleId,
120
- // },
121
- // },
122
- handler: (id) => {
123
- if (id !== moduleId) return
124
-
125
- const data = getData()
126
- // Return just the raw JSON string - let the JSON plugin handle the transformation
127
- return codec.stringify(data)
128
- },
111
+ // todo make use of Vite's builtin json plugin
112
+ // for example, call it here somehow
113
+ load(id) {
114
+ if (id !== moduleId) return
115
+
116
+ const data = getData()
117
+ debug('hook load', { data })
118
+
119
+ return {
120
+ code: codec.stringify(data),
121
+ map: null,
122
+ }
129
123
  },
130
124
  }
131
125
  }
@@ -49,11 +49,11 @@ export const packagePaths: PackagePaths = {
49
49
  template: {
50
50
  rootDir: templateDir,
51
51
  server: {
52
- app: Path.join(templateDir, `server/app.js`),
53
- entrypoint: Path.join(templateDir, `server/main.js`),
52
+ app: Path.join(templateDir, `server/app${sourceKind}`),
53
+ entrypoint: Path.join(templateDir, `server/main${sourceKind}`),
54
54
  },
55
55
  client: {
56
- entrypoint: Path.join(templateDir, `entry.client.jsx`),
56
+ entrypoint: Path.join(templateDir, `entry.client${isRunningFromSource ? `.tsx` : `.js`}`),
57
57
  },
58
58
  },
59
59
  }
@@ -3,6 +3,7 @@ import type { LinkProps as LinkPropsReactRouter } from 'react-router'
3
3
  import { Link as LinkReactRouter, useLocation } from 'react-router'
4
4
  // todo: #lib/kit-temp does not work as import
5
5
  import { ObjPartition } from '../../lib/kit-temp.ts'
6
+ import { useClientOnly } from '../hooks/useClientOnly.ts'
6
7
  import type { LinkPropsRadix } from './RadixLink.tsx'
7
8
  import { LinkRadix } from './RadixLink.tsx'
8
9
 
@@ -22,18 +23,25 @@ const reactRouterPropKeys = [
22
23
  export const Link: FC<LinkPropsReactRouter & Omit<LinkPropsRadix, 'asChild'>> = props => {
23
24
  const location = useLocation()
24
25
  const toPathExp = typeof props.to === 'string' ? props.to : props.to.pathname || ''
25
- const active = getPathActiveReport(toPathExp, location.pathname)
26
+
27
+ const active = useClientOnly(
28
+ () => getPathActiveReport(toPathExp, location.pathname),
29
+ { is: false, isDirect: false, isDescendant: false },
30
+ )
26
31
 
27
32
  const { picked: reactRouterProps, omitted: radixProps } = ObjPartition(props, reactRouterPropKeys)
28
33
 
34
+ // Only add data attributes if they're true
35
+ const linkRadixProps = {
36
+ ...radixProps,
37
+ asChild: true,
38
+ ...(active.is && { 'data-active': true }),
39
+ ...(active.isDirect && { 'data-active-direct': true }),
40
+ ...(active.isDescendant && { 'data-active-descendant': true }),
41
+ }
42
+
29
43
  return (
30
- <LinkRadix
31
- asChild
32
- {...radixProps}
33
- data-active={active.is || undefined}
34
- data-active-direct={active.isDirect || undefined}
35
- data-active-descendant={active.isdescendant || undefined}
36
- >
44
+ <LinkRadix {...linkRadixProps}>
37
45
  <LinkReactRouter {...reactRouterProps} />
38
46
  </LinkRadix>
39
47
  )
@@ -42,7 +50,7 @@ export const Link: FC<LinkPropsReactRouter & Omit<LinkPropsRadix, 'asChild'>> =
42
50
  export interface PathActiveReport {
43
51
  is: boolean
44
52
  isDirect: boolean
45
- isdescendant: boolean
53
+ isDescendant: boolean
46
54
  }
47
55
 
48
56
  export const getPathActiveReport = (
@@ -54,13 +62,13 @@ export const getPathActiveReport = (
54
62
  const normalizedCurrentPath = currentPathExp.startsWith('/') ? currentPathExp.slice(1) : currentPathExp
55
63
 
56
64
  const isDirect = normalizedCurrentPath === normalizedPath
57
- const isdescendant = normalizedCurrentPath.startsWith(normalizedPath + '/')
65
+ const isDescendant = normalizedCurrentPath.startsWith(normalizedPath + '/')
58
66
  && normalizedCurrentPath !== normalizedPath
59
- const is = isDirect || isdescendant
67
+ const is = isDirect || isDescendant
60
68
 
61
69
  return {
62
70
  is,
63
71
  isDirect,
64
- isdescendant,
72
+ isDescendant,
65
73
  }
66
74
  }
@@ -1,4 +1,3 @@
1
- import type { GraphQLSchema } from 'graphql'
2
1
  import React from 'react'
3
2
  import PROJECT_DATA from 'virtual:polen/project/data.jsonsuper'
4
3
  import { GraphQLDocument } from '../../../lib/graphql-document/components/GraphQLDocument.tsx'
@@ -10,9 +9,5 @@ import type { GraphQLDocumentProps } from '../../../lib/graphql-document/compone
10
9
  */
11
10
  export const GraphQLDocumentWithSchema: React.FC<Omit<GraphQLDocumentProps, 'schema'>> = (props) => {
12
11
  const schema = PROJECT_DATA.schema?.versions[0]?.after
13
- console.log('Template wrapper - schema:', schema ? 'EXISTS' : 'UNDEFINED')
14
- console.log('Template wrapper - props:', props)
15
- console.log('Template wrapper - PROJECT_DATA.schema:', PROJECT_DATA.schema)
16
-
17
12
  return <GraphQLDocument {...props} schema={schema} />
18
13
  }
@@ -24,8 +24,8 @@ export const GraphQLDocumentWithSchema: React.FC<Omit<GraphQLDocumentProps, 'sch
24
24
  theme: 'light', // You can make this dynamic based on theme
25
25
  }).then(html => {
26
26
  setHighlightedHtml(html)
27
- }).catch(err => {
28
- console.error('Failed to highlight code:', err)
27
+ }).catch(() => {
28
+ // Silently fall back to unhighlighted code
29
29
  })
30
30
  }
31
31
 
@@ -33,11 +33,12 @@ export const GraphQLDocumentWithSchema: React.FC<Omit<GraphQLDocumentProps, 'sch
33
33
  if (typeof window !== 'undefined') {
34
34
  import('virtual:polen/project/data.jsonsuper').then(PROJECT_DATA => {
35
35
  const s = PROJECT_DATA.default?.schema?.versions?.[0]?.after
36
+ // Schema loaded successfully
36
37
  if (s) {
37
38
  setSchema(s)
38
39
  }
39
- }).catch(err => {
40
- console.error('Failed to load schema:', err)
40
+ }).catch(() => {
41
+ // Schema loading is optional - continue without it
41
42
  })
42
43
  }
43
44
  }, [props.children])
@@ -68,15 +69,21 @@ export const GraphQLDocumentWithSchema: React.FC<Omit<GraphQLDocumentProps, 'sch
68
69
  schema={schema}
69
70
  highlightedHtml={highlightedHtml || undefined}
70
71
  options={{
72
+ debug: false, // Default to false for shipping
71
73
  ...props.options,
72
74
  onNavigate: handleNavigate,
73
- debug: false, // Disable debug mode for shipping
74
75
  }}
75
76
  />
76
77
  </div>
77
78
  )
78
79
  } catch (err) {
79
- console.error('GraphQLDocumentWithSchema error:', err)
80
- return <div>Error rendering GraphQL document</div>
80
+ // Fall back to plain code block on error
81
+ return (
82
+ <div data-testid='graphql-document' className='graphql-document graphql-document-static'>
83
+ <pre className='shiki'>
84
+ <code className="language-graphql">{props.children}</code>
85
+ </pre>
86
+ </div>
87
+ )
81
88
  }
82
89
  }
@@ -63,7 +63,7 @@ const SBLink: React.FC<{
63
63
  display: `block`,
64
64
  textDecoration: `none`,
65
65
  color: active.is ? `var(--accent-12)` : undefined,
66
- backgroundColor: active.isDirect ? `var(--accent-2)` : active.isdescendant ? `var(--accent-1)` : `transparent`,
66
+ backgroundColor: active.isDirect ? `var(--accent-2)` : active.isDescendant ? `var(--accent-1)` : `transparent`,
67
67
  borderRadius: `var(--radius-2)`,
68
68
  }}
69
69
  >
@@ -145,7 +145,7 @@ const SectionLink: React.FC<{ link: Content.ItemLink }> = ({ link }) => {
145
145
  style={{
146
146
  textDecoration: `none`,
147
147
  color: active.is ? `var(--accent-12)` : undefined,
148
- backgroundColor: active.isDirect ? `var(--accent-2)` : active.isdescendant ? `var(--accent-1)` : `transparent`,
148
+ backgroundColor: active.isDirect ? `var(--accent-2)` : active.isDescendant ? `var(--accent-1)` : `transparent`,
149
149
  borderBottomRightRadius: `var(--radius-2)`,
150
150
  borderTopRightRadius: `var(--radius-2)`,
151
151
  }}
@@ -1,6 +1,3 @@
1
- // TODO it seems more logical to have this asset imported in the server entry.
2
- // But then, we won't get it from the client manifest. But we could get it from the server manifest. Should we do that?
3
- // But then, that wouldn't work for SPA. Does that matter? Just put a conditional here e.g. if (import.meta.env.PROD) ...?
4
1
  import '@radix-ui/themes/styles.css'
5
2
  import './styles/code-block.css'
6
3
  import { ReactDomClient } from '#dep/react-dom-client/index'
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /**
4
+ * Hook that returns a server value during SSR and switches to client value after hydration
5
+ *
6
+ * @param clientValue - Function that returns the value to use on the client
7
+ * @param serverValue - Value to use during SSR
8
+ * @returns The appropriate value based on rendering context
9
+ */
10
+ export function useClientOnly<T>(
11
+ clientValue: () => T,
12
+ serverValue: T,
13
+ ): T {
14
+ const [value, setValue] = useState<T>(serverValue)
15
+
16
+ useEffect(() => {
17
+ setValue(clientValue())
18
+ }, [])
19
+
20
+ return value
21
+ }