@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.
- package/dist/@types/db-types.d.ts +7 -0
- package/dist/config/config.d.ts +23 -0
- package/dist/config/config.js +37 -0
- package/dist/config/order-by-content-locale.test.node.d.ts +8 -0
- package/dist/config/order-by-content-locale.test.node.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -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;
|
package/dist/config/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/config/config.js
CHANGED
|
@@ -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.
|
|
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.
|
|
82
|
+
"@byline/auth": "3.1.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|