gt 2.14.18 → 2.14.19

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.14.19
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1244](https://github.com/generaltranslation/gt/pull/1244) [`c4c8b9c`](https://github.com/generaltranslation/gt/commit/c4c8b9c0429ce10d98ebdfaabc1213bd85a572bf) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Updating Mintlify $ref handling
8
+
9
+ - Updated dependencies [[`8b75420`](https://github.com/generaltranslation/gt/commit/8b7542091233fb2c87284a365cc9ab8ce70371d3)]:
10
+ - generaltranslation@8.2.6
11
+ - @generaltranslation/python-extractor@0.2.11
12
+
3
13
  ## 2.14.18
4
14
 
5
15
  ### Patch Changes
@@ -4,11 +4,13 @@ export function generatePreset(preset, type) {
4
4
  case 'mintlify':
5
5
  // https://mintlify.com/docs/navigation
6
6
  return {
7
+ resolveRefs: true,
7
8
  composite: {
8
9
  '$.navigation.languages': {
9
10
  type: 'array',
10
11
  key: '$.language',
11
12
  experimentalSort: 'localesAlphabetical',
13
+ splitEntries: true,
12
14
  include: [
13
15
  '$..group',
14
16
  '$..tab',
@@ -40,6 +42,36 @@ export function generatePreset(preset, type) {
40
42
  },
41
43
  },
42
44
  };
45
+ case 'mintlify-hide-default':
46
+ // Mintlify with hideDefaultLocale — paths don't have locale prefix in source
47
+ return {
48
+ resolveRefs: true,
49
+ composite: {
50
+ '$.navigation.languages': {
51
+ type: 'array',
52
+ key: '$.language',
53
+ experimentalSort: 'localesAlphabetical',
54
+ splitEntries: true,
55
+ include: [
56
+ '$..group',
57
+ '$..tab',
58
+ '$..item',
59
+ '$..anchor',
60
+ '$..dropdown',
61
+ ],
62
+ transform: {
63
+ '$..pages[*]': {
64
+ match: '^/?(.*)$',
65
+ replace: '{locale}/$1',
66
+ },
67
+ '$..root': {
68
+ match: '^/?(.*)$',
69
+ replace: '{locale}/$1',
70
+ },
71
+ },
72
+ },
73
+ },
74
+ };
43
75
  case 'openapi':
44
76
  return {
45
77
  include: ['$..summary', '$..description'],
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.14.18";
1
+ export declare const PACKAGE_VERSION = "2.14.19";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.14.18';
2
+ export const PACKAGE_VERSION = '2.14.19';
@@ -252,8 +252,9 @@ export type StructuralTransform = {
252
252
  destinationPointer: string;
253
253
  };
254
254
  export type JsonSchema = {
255
- preset?: 'mintlify' | 'openapi';
255
+ preset?: 'mintlify' | 'mintlify-hide-default' | 'openapi';
256
256
  structuralTransform?: StructuralTransform[];
257
+ resolveRefs?: boolean;
257
258
  include?: string[];
258
259
  composite?: {
259
260
  [sourceObjectPath: string]: SourceObjectOptions;
@@ -271,6 +272,7 @@ export type SourceObjectOptions = {
271
272
  localeProperty?: string;
272
273
  transform?: TransformOptions;
273
274
  experimentalSort?: 'locales' | 'localesAlphabetical';
275
+ splitEntries?: boolean;
274
276
  };
275
277
  export type TransformOptions = {
276
278
  [transformPath: string]: TransformOption;
@@ -19,10 +19,8 @@ export type ResolvedRefs = {
19
19
  export declare function resolveMintlifyRefs(json: unknown, filePath: string): ResolvedRefs;
20
20
  /**
21
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.
22
+ * Returns true if the file matches a jsonSchema entry with resolveRefs: true.
24
23
  */
25
24
  export declare function shouldResolveRefs(filePath: string, options?: {
26
- mintlify?: any;
27
25
  jsonSchema?: Record<string, any>;
28
26
  }): boolean;
@@ -92,17 +92,14 @@ function resolveRef(obj, baseDir, pointer, visiting, refMap) {
92
92
  }
93
93
  /**
94
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.
95
+ * Returns true if the file matches a jsonSchema entry with resolveRefs: true.
97
96
  */
98
97
  export function shouldResolveRefs(filePath, options) {
99
- if (!options?.mintlify)
100
- return false;
101
- if (!options.jsonSchema)
98
+ if (!options?.jsonSchema)
102
99
  return false;
103
100
  const relative = path.relative(process.cwd(), filePath);
104
101
  for (const [glob, schema] of Object.entries(options.jsonSchema)) {
105
- if (schema?.composite && micromatch.isMatch(relative, glob)) {
102
+ if (schema?.resolveRefs && micromatch.isMatch(relative, glob)) {
106
103
  return true;
107
104
  }
108
105
  }
@@ -1,13 +1,13 @@
1
1
  import { Settings } from '../types/index.js';
2
2
  /**
3
- * Post-processing step for Mintlify docs.json.
3
+ * Post-processing step for composite JSON files with splitEntries enabled.
4
4
  *
5
- * After mergeJson writes a fully-inlined docs.json, this function restores
6
- * the original $ref structure:
5
+ * After mergeJson writes a fully-inlined composite file, this function:
6
+ * 1. Restores the original $ref structure (if the source used $ref / resolveRefs)
7
+ * 2. Extracts non-default keyed entries into their own ref files
8
+ * to keep the source file compact
7
9
  *
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
10
+ * Driven entirely by the jsonSchema config — reads the composite path,
11
+ * key field, and splitEntries flag from the schema.
12
12
  */
13
13
  export declare function splitMintlifyLanguageRefs(settings: Settings): Promise<void>;
@@ -1,74 +1,151 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { logger } from '../console/logger.js';
4
- import { shouldResolveRefs } from './resolveMintlifyRefs.js';
4
+ import { validateJsonSchema } from '../formats/json/utils.js';
5
5
  import { getStoredRefMap, clearStoredRefMap } from '../state/mintlifyRefMap.js';
6
+ import { JSONPath } from 'jsonpath-plus';
7
+ import { getLocaleProperties } from 'generaltranslation';
6
8
  /**
7
- * Post-processing step for Mintlify docs.json.
9
+ * Post-processing step for composite JSON files with splitEntries enabled.
8
10
  *
9
- * After mergeJson writes a fully-inlined docs.json, this function restores
10
- * the original $ref structure:
11
+ * After mergeJson writes a fully-inlined composite file, this function:
12
+ * 1. Restores the original $ref structure (if the source used $ref / resolveRefs)
13
+ * 2. Extracts non-default keyed entries into their own ref files
14
+ * to keep the source file compact
11
15
  *
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
+ * Driven entirely by the jsonSchema config — reads the composite path,
17
+ * key field, and splitEntries flag from the schema.
16
18
  */
17
19
  export async function splitMintlifyLanguageRefs(settings) {
18
- const isMintlify = settings.framework === 'mintlify' || !!settings.options?.mintlify;
19
- if (!isMintlify)
20
- return;
21
20
  const refMap = getStoredRefMap();
22
- if (!refMap || refMap.size === 0)
23
- return;
24
21
  try {
25
22
  const resolvedJsonPaths = settings.files?.resolvedPaths?.json;
26
23
  if (!resolvedJsonPaths)
27
24
  return;
28
- const docsJsonPath = resolvedJsonPaths.find((p) => shouldResolveRefs(p, settings.options));
29
- if (!docsJsonPath)
25
+ // Find a JSON file that has splitEntries enabled or resolveRefs
26
+ const targetFile = findTargetFile(resolvedJsonPaths, settings);
27
+ if (!targetFile)
30
28
  return;
31
- if (!fs.existsSync(docsJsonPath))
29
+ const { filePath: compositeFilePath, splitConfig } = targetFile;
30
+ if (!fs.existsSync(compositeFilePath))
32
31
  return;
33
- let docsJson;
32
+ let fileJson;
34
33
  try {
35
- docsJson = JSON.parse(fs.readFileSync(docsJsonPath, 'utf-8'));
34
+ fileJson = JSON.parse(fs.readFileSync(compositeFilePath, 'utf-8'));
36
35
  }
37
36
  catch {
38
37
  return;
39
38
  }
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;
39
+ const docsDir = path.dirname(compositeFilePath);
40
+ // If splitEntries is configured, process it
41
+ if (splitConfig) {
42
+ processSplitEntries(fileJson, compositeFilePath, docsDir, splitConfig, settings, refMap);
49
43
  }
50
- const defaultIndex = languages.findIndex((e) => e?.language === defaultLocale);
51
- if (defaultIndex < 0) {
52
- restoreTopLevelRefs(docsJson, refMap, docsJsonPath);
53
- return;
44
+ // Restore top-level refs if any exist
45
+ if (refMap && refMap.size > 0) {
46
+ restoreTopLevelRefs(fileJson, refMap, splitConfig);
47
+ }
48
+ // Always write the composite file back — splitEntries modified the
49
+ // languages array, and restoreTopLevelRefs may not have written it
50
+ // (e.g., when all refs are inside language entries, not top-level)
51
+ fs.writeFileSync(compositeFilePath, JSON.stringify(fileJson, null, 2), 'utf-8');
52
+ }
53
+ finally {
54
+ clearStoredRefMap();
55
+ }
56
+ }
57
+ /**
58
+ * Find the target file and extract split configuration from the schema.
59
+ */
60
+ function findTargetFile(resolvedPaths, settings) {
61
+ if (!settings.options?.jsonSchema)
62
+ return null;
63
+ for (const filePath of resolvedPaths) {
64
+ const schema = validateJsonSchema(settings.options, filePath);
65
+ if (!schema)
66
+ continue;
67
+ const hasSplitEntries = schema.composite
68
+ ? Object.entries(schema.composite).some(([, opts]) => opts.splitEntries)
69
+ : false;
70
+ const hasResolveRefs = schema.resolveRefs;
71
+ if (!hasSplitEntries && !hasResolveRefs)
72
+ continue;
73
+ // Extract split config if available
74
+ let splitConfig = null;
75
+ if (schema.composite) {
76
+ for (const [compositePath, opts] of Object.entries(schema.composite)) {
77
+ if (opts.splitEntries && opts.type === 'array' && opts.key) {
78
+ splitConfig = {
79
+ compositePath,
80
+ jsonPointer: jsonPathToPointer(compositePath),
81
+ keyField: opts.key,
82
+ keyJsonPath: opts.key,
83
+ sourceObjectOptions: opts,
84
+ };
85
+ break;
86
+ }
87
+ }
54
88
  }
55
- const navDir = navRefEntry
56
- ? path.dirname(navRefEntry.sourceFile)
57
- : path.dirname(docsJsonPath);
58
- const defaultPointerPrefix = `/navigation/languages/${defaultIndex}`;
89
+ return { filePath, schema, splitConfig };
90
+ }
91
+ return null;
92
+ }
93
+ /**
94
+ * Process splitEntries: extract non-default keyed entries into ref files.
95
+ */
96
+ function processSplitEntries(fileJson, compositeFilePath, docsDir, splitConfig, settings, refMap) {
97
+ const { jsonPointer, keyJsonPath } = splitConfig;
98
+ // Find the composite array — may be behind a $ref
99
+ const parentPointer = jsonPointer.split('/').slice(0, -1).join('/') || '';
100
+ const arrayKey = jsonPointer.split('/').pop() || '';
101
+ const navRefEntry = refMap?.get(parentPointer || undefined);
102
+ // Get the array from the file
103
+ const arrayContainer = parentPointer
104
+ ? getAtPointer(fileJson, parentPointer)
105
+ : fileJson;
106
+ if (!arrayContainer)
107
+ return;
108
+ const entries = arrayContainer[arrayKey];
109
+ if (!Array.isArray(entries) || entries.length <= 1)
110
+ return;
111
+ // Determine the default key value (the source entry)
112
+ const defaultKeyValue = getDefaultKeyValue(settings.defaultLocale, splitConfig.sourceObjectOptions);
113
+ const defaultIndex = entries.findIndex((e) => {
114
+ if (!e || typeof e !== 'object')
115
+ return false;
116
+ const values = JSONPath({
117
+ json: e,
118
+ path: keyJsonPath,
119
+ resultType: 'value',
120
+ flatten: true,
121
+ wrap: true,
122
+ });
123
+ return values?.[0] === defaultKeyValue;
124
+ });
125
+ if (defaultIndex < 0)
126
+ return;
127
+ // Determine where the composite array actually lives on disk
128
+ const navDir = navRefEntry ? path.dirname(navRefEntry.sourceFile) : docsDir;
129
+ // Restore $ref structure if the source used $ref
130
+ if (refMap && refMap.size > 0) {
131
+ const defaultPointerPrefix = `${jsonPointer}/${defaultIndex}`;
59
132
  const internalRefs = collectInternalRefs(refMap, defaultPointerPrefix);
60
133
  if (internalRefs.length > 0) {
61
- const defaultEntry = languages[defaultIndex];
134
+ const defaultEntry = entries[defaultIndex];
62
135
  for (const ref of internalRefs) {
63
136
  setAtPointer(defaultEntry, ref.relativePointer, {
64
137
  $ref: ref.refPath,
65
138
  });
66
139
  }
67
- for (const entry of languages) {
68
- if (!entry || entry.language === defaultLocale)
69
- continue;
70
- const locale = entry.language;
71
- if (!locale)
140
+ for (const entry of entries) {
141
+ const entryKeyValues = JSONPath({
142
+ json: entry,
143
+ path: keyJsonPath,
144
+ resultType: 'value',
145
+ flatten: true,
146
+ wrap: true,
147
+ });
148
+ if (entryKeyValues?.[0] === defaultKeyValue)
72
149
  continue;
73
150
  for (const ref of internalRefs) {
74
151
  const subtree = getAtPointer(entry, ref.relativePointer);
@@ -76,66 +153,87 @@ export async function splitMintlifyLanguageRefs(settings) {
76
153
  continue;
77
154
  const originalAbsPath = path.resolve(ref.resolvedDir, ref.refPath);
78
155
  const relToNavDir = path.relative(navDir, originalAbsPath);
79
- const localeRelPath = path.join(locale, relToNavDir);
156
+ const keyValue = entryKeyValues?.[0] || 'unknown';
157
+ const localeRelPath = path.join(keyValue, relToNavDir);
80
158
  const outputPath = path.resolve(navDir, localeRelPath);
81
159
  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
160
  setAtPointer(entry, ref.relativePointer, { $ref: ref.refPath });
86
161
  }
87
162
  }
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}` };
163
+ logger.info(`Restored $ref structure for default entry`);
105
164
  }
106
- logger.info(`Split locale navigation entries into ref files`);
107
- restoreTopLevelRefs(docsJson, refMap, docsJsonPath);
108
165
  }
109
- finally {
110
- clearStoredRefMap();
166
+ // Extract each non-default entry into its own ref file
167
+ const navFileName = navRefEntry
168
+ ? path.basename(navRefEntry.sourceFile)
169
+ : path.basename(compositeFilePath);
170
+ // Get the actual property name from the key JSONPath (e.g., "$.language" → "language")
171
+ const keyPropertyName = keyJsonPath.replace(/^\$\.?/, '');
172
+ for (let i = 0; i < entries.length; i++) {
173
+ const entry = entries[i];
174
+ if (!entry || typeof entry !== 'object')
175
+ continue;
176
+ const keyValues = JSONPath({
177
+ json: entry,
178
+ path: keyJsonPath,
179
+ resultType: 'value',
180
+ flatten: true,
181
+ wrap: true,
182
+ });
183
+ const keyValue = keyValues?.[0];
184
+ if (!keyValue || keyValue === defaultKeyValue)
185
+ continue;
186
+ const { [keyPropertyName]: _, ...contentWithoutKey } = entry;
187
+ const entryFileName = `${keyValue}/${navFileName}`;
188
+ const entryFilePath = path.resolve(navDir, entryFileName);
189
+ writeJsonFile(entryFilePath, contentWithoutKey);
190
+ entries[i] = { [keyPropertyName]: keyValue, $ref: `./${entryFileName}` };
111
191
  }
192
+ logger.info(`Split keyed entries into ref files`);
193
+ }
194
+ /**
195
+ * Get the identifying key value for the default locale.
196
+ */
197
+ function getDefaultKeyValue(defaultLocale, sourceObjectOptions) {
198
+ const localeProperty = sourceObjectOptions.localeProperty || 'code';
199
+ const localeProperties = getLocaleProperties(defaultLocale);
200
+ return (localeProperties[localeProperty] ||
201
+ localeProperties.code ||
202
+ defaultLocale);
112
203
  }
113
204
  /**
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.
205
+ * Convert a JSONPath like "$.navigation.languages" to a JSON pointer like "/navigation/languages".
117
206
  */
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
207
+ function jsonPathToPointer(jsonPath) {
208
+ return jsonPath
209
+ .replace(/^\$\.?/, '')
210
+ .split('.')
211
+ .filter(Boolean)
212
+ .map((segment) => `/${segment}`)
213
+ .join('');
214
+ }
215
+ /**
216
+ * Restore top-level $ref pointers in the composite file.
217
+ * Sorted deepest-first so nested refs are written before parents.
218
+ */
219
+ function restoreTopLevelRefs(fileJson, refMap, splitConfig) {
220
+ // Build a regex to exclude entries inside the composite array
221
+ const arrayPointerPattern = splitConfig
222
+ ? new RegExp(`^${splitConfig.jsonPointer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\/\\d+`)
223
+ : null;
122
224
  const entries = [...refMap.entries()]
123
- .filter(([pointer]) => !pointer.match(/^\/navigation\/languages\/\d+/))
225
+ .filter(([pointer]) => !arrayPointerPattern || !arrayPointerPattern.test(pointer))
124
226
  .sort(([a], [b]) => b.length - a.length);
125
227
  for (const [pointer, entry] of entries) {
126
- const subtree = getAtPointer(docsJson, pointer);
228
+ const subtree = getAtPointer(fileJson, pointer);
127
229
  if (subtree === undefined)
128
230
  continue;
129
231
  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');
232
+ setAtPointer(fileJson, pointer, { $ref: entry.refPath });
135
233
  }
136
234
  }
137
235
  /**
138
- * Collect refMap entries that describe a language entry's internal $ref chain.
236
+ * Collect refMap entries that describe an entry's internal $ref chain.
139
237
  * Sorted deepest-first so nested content is extracted before parents.
140
238
  */
141
239
  function collectInternalRefs(refMap, entryPointerPrefix) {
@@ -146,7 +244,6 @@ function collectInternalRefs(refMap, entryPointerPrefix) {
146
244
  refs.push({
147
245
  relativePointer: pointer.slice(entryPointerPrefix.length),
148
246
  refPath: entry.refPath,
149
- // The directory from which this $ref path should be resolved
150
247
  resolvedDir: entry.containingDir,
151
248
  });
152
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.14.18",
3
+ "version": "2.14.19",
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": "8.2.5",
114
- "gt-remark": "1.0.7",
115
- "@generaltranslation/python-extractor": "0.2.10"
113
+ "@generaltranslation/python-extractor": "0.2.11",
114
+ "generaltranslation": "8.2.6",
115
+ "gt-remark": "1.0.7"
116
116
  },
117
117
  "devDependencies": {
118
118
  "@babel/types": "^7.28.4",