@byline/cli 3.20.4 → 4.0.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 (198) hide show
  1. package/README.md +42 -2
  2. package/dist/cli.js +5 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +1 -0
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.d.ts +3 -0
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +10 -14
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/setup-checks.d.ts +9 -0
  12. package/dist/commands/setup-checks.d.ts.map +1 -1
  13. package/dist/commands/setup-checks.js +43 -18
  14. package/dist/commands/setup-checks.js.map +1 -1
  15. package/dist/commands/setup.js +1 -1
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/context.d.ts +2 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +6 -0
  20. package/dist/context.js.map +1 -1
  21. package/dist/lib/dependency-version.d.ts +12 -0
  22. package/dist/lib/dependency-version.d.ts.map +1 -0
  23. package/dist/lib/dependency-version.js +101 -0
  24. package/dist/lib/dependency-version.js.map +1 -0
  25. package/dist/lib/planned-writes.d.ts +8 -0
  26. package/dist/lib/planned-writes.d.ts.map +1 -0
  27. package/dist/lib/planned-writes.js +177 -0
  28. package/dist/lib/planned-writes.js.map +1 -0
  29. package/dist/lib/planned-writes.test.d.ts +2 -0
  30. package/dist/lib/planned-writes.test.d.ts.map +1 -0
  31. package/dist/lib/planned-writes.test.js +113 -0
  32. package/dist/lib/planned-writes.test.js.map +1 -0
  33. package/dist/lib/release-policy.d.ts +12 -0
  34. package/dist/lib/release-policy.d.ts.map +1 -0
  35. package/dist/lib/release-policy.js +20 -0
  36. package/dist/lib/release-policy.js.map +1 -0
  37. package/dist/lib/release-policy.test.d.ts +2 -0
  38. package/dist/lib/release-policy.test.d.ts.map +1 -0
  39. package/dist/lib/release-policy.test.js +36 -0
  40. package/dist/lib/release-policy.test.js.map +1 -0
  41. package/dist/lib/route-config.d.ts +40 -0
  42. package/dist/lib/route-config.d.ts.map +1 -0
  43. package/dist/lib/route-config.js +322 -0
  44. package/dist/lib/route-config.js.map +1 -0
  45. package/dist/lib/static-config.d.ts +20 -0
  46. package/dist/lib/static-config.d.ts.map +1 -0
  47. package/dist/lib/static-config.js +444 -0
  48. package/dist/lib/static-config.js.map +1 -0
  49. package/dist/lib/static-config.test.d.ts +2 -0
  50. package/dist/lib/static-config.test.d.ts.map +1 -0
  51. package/dist/lib/static-config.test.js +105 -0
  52. package/dist/lib/static-config.test.js.map +1 -0
  53. package/dist/lib/template-hook-boundary.test.node.d.ts +2 -0
  54. package/dist/lib/template-hook-boundary.test.node.d.ts.map +1 -0
  55. package/dist/lib/template-hook-boundary.test.node.js +30 -0
  56. package/dist/lib/template-hook-boundary.test.node.js.map +1 -0
  57. package/dist/lib/template-path.d.ts +5 -0
  58. package/dist/lib/template-path.d.ts.map +1 -0
  59. package/dist/lib/template-path.js +9 -0
  60. package/dist/lib/template-path.js.map +1 -0
  61. package/dist/lib/workspace-root.d.ts +28 -0
  62. package/dist/lib/workspace-root.d.ts.map +1 -0
  63. package/dist/lib/workspace-root.js +205 -0
  64. package/dist/lib/workspace-root.js.map +1 -0
  65. package/dist/manifest/deps.d.ts +1 -1
  66. package/dist/manifest/deps.d.ts.map +1 -1
  67. package/dist/manifest/deps.js +18 -8
  68. package/dist/manifest/deps.js.map +1 -1
  69. package/dist/phases/deps.d.ts +14 -1
  70. package/dist/phases/deps.d.ts.map +1 -1
  71. package/dist/phases/deps.js +312 -137
  72. package/dist/phases/deps.js.map +1 -1
  73. package/dist/phases/deps.test.d.ts +2 -0
  74. package/dist/phases/deps.test.d.ts.map +1 -0
  75. package/dist/phases/deps.test.js +343 -0
  76. package/dist/phases/deps.test.js.map +1 -0
  77. package/dist/phases/preflight.d.ts.map +1 -1
  78. package/dist/phases/preflight.js +59 -11
  79. package/dist/phases/preflight.js.map +1 -1
  80. package/dist/phases/prompts.d.ts +2 -0
  81. package/dist/phases/prompts.d.ts.map +1 -1
  82. package/dist/phases/prompts.js +51 -2
  83. package/dist/phases/prompts.js.map +1 -1
  84. package/dist/phases/routes-ui.test.d.ts +2 -0
  85. package/dist/phases/routes-ui.test.d.ts.map +1 -0
  86. package/dist/phases/routes-ui.test.js +766 -0
  87. package/dist/phases/routes-ui.test.js.map +1 -0
  88. package/dist/phases/routes.d.ts +4 -1
  89. package/dist/phases/routes.d.ts.map +1 -1
  90. package/dist/phases/routes.js +610 -122
  91. package/dist/phases/routes.js.map +1 -1
  92. package/dist/phases/scaffold-smoke.test.d.ts +2 -0
  93. package/dist/phases/scaffold-smoke.test.d.ts.map +1 -0
  94. package/dist/phases/scaffold-smoke.test.js +168 -0
  95. package/dist/phases/scaffold-smoke.test.js.map +1 -0
  96. package/dist/phases/scaffold.d.ts +4 -1
  97. package/dist/phases/scaffold.d.ts.map +1 -1
  98. package/dist/phases/scaffold.js +275 -72
  99. package/dist/phases/scaffold.js.map +1 -1
  100. package/dist/phases/scaffold.test.d.ts +2 -0
  101. package/dist/phases/scaffold.test.d.ts.map +1 -0
  102. package/dist/phases/scaffold.test.js +178 -0
  103. package/dist/phases/scaffold.test.js.map +1 -0
  104. package/dist/phases/ui.d.ts +4 -1
  105. package/dist/phases/ui.d.ts.map +1 -1
  106. package/dist/phases/ui.js +51 -59
  107. package/dist/phases/ui.js.map +1 -1
  108. package/dist/phases/wire/index.d.ts.map +1 -1
  109. package/dist/phases/wire/index.js +10 -6
  110. package/dist/phases/wire/index.js.map +1 -1
  111. package/dist/phases/wire/shared.d.ts +4 -1
  112. package/dist/phases/wire/shared.d.ts.map +1 -1
  113. package/dist/phases/wire/vite-config.d.ts.map +1 -1
  114. package/dist/phases/wire/vite-config.js +69 -20
  115. package/dist/phases/wire/vite-config.js.map +1 -1
  116. package/dist/phases/wire/vite-config.test.d.ts +2 -0
  117. package/dist/phases/wire/vite-config.test.d.ts.map +1 -0
  118. package/dist/phases/wire/vite-config.test.js +97 -0
  119. package/dist/phases/wire/vite-config.test.js.map +1 -0
  120. package/dist/prompts.d.ts +1 -0
  121. package/dist/prompts.d.ts.map +1 -1
  122. package/dist/prompts.js +7 -2
  123. package/dist/prompts.js.map +1 -1
  124. package/dist/runner.js +1 -1
  125. package/dist/runner.js.map +1 -1
  126. package/dist/templates/byline/admin.config.ts +8 -7
  127. package/dist/templates/byline/clients.server.ts +77 -0
  128. package/dist/templates/byline/collection-types.contract.ts +67 -0
  129. package/dist/templates/byline/collections/index.ts +17 -0
  130. package/dist/templates/byline/collections/server-hooks.ts +7 -0
  131. package/dist/templates/byline/generated/collection-types.ts +11 -0
  132. package/dist/templates/byline/i18n.ts +3 -3
  133. package/dist/templates/byline/locales.ts +9 -9
  134. package/dist/templates/byline/public.ts +12 -0
  135. package/dist/templates/byline/routes.ts +7 -6
  136. package/dist/templates/byline/scripts/generate-types.ts +81 -0
  137. package/dist/templates/byline/server.config.ts +5 -5
  138. package/dist/templates/byline-examples/admin.config.ts +18 -21
  139. package/dist/templates/byline-examples/blocks/photo-block.ts +3 -2
  140. package/dist/templates/byline-examples/clients.server.ts +77 -0
  141. package/dist/templates/byline-examples/collection-types.contract.ts +67 -0
  142. package/dist/templates/byline-examples/collections/docs/hooks.ts +30 -20
  143. package/dist/templates/byline-examples/collections/docs/schema.ts +18 -18
  144. package/dist/templates/byline-examples/collections/index.ts +22 -0
  145. package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +11 -6
  146. package/dist/templates/byline-examples/collections/media/schema.ts +16 -0
  147. package/dist/templates/byline-examples/collections/news/schema.ts +17 -3
  148. package/dist/templates/byline-examples/collections/news-categories/schema.ts +13 -0
  149. package/dist/templates/byline-examples/collections/pages/schema.ts +13 -0
  150. package/dist/templates/byline-examples/collections/server-hooks.ts +8 -0
  151. package/dist/templates/byline-examples/fields/ai-text.ts +1 -1
  152. package/dist/templates/byline-examples/fields/ai-textarea.ts +1 -1
  153. package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +3 -3
  154. package/dist/templates/byline-examples/generated/collection-types.ts +139 -0
  155. package/dist/templates/byline-examples/i18n.ts +3 -3
  156. package/dist/templates/byline-examples/locales.ts +9 -9
  157. package/dist/templates/byline-examples/public.ts +12 -0
  158. package/dist/templates/byline-examples/routes.ts +7 -6
  159. package/dist/templates/byline-examples/scripts/backfill-version-locales.ts +1 -1
  160. package/dist/templates/byline-examples/scripts/generate-types.ts +81 -0
  161. package/dist/templates/byline-examples/scripts/import-docs.ts +11 -12
  162. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.test.node.ts +93 -233
  163. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +2 -2
  164. package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.test.node.ts +0 -4
  165. package/dist/templates/byline-examples/scripts/re-anchor.ts +3 -3
  166. package/dist/templates/byline-examples/scripts/regenerate-media.ts +6 -5
  167. package/dist/templates/byline-examples/server.config.ts +6 -15
  168. package/dist/templates/host/vite.config.ts +2 -0
  169. package/dist/templates/routes/_byline/admin/collections/$collection/$id/api.tsx +1 -6
  170. package/dist/templates/routes/_byline/admin/collections/$collection/$id/history.tsx +1 -7
  171. package/dist/templates/routes/_byline/admin/collections/$collection/$id/index.tsx +1 -6
  172. package/dist/templates/routes/_byline/route.lazy.tsx +1 -2
  173. package/dist/templates/routes/_byline/route.tsx +1 -1
  174. package/dist/templates/ui-byline/blocks/photo-block/index.tsx +18 -23
  175. package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +1 -1
  176. package/dist/templates/ui-byline/components/admonition/index.tsx +11 -8
  177. package/dist/templates/ui-byline/components/code/code.tsx +6 -4
  178. package/dist/templates/ui-byline/components/link/lang-link.tsx +4 -8
  179. package/dist/templates/ui-byline/components/link/link-field.tsx +3 -2
  180. package/dist/templates/ui-byline/components/link/link-lexical.tsx +9 -8
  181. package/dist/templates/ui-byline/render-blocks.tsx +38 -38
  182. package/dist/templates/ui-byline/types/content.ts +25 -0
  183. package/dist/templates/ui-byline/types/i18n.ts +4 -8
  184. package/dist/test-helpers.d.ts +5 -0
  185. package/dist/test-helpers.d.ts.map +1 -0
  186. package/dist/test-helpers.js +52 -0
  187. package/dist/test-helpers.js.map +1 -0
  188. package/dist/types.d.ts +21 -1
  189. package/dist/types.d.ts.map +1 -1
  190. package/dist/ui-template-contract.test.d.ts +2 -0
  191. package/dist/ui-template-contract.test.d.ts.map +1 -0
  192. package/dist/ui-template-contract.test.js +130 -0
  193. package/dist/ui-template-contract.test.js.map +1 -0
  194. package/dist/workspace-root.test.d.ts +2 -0
  195. package/dist/workspace-root.test.d.ts.map +1 -0
  196. package/dist/workspace-root.test.js +307 -0
  197. package/dist/workspace-root.test.js.map +1 -0
  198. package/package.json +9 -1
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Compile-time contract between this application's collection schemas and its
3
+ * generated collection-type projection.
4
+ *
5
+ * ```text
6
+ * collections/index.ts generated/collection-types.ts
7
+ * `typeof collections` `CollectionFields*ByPath`
8
+ * | |
9
+ * v v
10
+ * inferred registry ----------- Exact -------- generated registry
11
+ * |
12
+ * v
13
+ * TypeScript error on drift
14
+ * ```
15
+ *
16
+ * The generated file is deliberately a standalone projection for typed clients
17
+ * and frontend code; it does not import the runtime schemas. This module joins
18
+ * those two app-owned sources at compile time and requires their collection
19
+ * keys, ordinary field shapes, and all-locale field shapes to match exactly.
20
+ *
21
+ * This file has no runtime behavior. Keep it application-owned: `@byline/core`
22
+ * provides the inference and code-generation machinery, but only this
23
+ * application knows its concrete collection tuple and generated output.
24
+ */
25
+
26
+ import type { CollectionFieldData, CollectionFieldDataAllLocales } from '@byline/core'
27
+
28
+ import type { collections } from './collections/index.js'
29
+ import type {
30
+ CollectionFieldsAllLocalesByPath,
31
+ CollectionFieldsByPath,
32
+ } from './generated/collection-types.js'
33
+
34
+ type InferredFieldsByPath = {
35
+ [Definition in (typeof collections)[number] as Definition['path']]: CollectionFieldData<Definition>
36
+ }
37
+
38
+ type InferredFieldsAllLocalesByPath = {
39
+ [Definition in (typeof collections)[number] as Definition['path']]: CollectionFieldDataAllLocales<Definition>
40
+ }
41
+
42
+ type IsAny<Value> = 0 extends 1 & Value ? true : false
43
+ type Exact<Left, Right> = IsAny<Left> extends true
44
+ ? IsAny<Right>
45
+ : IsAny<Right> extends true
46
+ ? false
47
+ : (<Value>() => Value extends Left ? 1 : 2) extends <Value>() =>
48
+ Value extends Right ? 1 : 2
49
+ ? (<Value>() => Value extends Right ? 1 : 2) extends <Value>() =>
50
+ Value extends Left ? 1 : 2
51
+ ? true
52
+ : false
53
+ : false
54
+ type Assert<Value extends true> = Value
55
+
56
+ type GeneratedKeysMatchRuntimeRegistry = Assert<
57
+ Exact<keyof CollectionFieldsByPath, keyof InferredFieldsByPath>
58
+ >
59
+ type GeneratedFieldsMatchRuntimeRegistry = Assert<
60
+ Exact<CollectionFieldsByPath, InferredFieldsByPath>
61
+ >
62
+ type GeneratedAllLocalesKeysMatchRuntimeRegistry = Assert<
63
+ Exact<keyof CollectionFieldsAllLocalesByPath, keyof InferredFieldsAllLocalesByPath>
64
+ >
65
+ type GeneratedAllLocalesFieldsMatchRuntimeRegistry = Assert<
66
+ Exact<CollectionFieldsAllLocalesByPath, InferredFieldsAllLocalesByPath>
67
+ >
@@ -17,22 +17,20 @@
17
17
  * config from it. Anything the schema *statically imports* therefore ships to
18
18
  * the client. Hook *bodies*, however, only ever run server-side.
19
19
  *
20
- * Search indexing is the canonical reason this matters: it imports
21
- * `getSystemBylineClient` from `@byline/host-tanstack-start`, a server-only
22
- * module. Importing that from the schema would drag server code into the
23
- * browser bundle (fatal in dev). The schema avoids it by referencing these
24
- * hooks through a **dynamic import** instead of an inline object:
20
+ * Search indexing is the canonical reason this matters: it imports the
21
+ * server-only app client boundary from `byline/clients.server.ts`. Importing
22
+ * that from the schema would drag server code into the browser bundle (fatal
23
+ * in dev). The server bootstrap avoids it by registering these hooks from a
24
+ * server-only module:
25
25
  *
26
- * // schema.ts
27
- * hooks: () => import('./hooks.js')
26
+ * // collections/server-hooks.ts
27
+ * collections: { docs: () => import('./docs/hooks.js') }
28
28
  *
29
- * Because the schema reaches this module only through `import()`, this file —
30
- * and its entire transitive import graph — is **structurally absent** from
31
- * the client bundle. `@byline/core` resolves the loader once on the server
32
- * (memoized) and calls these hooks exactly as it would inline ones. The
33
- * upshot: **inside this file we may statically import server-only modules
34
- * directly.** See docs/04-collections → "Hooks must not statically import
35
- * server-only code".
29
+ * No client entry reaches that registry, so this file and its transitive graph
30
+ * are structurally absent from the client bundle without a host-framework
31
+ * transform. `@byline/core` attaches and resolves the loader on the server.
32
+ * The upshot: **inside this file we may statically import server-only modules
33
+ * directly.** See docs/04-collections → "Server-only hook registry".
36
34
  *
37
35
  * ──────────────────────────────────────────────────────────────────────────
38
36
  * Search indexing
@@ -52,7 +50,7 @@
52
50
  */
53
51
 
54
52
  import { defineHooks } from '@byline/core'
55
- import { getSystemBylineClient } from '@byline/host-tanstack-start/integrations/byline-client'
53
+ import { getSystemBylineClient } from '../../clients.server.js'
56
54
 
57
55
  export default defineHooks({
58
56
  beforeCreate: async ({ data, collectionPath }) => {
@@ -62,7 +60,7 @@ export default defineHooks({
62
60
  console.log(
63
61
  `afterCreate: document ${documentId} (version ${documentVersionId}) created in '${collectionPath}'`
64
62
  )
65
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
63
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
66
64
  },
67
65
  beforeUpdate: async ({ data, collectionPath }) => {
68
66
  console.log(`beforeUpdate: updating a document in '${collectionPath}'`, data)
@@ -71,7 +69,19 @@ export default defineHooks({
71
69
  console.log(
72
70
  `afterUpdate: document ${documentId} (version ${documentVersionId}) in '${collectionPath}' updated`
73
71
  )
74
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
72
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
73
+ },
74
+ // Path and advertised-locale edits are immediate document-grain writes, not
75
+ // content updates. Re-index their published view through this dedicated event.
76
+ afterSystemFieldsChange: async ({ documentId, collectionPath, requested, changed }) => {
77
+ console.log(
78
+ `afterSystemFieldsChange: ${documentId} in '${collectionPath}' changed`,
79
+ changed
80
+ )
81
+ // Advertised locales affect alternates/sitemaps, not indexed content.
82
+ if (requested.path) {
83
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
84
+ }
75
85
  },
76
86
  beforeStatusChange: async ({ documentId, collectionPath, previousStatus, nextStatus }) => {
77
87
  console.log(
@@ -85,21 +95,21 @@ export default defineHooks({
85
95
  console.log(
86
96
  `afterStatusChange: ${documentId} in '${collectionPath}' ${previousStatus} → ${nextStatus}`
87
97
  )
88
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
98
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
89
99
  },
90
100
  beforeUnpublish: async ({ documentId, collectionPath }) => {
91
101
  console.log(`beforeUnpublish: unpublishing ${documentId} in '${collectionPath}'`)
92
102
  },
93
103
  afterUnpublish: async ({ documentId, collectionPath }) => {
94
104
  console.log(`afterUnpublish: ${documentId} in '${collectionPath}' unpublished`)
95
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
105
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
96
106
  },
97
107
  beforeDelete: async ({ documentId, collectionPath }) => {
98
108
  console.log(`beforeDelete: deleting ${documentId} in '${collectionPath}'`)
99
109
  },
100
110
  afterDelete: async ({ documentId, collectionPath }) => {
101
111
  console.log(`afterDelete: ${documentId} in '${collectionPath}' deleted`)
102
- await getSystemBylineClient().collection(collectionPath).removeFromIndex(documentId)
112
+ await getSystemBylineClient().collection('docs').removeFromIndex(documentId)
103
113
  },
104
114
  // Fires on any structural tree change (place / reorder / re-parent /
105
115
  // promote-on-delete) for a `tree: true` collection. The payload carries the
@@ -41,13 +41,22 @@ export const Docs = defineCollection({
41
41
  // placement from the source directory layout (see
42
42
  // byline/scripts/import-docs.ts --tree).
43
43
  tree: true,
44
- // Collection search config (docs/05-reading-and-delivery/07-search.md).
45
- // Each key names the part a field plays in the index: `body` fields feed
46
- // the full-text search vector (`title` is display-only unless listed here,
47
- // so we include it and boost it into the heaviest weight class); `facets`,
48
- // `filters`, and `zones` round out the surface. Opting in here is what
49
- // makes the lifecycle hooks in ./hooks.ts index this collection — and it
50
- // requires a `search` provider registered in byline/server.config.ts.
44
+ // Admin list-view quick-search. The collection list route's search box
45
+ // matches these top-level text-store fields with substring queries (ILIKE, in
46
+ // the Postgres adapter) against `store_text` — a lightweight "find the row I
47
+ // mean" affordance that needs no indexing and no lifecycle hooks. Falls back
48
+ // to `useAsTitle` when omitted; declared explicitly here for guidance.
49
+ listSearch: ['title'],
50
+ // Search-*provider* config (docs/05-reading-and-delivery/07-search.md) —
51
+ // distinct from `listSearch` above. Each key names the part a field plays in
52
+ // the index: `body` fields feed the full-text search vector (`title` is
53
+ // display-only unless listed here, so we include it and boost it into the
54
+ // heaviest weight class); `facets`, `filters`, and `zones` round out the
55
+ // surface. This drives the pluggable SearchProvider (full-text search in
56
+ // @byline/search-postgres) and requires a `search` provider registered in
57
+ // byline/server.config.ts. Unlike `listSearch`, `search` is inert on its own
58
+ // — opting in here is what makes the index / reindex / deindex lifecycle
59
+ // hooks in ./hooks.ts keep this collection's provider index in sync.
51
60
  // `content` is a container (blocks) field: naming it walks every text-bearing
52
61
  // child (richtext body, image alt + caption) into the searchable body —
53
62
  // omit it and document bodies are silently absent from the index.
@@ -56,17 +65,8 @@ export const Docs = defineCollection({
56
65
  useAsPath: 'title',
57
66
  advertiseLocales: true, // Renders the available-locales sidebar widget.
58
67
  linksInEditor: true, // See type definition for details.
59
- // Collection lifecycle hooks. Hook *bodies* only ever run server-side, but
60
- // a collection *schema* is isomorphic — Byline bundles it into the browser
61
- // admin too (the admin reads field config from it), so anything the schema
62
- // *statically imports* ships to the client. Search indexing needs a
63
- // server-only import (`getSystemBylineClient` from
64
- // `@byline/host-tanstack-start`), which would crash the browser bundle if
65
- // imported here. The **loader form** — a thunk that dynamically imports a
66
- // sibling module — keeps `./hooks.ts` and its entire import graph out of
67
- // the client bundle while running exactly like inline hooks on the server.
68
- // See ./hooks.ts for the worked example and the full explanation.
69
- hooks: () => import('./hooks.js'),
68
+ // Server-only lifecycle hooks are registered in ../server-hooks.ts so this
69
+ // schema remains portable across host frameworks and safe in client graphs.
70
70
  fields: [
71
71
  {
72
72
  name: 'title',
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { InferCollectionRegistry } from '@byline/core'
10
+
11
+ import type { CollectionFieldsByPath } from '../generated/collection-types.js'
12
+
13
+ import { Docs } from './docs/schema.js'
14
+ import { Media } from './media/schema.js'
15
+ import { News } from './news/schema.js'
16
+ import { NewsCategories } from './news-categories/schema.js'
17
+ import { Pages } from './pages/schema.js'
18
+
19
+ export const collections = [Docs, News, Pages, Media, NewsCategories] as const
20
+
21
+ export type InferredBylineCollections = InferCollectionRegistry<typeof collections>
22
+ export type BylineCollections = CollectionFieldsByPath
@@ -19,11 +19,16 @@
19
19
  * - Top + bottom pagination (RouterPager)
20
20
  */
21
21
 
22
- import { Link, useNavigate, useRouterState } from '@tanstack/react-router'
22
+ import { useRouterState } from '@tanstack/react-router'
23
23
 
24
24
  import type { ListViewComponentProps, StoredFileValue, WorkflowStatus } from '@byline/core'
25
25
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
26
26
  import { RouterPager } from '@byline/host-tanstack-start/admin-shell/chrome/router-pager'
27
+ import {
28
+ Link,
29
+ useNavigate,
30
+ } from '@byline/host-tanstack-start/admin-shell/chrome/loose-router'
31
+ import { getAdminRoutePath } from '@byline/host-tanstack-start/routes/admin-path'
27
32
  import { Container, IconButton, LoaderRing, PlusIcon, Search, Section, Select } from '@byline/ui/react'
28
33
  import { LocalDateTime } from '@byline/admin/react'
29
34
 
@@ -128,7 +133,7 @@ export function MediaListView({
128
133
  delete params.page
129
134
  params.query = query
130
135
  navigate({
131
- to: '/admin/collections/$collection',
136
+ to: getAdminRoutePath('collections', '$collection'),
132
137
  params: { collection: collectionPath },
133
138
  search: params,
134
139
  })
@@ -140,7 +145,7 @@ export function MediaListView({
140
145
  delete params.page
141
146
  delete params.query
142
147
  navigate({
143
- to: '/admin/collections/$collection',
148
+ to: getAdminRoutePath('collections', '$collection'),
144
149
  params: { collection: collectionPath },
145
150
  search: params,
146
151
  })
@@ -158,7 +163,7 @@ export function MediaListView({
158
163
  params.order = order
159
164
  params.desc = desc
160
165
  navigate({
161
- to: '/admin/collections/$collection',
166
+ to: getAdminRoutePath('collections', '$collection'),
162
167
  params: { collection: collectionPath },
163
168
  search: params,
164
169
  })
@@ -178,7 +183,7 @@ export function MediaListView({
178
183
  render={
179
184
  <Link
180
185
  className={styles['create-link']}
181
- to="/admin/collections/$collection/create"
186
+ to={getAdminRoutePath('collections', '$collection', 'create')}
182
187
  params={{ collection: collectionPath }}
183
188
  />
184
189
  }
@@ -239,7 +244,7 @@ export function MediaListView({
239
244
  return (
240
245
  <Link
241
246
  key={doc.id}
242
- to="/admin/collections/$collection/$id"
247
+ to={getAdminRoutePath('collections', '$collection', '$id')}
243
248
  params={{ collection: collectionPath, id: doc.id }}
244
249
  className={styles.card}
245
250
  >
@@ -50,6 +50,19 @@ export const Media = defineCollection({
50
50
  archived: { label: 'Archived', verb: 'Archive' },
51
51
  }),
52
52
  showStats: true,
53
+ // Admin list-view quick-search. The collection list route's search box
54
+ // matches these top-level text-store fields with substring queries (ILIKE, in
55
+ // the Postgres adapter) against `store_text` — a lightweight "find the row I
56
+ // mean" affordance that needs no indexing and no lifecycle hooks. Falls back
57
+ // to `useAsTitle` when omitted; declared explicitly here for guidance.
58
+ listSearch: ['title'],
59
+ // Search-*provider* config (docs/05-reading-and-delivery/07-search.md) —
60
+ // distinct from `listSearch` above. This drives the pluggable SearchProvider
61
+ // (full-text search in @byline/search-postgres) and offers richer options:
62
+ // per-field weighting/`boost`, `facets`, `filters`, and `zones`. Unlike
63
+ // `listSearch`, `search` is inert on its own — it MUST be paired with index /
64
+ // reindex / deindex document-lifecycle hooks that keep the provider index in
65
+ // sync on create / update / publish / delete (see this collection's hooks).
53
66
  search: { body: ['title', 'caption'] },
54
67
  fields: [
55
68
  {
@@ -115,6 +128,9 @@ export const Media = defineCollection({
115
128
  quality: 55,
116
129
  },
117
130
  ],
131
+ // Worked example: definition-attached inline hooks are appropriate
132
+ // when their entire implementation is isomorphic-safe. Hooks that
133
+ // import server-only code belong in collections/server-hooks.ts.
118
134
  hooks: {
119
135
  beforeStore: (ctx: BeforeStoreContext) => {
120
136
  console.log('beforeStore hook called', ctx)
@@ -30,6 +30,19 @@ export const News = defineCollection({
30
30
  archived: { label: 'Archived', verb: 'Archive' },
31
31
  }),
32
32
  showStats: true,
33
+ // Admin list-view quick-search. The collection list route's search box
34
+ // matches these top-level text-store fields with substring queries (ILIKE, in
35
+ // the Postgres adapter) against `store_text` — a lightweight "find the row I
36
+ // mean" affordance that needs no indexing and no lifecycle hooks. Falls back
37
+ // to `useAsTitle` when omitted; declared explicitly here for guidance.
38
+ listSearch: ['title'],
39
+ // Search-*provider* config (docs/05-reading-and-delivery/07-search.md) —
40
+ // distinct from `listSearch` above. This drives the pluggable SearchProvider
41
+ // (full-text search in @byline/search-postgres) and offers richer options:
42
+ // per-field weighting/`boost`, `facets`, `filters`, and `zones`. Unlike
43
+ // `listSearch`, `search` is inert on its own — it MUST be paired with index /
44
+ // reindex / deindex document-lifecycle hooks that keep the provider index in
45
+ // sync on create / update / publish / delete (see this collection's hooks).
33
46
  search: { body: ['title'] },
34
47
  useAsTitle: 'title',
35
48
  useAsPath: 'title',
@@ -86,8 +99,9 @@ export const News = defineCollection({
86
99
  })
87
100
 
88
101
  /**
89
- * Field data shape inferred directly from the schema. Use this as the
90
- * generic to typed read calls — `client.collection('news').find<NewsFields>()`
91
- * — so dot-notation on `doc.fields` is fully checked.
102
+ * Schema-local field shape for hooks and other collection implementation details.
103
+ * Frontend/client code should import `NewsFields` from
104
+ * `byline/generated/collection-types.ts`; ordinary reads infer it from the
105
+ * client registry, while populate/select projections may supply an overlay.
92
106
  */
93
107
  export type NewsFields = CollectionFieldData<typeof News>
@@ -21,6 +21,19 @@ export const NewsCategories = defineCollection({
21
21
  // `published` and the form shows only Save / Close.
22
22
  workflow: SINGLE_STATUS_WORKFLOW,
23
23
  showStats: true,
24
+ // Admin list-view quick-search. The collection list route's search box
25
+ // matches these top-level text-store fields with substring queries (ILIKE, in
26
+ // the Postgres adapter) against `store_text` — a lightweight "find the row I
27
+ // mean" affordance that needs no indexing and no lifecycle hooks. Falls back
28
+ // to `useAsTitle` when omitted; declared explicitly here for guidance.
29
+ listSearch: ['name'],
30
+ // Search-*provider* config (docs/05-reading-and-delivery/07-search.md) —
31
+ // distinct from `listSearch` above. This drives the pluggable SearchProvider
32
+ // (full-text search in @byline/search-postgres) and offers richer options:
33
+ // per-field weighting/`boost`, `facets`, `filters`, and `zones`. Unlike
34
+ // `listSearch`, `search` is inert on its own — it MUST be paired with index /
35
+ // reindex / deindex document-lifecycle hooks that keep the provider index in
36
+ // sync on create / update / publish / delete (see this collection's hooks).
24
37
  search: { body: ['name'] },
25
38
  useAsTitle: 'name',
26
39
  useAsPath: 'name',
@@ -32,6 +32,19 @@ export const Pages = defineCollection({
32
32
  archived: { label: 'Archived', verb: 'Archive' },
33
33
  }),
34
34
  showStats: true,
35
+ // Admin list-view quick-search. The collection list route's search box
36
+ // matches these top-level text-store fields with substring queries (ILIKE, in
37
+ // the Postgres adapter) against `store_text` — a lightweight "find the row I
38
+ // mean" affordance that needs no indexing and no lifecycle hooks. Falls back
39
+ // to `useAsTitle` when omitted; declared explicitly here for guidance.
40
+ listSearch: ['title'],
41
+ // Search-*provider* config (docs/05-reading-and-delivery/07-search.md) —
42
+ // distinct from `listSearch` above. This drives the pluggable SearchProvider
43
+ // (full-text search in @byline/search-postgres) and offers richer options:
44
+ // per-field weighting/`boost`, `facets`, `filters`, and `zones`. Unlike
45
+ // `listSearch`, `search` is inert on its own — it MUST be paired with index /
46
+ // reindex / deindex document-lifecycle hooks that keep the provider index in
47
+ // sync on create / update / publish / delete (see this collection's hooks).
35
48
  search: { body: ['title'] },
36
49
  useAsTitle: 'title',
37
50
  useAsPath: 'title',
@@ -0,0 +1,8 @@
1
+ /** Server-only lifecycle hook registry. Imported only by server.config.ts. */
2
+ import type { ServerHooksConfig } from '@byline/core'
3
+
4
+ export const serverHooks = {
5
+ collections: {
6
+ docs: () => import('./docs/hooks.js'),
7
+ },
8
+ } satisfies ServerHooksConfig
@@ -27,7 +27,7 @@ import { AiFieldPanel } from './ai-widgets/ai-field-panel.js'
27
27
  *
28
28
  * @example
29
29
  * ```ts
30
- * // apps/webapp/byline/collections/news/admin.tsx
30
+ * // byline/collections/news/admin.tsx
31
31
  * fields: {
32
32
  * title: aiTextFieldAdmin(),
33
33
  * }
@@ -28,7 +28,7 @@ import { AiFieldPanel } from './ai-widgets/ai-field-panel.js'
28
28
  *
29
29
  * @example
30
30
  * ```ts
31
- * // apps/webapp/byline/collections/news/admin.tsx
31
+ * // byline/collections/news/admin.tsx
32
32
  * fields: {
33
33
  * summary: aiTextAreaFieldAdmin({
34
34
  * components: { HelpText: SummaryLength },
@@ -13,7 +13,7 @@
13
13
  * - `LexicalRichTextAi` — a `RichTextEditorComponent` that registers
14
14
  * the AI assistant by adding `AiLexicalExtension` to the editor's
15
15
  * extension graph. Register globally in
16
- * `apps/webapp/byline/admin.config.ts` under
16
+ * `byline/admin.config.ts` under
17
17
  * `fields.richText.editor` to AI-enable every richtext field.
18
18
  * - `aiRichTextAdmin()` — a `FieldAdminConfig` factory. Drop into a
19
19
  * collection's `fields` map in `<collection>/admin.tsx` to opt one
@@ -38,7 +38,7 @@ import { builtInExtensions, lexicalEditor } from '@byline/richtext-lexical/confi
38
38
  * drawer via `ReactExtension.decorators` — no `featureAfterEditor`, no
39
39
  * React-context registry hop.
40
40
  *
41
- * **Global** opt-in — register in `apps/webapp/byline/admin.config.ts`:
41
+ * **Global** opt-in — register in `byline/admin.config.ts`:
42
42
  *
43
43
  * ```ts
44
44
  * fields: {
@@ -77,7 +77,7 @@ export const LexicalRichTextAi = lexicalEditor((c) => {
77
77
  *
78
78
  * @example
79
79
  * ```ts
80
- * // apps/webapp/byline/collections/news/admin.tsx
80
+ * // byline/collections/news/admin.tsx
81
81
  * import { aiRichTextAdmin } from '../../fields/lexical-richtext-ai.js'
82
82
  *
83
83
  * fields: {
@@ -0,0 +1,139 @@
1
+ // Generated by @byline/core/codegen
2
+ // Format version: 1
3
+ // Hash: a32bba417302b4e2c90703e47a1f326d4db64cabd11194f8eed816e78442178e
4
+
5
+ import type {
6
+ JsonValue,
7
+ RelatedDocumentValue,
8
+ StoredFileValue,
9
+ } from '@byline/core'
10
+
11
+ export type PhotoBlockData = {
12
+ _id: string
13
+ _type: 'photoBlock'
14
+ display?: 'default' | 'wide' | 'full_width' | undefined
15
+ photo?: RelatedDocumentValue | undefined
16
+ alt: string
17
+ caption?: JsonValue | undefined
18
+ }
19
+
20
+ export type PhotoBlockDataAllLocales = {
21
+ _id: string
22
+ _type: 'photoBlock'
23
+ display?: 'default' | 'wide' | 'full_width' | undefined
24
+ photo?: RelatedDocumentValue | undefined
25
+ alt: string
26
+ caption?: { [locale: string]: JsonValue } | undefined
27
+ }
28
+
29
+ export type RichTextBlockData = {
30
+ _id: string
31
+ _type: 'richTextBlock'
32
+ richText: JsonValue
33
+ constrainedWidth?: boolean | undefined
34
+ }
35
+
36
+ export type RichTextBlockDataAllLocales = {
37
+ _id: string
38
+ _type: 'richTextBlock'
39
+ richText: { [locale: string]: JsonValue }
40
+ constrainedWidth?: boolean | undefined
41
+ }
42
+
43
+ export type DocsFields = {
44
+ title: string
45
+ summary: string
46
+ featureImage?: RelatedDocumentValue | undefined
47
+ publishedOn: Date
48
+ content?: Array<RichTextBlockData | PhotoBlockData> | undefined
49
+ }
50
+
51
+ export type DocsFieldsAllLocales = {
52
+ title: { [locale: string]: string }
53
+ summary: { [locale: string]: string }
54
+ featureImage?: RelatedDocumentValue | undefined
55
+ publishedOn: Date
56
+ content?: Array<RichTextBlockDataAllLocales | PhotoBlockDataAllLocales> | undefined
57
+ }
58
+
59
+ export type MediaFields = {
60
+ image: StoredFileValue
61
+ title: string
62
+ altText: string
63
+ caption?: string | undefined
64
+ credit?: string | undefined
65
+ }
66
+
67
+ export type MediaFieldsAllLocales = {
68
+ image: StoredFileValue
69
+ title: string
70
+ altText: string
71
+ caption?: string | undefined
72
+ credit?: string | undefined
73
+ }
74
+
75
+ export type NewsCategoriesFields = {
76
+ name: string
77
+ description: string
78
+ }
79
+
80
+ export type NewsCategoriesFieldsAllLocales = {
81
+ name: { [locale: string]: string }
82
+ description: { [locale: string]: string }
83
+ }
84
+
85
+ export type NewsFields = {
86
+ title: string
87
+ summary: string
88
+ category: RelatedDocumentValue
89
+ featureImage?: RelatedDocumentValue | undefined
90
+ featured?: boolean | undefined
91
+ content: JsonValue
92
+ publishedOn: Date
93
+ }
94
+
95
+ export type NewsFieldsAllLocales = {
96
+ title: { [locale: string]: string }
97
+ summary: { [locale: string]: string }
98
+ category: RelatedDocumentValue
99
+ featureImage?: RelatedDocumentValue | undefined
100
+ featured?: boolean | undefined
101
+ content: { [locale: string]: JsonValue }
102
+ publishedOn: Date
103
+ }
104
+
105
+ export type PagesFields = {
106
+ title: string
107
+ summary: string
108
+ featureImage?: RelatedDocumentValue | undefined
109
+ area: 'root' | 'about' | 'legal'
110
+ content?: Array<RichTextBlockData | PhotoBlockData> | undefined
111
+ publishedOn: Date
112
+ }
113
+
114
+ export type PagesFieldsAllLocales = {
115
+ title: { [locale: string]: string }
116
+ summary: { [locale: string]: string }
117
+ featureImage?: RelatedDocumentValue | undefined
118
+ area: 'root' | 'about' | 'legal'
119
+ content?: Array<RichTextBlockDataAllLocales | PhotoBlockDataAllLocales> | undefined
120
+ publishedOn: Date
121
+ }
122
+
123
+ export type CollectionFieldsByPath = {
124
+ docs: DocsFields
125
+ media: MediaFields
126
+ news: NewsFields
127
+ 'news-categories': NewsCategoriesFields
128
+ pages: PagesFields
129
+ }
130
+
131
+ export type CollectionFieldsAllLocalesByPath = {
132
+ docs: DocsFieldsAllLocales
133
+ media: MediaFieldsAllLocales
134
+ news: NewsFieldsAllLocales
135
+ 'news-categories': NewsCategoriesFieldsAllLocales
136
+ pages: PagesFieldsAllLocales
137
+ }
138
+
139
+ export type CollectionPath = keyof CollectionFieldsByPath
@@ -12,9 +12,9 @@
12
12
  * translation bundle.
13
13
  *
14
14
  * The locale arrays themselves live in `./locales.ts`, a dependency-free leaf
15
- * module, so the public frontend can import them without pulling in the admin
16
- * translation graph this file depends on. Re-exported here for back-compat
17
- * with existing `~/i18n` importers.
15
+ * module exposed to public frontend code by `./public.ts`, without pulling in
16
+ * the admin translation graph this file depends on. Re-exported here for
17
+ * back-compat with existing `~/i18n` importers.
18
18
  *
19
19
  * `interface` locales govern the CMS admin UI language.
20
20
  * `content` locales govern the languages a document can be published in.