@byline/core 3.1.0 → 3.1.1

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.
@@ -478,6 +478,13 @@ export interface IDocumentQueries {
478
478
  * contexts without widening `getCurrentVersionMetadata`.
479
479
  *
480
480
  * Returns `null` when the document has no path row (or does not exist).
481
+ *
482
+ * Source-locale only: this resolves the single canonical slug, which is the
483
+ * only path row a document has today. When per-locale paths land (see
484
+ * docs/DOCUMENT-PATHS.md → "Phase — per-locale paths"), the write-side hook
485
+ * contexts that consume this must be enriched to carry the locale each path
486
+ * was derived under (or the full `locale → path` set) — a single canonical
487
+ * `path` is no longer sufficient for per-localised-URL cache/CDN purges.
481
488
  */
482
489
  getCurrentPath(params: {
483
490
  collection_id: string;
@@ -16,5 +16,28 @@ export declare function defineClientConfig(config: ClientConfig): void;
16
16
  export declare function defineServerConfig(config: ServerConfig): void;
17
17
  export declare function getClientConfig(): ClientConfig;
18
18
  export declare function getServerConfig(): ServerConfig;
19
+ /**
20
+ * Order a set of locale codes by their position in the configured content
21
+ * locale list. The order source is `i18n.content.locales` — the authoritative,
22
+ * always-complete configured set — **not** `i18n.content.localeDefinitions`,
23
+ * which is an optional labels overlay a host may provide for only *some*
24
+ * codes (ordering off it would drop unlabelled content locales to the end).
25
+ *
26
+ * Codes absent from that order fall to the end, ordered alphabetically among
27
+ * themselves, so the result is always deterministic and never throws. This is
28
+ * deliberately origin-agnostic: a code that isn't a configured content
29
+ * locale — an interface-only locale, a stale/removed code, a typo — is
30
+ * preserved and sorted last rather than dropped or thrown. The function only
31
+ * sorts; it never filters, so set membership is never changed. The same holds
32
+ * when no server config is registered (the order is empty → plain a–z sort).
33
+ *
34
+ * `availableLocales` (and `_availableVersionLocales`) are *sets* — their
35
+ * array order carries no meaning — so this makes that order stable and
36
+ * config-driven at the read source. The payoff is canonical downstream
37
+ * ordering (display switcher, hreflang `alternates`, sitemap) regardless of
38
+ * the order a document declared its locales in. Read-time projection only;
39
+ * nothing persisted changes. See docs/I18N.md.
40
+ */
41
+ export declare function orderByContentLocale(codes: string[]): string[];
19
42
  export declare function defineBylineCore(core: unknown): void;
20
43
  export declare function getBylineCoreUnsafe(): unknown;
@@ -93,6 +93,43 @@ export function getServerConfig() {
93
93
  }
94
94
  return serverConfig;
95
95
  }
96
+ /**
97
+ * Order a set of locale codes by their position in the configured content
98
+ * locale list. The order source is `i18n.content.locales` — the authoritative,
99
+ * always-complete configured set — **not** `i18n.content.localeDefinitions`,
100
+ * which is an optional labels overlay a host may provide for only *some*
101
+ * codes (ordering off it would drop unlabelled content locales to the end).
102
+ *
103
+ * Codes absent from that order fall to the end, ordered alphabetically among
104
+ * themselves, so the result is always deterministic and never throws. This is
105
+ * deliberately origin-agnostic: a code that isn't a configured content
106
+ * locale — an interface-only locale, a stale/removed code, a typo — is
107
+ * preserved and sorted last rather than dropped or thrown. The function only
108
+ * sorts; it never filters, so set membership is never changed. The same holds
109
+ * when no server config is registered (the order is empty → plain a–z sort).
110
+ *
111
+ * `availableLocales` (and `_availableVersionLocales`) are *sets* — their
112
+ * array order carries no meaning — so this makes that order stable and
113
+ * config-driven at the read source. The payoff is canonical downstream
114
+ * ordering (display switcher, hreflang `alternates`, sitemap) regardless of
115
+ * the order a document declared its locales in. Read-time projection only;
116
+ * nothing persisted changes. See docs/I18N.md.
117
+ */
118
+ export function orderByContentLocale(codes) {
119
+ const content = getServerConfigInstance()?.i18n?.content;
120
+ const order = content?.locales ?? content?.localeDefinitions?.map((l) => l.code) ?? [];
121
+ const index = new Map(order.map((code, i) => [code, i]));
122
+ return [...codes].sort((a, b) => {
123
+ const ia = index.get(a) ?? Number.POSITIVE_INFINITY;
124
+ const ib = index.get(b) ?? Number.POSITIVE_INFINITY;
125
+ if (ia !== ib)
126
+ return ia - ib;
127
+ // Stable, deterministic tiebreak for codes that share a rank — i.e.
128
+ // multiple unknown codes (both +Infinity), or the no-config fallback
129
+ // where every code is unknown and this degrades to a plain a–z sort.
130
+ return a < b ? -1 : a > b ? 1 : 0;
131
+ });
132
+ }
96
133
  // ---------------------------------------------------------------------------
97
134
  // BylineCore singleton — the composed runtime returned by `initBylineCore`.
98
135
  // Server-side packages that need post-init state (the abilities registry,
@@ -0,0 +1,8 @@
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
+ export {};
@@ -0,0 +1,91 @@
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
+ import { beforeAll, describe, expect, it } from 'vitest';
9
+ import { defineServerConfig, orderByContentLocale } from './config.js';
10
+ /**
11
+ * `orderByContentLocale` sorts a set of locale codes by the configured
12
+ * content-locale order (`i18n.content.locales`), with unknown codes last.
13
+ * The advertised set's array order is meaningless,
14
+ * so this is what makes it deterministic and config-driven at the read
15
+ * source. Content locales here are configured `en, fr, es, de`.
16
+ */
17
+ describe('orderByContentLocale', () => {
18
+ beforeAll(() => {
19
+ // Only `collections` is validated by `defineServerConfig`; the rest of
20
+ // the shape is irrelevant to this helper, so a minimal cast is enough.
21
+ defineServerConfig({
22
+ serverURL: 'http://test.local',
23
+ i18n: {
24
+ interface: { defaultLocale: 'en', locales: ['en'] },
25
+ content: { defaultLocale: 'en', locales: ['en', 'fr', 'es', 'de'] },
26
+ },
27
+ collections: [],
28
+ });
29
+ });
30
+ it('orders codes by configured content-locale order', () => {
31
+ expect(orderByContentLocale(['de', 'en', 'es'])).toEqual(['en', 'es', 'de']);
32
+ });
33
+ it('keeps unknown codes present, ordered last', () => {
34
+ // `xx` is absent from the content config — it survives the read but sorts
35
+ // after every known code rather than throwing.
36
+ expect(orderByContentLocale(['de', 'xx', 'en'])).toEqual(['en', 'de', 'xx']);
37
+ });
38
+ it('is independent of the input (advertise-declaration) order', () => {
39
+ const expected = ['en', 'es', 'de'];
40
+ expect(orderByContentLocale(['es', 'de', 'en'])).toEqual(expected);
41
+ expect(orderByContentLocale(['de', 'es', 'en'])).toEqual(expected);
42
+ expect(orderByContentLocale(['en', 'de', 'es'])).toEqual(expected);
43
+ });
44
+ it('orders multiple unknown codes deterministically (alphabetical tiebreak)', () => {
45
+ expect(orderByContentLocale(['zz', 'en', 'aa'])).toEqual(['en', 'aa', 'zz']);
46
+ });
47
+ it('does not mutate the input array', () => {
48
+ const input = ['de', 'en'];
49
+ orderByContentLocale(input);
50
+ expect(input).toEqual(['de', 'en']);
51
+ });
52
+ });
53
+ /**
54
+ * Robustness around the configured-set boundary (raised by: interface locales
55
+ * are a *different* set from content locales — see `apps/webapp/byline/locales.ts`
56
+ * — and `localeDefinitions` may be partial). The order is taken from the
57
+ * authoritative `content.locales`; anything outside it sorts last but is never
58
+ * dropped.
59
+ */
60
+ describe('orderByContentLocale — boundary robustness', () => {
61
+ beforeAll(() => {
62
+ defineServerConfig({
63
+ serverURL: 'http://test.local',
64
+ i18n: {
65
+ // Interface set (`en`, `de`) deliberately overlaps content only on `en`
66
+ // — `de` is interface-only here, NOT a content locale.
67
+ interface: { defaultLocale: 'en', locales: ['en', 'de'] },
68
+ content: {
69
+ defaultLocale: 'en',
70
+ locales: ['en', 'fr', 'es'],
71
+ // Partial labels overlay — only `en`/`fr`. `es` is a content locale
72
+ // with no label; it must still order correctly (not fall to the end).
73
+ localeDefinitions: [
74
+ { code: 'en', nativeName: 'English' },
75
+ { code: 'fr', nativeName: 'Français' },
76
+ ],
77
+ },
78
+ },
79
+ collections: [],
80
+ });
81
+ });
82
+ it('orders by content.locales even when localeDefinitions is partial', () => {
83
+ // `es` (unlabelled content locale) must keep its content-order slot, not
84
+ // be treated as unknown.
85
+ expect(orderByContentLocale(['es', 'en', 'fr'])).toEqual(['en', 'fr', 'es']);
86
+ });
87
+ it('sorts an interface-only locale (not in content) last, never dropping it', () => {
88
+ // `de` is an interface locale but not a content locale — preserved, last.
89
+ expect(orderByContentLocale(['de', 'en', 'fr'])).toEqual(['en', 'fr', 'de']);
90
+ });
91
+ });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './@types/index.js';
2
2
  export { applyBeforeRead, assertActorCanPerform, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
3
- export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, } from './config/config.js';
3
+ export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
4
4
  export { resolveRoutes } from './config/routes.js';
5
5
  export { validateAdminConfigs } 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, COLLECTION_ABILITY_VERBS, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
19
- export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, } from './config/config.js';
19
+ export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
20
20
  export { resolveRoutes } from './config/routes.js';
21
21
  export { validateAdminConfigs } from './config/validate-admin-configs.js';
22
22
  export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
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": "3.1.0",
5
+ "version": "3.1.1",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -79,7 +79,7 @@
79
79
  "sharp": "^0.34.5",
80
80
  "uuid": "^14.0.0",
81
81
  "zod": "^4.4.3",
82
- "@byline/auth": "3.1.0"
82
+ "@byline/auth": "3.1.1"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@biomejs/biome": "2.4.15",