gtx-cli 2.1.9 → 2.1.10
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 +6 -0
- package/dist/cli/commands/translate.js +5 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/utils/addExplicitAnchorIds.d.ts +24 -0
- package/dist/utils/addExplicitAnchorIds.js +263 -0
- package/dist/utils/localizeStaticUrls.js +4 -0
- package/dist/utils/processAnchorIds.d.ts +6 -0
- package/dist/utils/processAnchorIds.js +43 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.1.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#600](https://github.com/generaltranslation/gt/pull/600) [`e94aac2`](https://github.com/generaltranslation/gt/commit/e94aac2b2554a279245d090b0872f6f64eb71c62) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Added handling of fragment URLs (i.e. href="#my-mdx-id") for correct routing across locales.
|
|
8
|
+
|
|
3
9
|
## 2.1.9
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -5,6 +5,7 @@ import { getStagedVersions } from '../../fs/config/updateVersions.js';
|
|
|
5
5
|
import copyFile from '../../fs/copyFile.js';
|
|
6
6
|
import flattenJsonFiles from '../../utils/flattenJsonFiles.js';
|
|
7
7
|
import localizeStaticUrls from '../../utils/localizeStaticUrls.js';
|
|
8
|
+
import processAnchorIds from '../../utils/processAnchorIds.js';
|
|
8
9
|
import { noFilesError, noVersionIdError } from '../../console/index.js';
|
|
9
10
|
// Downloads translations that were completed
|
|
10
11
|
export async function handleTranslate(options, settings, filesTranslationResponse) {
|
|
@@ -34,13 +35,16 @@ export async function handleDownload(options, settings) {
|
|
|
34
35
|
await checkFileTranslations(stagedVersionData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings);
|
|
35
36
|
}
|
|
36
37
|
export async function postProcessTranslations(settings) {
|
|
37
|
-
// Localize static urls (/docs -> /[locale]/docs) for non-default locales
|
|
38
|
+
// Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales
|
|
38
39
|
// Default locale is processed earlier in the flow in base.ts
|
|
39
40
|
if (settings.options?.experimentalLocalizeStaticUrls) {
|
|
40
41
|
const nonDefaultLocales = settings.locales.filter((locale) => locale !== settings.defaultLocale);
|
|
41
42
|
if (nonDefaultLocales.length > 0) {
|
|
42
43
|
await localizeStaticUrls(settings, nonDefaultLocales);
|
|
43
44
|
}
|
|
45
|
+
// Add explicit anchor IDs to translated MDX/MD files to preserve navigation
|
|
46
|
+
// Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'
|
|
47
|
+
await processAnchorIds(settings);
|
|
44
48
|
}
|
|
45
49
|
// Flatten json files into a single file
|
|
46
50
|
if (settings.options?.experimentalFlattenJsonFiles) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type Options = {
|
|
|
21
21
|
experimentalHideDefaultLocale?: boolean;
|
|
22
22
|
experimentalFlattenJsonFiles?: boolean;
|
|
23
23
|
experimentalLocalizeStaticImports?: boolean;
|
|
24
|
+
experimentalAddHeaderAnchorIds?: 'mintlify';
|
|
24
25
|
};
|
|
25
26
|
export type TranslateFlags = {
|
|
26
27
|
config?: string;
|
|
@@ -41,6 +42,7 @@ export type TranslateFlags = {
|
|
|
41
42
|
experimentalHideDefaultLocale?: boolean;
|
|
42
43
|
experimentalFlattenJsonFiles?: boolean;
|
|
43
44
|
experimentalLocalizeStaticImports?: boolean;
|
|
45
|
+
experimentalAddHeaderAnchorIds?: 'mintlify';
|
|
44
46
|
excludeStaticUrls?: string[];
|
|
45
47
|
excludeStaticImports?: string[];
|
|
46
48
|
};
|
|
@@ -139,6 +141,7 @@ export type AdditionalOptions = {
|
|
|
139
141
|
copyFiles?: string[];
|
|
140
142
|
experimentalLocalizeStaticImports?: boolean;
|
|
141
143
|
experimentalLocalizeStaticUrls?: boolean;
|
|
144
|
+
experimentalAddHeaderAnchorIds?: 'mintlify';
|
|
142
145
|
experimentalHideDefaultLocale?: boolean;
|
|
143
146
|
experimentalFlattenJsonFiles?: boolean;
|
|
144
147
|
baseDomain?: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a heading with its position and metadata
|
|
3
|
+
*/
|
|
4
|
+
export interface HeadingInfo {
|
|
5
|
+
text: string;
|
|
6
|
+
level: number;
|
|
7
|
+
slug: string;
|
|
8
|
+
position: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extracts heading information from content (read-only, no modifications)
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractHeadingInfo(mdxContent: string): HeadingInfo[];
|
|
14
|
+
/**
|
|
15
|
+
* Applies anchor IDs to translated content based on source heading mapping
|
|
16
|
+
*/
|
|
17
|
+
export declare function addExplicitAnchorIds(translatedContent: string, sourceHeadingMap: HeadingInfo[], settings?: any, sourcePath?: string, translatedPath?: string): {
|
|
18
|
+
content: string;
|
|
19
|
+
hasChanges: boolean;
|
|
20
|
+
addedIds: Array<{
|
|
21
|
+
heading: string;
|
|
22
|
+
id: string;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkMdx from 'remark-mdx';
|
|
4
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
5
|
+
import remarkStringify from 'remark-stringify';
|
|
6
|
+
import { visit } from 'unist-util-visit';
|
|
7
|
+
import { logErrorAndExit } from '../console/logging.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates a slug from heading text
|
|
10
|
+
*/
|
|
11
|
+
function generateSlug(text) {
|
|
12
|
+
return text
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^\w\s-]/g, '') // Remove special chars except spaces and hyphens
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
17
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
|
|
18
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extracts text content from heading nodes
|
|
22
|
+
*/
|
|
23
|
+
function extractHeadingText(heading) {
|
|
24
|
+
let text = '';
|
|
25
|
+
visit(heading, ['text', 'inlineCode'], (node) => {
|
|
26
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
27
|
+
text += node.value;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks if a heading is already wrapped in a div with id
|
|
34
|
+
*/
|
|
35
|
+
function hasExplicitId(heading, ast) {
|
|
36
|
+
const lastChild = heading.children[heading.children.length - 1];
|
|
37
|
+
if (lastChild?.type === 'text') {
|
|
38
|
+
return /(\{#[^}]+\}|\[[^\]]+\])$/.test(lastChild.value);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extracts heading information from content (read-only, no modifications)
|
|
44
|
+
*/
|
|
45
|
+
export function extractHeadingInfo(mdxContent) {
|
|
46
|
+
const headings = [];
|
|
47
|
+
// Parse the MDX content into an AST
|
|
48
|
+
let processedAst;
|
|
49
|
+
try {
|
|
50
|
+
const parseProcessor = unified()
|
|
51
|
+
.use(remarkParse)
|
|
52
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
53
|
+
.use(remarkMdx);
|
|
54
|
+
const ast = parseProcessor.parse(mdxContent);
|
|
55
|
+
processedAst = parseProcessor.runSync(ast);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn(`Failed to parse MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
let position = 0;
|
|
62
|
+
visit(processedAst, 'heading', (heading) => {
|
|
63
|
+
const headingText = extractHeadingText(heading);
|
|
64
|
+
if (headingText) {
|
|
65
|
+
const slug = generateSlug(headingText);
|
|
66
|
+
headings.push({
|
|
67
|
+
text: headingText,
|
|
68
|
+
level: heading.depth,
|
|
69
|
+
slug,
|
|
70
|
+
position: position++,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return headings;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Applies anchor IDs to translated content based on source heading mapping
|
|
78
|
+
*/
|
|
79
|
+
export function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sourcePath, translatedPath) {
|
|
80
|
+
const addedIds = [];
|
|
81
|
+
const useDivWrapping = settings?.options?.experimentalAddHeaderAnchorIds === 'mintlify';
|
|
82
|
+
// Extract headings from translated content
|
|
83
|
+
const translatedHeadings = extractHeadingInfo(translatedContent);
|
|
84
|
+
// Pre-processing validation: check if header counts match
|
|
85
|
+
if (sourceHeadingMap.length !== translatedHeadings.length) {
|
|
86
|
+
const sourceFile = sourcePath
|
|
87
|
+
? `Source file: ${sourcePath}`
|
|
88
|
+
: 'Source file';
|
|
89
|
+
const translatedFile = translatedPath
|
|
90
|
+
? `translated file: ${translatedPath}`
|
|
91
|
+
: 'translated file';
|
|
92
|
+
logErrorAndExit(`Header count mismatch detected! ${sourceFile} has ${sourceHeadingMap.length} headers but ${translatedFile} has ${translatedHeadings.length} headers. ` +
|
|
93
|
+
`This likely means your source file was edited after translation was requested, causing a mismatch between ` +
|
|
94
|
+
`the number of headers in your source file vs the translated file. Please re-translate this file to resolve the issue.`);
|
|
95
|
+
}
|
|
96
|
+
// Create ID mapping based on positional matching
|
|
97
|
+
const idMappings = new Map();
|
|
98
|
+
sourceHeadingMap.forEach((sourceHeading, index) => {
|
|
99
|
+
const translatedHeading = translatedHeadings[index];
|
|
100
|
+
// Match by position and level for safety
|
|
101
|
+
if (translatedHeading && translatedHeading.level === sourceHeading.level) {
|
|
102
|
+
idMappings.set(index, sourceHeading.slug);
|
|
103
|
+
addedIds.push({
|
|
104
|
+
heading: translatedHeading.text,
|
|
105
|
+
id: sourceHeading.slug,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
if (idMappings.size === 0) {
|
|
110
|
+
return {
|
|
111
|
+
content: translatedContent,
|
|
112
|
+
hasChanges: false,
|
|
113
|
+
addedIds: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Apply IDs to translated content
|
|
117
|
+
let content;
|
|
118
|
+
if (useDivWrapping) {
|
|
119
|
+
content = applyDivWrappedIds(translatedContent, translatedHeadings, idMappings);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
content = applyInlineIds(translatedContent, idMappings);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content,
|
|
126
|
+
hasChanges: addedIds.length > 0,
|
|
127
|
+
addedIds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Adds inline {#id} syntax to headings (standard markdown approach)
|
|
132
|
+
*/
|
|
133
|
+
function applyInlineIds(translatedContent, idMappings) {
|
|
134
|
+
// Parse the translated content
|
|
135
|
+
let processedAst;
|
|
136
|
+
try {
|
|
137
|
+
const parseProcessor = unified()
|
|
138
|
+
.use(remarkParse)
|
|
139
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
140
|
+
.use(remarkMdx);
|
|
141
|
+
const ast = parseProcessor.parse(translatedContent);
|
|
142
|
+
processedAst = parseProcessor.runSync(ast);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.warn(`Failed to parse translated MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
|
+
return translatedContent;
|
|
147
|
+
}
|
|
148
|
+
// Apply IDs to headings based on position
|
|
149
|
+
let headingIndex = 0;
|
|
150
|
+
visit(processedAst, 'heading', (heading) => {
|
|
151
|
+
const id = idMappings.get(headingIndex);
|
|
152
|
+
if (id) {
|
|
153
|
+
// Skip if heading already has explicit ID
|
|
154
|
+
if (hasExplicitId(heading, processedAst)) {
|
|
155
|
+
headingIndex++;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Add the ID to the heading
|
|
159
|
+
const lastChild = heading.children[heading.children.length - 1];
|
|
160
|
+
if (lastChild?.type === 'text') {
|
|
161
|
+
lastChild.value += ` {#${id}}`;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// If last child is not text, add a new text node
|
|
165
|
+
heading.children.push({
|
|
166
|
+
type: 'text',
|
|
167
|
+
value: ` {#${id}}`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
headingIndex++;
|
|
172
|
+
});
|
|
173
|
+
// Convert the modified AST back to MDX string
|
|
174
|
+
try {
|
|
175
|
+
const stringifyProcessor = unified()
|
|
176
|
+
.use(remarkStringify, {
|
|
177
|
+
bullet: '-',
|
|
178
|
+
emphasis: '_',
|
|
179
|
+
strong: '*',
|
|
180
|
+
rule: '-',
|
|
181
|
+
ruleRepetition: 3,
|
|
182
|
+
ruleSpaces: false,
|
|
183
|
+
handlers: {
|
|
184
|
+
// Custom handler to prevent escaping of {#id} syntax
|
|
185
|
+
text(node) {
|
|
186
|
+
return node.value;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
191
|
+
.use(remarkMdx);
|
|
192
|
+
let content = stringifyProcessor.stringify(processedAst);
|
|
193
|
+
// Handle newline formatting to match original input
|
|
194
|
+
if (content.endsWith('\n') && !translatedContent.endsWith('\n')) {
|
|
195
|
+
content = content.slice(0, -1);
|
|
196
|
+
}
|
|
197
|
+
// Preserve leading newlines from original content
|
|
198
|
+
if (translatedContent.startsWith('\n') && !content.startsWith('\n')) {
|
|
199
|
+
content = '\n' + content;
|
|
200
|
+
}
|
|
201
|
+
return content;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.warn(`Failed to stringify translated MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
return translatedContent;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Wraps headings in divs with IDs (Mintlify approach)
|
|
210
|
+
*/
|
|
211
|
+
function applyDivWrappedIds(translatedContent, translatedHeadings, idMappings) {
|
|
212
|
+
// Extract all heading lines from the translated markdown
|
|
213
|
+
const lines = translatedContent.split('\n');
|
|
214
|
+
const headingLines = [];
|
|
215
|
+
lines.forEach((line, index) => {
|
|
216
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
217
|
+
if (headingMatch) {
|
|
218
|
+
const level = headingMatch[1].length;
|
|
219
|
+
headingLines.push({ line, level, index });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// Use string-based approach to wrap headings in divs
|
|
223
|
+
let content = translatedContent;
|
|
224
|
+
const headingsToWrap = [];
|
|
225
|
+
// Match translated headings with their corresponding lines by position and level
|
|
226
|
+
translatedHeadings.forEach((heading, position) => {
|
|
227
|
+
const id = idMappings.get(position);
|
|
228
|
+
if (id) {
|
|
229
|
+
// Find the corresponding original line for this heading
|
|
230
|
+
const matchingLine = headingLines.find((hl) => {
|
|
231
|
+
// Extract clean text from the original line for comparison
|
|
232
|
+
const lineCleanText = hl.line.replace(/^#{1,6}\s+/, '').trim();
|
|
233
|
+
// Create a version without markdown formatting for comparison
|
|
234
|
+
const cleanLineText = lineCleanText
|
|
235
|
+
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold
|
|
236
|
+
.replace(/\*(.*?)\*/g, '$1') // Remove italic
|
|
237
|
+
.replace(/`(.*?)`/g, '$1') // Remove inline code
|
|
238
|
+
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // Remove links, keep text
|
|
239
|
+
.trim();
|
|
240
|
+
return cleanLineText === heading.text && hl.level === heading.level;
|
|
241
|
+
});
|
|
242
|
+
if (matchingLine) {
|
|
243
|
+
headingsToWrap.push({
|
|
244
|
+
originalLine: matchingLine.line,
|
|
245
|
+
id,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (headingsToWrap.length > 0) {
|
|
251
|
+
// Process headings from longest to shortest original line to avoid partial matches
|
|
252
|
+
const sortedHeadings = headingsToWrap.sort((a, b) => b.originalLine.length - a.originalLine.length);
|
|
253
|
+
for (const heading of sortedHeadings) {
|
|
254
|
+
// Escape the original line for use in regex
|
|
255
|
+
const escapedLine = heading.originalLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
256
|
+
const headingPattern = new RegExp(`^${escapedLine}\\s*$`, 'gm');
|
|
257
|
+
content = content.replace(headingPattern, (match) => {
|
|
258
|
+
return `<div id="${heading.id}">\n ${match.trim()}\n</div>\n`;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return content;
|
|
263
|
+
}
|
|
@@ -87,6 +87,10 @@ export default async function localizeStaticUrls(settings, targetLocales) {
|
|
|
87
87
|
* Determines if a URL should be processed based on pattern matching
|
|
88
88
|
*/
|
|
89
89
|
function shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale, baseDomain) {
|
|
90
|
+
// Check fragment-only URLs like "#id-name"
|
|
91
|
+
if (/^\s*#/.test(originalUrl)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
90
94
|
const patternWithoutSlash = patternHead.replace(/\/$/, '');
|
|
91
95
|
// Handle absolute URLs with baseDomain
|
|
92
96
|
let urlToCheck = originalUrl;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addExplicitAnchorIds, extractHeadingInfo, } from './addExplicitAnchorIds.js';
|
|
2
|
+
import { readFile } from '../fs/findFilepath.js';
|
|
3
|
+
import { createFileMapping } from '../formats/files/fileMapping.js';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
/**
|
|
6
|
+
* Processes all translated MD/MDX files to add explicit anchor IDs
|
|
7
|
+
* This preserves navigation links when headings are translated
|
|
8
|
+
*/
|
|
9
|
+
export default async function processAnchorIds(settings) {
|
|
10
|
+
if (!settings.files)
|
|
11
|
+
return;
|
|
12
|
+
const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
|
|
13
|
+
const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
|
|
14
|
+
// Process each locale's translated files
|
|
15
|
+
const processPromises = Object.entries(fileMapping)
|
|
16
|
+
.filter(([locale, filesMap]) => locale !== settings.defaultLocale) // Skip default locale
|
|
17
|
+
.map(async ([locale, filesMap]) => {
|
|
18
|
+
// Get all translated files that are md or mdx
|
|
19
|
+
const translatedFiles = Object.values(filesMap).filter((path) => path && (path.endsWith('.md') || path.endsWith('.mdx')));
|
|
20
|
+
for (const translatedPath of translatedFiles) {
|
|
21
|
+
try {
|
|
22
|
+
// Find the corresponding source file
|
|
23
|
+
const sourcePath = Object.keys(filesMap).find((key) => filesMap[key] === translatedPath);
|
|
24
|
+
if (!sourcePath) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Extract heading info from source file
|
|
28
|
+
const sourceContent = readFile(sourcePath);
|
|
29
|
+
const sourceHeadingMap = extractHeadingInfo(sourceContent);
|
|
30
|
+
// Read translated file and apply anchor IDs
|
|
31
|
+
const translatedContent = readFile(translatedPath);
|
|
32
|
+
const result = addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sourcePath, translatedPath);
|
|
33
|
+
if (result.hasChanges) {
|
|
34
|
+
fs.writeFileSync(translatedPath, result.content, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn(`Failed to process IDs for ${translatedPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
await Promise.all(processPromises);
|
|
43
|
+
}
|