@edgekits/i18n-core 0.2.1 → 0.3.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.
- package/CHANGELOG.md +51 -0
- package/README.md +124 -38
- package/dist/assemble.d.ts +5 -0
- package/dist/assemble.js +6 -1
- package/dist/cli/collect.d.ts +13 -0
- package/dist/cli/collect.js +54 -0
- package/dist/cli/dev-vars.d.ts +8 -0
- package/dist/cli/dev-vars.js +13 -0
- package/dist/cli/generate.d.ts +16 -4
- package/dist/cli/generate.js +171 -20
- package/dist/cli/hashes.d.ts +23 -3
- package/dist/cli/hashes.js +56 -2
- package/dist/cli/index.js +344 -187
- package/dist/cli/wrangler.d.ts +16 -2
- package/dist/cli/wrangler.js +63 -10
- package/dist/create-i18n.js +3 -3
- package/dist/deep-merge.d.ts +4 -1
- package/dist/deep-merge.js +7 -6
- package/dist/fetcher.js +80 -66
- package/dist/format.d.ts +15 -5
- package/dist/format.js +41 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/key-format.js +5 -2
- package/dist/types.d.ts +2 -1
- package/dist/types.js +19 -1
- package/package.json +2 -2
package/dist/cli/generate.js
CHANGED
|
@@ -6,11 +6,14 @@ const HEADER = `// Auto-generated by @edgekits/astro-i18n (edgekits-i18n CLI)
|
|
|
6
6
|
// `FALLBACK_<NAMESPACE>` identifier, so its file name is restricted to what is valid in
|
|
7
7
|
// all three: segments of [a-z0-9_], joined by single dots (`app.billing`).
|
|
8
8
|
export const NAMESPACE_NAME = /^[a-z0-9][a-z0-9_]*(\.[a-z0-9][a-z0-9_]*)*$/;
|
|
9
|
+
// `fallbacks.generated.ts` exports FALLBACK_LOCALE (the default locale's code) beside one
|
|
10
|
+
// FALLBACK_<NAMESPACE> per namespace: a namespace `locale` would export it twice.
|
|
11
|
+
const RESERVED_NAMESPACES = new Set(['locale']);
|
|
9
12
|
/**
|
|
10
13
|
* Throws, naming the offender, when a locale folder is not one of the config's
|
|
11
|
-
* `languages`, a namespace file name does not match NAMESPACE_NAME
|
|
12
|
-
* compile to the same fallback identifier (`app.billing` and
|
|
13
|
-
* collected tree before anything is generated or pushed.
|
|
14
|
+
* `languages`, a namespace file name does not match NAMESPACE_NAME or is reserved
|
|
15
|
+
* (`locale`), or two namespaces compile to the same fallback identifier (`app.billing` and
|
|
16
|
+
* `app__billing`). Run on the collected tree before anything is generated or pushed.
|
|
14
17
|
*/
|
|
15
18
|
export function assertValidNames(collected, languages) {
|
|
16
19
|
const configured = Object.keys(languages);
|
|
@@ -24,6 +27,11 @@ export function assertValidNames(collected, languages) {
|
|
|
24
27
|
throw new Error(`Namespace "${ns}" (${locale}/${ns}.json) must match ${NAMESPACE_NAME}: ` +
|
|
25
28
|
'segments of lowercase letters, digits and underscores, joined by single dots.');
|
|
26
29
|
}
|
|
30
|
+
if (RESERVED_NAMESPACES.has(ns)) {
|
|
31
|
+
throw new Error(`Namespace "${ns}" (${locale}/${ns}.json) is reserved: its fallback would be exported as ` +
|
|
32
|
+
`${fallbackIdentifier(ns)}, which fallbacks.generated.ts already exports (the default locale's code). ` +
|
|
33
|
+
'Rename the file.');
|
|
34
|
+
}
|
|
27
35
|
const identifier = fallbackIdentifier(ns);
|
|
28
36
|
const other = identifiers.get(identifier);
|
|
29
37
|
if (other !== undefined) {
|
|
@@ -34,6 +42,43 @@ export function assertValidNames(collected, languages) {
|
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
function nullValueError(keyPath, file) {
|
|
46
|
+
return new Error(`Key "${keyPath}" (${file}) is null: it would override the default locale's text with null. ` +
|
|
47
|
+
'Remove the key to fall back, or give it a value.');
|
|
48
|
+
}
|
|
49
|
+
// The first null in `value`, as a key path below `keyPath` (`a.b`, `list[2]`), or null.
|
|
50
|
+
function findNull(value, keyPath) {
|
|
51
|
+
if (value === null)
|
|
52
|
+
return keyPath;
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
for (let i = 0; i < value.length; i++) {
|
|
55
|
+
const found = findNull(value[i], `${keyPath}[${i}]`);
|
|
56
|
+
if (found !== null)
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (isTree(value)) {
|
|
61
|
+
for (const [key, child] of Object.entries(value)) {
|
|
62
|
+
const found = findNull(child, `${keyPath}.${key}`);
|
|
63
|
+
if (found !== null)
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Throws, naming the key path and the file, on the first `null` in any locale: merged over
|
|
71
|
+
* the compiled fallback, a null replaces the default locale's text.
|
|
72
|
+
*/
|
|
73
|
+
export function assertNoNullValues(collected) {
|
|
74
|
+
for (const [locale, namespaces] of Object.entries(collected)) {
|
|
75
|
+
for (const [ns, json] of Object.entries(namespaces)) {
|
|
76
|
+
const found = findNull(json, ns);
|
|
77
|
+
if (found !== null)
|
|
78
|
+
throw nullValueError(found, `${locale}/${ns}.json`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
37
82
|
/**
|
|
38
83
|
* A single-quoted TS string literal, escaped by JSON.stringify. For every name the
|
|
39
84
|
* validation accepts it is byte-identical to the plain `'name'` the CLI always wrote, so
|
|
@@ -77,39 +122,93 @@ function jsonKind(value) {
|
|
|
77
122
|
return 'array';
|
|
78
123
|
return typeof value;
|
|
79
124
|
}
|
|
125
|
+
// A scalar leaf's TS type: numbers and booleans as such, everything else `string`.
|
|
126
|
+
function scalarType(value) {
|
|
127
|
+
if (typeof value === 'number')
|
|
128
|
+
return 'number';
|
|
129
|
+
if (typeof value === 'boolean')
|
|
130
|
+
return 'boolean';
|
|
131
|
+
return 'string';
|
|
132
|
+
}
|
|
80
133
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* (objects, nested arrays); every other leaf stays `string`. `keyPath` and `file` name the
|
|
84
|
-
* value in the error a mixed array raises.
|
|
134
|
+
* Throws, naming the key path (with element indexes) and the file, on what the schema
|
|
135
|
+
* cannot type: a null, or an array that mixes kinds of elements (strings and objects).
|
|
85
136
|
*/
|
|
86
|
-
function
|
|
137
|
+
function assertTypeable(value, keyPath, file) {
|
|
138
|
+
if (value === null)
|
|
139
|
+
throw nullValueError(keyPath, file);
|
|
87
140
|
if (Array.isArray(value)) {
|
|
88
|
-
|
|
89
|
-
|
|
141
|
+
const nullAt = value.indexOf(null);
|
|
142
|
+
if (nullAt !== -1)
|
|
143
|
+
throw nullValueError(`${keyPath}[${nullAt}]`, file);
|
|
90
144
|
const kinds = [...new Set(value.map(jsonKind))];
|
|
91
145
|
if (kinds.length > 1) {
|
|
92
146
|
throw new Error(`Key "${keyPath}" (${file}) is an array that mixes ${kinds.join(', ')} elements: ` +
|
|
93
147
|
'I18n.Schema cannot type it. Use one kind of element per array.');
|
|
94
148
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
value.forEach((element, i) => assertTypeable(element, `${keyPath}[${i}]`, file));
|
|
150
|
+
}
|
|
151
|
+
else if (isTree(value)) {
|
|
152
|
+
for (const [key, child] of Object.entries(value))
|
|
153
|
+
assertTypeable(child, `${keyPath}.${key}`, file);
|
|
99
154
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The TS type of the values found at one place in the tree (one value, or the same key
|
|
158
|
+
* across the elements of an array), checked by assertTypeable first. Objects merge into one
|
|
159
|
+
* object type, arrays into one array type, and the distinct kinds that remain form a union
|
|
160
|
+
* (`string | number` for a key that is a string in one element and a number in another).
|
|
161
|
+
*/
|
|
162
|
+
function typeOfValues(values, indent) {
|
|
163
|
+
const objects = values.filter(isTree);
|
|
164
|
+
const arrays = values.filter(Array.isArray);
|
|
165
|
+
const scalars = [...new Set(values.filter((value) => !isTree(value) && !Array.isArray(value)).map(scalarType))];
|
|
166
|
+
return [
|
|
167
|
+
...(objects.length > 0 ? [objectType(objects, indent)] : []),
|
|
168
|
+
...(arrays.length > 0 ? [arrayType(arrays, indent)] : []),
|
|
169
|
+
...scalars,
|
|
170
|
+
].join(' | ');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One object type for a list of objects (one object, or every element of an array): each
|
|
174
|
+
* key in first-seen order, required when every object has it, optional (`?:`) otherwise,
|
|
175
|
+
* typed by typeOfValues over the objects that have it.
|
|
176
|
+
*/
|
|
177
|
+
function objectType(objects, indent) {
|
|
178
|
+
const keys = [...new Set(objects.flatMap((object) => Object.keys(object)))];
|
|
179
|
+
if (keys.length === 0)
|
|
104
180
|
return '{}';
|
|
105
181
|
const pad = ' '.repeat(indent);
|
|
106
182
|
const padInner = ' '.repeat(indent + 2);
|
|
107
|
-
const lines =
|
|
183
|
+
const lines = keys.map((key) => {
|
|
184
|
+
const present = objects.filter((object) => Object.hasOwn(object, key));
|
|
185
|
+
const optional = present.length < objects.length ? '?' : '';
|
|
186
|
+
return `${padInner}${JSON.stringify(key)}${optional}: ${typeOfValues(present.map((object) => object[key]), indent + 2)}`;
|
|
187
|
+
});
|
|
108
188
|
return `{\n${lines.join(';\n')};\n${pad}}`;
|
|
109
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* One array type for a list of arrays (one array, or the same key's arrays across the
|
|
192
|
+
* elements of an outer one): the elements of all of them typed together. `string[]` when
|
|
193
|
+
* they are all empty, `<scalar>[]` for one scalar kind, `Array<…>` otherwise.
|
|
194
|
+
*/
|
|
195
|
+
function arrayType(arrays, indent) {
|
|
196
|
+
const elements = arrays.flat(1);
|
|
197
|
+
if (elements.length === 0)
|
|
198
|
+
return 'string[]';
|
|
199
|
+
const elementType = typeOfValues(elements, indent);
|
|
200
|
+
return elements.every((element) => !isTree(element) && !Array.isArray(element)) && !elementType.includes('|')
|
|
201
|
+
? `${elementType}[]`
|
|
202
|
+
: `Array<${elementType}>`;
|
|
203
|
+
}
|
|
204
|
+
/** The TS type of one namespace's JSON (see typeOfValues). */
|
|
205
|
+
function generateSchemaTs(value, indent, keyPath, file) {
|
|
206
|
+
assertTypeable(value, keyPath, file);
|
|
207
|
+
return typeOfValues([value], indent);
|
|
208
|
+
}
|
|
110
209
|
/**
|
|
111
210
|
* `src/i18n.generated.d.ts`: the global `I18n.Schema`, from the schema source locale.
|
|
112
|
-
* Throws, naming the key, when an array in it mixes kinds of elements.
|
|
211
|
+
* Throws, naming the key, when an array in it mixes kinds of elements or a value is null.
|
|
113
212
|
*/
|
|
114
213
|
export function buildTypesContent(collected, defaultLocale) {
|
|
115
214
|
const source = schemaSourceLocale(collected, defaultLocale);
|
|
@@ -169,3 +268,55 @@ export function buildFallbacksContent(collected, defaultLocale) {
|
|
|
169
268
|
}
|
|
170
269
|
return lines.join('\n');
|
|
171
270
|
}
|
|
271
|
+
// Leaf key paths in `tree` that `base` lacks: a key missing from `base` counts once, with
|
|
272
|
+
// whatever it holds; arrays are leaves.
|
|
273
|
+
function extraKeyPaths(tree, base, prefix = '') {
|
|
274
|
+
const paths = [];
|
|
275
|
+
for (const [key, child] of Object.entries(tree)) {
|
|
276
|
+
const keyPath = prefix ? `${prefix}.${key}` : key;
|
|
277
|
+
if (!Object.hasOwn(base, key)) {
|
|
278
|
+
paths.push(keyPath);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const baseChild = base[key];
|
|
282
|
+
if (isTree(child) && isTree(baseChild))
|
|
283
|
+
paths.push(...extraKeyPaths(child, baseChild, keyPath));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return paths;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Warnings about what I18n.Schema and the fallbacks will not cover, compared with the
|
|
290
|
+
* default locale: a namespace no default-locale file has, and keys a locale's file has
|
|
291
|
+
* that the default locale's lacks (a count per file, the first three paths). Empty when
|
|
292
|
+
* the default locale has no translations (the CLI warns about that on its own).
|
|
293
|
+
*/
|
|
294
|
+
export function translationWarnings(collected, defaultLocale) {
|
|
295
|
+
const base = collected[defaultLocale];
|
|
296
|
+
if (!base)
|
|
297
|
+
return [];
|
|
298
|
+
const orphans = new Map();
|
|
299
|
+
const extraKeys = [];
|
|
300
|
+
for (const [locale, namespaces] of Object.entries(collected)) {
|
|
301
|
+
if (locale === defaultLocale)
|
|
302
|
+
continue;
|
|
303
|
+
for (const [ns, tree] of Object.entries(namespaces)) {
|
|
304
|
+
const baseTree = base[ns];
|
|
305
|
+
if (baseTree === undefined) {
|
|
306
|
+
orphans.set(ns, [...(orphans.get(ns) ?? []), locale]);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const extra = isTree(tree) && isTree(baseTree) ? extraKeyPaths(tree, baseTree) : [];
|
|
310
|
+
if (extra.length > 0) {
|
|
311
|
+
const shownPaths = extra.slice(0, 3).join(', ') + (extra.length > 3 ? ', …' : '');
|
|
312
|
+
extraKeys.push(`${locale}/${ns}.json has ${extra.length} ${extra.length === 1 ? 'key' : 'keys'} that ` +
|
|
313
|
+
`${defaultLocale}/${ns}.json lacks (not in I18n.Schema): ${shownPaths}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return [
|
|
318
|
+
...[...orphans].map(([ns, locales]) => `Namespace "${ns}" exists only in ${locales.join(', ')} (no ${defaultLocale}/${ns}.json): ` +
|
|
319
|
+
'it is not in I18n.Schema and has no fallback.'),
|
|
320
|
+
...extraKeys,
|
|
321
|
+
];
|
|
322
|
+
}
|
package/dist/cli/hashes.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CollectedData } from './generate.ts';
|
|
2
2
|
/**
|
|
3
|
-
* Per-namespace content hashes
|
|
4
|
-
* Pure apart from hashing.
|
|
5
|
-
*
|
|
3
|
+
* Per-namespace content hashes, the change diff that drives the cache purge, and the
|
|
4
|
+
* baseline file. Pure apart from hashing. The site's `scripts/bundle-translations.ts`
|
|
5
|
+
* algorithm, with keys sorted by code point instead of `localeCompare` (same order for
|
|
6
|
+
* lowercase ASCII keys; mixed-case keys hash differently once, ADR-0034).
|
|
6
7
|
*/
|
|
7
8
|
export type HashMap = Record<string, string>;
|
|
9
|
+
export type BaselineFile = Record<string, HashMap>;
|
|
8
10
|
export declare function stableStringify(value: unknown): string;
|
|
9
11
|
export declare function computeHash(value: unknown): string;
|
|
10
12
|
/**
|
|
@@ -15,5 +17,23 @@ export declare function diffHashes(collected: CollectedData, previousHashes: Has
|
|
|
15
17
|
currentHashes: HashMap;
|
|
16
18
|
changedKeys: string[];
|
|
17
19
|
};
|
|
20
|
+
/** KV keys of "<locale>:<namespace>" pairs (the removed ones, for the bulk delete). */
|
|
21
|
+
export declare function kvKeysFor(hashKeys: readonly string[], keyPrefix: string): string[];
|
|
18
22
|
/** Purge API URLs for the changed "<locale>:<namespace>" pairs. */
|
|
19
23
|
export declare function purgeUrlsFor(changedKeys: readonly string[], keyPrefix: string): string[];
|
|
24
|
+
/** Pairs in `previous` that `current` no longer has: translation files removed since. */
|
|
25
|
+
export declare function removedKeys(previous: HashMap, current: HashMap): string[];
|
|
26
|
+
/**
|
|
27
|
+
* The baseline of KV namespace `kvId` in a parsed `.i18n-hashes.json` (`undefined`: no
|
|
28
|
+
* file). A file in the old flat shape is the baseline of `flatOwner`, the top-level
|
|
29
|
+
* TRANSLATIONS id (0.2.x wrote it, and had no `--env`); another `kvId` gets none (`{}`).
|
|
30
|
+
* Without a `flatOwner` it is `kvId`'s. With no `kvId` (bundle, which reads no wrangler
|
|
31
|
+
* config) the only baseline the file holds, or `undefined` when it holds several.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readBaseline(file: unknown, kvId?: string, flatOwner?: string): HashMap | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* The file with `kvId`'s baseline replaced. An old flat file stays the baseline of its
|
|
36
|
+
* owner (`flatOwner`, see readBaseline): `{ [kvId]: hashes }` when that is `kvId`,
|
|
37
|
+
* `{ [flatOwner]: flat, [kvId]: hashes }` otherwise.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeBaseline(file: unknown, kvId: string, hashes: HashMap, flatOwner?: string): BaselineFile;
|
package/dist/cli/hashes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { formatTranslationCacheUrl } from "../key-format.js";
|
|
2
|
+
import { formatTranslationCacheUrl, formatTranslationKvKey } from "../key-format.js";
|
|
3
3
|
export function stableStringify(value) {
|
|
4
4
|
if (value === null || typeof value !== 'object') {
|
|
5
5
|
return JSON.stringify(value);
|
|
@@ -7,7 +7,8 @@ export function stableStringify(value) {
|
|
|
7
7
|
if (Array.isArray(value)) {
|
|
8
8
|
return '[' + value.map(stableStringify).join(',') + ']';
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
// Code point order: the same on every machine, whatever its ICU data and locale.
|
|
11
|
+
const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
11
12
|
return '{' + entries.map(([k, v]) => JSON.stringify(k) + ':' + stableStringify(v)).join(',') + '}';
|
|
12
13
|
}
|
|
13
14
|
export function computeHash(value) {
|
|
@@ -33,6 +34,13 @@ export function diffHashes(collected, previousHashes) {
|
|
|
33
34
|
}
|
|
34
35
|
return { currentHashes, changedKeys };
|
|
35
36
|
}
|
|
37
|
+
/** KV keys of "<locale>:<namespace>" pairs (the removed ones, for the bulk delete). */
|
|
38
|
+
export function kvKeysFor(hashKeys, keyPrefix) {
|
|
39
|
+
return hashKeys.map((hashKey) => {
|
|
40
|
+
const [locale, ns] = hashKey.split(':');
|
|
41
|
+
return formatTranslationKvKey(keyPrefix, locale, ns);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
36
44
|
/** Purge API URLs for the changed "<locale>:<namespace>" pairs. */
|
|
37
45
|
export function purgeUrlsFor(changedKeys, keyPrefix) {
|
|
38
46
|
return changedKeys.map((hashKey) => {
|
|
@@ -40,3 +48,49 @@ export function purgeUrlsFor(changedKeys, keyPrefix) {
|
|
|
40
48
|
return formatTranslationCacheUrl(keyPrefix, locale, ns);
|
|
41
49
|
});
|
|
42
50
|
}
|
|
51
|
+
/** Pairs in `previous` that `current` no longer has: translation files removed since. */
|
|
52
|
+
export function removedKeys(previous, current) {
|
|
53
|
+
return Object.keys(previous).filter((key) => !Object.hasOwn(current, key));
|
|
54
|
+
}
|
|
55
|
+
function isRecord(value) {
|
|
56
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
// The pre-0.3 shape: one flat baseline, every value a hash string.
|
|
59
|
+
function isFlatBaseline(file) {
|
|
60
|
+
const values = Object.values(file);
|
|
61
|
+
return values.length > 0 && values.every((value) => typeof value === 'string');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The baseline of KV namespace `kvId` in a parsed `.i18n-hashes.json` (`undefined`: no
|
|
65
|
+
* file). A file in the old flat shape is the baseline of `flatOwner`, the top-level
|
|
66
|
+
* TRANSLATIONS id (0.2.x wrote it, and had no `--env`); another `kvId` gets none (`{}`).
|
|
67
|
+
* Without a `flatOwner` it is `kvId`'s. With no `kvId` (bundle, which reads no wrangler
|
|
68
|
+
* config) the only baseline the file holds, or `undefined` when it holds several.
|
|
69
|
+
*/
|
|
70
|
+
export function readBaseline(file, kvId, flatOwner) {
|
|
71
|
+
if (!isRecord(file))
|
|
72
|
+
return {};
|
|
73
|
+
if (isFlatBaseline(file))
|
|
74
|
+
return kvId === undefined || kvId === (flatOwner ?? kvId) ? file : {};
|
|
75
|
+
const baselines = file;
|
|
76
|
+
if (kvId !== undefined)
|
|
77
|
+
return baselines[kvId] ?? {};
|
|
78
|
+
const ids = Object.keys(baselines);
|
|
79
|
+
if (ids.length > 1)
|
|
80
|
+
return undefined;
|
|
81
|
+
return ids.length === 1 ? baselines[ids[0]] : {};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The file with `kvId`'s baseline replaced. An old flat file stays the baseline of its
|
|
85
|
+
* owner (`flatOwner`, see readBaseline): `{ [kvId]: hashes }` when that is `kvId`,
|
|
86
|
+
* `{ [flatOwner]: flat, [kvId]: hashes }` otherwise.
|
|
87
|
+
*/
|
|
88
|
+
export function writeBaseline(file, kvId, hashes, flatOwner) {
|
|
89
|
+
if (!isRecord(file))
|
|
90
|
+
return { [kvId]: hashes };
|
|
91
|
+
if (isFlatBaseline(file)) {
|
|
92
|
+
const owner = flatOwner ?? kvId;
|
|
93
|
+
return owner === kvId ? { [kvId]: hashes } : { [owner]: file, [kvId]: hashes };
|
|
94
|
+
}
|
|
95
|
+
return { ...file, [kvId]: hashes };
|
|
96
|
+
}
|