@arkebcacy/beacon-cli-temp 0.1.6 → 0.1.8
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/cs/labels/Label.d.ts +1 -1
- package/dist/cs/labels/Label.d.ts.map +1 -1
- package/dist/cs/labels/Label.js +1 -3
- package/dist/cs/labels/Label.js.map +1 -1
- package/dist/cs/labels/getAllLabels.js +1 -1
- package/dist/cs/labels/getAllLabels.js.map +1 -1
- package/dist/cs/locales/addLocale.d.ts +11 -0
- package/dist/cs/locales/addLocale.d.ts.map +1 -0
- package/dist/cs/locales/addLocale.js +36 -0
- package/dist/cs/locales/addLocale.js.map +1 -0
- package/dist/cs/locales/ensureLocaleExists.d.ts +11 -0
- package/dist/cs/locales/ensureLocaleExists.d.ts.map +1 -0
- package/dist/cs/locales/ensureLocaleExists.js +38 -0
- package/dist/cs/locales/ensureLocaleExists.js.map +1 -0
- package/dist/cs/locales/getLocales.d.ts +12 -0
- package/dist/cs/locales/getLocales.d.ts.map +1 -0
- package/dist/cs/locales/getLocales.js +23 -0
- package/dist/cs/locales/getLocales.js.map +1 -0
- package/dist/dto/labels/flatten.js +1 -1
- package/dist/dto/labels/flatten.js.map +1 -1
- package/dist/dto/labels/organize.d.ts.map +1 -1
- package/dist/dto/labels/organize.js +15 -14
- package/dist/dto/labels/organize.js.map +1 -1
- package/dist/schema/entries/toContentstack.d.ts.map +1 -1
- package/dist/schema/entries/toContentstack.js +30 -5
- package/dist/schema/entries/toContentstack.js.map +1 -1
- package/dist/schema/entries/toFilesystem.js +23 -10
- package/dist/schema/entries/toFilesystem.js.map +1 -1
- package/dist/schema/labels/lib/labelHelpers.d.ts +3 -0
- package/dist/schema/labels/lib/labelHelpers.d.ts.map +1 -0
- package/dist/schema/labels/lib/labelHelpers.js +34 -0
- package/dist/schema/labels/lib/labelHelpers.js.map +1 -0
- package/dist/schema/labels/lib/labelOperations.d.ts +6 -0
- package/dist/schema/labels/lib/labelOperations.d.ts.map +1 -0
- package/dist/schema/labels/lib/labelOperations.js +57 -0
- package/dist/schema/labels/lib/labelOperations.js.map +1 -0
- package/dist/schema/labels/toContentstack.d.ts.map +1 -1
- package/dist/schema/labels/toContentstack.js +142 -105
- package/dist/schema/labels/toContentstack.js.map +1 -1
- package/dist/schema/labels/toFilesystem.d.ts.map +1 -1
- package/dist/schema/labels/toFilesystem.js +6 -3
- package/dist/schema/labels/toFilesystem.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cs/labels/Label.ts +2 -6
- package/src/cs/labels/getAllLabels.ts +1 -1
- package/src/cs/locales/addLocale.ts +59 -0
- package/src/cs/locales/ensureLocaleExists.ts +51 -0
- package/src/cs/locales/getLocales.ts +38 -0
- package/src/dto/labels/flatten.test.ts +16 -16
- package/src/dto/labels/flatten.ts +1 -1
- package/src/dto/labels/organize.test.ts +19 -19
- package/src/dto/labels/organize.ts +15 -14
- package/src/schema/entries/toContentstack.ts +75 -6
- package/src/schema/entries/toFilesystem.ts +29 -14
- package/src/schema/labels/lib/labelHelpers.ts +36 -0
- package/src/schema/labels/lib/labelOperations.ts +74 -0
- package/src/schema/labels/toContentstack.ts +204 -109
- package/src/schema/labels/toFilesystem.ts +10 -3
package/package.json
CHANGED
package/src/cs/labels/Label.ts
CHANGED
|
@@ -4,13 +4,9 @@ import { isItem } from '../Types.js';
|
|
|
4
4
|
|
|
5
5
|
export default interface Label extends OmitIndex<Item> {
|
|
6
6
|
readonly name: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly parent: readonly string[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function isLabel(o: unknown): o is Label {
|
|
11
|
-
return (
|
|
12
|
-
isItem(o) &&
|
|
13
|
-
typeof o.name === 'string' &&
|
|
14
|
-
(o.parent_uid === null || typeof o.parent_uid === 'string')
|
|
15
|
-
);
|
|
11
|
+
return isItem(o) && typeof o.name === 'string' && Array.isArray(o.parent);
|
|
16
12
|
}
|
|
@@ -30,7 +30,7 @@ export default async function getAllLabels(client: Client): Promise<Label[]> {
|
|
|
30
30
|
const label: Label = {
|
|
31
31
|
...raw,
|
|
32
32
|
name: raw.name,
|
|
33
|
-
|
|
33
|
+
parent: Array.isArray(raw.parent) ? raw.parent : [],
|
|
34
34
|
uid: raw.uid,
|
|
35
35
|
};
|
|
36
36
|
if (isLabel(label)) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type Client from '../api/Client.js';
|
|
2
|
+
import ContentstackError from '../api/ContentstackError.js';
|
|
3
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
4
|
+
import type { Locale } from './getLocales.js';
|
|
5
|
+
|
|
6
|
+
interface AddLocaleRequest {
|
|
7
|
+
readonly locale: {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly fallback_locale?: string;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Adds a new locale to the stack.
|
|
16
|
+
* @param client - Contentstack API client
|
|
17
|
+
* @param code - Locale code (e.g., 'zh-cn', 'zh-chs')
|
|
18
|
+
* @param name - Display name for the locale (e.g., 'Chinese - China')
|
|
19
|
+
* @param fallbackLocale - Optional fallback locale code
|
|
20
|
+
*/
|
|
21
|
+
export async function addLocale(
|
|
22
|
+
client: Client,
|
|
23
|
+
code: string,
|
|
24
|
+
name?: string,
|
|
25
|
+
fallbackLocale?: string,
|
|
26
|
+
): Promise<Locale> {
|
|
27
|
+
const requestBody: AddLocaleRequest = {
|
|
28
|
+
locale: {
|
|
29
|
+
code,
|
|
30
|
+
...(fallbackLocale !== undefined && { fallback_locale: fallbackLocale }),
|
|
31
|
+
...(name !== undefined && { name }),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const response = await client.POST('/v3/locales', {
|
|
36
|
+
body: requestBody as never,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const msg = `Failed to add locale '${code}'`;
|
|
40
|
+
ContentstackError.throwIfError(response.error, msg);
|
|
41
|
+
|
|
42
|
+
if (!response.response.ok) {
|
|
43
|
+
throw new Error(msg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data } = response;
|
|
47
|
+
|
|
48
|
+
if (!isRecord(data)) {
|
|
49
|
+
throw new TypeError(msg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { locale } = data;
|
|
53
|
+
|
|
54
|
+
if (!isRecord(locale)) {
|
|
55
|
+
throw new TypeError(msg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return locale as Locale;
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type Client from '../api/Client.js';
|
|
2
|
+
import { addLocale } from './addLocale.js';
|
|
3
|
+
import { getLocales } from './getLocales.js';
|
|
4
|
+
import type { Locale } from './getLocales.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensures a locale exists in the target stack. If it doesn't exist, creates it.
|
|
8
|
+
* @param client - Contentstack API client
|
|
9
|
+
* @param localeCode - Locale code to ensure exists (e.g., 'zh-cn')
|
|
10
|
+
* @param fallbackLocale - Optional fallback locale code (defaults to 'en-us')
|
|
11
|
+
* @returns The locale object (either existing or newly created)
|
|
12
|
+
*/
|
|
13
|
+
export async function ensureLocaleExists(
|
|
14
|
+
client: Client,
|
|
15
|
+
localeCode: string,
|
|
16
|
+
fallbackLocale = 'en-us',
|
|
17
|
+
): Promise<Locale> {
|
|
18
|
+
const existingLocales = await getLocales(client);
|
|
19
|
+
const existing = existingLocales.find((loc) => loc.code === localeCode);
|
|
20
|
+
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Locale doesn't exist - create it
|
|
26
|
+
|
|
27
|
+
// Derive a reasonable display name from the locale code
|
|
28
|
+
const name = deriveLocaleName(localeCode);
|
|
29
|
+
|
|
30
|
+
const newLocale = await addLocale(client, localeCode, name, fallbackLocale);
|
|
31
|
+
|
|
32
|
+
return newLocale;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derives a human-readable name from a locale code.
|
|
37
|
+
* This is a basic implementation that can be expanded as needed.
|
|
38
|
+
*/
|
|
39
|
+
function deriveLocaleName(code: string): string {
|
|
40
|
+
const nameMap: Record<string, string> = {
|
|
41
|
+
'en-us': 'English - United States',
|
|
42
|
+
'zh-chs': 'Chinese (simplified)',
|
|
43
|
+
'zh-cht': 'Chinese (traditional)',
|
|
44
|
+
'zh-cn': 'Chinese - China',
|
|
45
|
+
'zh-hans': 'Chinese (simplified)',
|
|
46
|
+
'zh-hant': 'Chinese (traditional)',
|
|
47
|
+
'zh-tw': 'Chinese - Taiwan',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return nameMap[code.toLowerCase()] ?? code.toUpperCase();
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type Client from '../api/Client.js';
|
|
2
|
+
import ContentstackError from '../api/ContentstackError.js';
|
|
3
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
4
|
+
|
|
5
|
+
export interface Locale {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly fallback_locale?: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly uid: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetches all locales configured for a stack.
|
|
14
|
+
*/
|
|
15
|
+
export async function getLocales(client: Client): Promise<readonly Locale[]> {
|
|
16
|
+
const response = await client.GET('/v3/locales', {});
|
|
17
|
+
|
|
18
|
+
const msg = 'Failed to fetch locales';
|
|
19
|
+
ContentstackError.throwIfError(response.error, msg);
|
|
20
|
+
|
|
21
|
+
if (!response.response.ok) {
|
|
22
|
+
throw new Error(msg);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { data } = response;
|
|
26
|
+
|
|
27
|
+
if (!isRecord(data)) {
|
|
28
|
+
throw new TypeError(msg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { locales } = data;
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(locales)) {
|
|
34
|
+
throw new TypeError(msg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return locales as Locale[];
|
|
38
|
+
}
|
|
@@ -18,9 +18,9 @@ describe('flatten', () => {
|
|
|
18
18
|
const result = flatten(tree);
|
|
19
19
|
|
|
20
20
|
expect(result).toEqual([
|
|
21
|
-
{ name: 'Parent',
|
|
22
|
-
{ name: 'Child 1',
|
|
23
|
-
{ name: 'Child 2',
|
|
21
|
+
{ name: 'Parent', parent: [], uid: 'parent' },
|
|
22
|
+
{ name: 'Child 1', parent: ['parent'], uid: 'child1' },
|
|
23
|
+
{ name: 'Child 2', parent: ['parent'], uid: 'child2' },
|
|
24
24
|
]);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -40,10 +40,10 @@ describe('flatten', () => {
|
|
|
40
40
|
const result = flatten(tree);
|
|
41
41
|
|
|
42
42
|
expect(result).toEqual([
|
|
43
|
-
{ name: 'Component',
|
|
44
|
-
{ name: 'Calculator',
|
|
45
|
-
{ name: 'Data',
|
|
46
|
-
{ name: 'Page',
|
|
43
|
+
{ name: 'Component', parent: [], uid: 'component' },
|
|
44
|
+
{ name: 'Calculator', parent: ['component'], uid: 'calculator' },
|
|
45
|
+
{ name: 'Data', parent: ['component'], uid: 'data' },
|
|
46
|
+
{ name: 'Page', parent: ['component'], uid: 'page' },
|
|
47
47
|
]);
|
|
48
48
|
});
|
|
49
49
|
|
|
@@ -71,10 +71,10 @@ describe('flatten', () => {
|
|
|
71
71
|
const result = flatten(tree);
|
|
72
72
|
|
|
73
73
|
expect(result).toEqual([
|
|
74
|
-
{ name: 'Root',
|
|
75
|
-
{ name: 'Level 1',
|
|
76
|
-
{ name: 'Level 2',
|
|
77
|
-
{ name: 'Level 3',
|
|
74
|
+
{ name: 'Root', parent: [], uid: 'root' },
|
|
75
|
+
{ name: 'Level 1', parent: ['root'], uid: 'level1' },
|
|
76
|
+
{ name: 'Level 2', parent: ['level1'], uid: 'level2' },
|
|
77
|
+
{ name: 'Level 3', parent: ['level2'], uid: 'level3' },
|
|
78
78
|
]);
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -91,9 +91,9 @@ describe('flatten', () => {
|
|
|
91
91
|
const result = flatten(tree);
|
|
92
92
|
|
|
93
93
|
expect(result).toEqual([
|
|
94
|
-
{ name: 'Label 1',
|
|
95
|
-
{ name: 'Label 2',
|
|
96
|
-
{ name: 'Child',
|
|
94
|
+
{ name: 'Label 1', parent: [], uid: 'label1' },
|
|
95
|
+
{ name: 'Label 2', parent: [], uid: 'label2' },
|
|
96
|
+
{ name: 'Child', parent: ['label2'], uid: 'child' },
|
|
97
97
|
]);
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -111,8 +111,8 @@ describe('flatten', () => {
|
|
|
111
111
|
const result = flatten(tree);
|
|
112
112
|
|
|
113
113
|
expect(result).toEqual([
|
|
114
|
-
{ name: 'Label 1',
|
|
115
|
-
{ name: 'Label 2',
|
|
114
|
+
{ name: 'Label 1', parent: [], uid: 'label1' },
|
|
115
|
+
{ name: 'Label 2', parent: [], uid: 'label2' },
|
|
116
116
|
]);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
@@ -5,9 +5,9 @@ import type Label from '#cli/cs/labels/Label.js';
|
|
|
5
5
|
describe('organize', () => {
|
|
6
6
|
it('organizes flat labels into hierarchy', () => {
|
|
7
7
|
const labels: Label[] = [
|
|
8
|
-
{ name: 'Parent',
|
|
9
|
-
{ name: 'Child 1',
|
|
10
|
-
{ name: 'Child 2',
|
|
8
|
+
{ name: 'Parent', parent: [], uid: 'parent' },
|
|
9
|
+
{ name: 'Child 1', parent: ['parent'], uid: 'child1' },
|
|
10
|
+
{ name: 'Child 2', parent: ['parent'], uid: 'child2' },
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
const result = organize(labels);
|
|
@@ -26,10 +26,10 @@ describe('organize', () => {
|
|
|
26
26
|
|
|
27
27
|
it('organizes component label structure from screenshot', () => {
|
|
28
28
|
const labels: Label[] = [
|
|
29
|
-
{ name: 'Component',
|
|
30
|
-
{ name: 'Calculator',
|
|
31
|
-
{ name: 'Data',
|
|
32
|
-
{ name: 'Page',
|
|
29
|
+
{ name: 'Component', parent: [], uid: 'component' },
|
|
30
|
+
{ name: 'Calculator', parent: ['component'], uid: 'calculator' },
|
|
31
|
+
{ name: 'Data', parent: ['component'], uid: 'data' },
|
|
32
|
+
{ name: 'Page', parent: ['component'], uid: 'page' },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
35
|
const result = organize(labels);
|
|
@@ -49,10 +49,10 @@ describe('organize', () => {
|
|
|
49
49
|
|
|
50
50
|
it('handles deeply nested labels', () => {
|
|
51
51
|
const labels: Label[] = [
|
|
52
|
-
{ name: 'Root',
|
|
53
|
-
{ name: 'Level 1',
|
|
54
|
-
{ name: 'Level 2',
|
|
55
|
-
{ name: 'Level 3',
|
|
52
|
+
{ name: 'Root', parent: [], uid: 'root' },
|
|
53
|
+
{ name: 'Level 1', parent: ['root'], uid: 'level1' },
|
|
54
|
+
{ name: 'Level 2', parent: ['level1'], uid: 'level2' },
|
|
55
|
+
{ name: 'Level 3', parent: ['level2'], uid: 'level3' },
|
|
56
56
|
];
|
|
57
57
|
|
|
58
58
|
const result = organize(labels);
|
|
@@ -80,9 +80,9 @@ describe('organize', () => {
|
|
|
80
80
|
|
|
81
81
|
it('handles multiple top-level labels', () => {
|
|
82
82
|
const labels: Label[] = [
|
|
83
|
-
{ name: 'Label 1',
|
|
84
|
-
{ name: 'Label 2',
|
|
85
|
-
{ name: 'Child',
|
|
83
|
+
{ name: 'Label 1', parent: [], uid: 'label1' },
|
|
84
|
+
{ name: 'Label 2', parent: [], uid: 'label2' },
|
|
85
|
+
{ name: 'Child', parent: ['label2'], uid: 'child' },
|
|
86
86
|
];
|
|
87
87
|
|
|
88
88
|
const result = organize(labels);
|
|
@@ -104,7 +104,7 @@ describe('organize', () => {
|
|
|
104
104
|
|
|
105
105
|
it('throws error for orphaned label', () => {
|
|
106
106
|
const labels: Label[] = [
|
|
107
|
-
{ name: 'Child',
|
|
107
|
+
{ name: 'Child', parent: ['nonexistent'], uid: 'child' },
|
|
108
108
|
];
|
|
109
109
|
|
|
110
110
|
expect(() => organize(labels)).toThrow(
|
|
@@ -114,10 +114,10 @@ describe('organize', () => {
|
|
|
114
114
|
|
|
115
115
|
it('preserves order of siblings', () => {
|
|
116
116
|
const labels: Label[] = [
|
|
117
|
-
{ name: 'Parent',
|
|
118
|
-
{ name: 'Child 3',
|
|
119
|
-
{ name: 'Child 1',
|
|
120
|
-
{ name: 'Child 2',
|
|
117
|
+
{ name: 'Parent', parent: [], uid: 'parent' },
|
|
118
|
+
{ name: 'Child 3', parent: ['parent'], uid: 'child3' },
|
|
119
|
+
{ name: 'Child 1', parent: ['parent'], uid: 'child1' },
|
|
120
|
+
{ name: 'Child 2', parent: ['parent'], uid: 'child2' },
|
|
121
121
|
];
|
|
122
122
|
|
|
123
123
|
const result = organize(labels);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type Label from '#cli/cs/labels/Label.js';
|
|
2
2
|
import type { LabelTreeNode } from './NormalizedLabels.js';
|
|
3
3
|
|
|
4
|
-
// Labels have a tree structure defined by uid/
|
|
4
|
+
// Labels have a tree structure defined by uid/parent array.
|
|
5
5
|
//
|
|
6
|
-
// The API returns a flat array that includes uid/
|
|
7
|
-
//
|
|
6
|
+
// The API returns a flat array that includes uid/parent, where parent is an array
|
|
7
|
+
// containing the parent UID (or empty array for top-level labels).
|
|
8
8
|
//
|
|
9
9
|
// We organize labels into a tree structure for serialization.
|
|
10
10
|
//
|
|
@@ -17,8 +17,8 @@ export default function organize(
|
|
|
17
17
|
const topLevel: MutableNode[] = [];
|
|
18
18
|
|
|
19
19
|
for (const label of labels) {
|
|
20
|
-
const {
|
|
21
|
-
// Preserve all fields except
|
|
20
|
+
const { parent, ...labelWithoutParent } = label;
|
|
21
|
+
// Preserve all fields except parent (which is represented by the tree structure)
|
|
22
22
|
byUid.set(label.uid, {
|
|
23
23
|
...labelWithoutParent,
|
|
24
24
|
name: label.name,
|
|
@@ -26,24 +26,25 @@ export default function organize(
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
for (const
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
throw new Error(`Label ${uid} not found`);
|
|
29
|
+
for (const label of labels) {
|
|
30
|
+
const node = byUid.get(label.uid);
|
|
31
|
+
if (!node) {
|
|
32
|
+
throw new Error(`Label ${label.uid} not found`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const parentUid = label.parent.length > 0 ? label.parent[0] : null;
|
|
36
|
+
if (!parentUid) {
|
|
37
|
+
topLevel.push(node);
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const parent = byUid.get(
|
|
41
|
+
const parent = byUid.get(parentUid);
|
|
41
42
|
if (!parent) {
|
|
42
|
-
const msg = `Orphaned label ${uid} with parent ${
|
|
43
|
+
const msg = `Orphaned label ${label.uid} with parent ${parentUid}`;
|
|
43
44
|
throw new Error(msg);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
(parent.children ??= []).push(
|
|
47
|
+
(parent.children ??= []).push(node);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
return topLevel;
|
|
@@ -3,6 +3,7 @@ import deleteEntry from '#cli/cs/entries/delete.js';
|
|
|
3
3
|
import getEntryLocales from '#cli/cs/entries/getEntryLocales.js';
|
|
4
4
|
import importEntry from '#cli/cs/entries/import.js';
|
|
5
5
|
import type { Entry } from '#cli/cs/entries/Types.js';
|
|
6
|
+
import { ensureLocaleExists } from '#cli/cs/locales/ensureLocaleExists.js';
|
|
6
7
|
import BeaconReplacer from '#cli/dto/entry/BeaconReplacer.js';
|
|
7
8
|
import type ProgressBar from '#cli/ui/progress/ProgressBar.js';
|
|
8
9
|
import type Ctx from '../ctx/Ctx.js';
|
|
@@ -45,7 +46,45 @@ export default async function toContentstack(
|
|
|
45
46
|
update,
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
// Process unmodified entries to ensure all locale versions are synced
|
|
50
|
+
await processUnmodifiedEntries(
|
|
51
|
+
result.unmodified,
|
|
52
|
+
ctx,
|
|
53
|
+
contentType,
|
|
54
|
+
csEntriesByTitle,
|
|
55
|
+
fsEntriesByTitle,
|
|
56
|
+
transformer,
|
|
57
|
+
filenamesByTitle,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shouldSyncLocales(
|
|
64
|
+
fsLocaleVersions: Awaited<ReturnType<typeof loadFsLocaleVersions>>,
|
|
65
|
+
csLocaleSet: Set<string>,
|
|
66
|
+
): boolean {
|
|
67
|
+
// Check if we have new locale versions that don't exist in Contentstack
|
|
68
|
+
const fsLocaleSet = new Set(
|
|
69
|
+
fsLocaleVersions.map((lv) => (lv.locale === 'default' ? null : lv.locale)),
|
|
70
|
+
);
|
|
71
|
+
return Array.from(fsLocaleSet).some(
|
|
72
|
+
(locale) => locale !== null && !csLocaleSet.has(locale),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function processUnmodifiedEntries(
|
|
77
|
+
unmodified: Iterable<string>,
|
|
78
|
+
ctx: Ctx,
|
|
79
|
+
contentType: ContentType,
|
|
80
|
+
csEntriesByTitle: ReadonlyMap<string, Entry>,
|
|
81
|
+
fsEntriesByTitle: ReadonlyMap<string, Entry>,
|
|
82
|
+
transformer: BeaconReplacer,
|
|
83
|
+
filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
|
|
84
|
+
) {
|
|
85
|
+
const ui = getUi();
|
|
86
|
+
|
|
87
|
+
for (const title of unmodified) {
|
|
49
88
|
const cs = csEntriesByTitle.get(title);
|
|
50
89
|
const fs = fsEntriesByTitle.get(title);
|
|
51
90
|
|
|
@@ -60,11 +99,33 @@ export default async function toContentstack(
|
|
|
60
99
|
throw new Error(`No matching entry found for ${title}.`);
|
|
61
100
|
}
|
|
62
101
|
|
|
102
|
+
// Load all locale versions from filesystem
|
|
103
|
+
const fsLocaleVersions = await loadFsLocaleVersions(
|
|
104
|
+
fs,
|
|
105
|
+
contentType.uid,
|
|
106
|
+
filenamesByTitle,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const csLocaleSet = await getExistingLocales(ctx, contentType, cs.uid);
|
|
110
|
+
|
|
111
|
+
// Only push if there are new locale versions to sync
|
|
112
|
+
if (!shouldSyncLocales(fsLocaleVersions, csLocaleSet)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Push all locale versions (including new locales like zh-cn)
|
|
117
|
+
await updateAllLocales(
|
|
118
|
+
ctx,
|
|
119
|
+
transformer,
|
|
120
|
+
contentType,
|
|
121
|
+
fsLocaleVersions,
|
|
122
|
+
cs.uid,
|
|
123
|
+
csLocaleSet,
|
|
124
|
+
);
|
|
125
|
+
|
|
63
126
|
const entry = { ...fs, uid: cs.uid };
|
|
64
127
|
ctx.references.recordEntryForReferences(contentType.uid, entry);
|
|
65
128
|
}
|
|
66
|
-
|
|
67
|
-
return result;
|
|
68
129
|
}
|
|
69
130
|
|
|
70
131
|
function buildUpdateFn(
|
|
@@ -141,6 +202,13 @@ async function updateAllLocales(
|
|
|
141
202
|
entryUid: string,
|
|
142
203
|
csLocaleSet: Set<string>,
|
|
143
204
|
) {
|
|
205
|
+
// Ensure all required locales exist in the target stack before pushing entries
|
|
206
|
+
const localeEnsurePromises = fsLocaleVersions
|
|
207
|
+
.filter((lv) => lv.locale !== 'default')
|
|
208
|
+
.map(async (lv) => ensureLocaleExists(ctx.cs.client, lv.locale));
|
|
209
|
+
|
|
210
|
+
await Promise.all(localeEnsurePromises);
|
|
211
|
+
|
|
144
212
|
// Import all locale versions in parallel for better performance
|
|
145
213
|
const importPromises = fsLocaleVersions.map(async (localeVersion) => {
|
|
146
214
|
const transformed = transformer.process(localeVersion.entry);
|
|
@@ -148,9 +216,10 @@ async function updateAllLocales(
|
|
|
148
216
|
// Pass undefined for 'default' locale (single-locale backward compat)
|
|
149
217
|
const locale =
|
|
150
218
|
localeVersion.locale === 'default' ? undefined : localeVersion.locale;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
219
|
+
|
|
220
|
+
// Always use overwrite=true for locale-specific versions since the entry exists.
|
|
221
|
+
// For 'default' locale (single-locale backward compat), only overwrite if entry has locales.
|
|
222
|
+
const overwrite = locale ? true : csLocaleSet.size > 0;
|
|
154
223
|
|
|
155
224
|
return importEntry(
|
|
156
225
|
ctx.cs.client,
|
|
@@ -80,28 +80,24 @@ function createWriteFn(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// If only one locale, save without locale suffix for backward compatibility.
|
|
83
|
-
// When multiple locales exist
|
|
84
|
-
//
|
|
85
|
-
// `Autumn Feast and Social.yaml`). Only use locale suffix when no
|
|
86
|
-
// English locale is available.
|
|
87
|
-
const hasEnglish = locales.some((l) => /^en(?:[-_]|$)/iu.test(l.code));
|
|
88
|
-
const useLocaleSuffix = locales.length > 1 && !hasEnglish;
|
|
89
|
-
|
|
90
|
-
// Log locale details for debugging why specific locales (e.g. Chinese)
|
|
91
|
-
// may not be written to the filesystem.
|
|
92
|
-
// Debug logging removed to avoid unsafe-call lint errors.
|
|
83
|
+
// When multiple locales exist, write English as base file and other locales
|
|
84
|
+
// with locale suffixes (e.g., Entry.yaml for English, Entry.zh-chs.yaml for Chinese)
|
|
93
85
|
|
|
94
86
|
// Write all locale versions in parallel for better performance
|
|
95
|
-
const writePromises = locales.map(async (locale) =>
|
|
96
|
-
|
|
87
|
+
const writePromises = locales.map(async (locale) => {
|
|
88
|
+
const isEnglish = /^en(?:[-_]|$)/iu.test(locale.code);
|
|
89
|
+
// Use locale suffix for non-English locales when multiple locales exist
|
|
90
|
+
const useLocaleSuffix = locales.length > 1 && !isEnglish;
|
|
91
|
+
|
|
92
|
+
return writeLocaleVersion(
|
|
97
93
|
ctx,
|
|
98
94
|
contentType,
|
|
99
95
|
entry,
|
|
100
96
|
locale.code,
|
|
101
97
|
getBasePath,
|
|
102
98
|
useLocaleSuffix,
|
|
103
|
-
)
|
|
104
|
-
);
|
|
99
|
+
);
|
|
100
|
+
});
|
|
105
101
|
|
|
106
102
|
await Promise.all(writePromises);
|
|
107
103
|
};
|
|
@@ -122,8 +118,27 @@ async function writeLocaleVersion(
|
|
|
122
118
|
localeCode,
|
|
123
119
|
);
|
|
124
120
|
|
|
121
|
+
// Skip writing this locale version if it's a fallback (locale doesn't match requested)
|
|
122
|
+
// This happens when Contentstack returns the default locale content because
|
|
123
|
+
// no localized version exists for the requested locale
|
|
124
|
+
if (
|
|
125
|
+
useLocaleSuffix &&
|
|
126
|
+
exported.locale &&
|
|
127
|
+
typeof exported.locale === 'string' &&
|
|
128
|
+
exported.locale !== localeCode
|
|
129
|
+
) {
|
|
130
|
+
// This is a fallback locale, skip writing it
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
const { uid, ...transformed } = transformEntry(ctx, contentType, exported);
|
|
126
135
|
|
|
136
|
+
// Preserve the actual locale code from the source stack
|
|
137
|
+
// This ensures we maintain the exact language-region configuration
|
|
138
|
+
if ('locale' in transformed) {
|
|
139
|
+
transformed.locale = localeCode;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
const basePath = getBasePath(entry);
|
|
128
143
|
const filePath = useLocaleSuffix
|
|
129
144
|
? basePath.replace(/\.yaml$/u, `.${localeCode}.yaml`)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
2
|
+
|
|
3
|
+
export function canonicalize(value: unknown): unknown {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
const result: unknown[] = [];
|
|
6
|
+
for (const item of value) {
|
|
7
|
+
result.push(canonicalize(item));
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(value)) {
|
|
12
|
+
const obj: Record<string, unknown> = value;
|
|
13
|
+
const out: Record<string, unknown> = {};
|
|
14
|
+
const objKeys: string[] = Object.keys(obj);
|
|
15
|
+
objKeys.sort();
|
|
16
|
+
for (const key of objKeys) {
|
|
17
|
+
if (key === 'uid' || key === 'created_at' || key === 'updated_at')
|
|
18
|
+
continue;
|
|
19
|
+
// Normalize parent: skip if empty array
|
|
20
|
+
const val: unknown = obj[key];
|
|
21
|
+
if (key === 'parent' && Array.isArray(val) && val.length === 0) continue;
|
|
22
|
+
const canonicalizedVal: unknown = canonicalize(val);
|
|
23
|
+
out[key] = canonicalizedVal;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function prepareLabel(
|
|
31
|
+
label: Record<string, unknown>,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
// Labels are sent to the API with all fields intact
|
|
34
|
+
// The API expects the parent field as an array
|
|
35
|
+
return label;
|
|
36
|
+
}
|