gt 2.14.17 → 2.14.18
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/api/downloadFileBatch.js +26 -2
- package/dist/cli/base.js +3 -0
- package/dist/cli/commands/upload.js +9 -7
- package/dist/formats/files/aggregateFiles.js +10 -7
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/state/mintlifyRefMap.d.ts +4 -0
- package/dist/state/mintlifyRefMap.js +12 -0
- package/dist/utils/resolveMintlifyRefs.d.ts +28 -0
- package/dist/utils/resolveMintlifyRefs.js +117 -0
- package/dist/utils/splitMintlifyLanguageRefs.d.ts +13 -0
- package/dist/utils/splitMintlifyLanguageRefs.js +190 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.14.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1243](https://github.com/generaltranslation/gt/pull/1243) [`a0e19f6`](https://github.com/generaltranslation/gt/commit/a0e19f64a17d1a439d2352a6bc3ca7390c4ed401) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Handling Mintlify $ref
|
|
8
|
+
|
|
3
9
|
## 2.14.17
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -8,6 +8,7 @@ import { mergeJson } from '../formats/json/mergeJson.js';
|
|
|
8
8
|
import { extractJson } from '../formats/json/extractJson.js';
|
|
9
9
|
import mergeYaml from '../formats/yaml/mergeYaml.js';
|
|
10
10
|
import { extractYaml } from '../formats/yaml/extractYaml.js';
|
|
11
|
+
import { resolveMintlifyRefs, shouldResolveRefs, } from '../utils/resolveMintlifyRefs.js';
|
|
11
12
|
import { readLockfile, writeLockfile, findOrCreateEntry, } from '../fs/config/downloadedVersions.js';
|
|
12
13
|
import { recordDownloaded, recordRemerged } from '../state/recentDownloads.js';
|
|
13
14
|
import { recordWarning } from '../state/translateWarnings.js';
|
|
@@ -30,7 +31,19 @@ function mergeWithSource(translatedContent, locale, inputPath, options) {
|
|
|
30
31
|
if (!sourceContent)
|
|
31
32
|
return translatedContent;
|
|
32
33
|
if (jsonSchema) {
|
|
33
|
-
|
|
34
|
+
// Resolve $ref before merging if configured
|
|
35
|
+
let resolvedSourceContent = sourceContent;
|
|
36
|
+
if (shouldResolveRefs(inputPath, options.options)) {
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.parse(sourceContent);
|
|
39
|
+
const { resolved } = resolveMintlifyRefs(json, inputPath);
|
|
40
|
+
resolvedSourceContent = JSON.stringify(resolved, null, 2);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Fall through with original content
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return mergeJson(resolvedSourceContent, inputPath, options.options, [{ translatedContent, targetLocale: locale }], options.defaultLocale, options.locales)[0];
|
|
34
47
|
}
|
|
35
48
|
else {
|
|
36
49
|
return mergeYaml(sourceContent, inputPath, options.options, [{ translatedContent, targetLocale: locale }], options.defaultLocale)[0];
|
|
@@ -103,7 +116,18 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
|
|
|
103
116
|
// For schema-based files, re-merge with current source in case
|
|
104
117
|
// non-translatable fields changed (skip the API download, not the merge)
|
|
105
118
|
try {
|
|
106
|
-
|
|
119
|
+
let existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
120
|
+
// Resolve $ref before extraction if configured
|
|
121
|
+
if (shouldResolveRefs(outputPath, options.options)) {
|
|
122
|
+
try {
|
|
123
|
+
const json = JSON.parse(existingContent);
|
|
124
|
+
const { resolved } = resolveMintlifyRefs(json, outputPath);
|
|
125
|
+
existingContent = JSON.stringify(resolved, null, 2);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Fall through with original content
|
|
129
|
+
}
|
|
130
|
+
}
|
|
107
131
|
const jsonExtracted = options.options?.jsonSchema
|
|
108
132
|
? extractJson(existingContent, inputPath, options.options, locale, options.defaultLocale)
|
|
109
133
|
: null;
|
package/dist/cli/base.js
CHANGED
|
@@ -33,6 +33,7 @@ import { detectFramework } from '../setup/detectFramework.js';
|
|
|
33
33
|
import { getFrameworkDisplayName, getReactFrameworkLibrary, } from '../setup/frameworkUtils.js';
|
|
34
34
|
import { INLINE_LIBRARIES } from '../types/libraries.js';
|
|
35
35
|
import { handleEnqueue } from './commands/enqueue.js';
|
|
36
|
+
import { splitMintlifyLanguageRefs } from '../utils/splitMintlifyLanguageRefs.js';
|
|
36
37
|
export class BaseCLI {
|
|
37
38
|
library;
|
|
38
39
|
additionalModules;
|
|
@@ -198,6 +199,8 @@ export class BaseCLI {
|
|
|
198
199
|
if (include.size > 0) {
|
|
199
200
|
await postProcessTranslations(settings, include);
|
|
200
201
|
}
|
|
202
|
+
// Split Mintlify language entries into $ref files to keep docs.json small
|
|
203
|
+
await splitMintlifyLanguageRefs(settings);
|
|
201
204
|
// Mirror assets after translations are downloaded and locale dirs are populated
|
|
202
205
|
await mirrorAssetsToLocales(settings);
|
|
203
206
|
clearDownloaded();
|
|
@@ -2,12 +2,12 @@ import { noSupportedFormatError, noDefaultLocaleError, } from '../../console/ind
|
|
|
2
2
|
import { exitSync, logErrorAndExit } from '../../console/logging.js';
|
|
3
3
|
import { logger } from '../../console/logger.js';
|
|
4
4
|
import { getRelative, readFile } from '../../fs/findFilepath.js';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';
|
|
7
6
|
import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
|
|
8
7
|
import { parseJson } from '../../formats/json/parseJson.js';
|
|
9
8
|
import { extractJson } from '../../formats/json/extractJson.js';
|
|
10
|
-
import { validateJsonSchema
|
|
9
|
+
import { validateJsonSchema } from '../../formats/json/utils.js';
|
|
10
|
+
import { resolveMintlifyRefs, shouldResolveRefs, } from '../../utils/resolveMintlifyRefs.js';
|
|
11
11
|
import { runUploadFilesWorkflow } from '../../workflows/upload.js';
|
|
12
12
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { createFileMapping } from '../../formats/files/fileMapping.js';
|
|
@@ -38,17 +38,19 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
|
|
|
38
38
|
}
|
|
39
39
|
const jsonFiles = filePaths.json.map((filePath) => {
|
|
40
40
|
const content = readFile(filePath);
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Resolve $ref before parsing if configured
|
|
42
|
+
let contentForParsing = content;
|
|
43
|
+
if (shouldResolveRefs(filePath, additionalOptions)) {
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
const json = JSON.parse(content);
|
|
46
|
+
const { resolved } = resolveMintlifyRefs(json, filePath);
|
|
47
|
+
contentForParsing = JSON.stringify(resolved, null, 2);
|
|
46
48
|
}
|
|
47
49
|
catch {
|
|
48
50
|
// JSON parse errors are handled below by parseJson
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
const parsedJson = parseJson(
|
|
53
|
+
const parsedJson = parseJson(contentForParsing, filePath, additionalOptions, settings.defaultLocale);
|
|
52
54
|
const relativePath = getRelative(filePath);
|
|
53
55
|
const jsonSchema = validateJsonSchema(additionalOptions, filePath);
|
|
54
56
|
if (jsonSchema?.composite) {
|
|
@@ -3,8 +3,8 @@ import { recordWarning } from '../../state/translateWarnings.js';
|
|
|
3
3
|
import { getRelative, readFile } from '../../fs/findFilepath.js';
|
|
4
4
|
import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
|
|
5
5
|
import { parseJson } from '../json/parseJson.js';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { resolveMintlifyRefs, shouldResolveRefs, } from '../../utils/resolveMintlifyRefs.js';
|
|
7
|
+
import { storeRefMap } from '../../state/mintlifyRefMap.js';
|
|
8
8
|
import parseYaml from '../yaml/parseYaml.js';
|
|
9
9
|
import { validateYamlSchema } from '../yaml/utils.js';
|
|
10
10
|
import { flattenJson } from '../json/flattenJson.js';
|
|
@@ -75,17 +75,20 @@ export async function aggregateFiles(settings) {
|
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Resolve $ref before parsing if configured
|
|
79
|
+
let contentForParsing = content;
|
|
80
|
+
if (shouldResolveRefs(filePath, settings.options)) {
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
const json = JSON.parse(content);
|
|
83
|
+
const { resolved, refMap } = resolveMintlifyRefs(json, filePath);
|
|
84
|
+
storeRefMap(refMap);
|
|
85
|
+
contentForParsing = JSON.stringify(resolved, null, 2);
|
|
83
86
|
}
|
|
84
87
|
catch {
|
|
85
88
|
// JSON parse errors are handled below by parseJson
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
|
-
const parsedJson = parseJson(
|
|
91
|
+
const parsedJson = parseJson(contentForParsing, filePath, settings.options || {}, settings.defaultLocale);
|
|
89
92
|
// Detect companion metadata file
|
|
90
93
|
let keyedMetadata;
|
|
91
94
|
let parsedContent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "2.14.
|
|
1
|
+
export declare const PACKAGE_VERSION = "2.14.18";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated. Do not edit manually.
|
|
2
|
-
export const PACKAGE_VERSION = '2.14.
|
|
2
|
+
export const PACKAGE_VERSION = '2.14.18';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let storedRefMap = new Map();
|
|
2
|
+
export function storeRefMap(refMap) {
|
|
3
|
+
for (const [key, value] of refMap.entries()) {
|
|
4
|
+
storedRefMap.set(key, value);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function getStoredRefMap() {
|
|
8
|
+
return storedRefMap.size > 0 ? storedRefMap : null;
|
|
9
|
+
}
|
|
10
|
+
export function clearStoredRefMap() {
|
|
11
|
+
storedRefMap = new Map();
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type RefMapEntry = {
|
|
2
|
+
sourceFile: string;
|
|
3
|
+
refPath: string;
|
|
4
|
+
containingDir: string;
|
|
5
|
+
originalContent: unknown;
|
|
6
|
+
};
|
|
7
|
+
export type RefMap = Map<string, RefMapEntry>;
|
|
8
|
+
export type ResolvedRefs = {
|
|
9
|
+
resolved: unknown;
|
|
10
|
+
refMap: RefMap;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve all Mintlify $ref references in a parsed JSON object.
|
|
14
|
+
*
|
|
15
|
+
* Returns the fully-expanded JSON and a refMap that tracks which subtrees
|
|
16
|
+
* came from which files (used later to split translated output back into
|
|
17
|
+
* the same file topology).
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveMintlifyRefs(json: unknown, filePath: string): ResolvedRefs;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a file should have $ref resolution applied based on the settings.
|
|
22
|
+
* Returns true if the file has mintlify options configured AND matches a
|
|
23
|
+
* composite jsonSchema entry.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldResolveRefs(filePath: string, options?: {
|
|
26
|
+
mintlify?: any;
|
|
27
|
+
jsonSchema?: Record<string, any>;
|
|
28
|
+
}): boolean;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from '../console/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import micromatch from 'micromatch';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve all Mintlify $ref references in a parsed JSON object.
|
|
8
|
+
*
|
|
9
|
+
* Returns the fully-expanded JSON and a refMap that tracks which subtrees
|
|
10
|
+
* came from which files (used later to split translated output back into
|
|
11
|
+
* the same file topology).
|
|
12
|
+
*/
|
|
13
|
+
export function resolveMintlifyRefs(json, filePath) {
|
|
14
|
+
const refMap = new Map();
|
|
15
|
+
const resolved = resolveNode(json, path.dirname(path.resolve(filePath)), '', new Set(), refMap);
|
|
16
|
+
return { resolved, refMap };
|
|
17
|
+
}
|
|
18
|
+
function resolveNode(node, baseDir, pointer, visiting, refMap) {
|
|
19
|
+
if (node === null || typeof node !== 'object')
|
|
20
|
+
return node;
|
|
21
|
+
if (Array.isArray(node)) {
|
|
22
|
+
return node.map((item, i) => resolveNode(item, baseDir, `${pointer}/${i}`, visiting, refMap));
|
|
23
|
+
}
|
|
24
|
+
const obj = node;
|
|
25
|
+
if (typeof obj['$ref'] === 'string') {
|
|
26
|
+
return resolveRef(obj, baseDir, pointer, visiting, refMap);
|
|
27
|
+
}
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
result[key] = resolveNode(value, baseDir, `${pointer}/${key}`, visiting, refMap);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function resolveRef(obj, baseDir, pointer, visiting, refMap) {
|
|
35
|
+
const refPath = obj['$ref'];
|
|
36
|
+
if (!isRelativePath(refPath)) {
|
|
37
|
+
logger.warn(chalk.yellow(`Skipping non-relative $ref at ${pointer || '/'}: ${refPath}`));
|
|
38
|
+
const { $ref: _, ...rest } = obj;
|
|
39
|
+
return rest;
|
|
40
|
+
}
|
|
41
|
+
const resolvedFilePath = path.resolve(baseDir, refPath);
|
|
42
|
+
if (visiting.has(resolvedFilePath)) {
|
|
43
|
+
logger.warn(chalk.yellow(`Circular $ref detected at ${pointer || '/'}: ${refPath}`));
|
|
44
|
+
const { $ref: _, ...rest } = obj;
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
47
|
+
if (!fs.existsSync(resolvedFilePath)) {
|
|
48
|
+
logger.warn(chalk.yellow(`$ref file not found at ${pointer || '/'}: ${refPath} (resolved to ${resolvedFilePath})`));
|
|
49
|
+
const { $ref: _, ...rest } = obj;
|
|
50
|
+
return rest;
|
|
51
|
+
}
|
|
52
|
+
let fileContent;
|
|
53
|
+
try {
|
|
54
|
+
fileContent = fs.readFileSync(resolvedFilePath, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
logger.warn(chalk.yellow(`Failed to read $ref file: ${resolvedFilePath}`));
|
|
58
|
+
const { $ref: _, ...rest } = obj;
|
|
59
|
+
return rest;
|
|
60
|
+
}
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(fileContent);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
logger.warn(chalk.yellow(`$ref file is not valid JSON: ${resolvedFilePath}`));
|
|
67
|
+
const { $ref: _, ...rest } = obj;
|
|
68
|
+
return rest;
|
|
69
|
+
}
|
|
70
|
+
// Record provenance before recursive resolution
|
|
71
|
+
refMap.set(pointer, {
|
|
72
|
+
sourceFile: resolvedFilePath,
|
|
73
|
+
refPath,
|
|
74
|
+
containingDir: baseDir,
|
|
75
|
+
originalContent: parsed,
|
|
76
|
+
});
|
|
77
|
+
// Recursively resolve nested $ref in the referenced file
|
|
78
|
+
const refFileDir = path.dirname(resolvedFilePath);
|
|
79
|
+
const nextVisiting = new Set(visiting);
|
|
80
|
+
nextVisiting.add(resolvedFilePath);
|
|
81
|
+
const resolvedContent = resolveNode(parsed, refFileDir, pointer, nextVisiting, refMap);
|
|
82
|
+
// Apply Mintlify merge rules
|
|
83
|
+
const { $ref: _, ...siblings } = obj;
|
|
84
|
+
if (resolvedContent !== null &&
|
|
85
|
+
typeof resolvedContent === 'object' &&
|
|
86
|
+
!Array.isArray(resolvedContent)) {
|
|
87
|
+
// Object: merge siblings on top (siblings take precedence)
|
|
88
|
+
return { ...resolvedContent, ...siblings };
|
|
89
|
+
}
|
|
90
|
+
// Non-object (array, string, number, etc.): replace entirely, drop siblings
|
|
91
|
+
return resolvedContent;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a file should have $ref resolution applied based on the settings.
|
|
95
|
+
* Returns true if the file has mintlify options configured AND matches a
|
|
96
|
+
* composite jsonSchema entry.
|
|
97
|
+
*/
|
|
98
|
+
export function shouldResolveRefs(filePath, options) {
|
|
99
|
+
if (!options?.mintlify)
|
|
100
|
+
return false;
|
|
101
|
+
if (!options.jsonSchema)
|
|
102
|
+
return false;
|
|
103
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
104
|
+
for (const [glob, schema] of Object.entries(options.jsonSchema)) {
|
|
105
|
+
if (schema?.composite && micromatch.isMatch(relative, glob)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function isRelativePath(refPath) {
|
|
112
|
+
if (path.isAbsolute(refPath))
|
|
113
|
+
return false;
|
|
114
|
+
if (/^[a-z]+:\/\//i.test(refPath))
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Post-processing step for Mintlify docs.json.
|
|
4
|
+
*
|
|
5
|
+
* After mergeJson writes a fully-inlined docs.json, this function restores
|
|
6
|
+
* the original $ref structure:
|
|
7
|
+
*
|
|
8
|
+
* - Default locale: restores original $ref paths
|
|
9
|
+
* - Non-default locales: prefixes ref paths with {locale}/, writes translated
|
|
10
|
+
* content to the prefixed paths
|
|
11
|
+
* - Top-level refs (navigation, navbar): restored in docs.json
|
|
12
|
+
*/
|
|
13
|
+
export declare function splitMintlifyLanguageRefs(settings: Settings): Promise<void>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from '../console/logger.js';
|
|
4
|
+
import { shouldResolveRefs } from './resolveMintlifyRefs.js';
|
|
5
|
+
import { getStoredRefMap, clearStoredRefMap } from '../state/mintlifyRefMap.js';
|
|
6
|
+
/**
|
|
7
|
+
* Post-processing step for Mintlify docs.json.
|
|
8
|
+
*
|
|
9
|
+
* After mergeJson writes a fully-inlined docs.json, this function restores
|
|
10
|
+
* the original $ref structure:
|
|
11
|
+
*
|
|
12
|
+
* - Default locale: restores original $ref paths
|
|
13
|
+
* - Non-default locales: prefixes ref paths with {locale}/, writes translated
|
|
14
|
+
* content to the prefixed paths
|
|
15
|
+
* - Top-level refs (navigation, navbar): restored in docs.json
|
|
16
|
+
*/
|
|
17
|
+
export async function splitMintlifyLanguageRefs(settings) {
|
|
18
|
+
const isMintlify = settings.framework === 'mintlify' || !!settings.options?.mintlify;
|
|
19
|
+
if (!isMintlify)
|
|
20
|
+
return;
|
|
21
|
+
const refMap = getStoredRefMap();
|
|
22
|
+
if (!refMap || refMap.size === 0)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
const resolvedJsonPaths = settings.files?.resolvedPaths?.json;
|
|
26
|
+
if (!resolvedJsonPaths)
|
|
27
|
+
return;
|
|
28
|
+
const docsJsonPath = resolvedJsonPaths.find((p) => shouldResolveRefs(p, settings.options));
|
|
29
|
+
if (!docsJsonPath)
|
|
30
|
+
return;
|
|
31
|
+
if (!fs.existsSync(docsJsonPath))
|
|
32
|
+
return;
|
|
33
|
+
let docsJson;
|
|
34
|
+
try {
|
|
35
|
+
docsJson = JSON.parse(fs.readFileSync(docsJsonPath, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const defaultLocale = settings.defaultLocale;
|
|
41
|
+
const navRefEntry = refMap.get('/navigation');
|
|
42
|
+
const navContent = navRefEntry
|
|
43
|
+
? getAtPointer(docsJson, '/navigation')
|
|
44
|
+
: docsJson?.navigation;
|
|
45
|
+
const languages = navContent?.languages;
|
|
46
|
+
if (!Array.isArray(languages) || languages.length <= 1) {
|
|
47
|
+
restoreTopLevelRefs(docsJson, refMap, docsJsonPath);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const defaultIndex = languages.findIndex((e) => e?.language === defaultLocale);
|
|
51
|
+
if (defaultIndex < 0) {
|
|
52
|
+
restoreTopLevelRefs(docsJson, refMap, docsJsonPath);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const navDir = navRefEntry
|
|
56
|
+
? path.dirname(navRefEntry.sourceFile)
|
|
57
|
+
: path.dirname(docsJsonPath);
|
|
58
|
+
const defaultPointerPrefix = `/navigation/languages/${defaultIndex}`;
|
|
59
|
+
const internalRefs = collectInternalRefs(refMap, defaultPointerPrefix);
|
|
60
|
+
if (internalRefs.length > 0) {
|
|
61
|
+
const defaultEntry = languages[defaultIndex];
|
|
62
|
+
for (const ref of internalRefs) {
|
|
63
|
+
setAtPointer(defaultEntry, ref.relativePointer, {
|
|
64
|
+
$ref: ref.refPath,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const entry of languages) {
|
|
68
|
+
if (!entry || entry.language === defaultLocale)
|
|
69
|
+
continue;
|
|
70
|
+
const locale = entry.language;
|
|
71
|
+
if (!locale)
|
|
72
|
+
continue;
|
|
73
|
+
for (const ref of internalRefs) {
|
|
74
|
+
const subtree = getAtPointer(entry, ref.relativePointer);
|
|
75
|
+
if (subtree === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
const originalAbsPath = path.resolve(ref.resolvedDir, ref.refPath);
|
|
78
|
+
const relToNavDir = path.relative(navDir, originalAbsPath);
|
|
79
|
+
const localeRelPath = path.join(locale, relToNavDir);
|
|
80
|
+
const outputPath = path.resolve(navDir, localeRelPath);
|
|
81
|
+
writeJsonFile(outputPath, subtree);
|
|
82
|
+
// All refs inside the locale's files use original paths — the locale
|
|
83
|
+
// directory mirrors the source structure, so relative resolution works.
|
|
84
|
+
// The locale prefix only appears in the parent navigation.json entry.
|
|
85
|
+
setAtPointer(entry, ref.relativePointer, { $ref: ref.refPath });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
logger.info(`Restored $ref structure for source locale navigation`);
|
|
89
|
+
}
|
|
90
|
+
const navFileName = navRefEntry
|
|
91
|
+
? path.basename(navRefEntry.sourceFile)
|
|
92
|
+
: 'navigation.json';
|
|
93
|
+
for (let i = 0; i < languages.length; i++) {
|
|
94
|
+
const entry = languages[i];
|
|
95
|
+
if (!entry || entry.language === defaultLocale)
|
|
96
|
+
continue;
|
|
97
|
+
const locale = entry.language;
|
|
98
|
+
if (!locale)
|
|
99
|
+
continue;
|
|
100
|
+
const { language, ...contentWithoutLanguage } = entry;
|
|
101
|
+
const entryFileName = `${locale}/${navFileName}`;
|
|
102
|
+
const entryFilePath = path.resolve(navDir, entryFileName);
|
|
103
|
+
writeJsonFile(entryFilePath, contentWithoutLanguage);
|
|
104
|
+
languages[i] = { language: locale, $ref: `./${entryFileName}` };
|
|
105
|
+
}
|
|
106
|
+
logger.info(`Split locale navigation entries into ref files`);
|
|
107
|
+
restoreTopLevelRefs(docsJson, refMap, docsJsonPath);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearStoredRefMap();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Restore top-level $ref pointers in docs.json.
|
|
115
|
+
* Writes each resolved subtree to its original source file and replaces
|
|
116
|
+
* the subtree in docs.json with the $ref pointer.
|
|
117
|
+
*/
|
|
118
|
+
function restoreTopLevelRefs(docsJson, refMap, docsJsonPath) {
|
|
119
|
+
let changed = false;
|
|
120
|
+
// Sort deepest-first so nested refs are written before their parent
|
|
121
|
+
// replaces the ancestor subtree with a $ref pointer
|
|
122
|
+
const entries = [...refMap.entries()]
|
|
123
|
+
.filter(([pointer]) => !pointer.match(/^\/navigation\/languages\/\d+/))
|
|
124
|
+
.sort(([a], [b]) => b.length - a.length);
|
|
125
|
+
for (const [pointer, entry] of entries) {
|
|
126
|
+
const subtree = getAtPointer(docsJson, pointer);
|
|
127
|
+
if (subtree === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
writeJsonFile(entry.sourceFile, subtree);
|
|
130
|
+
setAtPointer(docsJson, pointer, { $ref: entry.refPath });
|
|
131
|
+
changed = true;
|
|
132
|
+
}
|
|
133
|
+
if (changed) {
|
|
134
|
+
fs.writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2), 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Collect refMap entries that describe a language entry's internal $ref chain.
|
|
139
|
+
* Sorted deepest-first so nested content is extracted before parents.
|
|
140
|
+
*/
|
|
141
|
+
function collectInternalRefs(refMap, entryPointerPrefix) {
|
|
142
|
+
const refs = [];
|
|
143
|
+
for (const [pointer, entry] of refMap.entries()) {
|
|
144
|
+
if (!pointer.startsWith(entryPointerPrefix + '/'))
|
|
145
|
+
continue;
|
|
146
|
+
refs.push({
|
|
147
|
+
relativePointer: pointer.slice(entryPointerPrefix.length),
|
|
148
|
+
refPath: entry.refPath,
|
|
149
|
+
// The directory from which this $ref path should be resolved
|
|
150
|
+
resolvedDir: entry.containingDir,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
refs.sort((a, b) => b.relativePointer.length - a.relativePointer.length);
|
|
154
|
+
return refs;
|
|
155
|
+
}
|
|
156
|
+
function writeJsonFile(filePath, data) {
|
|
157
|
+
const dir = path.dirname(filePath);
|
|
158
|
+
if (!fs.existsSync(dir)) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
162
|
+
}
|
|
163
|
+
function getAtPointer(obj, pointer) {
|
|
164
|
+
if (!pointer || pointer === '/')
|
|
165
|
+
return obj;
|
|
166
|
+
const parts = pointer.split('/').filter(Boolean);
|
|
167
|
+
let current = obj;
|
|
168
|
+
for (const part of parts) {
|
|
169
|
+
if (current === null || current === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
const index = /^\d+$/.test(part) ? parseInt(part) : part;
|
|
172
|
+
current = current[index];
|
|
173
|
+
}
|
|
174
|
+
return current;
|
|
175
|
+
}
|
|
176
|
+
function setAtPointer(obj, pointer, value) {
|
|
177
|
+
if (!pointer || pointer === '/')
|
|
178
|
+
return;
|
|
179
|
+
const parts = pointer.split('/').filter(Boolean);
|
|
180
|
+
let current = obj;
|
|
181
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
182
|
+
const index = /^\d+$/.test(parts[i]) ? parseInt(parts[i]) : parts[i];
|
|
183
|
+
if (current[index] === undefined)
|
|
184
|
+
return;
|
|
185
|
+
current = current[index];
|
|
186
|
+
}
|
|
187
|
+
const lastPart = parts[parts.length - 1];
|
|
188
|
+
const lastIndex = /^\d+$/.test(lastPart) ? parseInt(lastPart) : lastPart;
|
|
189
|
+
current[lastIndex] = value;
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gt",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.18",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -110,9 +110,9 @@
|
|
|
110
110
|
"unified": "^11.0.5",
|
|
111
111
|
"unist-util-visit": "^5.0.0",
|
|
112
112
|
"yaml": "^2.8.0",
|
|
113
|
-
"@generaltranslation/python-extractor": "0.2.10",
|
|
114
113
|
"generaltranslation": "8.2.5",
|
|
115
|
-
"gt-remark": "1.0.7"
|
|
114
|
+
"gt-remark": "1.0.7",
|
|
115
|
+
"@generaltranslation/python-extractor": "0.2.10"
|
|
116
116
|
},
|
|
117
117
|
"devDependencies": {
|
|
118
118
|
"@babel/types": "^7.28.4",
|