@byline/core 4.11.2 → 4.12.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.
@@ -155,7 +155,7 @@ export interface FieldAdminConfig {
155
155
  * Per-field rich-text editor component override. When set on a
156
156
  * `type: 'richText'` field's admin entry, the framework renders this
157
157
  * component instead of the globally registered
158
- * `ClientConfig.fields.richText.editor`. Use to opt one specific field
158
+ * `AdminConfig.fields.richText.editor`. Use to opt one specific field
159
159
  * into an alternate editor (e.g. an AI-enabled wrapper around the
160
160
  * default Lexical field) without changing the site-wide registration.
161
161
  *
@@ -225,8 +225,6 @@ export interface CollectionAdminConfig<T = any> {
225
225
  * formatters are shared with the list view. Omit to fall back to a
226
226
  * single-line render of `useAsTitle` + `path`.
227
227
  *
228
- * Resolve it through `resolveItemViewColumns(config)` rather than reading
229
- * the field directly, so the deprecated `picker` alias keeps working.
230
228
  */
231
229
  itemView?: ColumnDefinition<T>[];
232
230
  /**
@@ -248,12 +246,6 @@ export interface CollectionAdminConfig<T = any> {
248
246
  * omitted.
249
247
  */
250
248
  itemViewSort?: ListDefaultSort<T>;
251
- /**
252
- * @deprecated Renamed to {@link itemView}. Kept as a backwards-compatible
253
- * alias — `itemView` wins when both are present. Read both via
254
- * `resolveItemViewColumns(config)`. Will be removed in a future major.
255
- */
256
- picker?: ColumnDefinition<T>[];
257
249
  /** Default columns to show when no explicit column config is provided. */
258
250
  defaultColumns?: string[];
259
251
  /** Named tab components. Each set is one tab bar. */
@@ -275,7 +267,7 @@ export interface CollectionAdminConfig<T = any> {
275
267
  * structure fields (`files.filesGroup.publicationFile`). Paths address
276
268
  * field *declarations*, never item instances (no `[0]` indices), and never
277
269
  * traverse a `blocks` field — blocks take their overrides from the
278
- * blockType-keyed `ClientConfig.blockAdmin` registry instead.
270
+ * blockType-keyed `AdminConfig.blockAdmin` registry instead.
279
271
  * Placement is no longer expressed here — see the layout primitives above.
280
272
  */
281
273
  fields?: Record<string, FieldAdminConfig>;
@@ -329,9 +321,11 @@ export interface CollectionAdminConfig<T = any> {
329
321
  * }
330
322
  * ```
331
323
  *
332
- * Returned URLs may be relative (`/news/foo`) for same-origin hosts
333
- * or absolute (`https://example.com/news/foo`) for hosts deployed
334
- * separately from the admin.
324
+ * Returned URLs may be relative (`/news/foo`) for same-origin hosts or
325
+ * absolute (`https://example.com/news/foo`) for navigation to another
326
+ * origin. An absolute URL does not transfer Byline's host-only admin and
327
+ * preview cookies; cross-origin draft preview therefore requires a separate
328
+ * authentication and preview-state handoff.
335
329
  *
336
330
  * Future consideration — a per-collection `preview.populate` hint
337
331
  * (`PopulateSpec`) was prototyped and removed. The edit-view loader
@@ -403,7 +397,7 @@ export declare function defineAdmin<T = any>(schema: CollectionDefinition, confi
403
397
  * `CollectionAdminConfig`, carrying **rendering overrides only**.
404
398
  *
405
399
  * Linked to a `Block` by `blockType` matching the block's `blockType`, and
406
- * registered site-wide on `ClientConfig.blockAdmin`. Because blocks are
400
+ * registered site-wide on `AdminConfig.blockAdmin`. Because blocks are
407
401
  * cross-collection units (the same `defineBlock()` object is typically shared
408
402
  * by several collections, and codegen dedupes their contracts structurally),
409
403
  * a block's admin config applies wherever the block renders. If one
@@ -8,7 +8,7 @@
8
8
  import type { RequestContext } from '@byline/auth';
9
9
  import type { ReadContext } from './db-types.js';
10
10
  import type { FieldSetData, FieldSetDataAllLocales, StoredFileValue } from './field-data-types.js';
11
- import type { Block, DefaultValue, Field, FileField, ImageField } from './field-types.js';
11
+ import type { Block, Field, FileField, ImageField } from './field-types.js';
12
12
  import type { QueryPredicate } from './query-predicate.js';
13
13
  import type { SearchFieldDecl } from './search-types.js';
14
14
  import type { IStorageProvider } from './storage-types.js';
@@ -1340,60 +1340,3 @@ export type CollectionDataAllLocales<C extends CollectionDefinition> = Prettify<
1340
1340
  created_at: Date;
1341
1341
  updated_at: Date;
1342
1342
  } & CollectionFieldDataAllLocales<C>>;
1343
- /**
1344
- * A field definition with all function-valued properties stripped.
1345
- *
1346
- * Safe for JSON serialization — use for API responses, SSR loader return
1347
- * values, RSC props, mobile clients, and CLI introspection. The following
1348
- * are omitted:
1349
- * - `validate` — client UI concern, cannot cross a network boundary
1350
- * - `hooks` — field-level hooks are always functions
1351
- * - `defaultValue` — only literal (non-function) defaults are preserved
1352
- *
1353
- * Nested `fields` (composite / array / blocks) are recursively serialized.
1354
- */
1355
- export type SerializableField = Omit<Field, 'validate' | 'hooks' | 'defaultValue' | 'fields' | 'blocks'> & {
1356
- /** Only literal defaults are serializable; function defaults are dropped. */
1357
- defaultValue?: Exclude<DefaultValue, (...args: any[]) => any>;
1358
- /** Recursively serializable child fields (group / array). */
1359
- fields?: SerializableField[];
1360
- /** Recursively serializable blocks (blocks field). */
1361
- blocks?: SerializableBlock[];
1362
- };
1363
- /**
1364
- * A block definition with all function-valued properties stripped.
1365
- */
1366
- export type SerializableBlock = Omit<Block, 'validate' | 'hooks' | 'fields'> & {
1367
- fields: SerializableField[];
1368
- };
1369
- /**
1370
- * A collection definition with all function-valued properties stripped.
1371
- *
1372
- * Safe for JSON serialization — use for API schema endpoints, SSR loaders,
1373
- * RSC components, mobile clients, and any context where the full definition
1374
- * (with live functions) cannot be transmitted.
1375
- *
1376
- * - Collection-level `hooks` are entirely omitted (all entries are functions).
1377
- * - Field `validate`, `hooks`, and function `defaultValue` are stripped.
1378
- *
1379
- * On the receiving end, resolve the full `CollectionDefinition` from the
1380
- * local config store via `getCollectionDefinition(path)` to regain access
1381
- * to validators, hooks, and computed defaults.
1382
- */
1383
- export type SerializableCollectionDefinition = Omit<CollectionDefinition, 'hooks' | 'fields'> & {
1384
- fields: SerializableField[];
1385
- };
1386
- /**
1387
- * Strips all function-valued properties from a `CollectionDefinition`,
1388
- * producing a version safe for JSON serialization.
1389
- *
1390
- * @example
1391
- * ```ts
1392
- * // In an API route:
1393
- * return Response.json(toSerializableCollection(collectionDef))
1394
- *
1395
- * // In an SSR loader:
1396
- * return { schema: toSerializableCollection(collectionDef), document: data }
1397
- * ```
1398
- */
1399
- export declare function toSerializableCollection(def: CollectionDefinition): SerializableCollectionDefinition;
@@ -277,48 +277,3 @@ export function defineBlock(definition) {
277
277
  export function defineField(definition) {
278
278
  return definition;
279
279
  }
280
- /**
281
- * Strips all function-valued properties from a `CollectionDefinition`,
282
- * producing a version safe for JSON serialization.
283
- *
284
- * @example
285
- * ```ts
286
- * // In an API route:
287
- * return Response.json(toSerializableCollection(collectionDef))
288
- *
289
- * // In an SSR loader:
290
- * return { schema: toSerializableCollection(collectionDef), document: data }
291
- * ```
292
- */
293
- export function toSerializableCollection(def) {
294
- function serializeField(field) {
295
- // biome-ignore lint/suspicious/noExplicitAny: intentional structural spread
296
- const { validate: _v, hooks: _h, defaultValue, fields, blocks, ...rest } = field;
297
- const serialized = { ...rest };
298
- // Keep defaultValue only when it is a literal (not a factory function)
299
- if (defaultValue !== undefined && typeof defaultValue !== 'function') {
300
- serialized.defaultValue = defaultValue;
301
- }
302
- // Recurse into nested child fields (group / array)
303
- if (Array.isArray(fields)) {
304
- serialized.fields = fields.map(serializeField);
305
- }
306
- // Recurse into blocks (blocks field)
307
- if (Array.isArray(blocks)) {
308
- serialized.blocks = blocks.map(serializeBlock);
309
- }
310
- return serialized;
311
- }
312
- function serializeBlock(block) {
313
- const { validate: _v, hooks: _h, fields, ...rest } = block;
314
- return {
315
- ...rest,
316
- fields: fields.map(serializeField),
317
- };
318
- }
319
- const { hooks: _hooks, fields, ...rest } = def;
320
- return {
321
- ...rest,
322
- fields: fields.map(serializeField),
323
- };
324
- }
@@ -1,7 +1,6 @@
1
1
  import type { RequestContext } from '@byline/auth';
2
2
  import type { CollectionDefinition } from '@byline/core';
3
3
  import type { DbErrorClassification } from '../lib/errors.js';
4
- import type { QueryPredicate } from './query-predicate.js';
5
4
  /**
6
5
  * Read mode for document queries.
7
6
  *
@@ -61,11 +60,6 @@ export interface ReadContext {
61
60
  * (the cycle-stub path).
62
61
  */
63
62
  visited: Set<string>;
64
- /**
65
- * @deprecated Ignored. `beforeRead` authorization state is module-private
66
- * and authority-bound; this optional slot remains only for source compatibility.
67
- */
68
- beforeReadCache?: Map<string, QueryPredicate | null>;
69
63
  /** Monotonic count of document materialisations; compared against `maxReads`. */
70
64
  readCount: number;
71
65
  /** Hard ceiling on materialisations per request. Default 500. */
@@ -715,7 +715,7 @@ export interface FieldComponentSlots {
715
715
  *
716
716
  * Editor packages (e.g. `@byline/richtext-lexical`) expose a component
717
717
  * matching this shape, and the installation registers it as the
718
- * site-wide default via `ClientConfig.fields.richText.editor`.
718
+ * site-wide default via `AdminConfig.fields.richText.editor`.
719
719
  */
720
720
  export interface RichTextEditorProps {
721
721
  /** The field definition (label / helpText / optional / hooks / validate). */
@@ -733,7 +733,7 @@ export interface RichTextEditorProps {
733
733
  }
734
734
  /**
735
735
  * Component shape an editor package must export to be plugged in via
736
- * `ClientConfig.fields.richText.editor`. Built on `SlotComponent` so the
736
+ * `AdminConfig.fields.richText.editor`. Built on `SlotComponent` so the
737
737
  * type stays React-agnostic at the `@byline/core` boundary.
738
738
  */
739
739
  export type RichTextEditorComponent = SlotComponent<RichTextEditorProps>;
@@ -33,14 +33,12 @@ export interface RoutesConfigInput {
33
33
  signIn?: string;
34
34
  }
35
35
  /**
36
- * Common configuration shared by the server and client. Contains only
37
- * serializable, framework-agnostic properties no React components, no
38
- * database adapters, no storage providers.
36
+ * Common live configuration shared by the server and admin client. This is an
37
+ * in-process runtime contract, not a JSON or transport representation.
39
38
  */
40
39
  export interface BaseConfig {
41
- serverURL: string;
42
40
  i18n: {
43
- interface: {
41
+ admin: {
44
42
  defaultLocale: string;
45
43
  locales: string[];
46
44
  /**
@@ -80,7 +78,7 @@ export interface BaseConfig {
80
78
  locales: string[];
81
79
  /**
82
80
  * Optional display names for the content locales a document can be
83
- * published in. Mirrors `interface.localeDefinitions`, but for the
81
+ * published in. Mirrors `admin.localeDefinitions`, but for the
84
82
  * *content* dimension rather than the admin chrome.
85
83
  *
86
84
  * Byline itself does not render these — the content-locale set has
@@ -109,8 +107,8 @@ export interface BaseConfig {
109
107
  *
110
108
  * Optional at the type level so `BaseConfig` stays loose for tests
111
109
  * and seed scripts; required at runtime via `validateTranslations`
112
- * whenever `interface.locales` is non-empty. See `docs/08-internationalization/index.md` for
113
- * the design.
110
+ * whenever `admin.locales` is non-empty. See
111
+ * `docs/08-internationalization/index.md` for the design.
114
112
  *
115
113
  * The shape is declared inline (rather than imported from
116
114
  * `@byline/i18n`) so `@byline/core` stays a leaf-ish package. The
@@ -140,12 +138,12 @@ export type TranslationBundleShape = Readonly<{
140
138
  }>;
141
139
  }>;
142
140
  /**
143
- * Client-side configuration. Extends BaseConfig with admin UI presentation
141
+ * Admin configuration. Extends BaseConfig with admin UI presentation
144
142
  * config (React components, formatters, column definitions, etc.).
145
143
  *
146
- * Used by `defineClientConfig()` and consumed by admin UI routes.
144
+ * Used by `defineAdminConfig()` and consumed by admin UI routes.
147
145
  */
148
- export interface ClientConfig extends BaseConfig {
146
+ export interface AdminConfig extends BaseConfig {
149
147
  /** Admin UI configuration for collections (client-side only). */
150
148
  admin?: CollectionAdminConfig[];
151
149
  /**
@@ -169,8 +167,9 @@ export interface ClientConfig extends BaseConfig {
169
167
  * back to the default `slugify` from `@byline/core` when not set — so
170
168
  * installations that keep the default slugifier need not set this at all.
171
169
  *
172
- * Lives on `ClientConfig` rather than `BaseConfig` because it is a function
173
- * (not serialisable), and `BaseConfig` is contractually serialisable.
170
+ * Lives on `AdminConfig` because only the admin path widget consumes this
171
+ * client-side copy. `ServerConfig` declares the authoritative server copy
172
+ * separately.
174
173
  *
175
174
  * @see ServerConfig.slugifier
176
175
  */
@@ -184,7 +183,7 @@ export interface ClientConfig extends BaseConfig {
184
183
  * ```ts
185
184
  * import { RichTextField } from '@byline/richtext-lexical'
186
185
  *
187
- * defineClientConfig({
186
+ * defineAdminConfig({
188
187
  * // ...
189
188
  * fields: {
190
189
  * richText: { editor: RichTextField },
@@ -203,8 +202,8 @@ export interface ClientConfig extends BaseConfig {
203
202
  };
204
203
  };
205
204
  }
206
- /** Client config returned after boundary validation and canonicalization. */
207
- export type ResolvedClientConfig = Omit<ClientConfig, 'routes'> & {
205
+ /** Admin config returned after boundary validation and canonicalization. */
206
+ export type ResolvedAdminConfig = Omit<AdminConfig, 'routes'> & {
208
207
  routes: RoutesConfig;
209
208
  };
210
209
  /**
@@ -230,7 +229,7 @@ export interface ServerHooksConfig {
230
229
  }
231
230
  /**
232
231
  * Server-side configuration. Extends BaseConfig with database and storage
233
- * adapters. Deliberately does NOT extend ClientConfig — the server has no
232
+ * adapters. Deliberately does NOT extend AdminConfig — the server has no
234
233
  * knowledge of React components or admin UI presentation logic.
235
234
  *
236
235
  * Generic over `TAdminStore` so installations can thread an adapter-built
@@ -280,8 +279,8 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
280
279
  *
281
280
  * This is the **server-side** copy (the authoritative one — its output is
282
281
  * what gets persisted). For the admin path-widget's live preview to match,
283
- * register the *same* function on `ClientConfig.slugifier` via
284
- * `defineClientConfig`. See {@link ClientConfig.slugifier}.
282
+ * register the *same* function on `AdminConfig.slugifier` via
283
+ * `defineAdminConfig`. See {@link AdminConfig.slugifier}.
285
284
  */
286
285
  slugifier?: SlugifierFn;
287
286
  /**
@@ -353,7 +352,7 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
353
352
  adminStore?: TAdminStore;
354
353
  /**
355
354
  * Site-wide field-level server adapter slots. Mirrors
356
- * `ClientConfig.fields` for the server side — each entry plugs an
355
+ * `AdminConfig.fields` for the server side — each entry plugs an
357
356
  * adapter package into a framework-managed read or write phase.
358
357
  *
359
358
  * @example
@@ -5,7 +5,6 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { UnifiedFieldValue } from '../storage/storage-row-types.js';
9
8
  export interface BaseStore {
10
9
  field_path: string;
11
10
  field_name: string;
@@ -90,8 +89,3 @@ export declare function isRelationStore(fieldValue: FlattenedStore): fieldValue
90
89
  export declare function isJsonStore(fieldValue: FlattenedStore): fieldValue is JsonStore;
91
90
  export declare function isNumericStore(fieldValue: FlattenedStore): fieldValue is NumericStore;
92
91
  export declare function isDateTimeStore(fieldValue: FlattenedStore): fieldValue is DateTimeStore;
93
- /**
94
- * @deprecated Use {@link UnifiedFieldValue} from '@byline/core' — same shape;
95
- * this alias remains for source compatibility.
96
- */
97
- export type UnionRowValue = UnifiedFieldValue;
@@ -102,36 +102,6 @@ describe('applyBeforeRead', () => {
102
102
  expect(second).toBeNull();
103
103
  expect(callCount).toBe(1);
104
104
  });
105
- it('ignores a caller-preseeded public cache entry', async () => {
106
- const hook = vi.fn(() => ({ title: 'allowed' }));
107
- const readContext = createReadContext({
108
- beforeReadCache: new Map([['posts:any', null]]),
109
- });
110
- const result = await applyBeforeRead({
111
- definition: baseCollection(hook),
112
- requestContext: createRequestContext(),
113
- readContext,
114
- });
115
- expect(hook).toHaveBeenCalledOnce();
116
- expect(result).toEqual({ title: 'allowed' });
117
- });
118
- it('ignores a caller-preseeded public cache entry on the compiled-filter path', async () => {
119
- const hook = vi.fn(() => ({ title: 'allowed' }));
120
- const readContext = createReadContext({
121
- beforeReadCache: new Map([['posts:any', null]]),
122
- });
123
- const filters = await compileBeforeReadFilters({
124
- definition: baseCollection(hook),
125
- requestContext: createRequestContext(),
126
- readContext,
127
- securityDomain: {},
128
- parseContext: { collections: [], resolveCollectionId: vi.fn() },
129
- });
130
- expect(hook).toHaveBeenCalledOnce();
131
- expect(filters).toEqual([
132
- expect.objectContaining({ kind: 'field', fieldName: 'title', value: 'allowed' }),
133
- ]);
134
- });
135
105
  it('rejects reuse under a different actor authority', async () => {
136
106
  const hook = vi.fn(({ requestContext }) => ({
137
107
  title: requestContext.actor?.id,
@@ -22,11 +22,6 @@ export function createHookReadContext(parent, entry) {
22
22
  const scoped = {};
23
23
  Object.defineProperties(scoped, {
24
24
  visited: { enumerable: true, get: () => root.visited, set: (value) => (root.visited = value) },
25
- beforeReadCache: {
26
- enumerable: true,
27
- get: () => root.beforeReadCache,
28
- set: (value) => (root.beforeReadCache = value),
29
- },
30
25
  readCount: {
31
26
  enumerable: true,
32
27
  get: () => root.readCount,
@@ -137,12 +137,11 @@ describe('server hook attachment', () => {
137
137
  hooks: { collections: { documents: stable } },
138
138
  }));
139
139
  const invalid = {
140
- serverURL: 'https://example.test',
141
140
  collections: [definition],
142
141
  db: {},
143
142
  hooks: { collections: { documents: replacement } },
144
143
  i18n: {
145
- interface: { defaultLocale: 'en', locales: ['en'] },
144
+ admin: { defaultLocale: 'en', locales: ['en'] },
146
145
  content: { defaultLocale: 'en', locales: [] },
147
146
  },
148
147
  };
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
- import { defineClientConfig, defineServerConfig, getCollectionDefinition } from './config.js';
2
+ import { defineAdminConfig, defineServerConfig, getCollectionDefinition } from './config.js';
3
3
  const SERVER_CONFIG = Symbol.for('__byline_server_config__');
4
4
  const CLIENT_CONFIG = Symbol.for('__byline_client_config__');
5
5
  const previousServer = globalThis[SERVER_CONFIG];
@@ -23,16 +23,16 @@ function definition(path) {
23
23
  };
24
24
  }
25
25
  const i18n = {
26
- interface: { defaultLocale: 'en', locales: [] },
26
+ admin: { defaultLocale: 'en', locales: [] },
27
27
  content: { defaultLocale: 'en', locales: [] },
28
28
  };
29
29
  describe('collection definition config preference', () => {
30
- it('uses client definitions when only client config is registered', () => {
30
+ it('uses client definitions when only admin config is registered', () => {
31
31
  const globals = globalThis;
32
32
  delete globals[SERVER_CONFIG];
33
33
  delete globals[CLIENT_CONFIG];
34
34
  const client = definition('client-only');
35
- defineClientConfig({ serverURL: 'https://example.test', collections: [client], i18n });
35
+ defineAdminConfig({ collections: [client], i18n });
36
36
  expect(getCollectionDefinition('client-only')).toBe(client);
37
37
  });
38
38
  it('prefers hook-attached server definitions when both configs are registered', () => {
@@ -42,9 +42,8 @@ describe('collection definition config preference', () => {
42
42
  const client = definition('docs');
43
43
  const server = definition('docs');
44
44
  const hooks = {};
45
- defineClientConfig({ serverURL: 'https://example.test', collections: [client], i18n });
45
+ defineAdminConfig({ collections: [client], i18n });
46
46
  defineServerConfig({
47
- serverURL: 'https://example.test',
48
47
  collections: [server],
49
48
  db: {},
50
49
  hooks: { collections: { docs: hooks } },
@@ -1,4 +1,4 @@
1
- import type { ClientConfig, CollectionAdminConfig, CollectionDefinition, ColumnDefinition, ResolvedClientConfig, ResolvedServerConfig, ServerConfig } from '../@types/index.js';
1
+ import type { AdminConfig, CollectionAdminConfig, CollectionDefinition, ResolvedAdminConfig, ResolvedServerConfig, ServerConfig } from '../@types/index.js';
2
2
  /**
3
3
  * Resolve a collection definition by `path`. Returns `null` either when
4
4
  * no config has been registered (e.g. unit tests, isolated tooling) or
@@ -7,29 +7,18 @@ import type { ClientConfig, CollectionAdminConfig, CollectionDefinition, ColumnD
7
7
  * without try/catch.
8
8
  *
9
9
  * If a caller genuinely *requires* a registered config to proceed, it
10
- * should reach for `getClientConfig()` / `getServerConfig()` — those
10
+ * should reach for `getAdminConfig()` / `getServerConfig()` — those
11
11
  * still throw the loud "Byline has not been configured" error.
12
12
  */
13
13
  export declare const getCollectionDefinition: (path: string) => CollectionDefinition | null;
14
14
  export declare const getCollectionAdminConfig: (slug: string) => CollectionAdminConfig | null;
15
- /**
16
- * Resolve a collection's item-row/tile columns — the per-collection projection
17
- * + presentation contract used by the relation picker, relation/`hasMany`
18
- * tiles, and (planned) search-result rows.
19
- *
20
- * Prefers the canonical {@link CollectionAdminConfig.itemView}, falling back to
21
- * the deprecated `picker` alias. Always read item-view columns through this
22
- * helper rather than touching `config.picker` directly, so the alias keeps
23
- * working until it is removed.
24
- */
25
- export declare const resolveItemViewColumns: (config: CollectionAdminConfig | null | undefined) => ColumnDefinition[] | undefined;
26
- export declare function defineClientConfig(config: ClientConfig): ResolvedClientConfig;
15
+ export declare function defineAdminConfig(config: AdminConfig): ResolvedAdminConfig;
27
16
  export declare function defineServerConfig<TAdminStore = unknown>(config: ServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
28
17
  /** Resolve and validate server config without replacing the registered singleton. */
29
18
  export declare function resolveServerConfig<TAdminStore = unknown>(config: ServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
30
19
  /** Internal commit step used after initialization has completed successfully. */
31
20
  export declare function registerServerConfig<TAdminStore = unknown>(config: ResolvedServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
32
- export declare function getClientConfig(): ResolvedClientConfig;
21
+ export declare function getAdminConfig(): ResolvedAdminConfig;
33
22
  export declare function getServerConfig(): ResolvedServerConfig;
34
23
  /**
35
24
  * Order a set of locale codes by their position in the configured content
@@ -10,7 +10,7 @@ import { validateCollections } from './validate-collections.js';
10
10
  // through different module graphs) shares the same state.
11
11
  // ---------------------------------------------------------------------------
12
12
  const BYLINE_SERVER_CONFIG = Symbol.for('__byline_server_config__');
13
- const BYLINE_CLIENT_CONFIG = Symbol.for('__byline_client_config__');
13
+ const BYLINE_ADMIN_CONFIG = Symbol.for('__byline_admin_config__');
14
14
  const BYLINE_CORE = Symbol.for('__byline_core__');
15
15
  const preparedHookAttachments = new WeakMap();
16
16
  function getServerConfigInstance() {
@@ -20,12 +20,12 @@ function setServerConfigInstance(config) {
20
20
  ;
21
21
  globalThis[BYLINE_SERVER_CONFIG] = config;
22
22
  }
23
- function getClientConfigInstance() {
24
- return globalThis[BYLINE_CLIENT_CONFIG] ?? null;
23
+ function getAdminConfigInstance() {
24
+ return globalThis[BYLINE_ADMIN_CONFIG] ?? null;
25
25
  }
26
- function setClientConfigInstance(config) {
26
+ function setAdminConfigInstance(config) {
27
27
  ;
28
- globalThis[BYLINE_CLIENT_CONFIG] = config;
28
+ globalThis[BYLINE_ADMIN_CONFIG] = config;
29
29
  }
30
30
  function getBylineCoreInstance() {
31
31
  return globalThis[BYLINE_CORE] ?? null;
@@ -42,38 +42,27 @@ function setBylineCoreInstance(core) {
42
42
  * without try/catch.
43
43
  *
44
44
  * If a caller genuinely *requires* a registered config to proceed, it
45
- * should reach for `getClientConfig()` / `getServerConfig()` — those
45
+ * should reach for `getAdminConfig()` / `getServerConfig()` — those
46
46
  * still throw the loud "Byline has not been configured" error.
47
47
  */
48
48
  export const getCollectionDefinition = (path) => {
49
- const config = getServerConfigInstance() ?? getClientConfigInstance();
49
+ const config = getServerConfigInstance() ?? getAdminConfigInstance();
50
50
  if (config == null)
51
51
  return null;
52
52
  return config.collections.find((collection) => collection.path === path) ?? null;
53
53
  };
54
54
  export const getCollectionAdminConfig = (slug) => {
55
- const clientConfig = getClientConfigInstance();
56
- if (clientConfig == null)
55
+ const adminConfig = getAdminConfigInstance();
56
+ if (adminConfig == null)
57
57
  return null;
58
- return clientConfig.admin?.find((admin) => admin.slug === slug) ?? null;
58
+ return adminConfig.admin?.find((admin) => admin.slug === slug) ?? null;
59
59
  };
60
- /**
61
- * Resolve a collection's item-row/tile columns — the per-collection projection
62
- * + presentation contract used by the relation picker, relation/`hasMany`
63
- * tiles, and (planned) search-result rows.
64
- *
65
- * Prefers the canonical {@link CollectionAdminConfig.itemView}, falling back to
66
- * the deprecated `picker` alias. Always read item-view columns through this
67
- * helper rather than touching `config.picker` directly, so the alias keeps
68
- * working until it is removed.
69
- */
70
- export const resolveItemViewColumns = (config) => config?.itemView ?? config?.picker;
71
- export function defineClientConfig(config) {
60
+ export function defineAdminConfig(config) {
72
61
  validateCollections(config.collections);
73
62
  validateAdminConfigs(config.admin, config.collections);
74
63
  validateBlockAdminConfigs(config.blockAdmin, config.collections);
75
64
  const resolved = { ...config, routes: resolveRoutes(config.routes) };
76
- setClientConfigInstance(resolved);
65
+ setAdminConfigInstance(resolved);
77
66
  return resolved;
78
67
  }
79
68
  export function defineServerConfig(config) {
@@ -93,18 +82,18 @@ export function registerServerConfig(config) {
93
82
  setServerConfigInstance(config);
94
83
  return config;
95
84
  }
96
- export function getClientConfig() {
97
- const clientConfig = getClientConfigInstance();
98
- if (clientConfig != null) {
99
- return clientConfig;
85
+ export function getAdminConfig() {
86
+ const adminConfig = getAdminConfigInstance();
87
+ if (adminConfig != null) {
88
+ return adminConfig;
100
89
  }
101
- // During SSR the client entry has not run yet, but the server config
102
- // carries the same collection definitions. Return a compatible object
103
- // so route loaders and components work in both contexts.
90
+ // During SSR the admin entry has not run yet, but the server config carries
91
+ // the shared i18n, route, collection, and slugifier values. Return a
92
+ // compatible object with empty admin presentation so route loaders and
93
+ // components work in both contexts without exposing the browser-only graph.
104
94
  const serverConfig = getServerConfigInstance();
105
95
  if (serverConfig != null) {
106
96
  return {
107
- serverURL: serverConfig.serverURL,
108
97
  i18n: serverConfig.i18n,
109
98
  routes: serverConfig.routes,
110
99
  collections: serverConfig.collections,
@@ -115,7 +104,7 @@ export function getClientConfig() {
115
104
  slugifier: serverConfig.slugifier,
116
105
  };
117
106
  }
118
- throw new Error('Byline has not been configured yet. Please call defineClientConfig in byline.config.ts first.');
107
+ throw new Error('Byline has not been configured yet. Please call defineAdminConfig in byline.config.ts first.');
119
108
  }
120
109
  export function getServerConfig() {
121
110
  if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
@@ -19,9 +19,8 @@ describe('orderByContentLocale', () => {
19
19
  // Only `collections` is validated by `defineServerConfig`; the rest of
20
20
  // the shape is irrelevant to this helper, so a minimal cast is enough.
21
21
  defineServerConfig({
22
- serverURL: 'http://test.local',
23
22
  i18n: {
24
- interface: { defaultLocale: 'en', locales: ['en'] },
23
+ admin: { defaultLocale: 'en', locales: ['en'] },
25
24
  content: { defaultLocale: 'en', locales: ['en', 'fr', 'es', 'de'] },
26
25
  },
27
26
  collections: [],
@@ -60,11 +59,10 @@ describe('orderByContentLocale', () => {
60
59
  describe('orderByContentLocale — boundary robustness', () => {
61
60
  beforeAll(() => {
62
61
  defineServerConfig({
63
- serverURL: 'http://test.local',
64
62
  i18n: {
65
63
  // Interface set (`en`, `de`) deliberately overlaps content only on `en`
66
64
  // — `de` is interface-only here, NOT a content locale.
67
- interface: { defaultLocale: 'en', locales: ['en', 'de'] },
65
+ admin: { defaultLocale: 'en', locales: ['en', 'de'] },
68
66
  content: {
69
67
  defaultLocale: 'en',
70
68
  locales: ['en', 'fr', 'es'],
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { normalizeRootRelativeRedirect } from '../utils/root-relative-redirect.js';
3
- import { defineClientConfig, getClientConfig } from './config.js';
3
+ import { defineAdminConfig, getAdminConfig } from './config.js';
4
4
  import { resolveRoutes } from './routes.js';
5
5
  describe('resolveRoutes', () => {
6
6
  it('returns canonical defaults', () => {
@@ -95,13 +95,12 @@ describe('normalizeRootRelativeRedirect', () => {
95
95
  describe('route configuration boundary', () => {
96
96
  it('stores and exposes resolved routes independently of partial input', () => {
97
97
  const input = { admin: 'internal/cms/' };
98
- const registered = defineClientConfig({
99
- serverURL: 'https://example.test',
98
+ const registered = defineAdminConfig({
100
99
  routes: input,
101
100
  collections: [],
102
101
  admin: [],
103
102
  i18n: {
104
- interface: { defaultLocale: 'en', locales: [] },
103
+ admin: { defaultLocale: 'en', locales: [] },
105
104
  content: { defaultLocale: 'en', locales: [] },
106
105
  },
107
106
  });
@@ -111,17 +110,16 @@ describe('route configuration boundary', () => {
111
110
  api: '/api',
112
111
  signIn: '/sign-in',
113
112
  });
114
- expect(getClientConfig().routes).toBe(registered.routes);
113
+ expect(getAdminConfig().routes).toBe(registered.routes);
115
114
  expect(Object.isFrozen(registered.routes)).toBe(true);
116
115
  });
117
116
  it('prevents post-registration route mutation', () => {
118
- const { routes } = defineClientConfig({
119
- serverURL: 'https://example.test',
117
+ const { routes } = defineAdminConfig({
120
118
  routes: { admin: '/internal/cms' },
121
119
  collections: [],
122
120
  admin: [],
123
121
  i18n: {
124
- interface: { defaultLocale: 'en', locales: [] },
122
+ admin: { defaultLocale: 'en', locales: [] },
125
123
  content: { defaultLocale: 'en', locales: [] },
126
124
  },
127
125
  });
@@ -129,16 +127,15 @@ describe('route configuration boundary', () => {
129
127
  ;
130
128
  routes.admin = '/changed';
131
129
  }).toThrow(TypeError);
132
- expect(getClientConfig().routes.admin).toBe('/internal/cms');
130
+ expect(getAdminConfig().routes.admin).toBe('/internal/cms');
133
131
  });
134
132
  it('rejects unsafe routes during registration', () => {
135
- expect(() => defineClientConfig({
136
- serverURL: 'https://example.test',
133
+ expect(() => defineAdminConfig({
137
134
  routes: { admin: '/cms?next=/account' },
138
135
  collections: [],
139
136
  admin: [],
140
137
  i18n: {
141
- interface: { defaultLocale: 'en', locales: [] },
138
+ admin: { defaultLocale: 'en', locales: [] },
142
139
  content: { defaultLocale: 'en', locales: [] },
143
140
  },
144
141
  })).toThrow(/routes\.admin/);
@@ -39,7 +39,7 @@ export declare function validateAdminConfigs(admins: readonly CollectionAdminCon
39
39
  /**
40
40
  * Validate every block admin config in a configuration.
41
41
  *
42
- * Enforced rules (per `ClientConfig.blockAdmin` entry):
42
+ * Enforced rules (per `AdminConfig.blockAdmin` entry):
43
43
  * 1. Block pairing — `blockType` matches a block declared on at least one
44
44
  * `type: 'blocks'` field across the registered collections (blocks have
45
45
  * no global registry; the collections walk is the source of truth).
@@ -81,7 +81,7 @@ export function validateAdminConfigs(admins, collections) {
81
81
  /**
82
82
  * Validate every block admin config in a configuration.
83
83
  *
84
- * Enforced rules (per `ClientConfig.blockAdmin` entry):
84
+ * Enforced rules (per `AdminConfig.blockAdmin` entry):
85
85
  * 1. Block pairing — `blockType` matches a block declared on at least one
86
86
  * `type: 'blocks'` field across the registered collections (blocks have
87
87
  * no global registry; the collections walk is the source of truth).
package/dist/core.js CHANGED
@@ -59,16 +59,16 @@ export const initBylineCore = async (config, pinoLogger) => {
59
59
  // lock, mutate, and append audit rows in one transaction.
60
60
  validateTreeAuditCapability(composed.collections, composed.db);
61
61
  // Validate the admin i18n translation registry against the configured
62
- // interface locale set. Throws on structural errors (missing bundle for
62
+ // admin locale set. Throws on structural errors (missing bundle for
63
63
  // a declared locale, defaultLocale outside the permitted set, …); soft
64
64
  // warnings (key-set drift between locales) are logged so contributors
65
65
  // see translation gaps without it blocking boot. The bundle lives at
66
- // `i18n.translations` (one axis, not nested under `interface`) — the
66
+ // `i18n.translations` (one axis, not nested under `admin`) — the
67
67
  // validator takes the assembled shape so locale config and bundle
68
68
  // travel together.
69
69
  const i18nValidation = validateTranslations({
70
- defaultLocale: resolvedConfig.i18n.interface.defaultLocale,
71
- locales: resolvedConfig.i18n.interface.locales,
70
+ defaultLocale: resolvedConfig.i18n.admin.defaultLocale,
71
+ locales: resolvedConfig.i18n.admin.locales,
72
72
  translations: resolvedConfig.i18n.translations,
73
73
  });
74
74
  for (const warning of i18nValidation.warnings) {
@@ -3,12 +3,11 @@ import { defineServerConfig, getServerConfig } from './config/config.js';
3
3
  import { initBylineCore } from './core.js';
4
4
  function serverConfig(admin) {
5
5
  return {
6
- serverURL: 'https://example.test',
7
6
  routes: { admin },
8
7
  collections: [],
9
8
  db: {},
10
9
  i18n: {
11
- interface: { defaultLocale: 'en', locales: [] },
10
+ admin: { defaultLocale: 'en', locales: [] },
12
11
  content: { defaultLocale: 'en', locales: [] },
13
12
  },
14
13
  };
@@ -18,7 +17,7 @@ describe('initBylineCore configuration registration', () => {
18
17
  const valid = defineServerConfig(serverConfig('/stable/admin'));
19
18
  const invalid = serverConfig('/replacement/admin');
20
19
  invalid.i18n = {
21
- interface: { defaultLocale: 'en', locales: ['en'] },
20
+ admin: { defaultLocale: 'en', locales: ['en'] },
22
21
  content: { defaultLocale: 'en', locales: [] },
23
22
  };
24
23
  await expect(initBylineCore(invalid, {})).rejects.toThrow(/translations bundle/i);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './@types/index.js';
2
2
  export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
3
- export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
3
+ export { defineAdminConfig, defineServerConfig, getAdminConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
4
4
  export { resolveRoutes } from './config/routes.js';
5
5
  export { validateAdminConfigs, validateBlockAdminConfigs, } from './config/validate-admin-configs.js';
6
6
  export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // ---------------------------------------------------------------------------
17
17
  export * from './@types/index.js';
18
18
  export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
19
- export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
19
+ export { defineAdminConfig, defineServerConfig, getAdminConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
20
20
  export { resolveRoutes } from './config/routes.js';
21
21
  export { validateAdminConfigs, validateBlockAdminConfigs, } from './config/validate-admin-configs.js';
22
22
  export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
@@ -25,7 +25,7 @@
25
25
  * normal during community-contributor flow.
26
26
  */
27
27
  import type { TranslationBundleShape } from '../@types/site-config.js';
28
- export interface InterfaceI18nConfig {
28
+ export interface AdminI18nConfig {
29
29
  defaultLocale: string;
30
30
  locales: string[];
31
31
  translations?: TranslationBundleShape;
@@ -45,4 +45,4 @@ export interface ValidateTranslationsResult {
45
45
  * locale set. Throws on any structural error; returns soft warnings for
46
46
  * key-set drift between locales.
47
47
  */
48
- export declare function validateTranslations(i18n: InterfaceI18nConfig): ValidateTranslationsResult;
48
+ export declare function validateTranslations(i18n: AdminI18nConfig): ValidateTranslationsResult;
@@ -15,7 +15,7 @@ export function validateTranslations(i18n) {
15
15
  const errors = [];
16
16
  // (1) Default must be in the permitted set.
17
17
  if (locales.length > 0 && !locales.includes(defaultLocale)) {
18
- errors.push(`defaultLocale '${defaultLocale}' is not in i18n.interface.locales [${locales.join(', ')}]. ` +
18
+ errors.push(`defaultLocale '${defaultLocale}' is not in i18n.admin.locales [${locales.join(', ')}]. ` +
19
19
  `Add it to locales, or change defaultLocale to one of the existing entries.`);
20
20
  }
21
21
  // No locales declared → skip the remaining checks. Hosts that don't
@@ -26,8 +26,8 @@ export function validateTranslations(i18n) {
26
26
  }
27
27
  // (2) Non-empty locale set requires a translations bundle.
28
28
  if (translations == null) {
29
- errors.push(`i18n.interface.locales declares [${locales.join(', ')}] but no translations bundle is registered. ` +
30
- `Pass one to defineClientConfig via i18n.translations — see @byline/i18n's adminTranslations() ` +
29
+ errors.push(`i18n.admin.locales declares [${locales.join(', ')}] but no translations bundle is registered. ` +
30
+ `Pass one to defineAdminConfig via i18n.translations — see @byline/i18n's adminTranslations() ` +
31
31
  `and mergeTranslations() helpers.`);
32
32
  if (errors.length > 0) {
33
33
  throw new Error(formatErrors(errors));
@@ -38,7 +38,7 @@ export function validateTranslations(i18n) {
38
38
  for (const locale of locales) {
39
39
  const localeBundle = translations[locale];
40
40
  if (localeBundle == null || namespaceKeyCount(localeBundle) === 0) {
41
- errors.push(`i18n.interface.locales includes '${locale}' but no translations are registered for it. ` +
41
+ errors.push(`i18n.admin.locales includes '${locale}' but no translations are registered for it. ` +
42
42
  `Either drop '${locale}' from locales or wire a community bundle (e.g. @byline/i18n-${locale}).`);
43
43
  }
44
44
  }
@@ -46,7 +46,7 @@ describe('validateTranslations — structural errors', () => {
46
46
  en: { 'byline-admin': { a: 'A' } },
47
47
  fr: { 'byline-admin': { a: 'A' } },
48
48
  },
49
- })).toThrow(/defaultLocale 'de' is not in i18n\.interface\.locales/);
49
+ })).toThrow(/defaultLocale 'de' is not in i18n\.admin\.locales/);
50
50
  });
51
51
  it('throws when locales is non-empty but no translations bundle is registered', () => {
52
52
  expect(() => validateTranslations({
@@ -8,7 +8,7 @@ export * from './document-lifecycle/index.js';
8
8
  export * from './document-read.js';
9
9
  export * from './document-to-markdown.js';
10
10
  export * from './field-upload.js';
11
- export { type InterfaceI18nConfig, type TranslationDriftWarning, type ValidateTranslationsResult, validateTranslations, } from './i18n-validator.js';
11
+ export { type AdminI18nConfig, type TranslationDriftWarning, type ValidateTranslationsResult, validateTranslations, } from './i18n-validator.js';
12
12
  export { type CanonicalNumericFieldType, type CanonicalNumericValue, isCanonicalNumericValue, normalizeNumericFields, normalizeNumericValue, } from './normalize-numeric-fields.js';
13
13
  export { type CycleRelationValue, createReadContext, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateOptions, type PopulateSpec, populateDocuments, type ReadContext, resolveIdentityField, type UnresolvedRelationValue, } from './populate.js';
14
14
  export { buildRelationSummaryPopulateMap, type RelationTargetResolver, resolveRelationProjection, } from './relation-projection.js';
@@ -5,6 +5,17 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
+ /**
9
+ * Relation projection helpers.
10
+ *
11
+ * Translate a source collection's relation fields into a `PopulateMap`
12
+ * projection that fetches just enough data from each target to render
13
+ * a relation-summary tile on the admin edit view (picker columns,
14
+ * `useAsTitle`, plus an optional source-side `displayField` override).
15
+ *
16
+ * Consumed by the admin webapp's edit-route server fn so relation tiles
17
+ * arrive pre-hydrated on first paint — no client-side fetch per relation.
18
+ */
8
19
  import type { CollectionAdminConfig, CollectionDefinition, FieldSet, RelationField } from '../@types/index.js';
9
20
  import type { PopulateMap } from './populate.js';
10
21
  /**
@@ -5,18 +5,6 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- /**
9
- * Relation projection helpers.
10
- *
11
- * Translate a source collection's relation fields into a `PopulateMap`
12
- * projection that fetches just enough data from each target to render
13
- * a relation-summary tile on the admin edit view (picker columns,
14
- * `useAsTitle`, plus an optional source-side `displayField` override).
15
- *
16
- * Consumed by the admin webapp's edit-route server fn so relation tiles
17
- * arrive pre-hydrated on first paint — no client-side fetch per relation.
18
- */
19
- import { resolveItemViewColumns } from '../config/config.js';
20
8
  /**
21
9
  * Union of the target-collection field names needed to render a
22
10
  * relation-summary tile for the given source relation field.
@@ -38,7 +26,7 @@ export function resolveRelationProjection(sourceField, targetDef, targetAdmin) {
38
26
  const firstText = targetDef?.fields.find((f) => f.type === 'text')?.name;
39
27
  if (firstText)
40
28
  out.add(firstText);
41
- const itemViewColumns = resolveItemViewColumns(targetAdmin);
29
+ const itemViewColumns = targetAdmin?.itemView;
42
30
  if (itemViewColumns) {
43
31
  for (const col of itemViewColumns) {
44
32
  const name = String(col.fieldName);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/core",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.11.2",
5
+ "version": "4.12.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -82,7 +82,7 @@
82
82
  "sharp": "^0.35.3",
83
83
  "uuid": "^14.0.1",
84
84
  "zod": "^4.4.3",
85
- "@byline/auth": "4.11.2"
85
+ "@byline/auth": "4.12.0"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@biomejs/biome": "2.5.6",