@byline/cli 3.20.4 → 3.21.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 (42) hide show
  1. package/README.md +14 -0
  2. package/dist/phases/scaffold.d.ts +2 -0
  3. package/dist/phases/scaffold.d.ts.map +1 -1
  4. package/dist/phases/scaffold.js +26 -0
  5. package/dist/phases/scaffold.js.map +1 -1
  6. package/dist/phases/scaffold.test.d.ts +2 -0
  7. package/dist/phases/scaffold.test.d.ts.map +1 -0
  8. package/dist/phases/scaffold.test.js +29 -0
  9. package/dist/phases/scaffold.test.js.map +1 -0
  10. package/dist/templates/byline/admin.config.ts +8 -7
  11. package/dist/templates/byline/client.server.ts +32 -0
  12. package/dist/templates/byline/collection-types.contract.ts +42 -0
  13. package/dist/templates/byline/collections/index.ts +17 -0
  14. package/dist/templates/byline/generated/collection-types.ts +11 -0
  15. package/dist/templates/byline/i18n.ts +3 -3
  16. package/dist/templates/byline/locales.ts +9 -9
  17. package/dist/templates/byline/public.ts +12 -0
  18. package/dist/templates/byline/scripts/generate-types.ts +81 -0
  19. package/dist/templates/byline/server.config.ts +3 -5
  20. package/dist/templates/byline-examples/admin.config.ts +12 -10
  21. package/dist/templates/byline-examples/client.server.ts +32 -0
  22. package/dist/templates/byline-examples/collection-types.contract.ts +42 -0
  23. package/dist/templates/byline-examples/collections/docs/hooks.ts +11 -11
  24. package/dist/templates/byline-examples/collections/docs/schema.ts +21 -12
  25. package/dist/templates/byline-examples/collections/index.ts +22 -0
  26. package/dist/templates/byline-examples/collections/media/schema.ts +13 -0
  27. package/dist/templates/byline-examples/collections/news/schema.ts +13 -0
  28. package/dist/templates/byline-examples/collections/news-categories/schema.ts +13 -0
  29. package/dist/templates/byline-examples/collections/pages/schema.ts +13 -0
  30. package/dist/templates/byline-examples/generated/collection-types.ts +139 -0
  31. package/dist/templates/byline-examples/i18n.ts +3 -3
  32. package/dist/templates/byline-examples/locales.ts +9 -9
  33. package/dist/templates/byline-examples/public.ts +12 -0
  34. package/dist/templates/byline-examples/scripts/generate-types.ts +81 -0
  35. package/dist/templates/byline-examples/server.config.ts +2 -12
  36. package/dist/templates/routes/_byline/admin/collections/$collection/$id/api.tsx +1 -6
  37. package/dist/templates/routes/_byline/admin/collections/$collection/$id/history.tsx +1 -7
  38. package/dist/templates/routes/_byline/admin/collections/$collection/$id/index.tsx +1 -6
  39. package/dist/templates/ui-byline/blocks/photo-block/index.tsx +1 -2
  40. package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +1 -1
  41. package/dist/templates/ui-byline/render-blocks.tsx +3 -7
  42. package/package.json +1 -1
@@ -17,11 +17,11 @@
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/client.server.ts`. Importing
22
+ * that from the schema would drag server code into the browser bundle (fatal
23
+ * in dev). The schema avoids it by referencing these hooks through a
24
+ * **dynamic import** instead of an inline object:
25
25
  *
26
26
  * // schema.ts
27
27
  * hooks: () => import('./hooks.js')
@@ -52,7 +52,7 @@
52
52
  */
53
53
 
54
54
  import { defineHooks } from '@byline/core'
55
- import { getSystemBylineClient } from '@byline/host-tanstack-start/integrations/byline-client'
55
+ import { getSystemBylineClient } from '../../client.server.js'
56
56
 
57
57
  export default defineHooks({
58
58
  beforeCreate: async ({ data, collectionPath }) => {
@@ -62,7 +62,7 @@ export default defineHooks({
62
62
  console.log(
63
63
  `afterCreate: document ${documentId} (version ${documentVersionId}) created in '${collectionPath}'`
64
64
  )
65
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
65
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
66
66
  },
67
67
  beforeUpdate: async ({ data, collectionPath }) => {
68
68
  console.log(`beforeUpdate: updating a document in '${collectionPath}'`, data)
@@ -71,7 +71,7 @@ export default defineHooks({
71
71
  console.log(
72
72
  `afterUpdate: document ${documentId} (version ${documentVersionId}) in '${collectionPath}' updated`
73
73
  )
74
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
74
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
75
75
  },
76
76
  beforeStatusChange: async ({ documentId, collectionPath, previousStatus, nextStatus }) => {
77
77
  console.log(
@@ -85,21 +85,21 @@ export default defineHooks({
85
85
  console.log(
86
86
  `afterStatusChange: ${documentId} in '${collectionPath}' ${previousStatus} → ${nextStatus}`
87
87
  )
88
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
88
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
89
89
  },
90
90
  beforeUnpublish: async ({ documentId, collectionPath }) => {
91
91
  console.log(`beforeUnpublish: unpublishing ${documentId} in '${collectionPath}'`)
92
92
  },
93
93
  afterUnpublish: async ({ documentId, collectionPath }) => {
94
94
  console.log(`afterUnpublish: ${documentId} in '${collectionPath}' unpublished`)
95
- await getSystemBylineClient().collection(collectionPath).indexDocument(documentId)
95
+ await getSystemBylineClient().collection('docs').indexDocument(documentId)
96
96
  },
97
97
  beforeDelete: async ({ documentId, collectionPath }) => {
98
98
  console.log(`beforeDelete: deleting ${documentId} in '${collectionPath}'`)
99
99
  },
100
100
  afterDelete: async ({ documentId, collectionPath }) => {
101
101
  console.log(`afterDelete: ${documentId} in '${collectionPath}' deleted`)
102
- await getSystemBylineClient().collection(collectionPath).removeFromIndex(documentId)
102
+ await getSystemBylineClient().collection('docs').removeFromIndex(documentId)
103
103
  },
104
104
  // Fires on any structural tree change (place / reorder / re-parent /
105
105
  // 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.
@@ -60,11 +69,11 @@ export const Docs = defineCollection({
60
69
  // a collection *schema* is isomorphic — Byline bundles it into the browser
61
70
  // admin too (the admin reads field config from it), so anything the schema
62
71
  // *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.
72
+ // server-only import from the app's `client.server.ts` boundary, which would
73
+ // crash the browser bundle if imported here. The **loader form** — a thunk
74
+ // that dynamically imports a sibling module — keeps `./hooks.ts` and its
75
+ // entire import graph out of the client bundle while running exactly like
76
+ // inline hooks on the server.
68
77
  // See ./hooks.ts for the worked example and the full explanation.
69
78
  hooks: () => import('./hooks.js'),
70
79
  fields: [
@@ -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
@@ -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
  {
@@ -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',
@@ -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,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.
@@ -12,12 +12,12 @@
12
12
  * registry). `byline/i18n.ts` consumes these to assemble the
13
13
  * `defineServerConfig` / `defineClientConfig` payload.
14
14
  *
15
- * Kept separate from `byline/i18n.ts` so a public frontend (or a collection
16
- * schema file loaded outside Vite — e.g. when running seeds via tsx) can
17
- * import the locale arrays without dragging in the admin translation graph
18
- * (`@byline/i18n/admin`) that `i18n.ts` depends on. Importing `byline/i18n.ts`
19
- * would; importing this won't — which keeps the lazy admin/public bundle split
20
- * (`src/routes/_byline/route.lazy.tsx`) intact.
15
+ * Kept separate from `byline/i18n.ts` so `byline/public.ts` can expose the
16
+ * locale arrays to a public frontend (or a collection schema file loaded
17
+ * outside Vite — e.g. when running seeds via tsx) without dragging in the
18
+ * admin translation graph (`@byline/i18n/admin`) that `i18n.ts` depends on.
19
+ * Importing `byline/i18n.ts` would; importing this won't — which keeps the lazy
20
+ * admin/public bundle split (`src/routes/_byline/route.lazy.tsx`) intact.
21
21
  *
22
22
  * `interface` locales govern the CMS admin UI language; `content` locales
23
23
  * govern the languages a document can be published in.
@@ -33,10 +33,10 @@ export interface LocaleDefinition {
33
33
  // (or a third-party plugin merged in via `mergeTranslations(...)`).
34
34
  // `adminTranslations({ locales })` in `byline/i18n.ts` throws at boot if a
35
35
  // requested code is not bundled — `@byline/i18n/admin` ships English and French.
36
- export const interfaceLocales: LocaleDefinition[] = [
36
+ export const interfaceLocales = [
37
37
  { code: 'en', label: 'English' },
38
38
  { code: 'fr', label: 'Français' },
39
- ]
39
+ ] as const satisfies readonly LocaleDefinition[]
40
40
 
41
41
  /** Locales a document can be published in. */
42
42
  export const contentLocales = [
@@ -44,4 +44,4 @@ export const contentLocales = [
44
44
  { code: 'fr', label: 'Français' },
45
45
  { code: 'es', label: 'Español' },
46
46
  { code: 'de', label: 'Deutsch' },
47
- ] as const
47
+ ] as const satisfies readonly LocaleDefinition[]
@@ -0,0 +1,12 @@
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
+ /** Client-safe public configuration. Keep this barrel free of admin and server imports. */
10
+ export { contentLocales, interfaceLocales } from './locales.js'
11
+ export { routes } from './routes.js'
12
+ export type { LocaleDefinition } from './locales.js'
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+
6
+ import { emitCollectionTypes } from '@byline/core/codegen'
7
+
8
+ import { collections } from '../collections/index.js'
9
+
10
+ const defaultOutputPath = fileURLToPath(
11
+ new URL('../generated/collection-types.ts', import.meta.url)
12
+ )
13
+
14
+ export type GenerationStatus = 'current' | 'unchanged' | 'written'
15
+
16
+ export function writeCollectionTypes(
17
+ outputPath: string,
18
+ source: string,
19
+ check: boolean
20
+ ): GenerationStatus {
21
+ const existing = existsSync(outputPath) ? readFileSync(outputPath, 'utf8') : undefined
22
+
23
+ if (check) {
24
+ if (existing === undefined) {
25
+ throw new Error(
26
+ `generated collection types are missing: ${outputPath}. Run pnpm byline:generate`
27
+ )
28
+ }
29
+ if (existing !== source) {
30
+ throw new Error(
31
+ `generated collection types are stale: ${outputPath}. Run pnpm byline:generate`
32
+ )
33
+ }
34
+ return 'current'
35
+ }
36
+
37
+ if (existing === source) return 'unchanged'
38
+
39
+ mkdirSync(dirname(outputPath), { recursive: true })
40
+ const temporaryPath = `${outputPath}.${process.pid}.${randomUUID()}.tmp`
41
+ try {
42
+ writeFileSync(temporaryPath, source, 'utf8')
43
+ renameSync(temporaryPath, outputPath)
44
+ } finally {
45
+ rmSync(temporaryPath, { force: true })
46
+ }
47
+ return 'written'
48
+ }
49
+
50
+ function parseArgs(args: readonly string[]): { check: boolean } {
51
+ const unknown = args.filter((arg) => arg !== '--check')
52
+ if (unknown.length > 0) throw new Error(`unknown argument: ${unknown.join(', ')}`)
53
+ if (args.filter((arg) => arg === '--check').length > 1) {
54
+ throw new Error('duplicate argument: --check')
55
+ }
56
+ return { check: args.includes('--check') }
57
+ }
58
+
59
+ export function generateCollectionTypes(
60
+ outputPath = defaultOutputPath,
61
+ check = false
62
+ ): { hash: string; status: GenerationStatus } {
63
+ const { source, hash } = emitCollectionTypes(collections)
64
+ return { hash, status: writeCollectionTypes(outputPath, source, check) }
65
+ }
66
+
67
+ function main(): void {
68
+ const { check } = parseArgs(process.argv.slice(2))
69
+ const { hash, status } = generateCollectionTypes(defaultOutputPath, check)
70
+ console.log(`collection types: ${collections.length} collections ${hash.slice(0, 12)} ${status}`)
71
+ }
72
+
73
+ const entryPath = process.argv[1]
74
+ if (entryPath !== undefined && pathToFileURL(resolve(entryPath)).href === import.meta.url) {
75
+ try {
76
+ main()
77
+ } catch (error) {
78
+ console.error(`collection type generation failed: ${(error as Error).message}`)
79
+ process.exitCode = 1
80
+ }
81
+ }
@@ -19,7 +19,6 @@ import { JwtSessionProvider } from '@byline/admin/auth'
19
19
  import { type BylineCore, initBylineCore } from '@byline/core'
20
20
  import { pgAdapter } from '@byline/db-postgres'
21
21
  import { createAdminStore } from '@byline/db-postgres/admin'
22
- import { getAdminBylineClient } from '@byline/host-tanstack-start/integrations/byline-client'
23
22
  import {
24
23
  lexicalEditorEmbedServer,
25
24
  lexicalEditorPopulateServer,
@@ -29,22 +28,13 @@ import {
29
28
  import { migrate, postgresSearch } from '@byline/search-postgres'
30
29
  import { localStorageProvider } from '@byline/storage-local'
31
30
 
32
- // Import collection definitions directly from schema files — NOT the full
33
- // admin config or index barrels. The admin config / index files pull in
34
- // admin UI configs (React components, CSS modules) that are not loadable
35
- // outside Vite (e.g. when running seeds via tsx).
36
- import { Docs } from './collections/docs/schema.js'
37
- import { Media } from './collections/media/schema.js'
38
- import { News } from './collections/news/schema.js'
39
- import { NewsCategories } from './collections/news-categories/schema.js'
40
- import { Pages } from './collections/pages/schema.js'
31
+ import { getAdminBylineClient } from './client.server.js'
32
+ import { collections } from './collections/index.js'
41
33
  import { i18n } from './i18n.js'
42
34
  import { DEFAULT_SERVER_URL, routes } from './routes.js'
43
35
 
44
36
  const serverURL = process.env.VITE_SERVER_URL || DEFAULT_SERVER_URL
45
37
 
46
- const collections = [Docs, News, Pages, Media, NewsCategories]
47
-
48
38
  // HMR-safe singleton. Vite's program reload re-evaluates this module
49
39
  // without disposing the previous module's resources — every reload
50
40
  // would otherwise allocate a fresh pg `Pool` (max: 20) inside
@@ -8,9 +8,4 @@
8
8
 
9
9
  import { createCollectionApiRoute } from '@byline/host-tanstack-start/routes'
10
10
 
11
- import { contentLocales, i18n } from '~/i18n'
12
-
13
- export const Route = createCollectionApiRoute('/_byline/admin/collections/$collection/$id/api', {
14
- contentLocales,
15
- defaultContentLocale: i18n.content.defaultLocale,
16
- })
11
+ export const Route = createCollectionApiRoute('/_byline/admin/collections/$collection/$id/api')
@@ -8,12 +8,6 @@
8
8
 
9
9
  import { createCollectionHistoryRoute } from '@byline/host-tanstack-start/routes'
10
10
 
11
- import { contentLocales, i18n } from '~/i18n'
12
-
13
11
  export const Route = createCollectionHistoryRoute(
14
- '/_byline/admin/collections/$collection/$id/history',
15
- {
16
- contentLocales,
17
- defaultContentLocale: i18n.content.defaultLocale,
18
- }
12
+ '/_byline/admin/collections/$collection/$id/history'
19
13
  )
@@ -8,9 +8,4 @@
8
8
 
9
9
  import { createCollectionEditRoute } from '@byline/host-tanstack-start/routes'
10
10
 
11
- import { contentLocales, i18n } from '~/i18n'
12
-
13
- export const Route = createCollectionEditRoute('/_byline/admin/collections/$collection/$id/', {
14
- contentLocales,
15
- defaultContentLocale: i18n.content.defaultLocale,
16
- })
11
+ export const Route = createCollectionEditRoute('/_byline/admin/collections/$collection/$id/')
@@ -5,8 +5,7 @@ import type React from 'react'
5
5
  import { Container, FadeInLift } from '@byline/ui/react'
6
6
  import cx from 'classnames'
7
7
 
8
- import type { PhotoBlockData } from '~/blocks/photo-block'
9
- import type { MediaFields } from '~/collections/media/schema'
8
+ import type { MediaFields, PhotoBlockData } from '~/generated/collection-types.js'
10
9
 
11
10
  import { ResponsiveImage } from '@/ui/byline/components/responsive-image'
12
11
  import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
@@ -3,7 +3,7 @@
3
3
  import { Container } from '@byline/ui/react'
4
4
  import cx from 'classnames'
5
5
 
6
- import type { RichTextBlockData } from '~/blocks/richtext-block'
6
+ import type { RichTextBlockData } from '~/generated/collection-types.js'
7
7
 
8
8
  import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
9
9
  import type { Locale } from '@/ui/byline/types/i18n'
@@ -1,20 +1,16 @@
1
- import type { BlocksUnion } from '@byline/core'
2
1
  import { Section } from '@byline/ui/react'
3
2
 
4
- import { PhotoBlock as PhotoBlockDef } from '~/blocks/photo-block'
5
- import { RichTextBlock as RichTextBlockDef } from '~/blocks/richtext-block'
3
+ import type { PhotoBlockData, RichTextBlockData } from '~/generated/collection-types.js'
6
4
 
7
5
  import { PhotoBlock } from '@/ui/byline/blocks/photo-block'
8
6
  import { RichTextBlock } from '@/ui/byline/blocks/richtext-block'
9
7
  import { toKebabCase } from '@/ui/utils/to-kebab-case'
10
8
  import type { Locale } from '@/i18n/i18n-config'
11
9
 
12
- const Blocks = [PhotoBlockDef, RichTextBlockDef] as const
13
-
14
- export type AnyBlock = BlocksUnion<typeof Blocks>
10
+ export type AnyBlock = PhotoBlockData | RichTextBlockData
15
11
 
16
12
  // Mapped type ensures every AnyBlock['_type'] has a registered component.
17
- // TypeScript errors here if Blocks gains a new type without a matching entry.
13
+ // TypeScript errors here if AnyBlock gains a new type without a matching entry.
18
14
  type BlockRegistry = {
19
15
  [K in AnyBlock['_type']]: React.ComponentType<{
20
16
  id: string
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/cli",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.20.4",
5
+ "version": "3.21.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },