gtx-cli 2.5.2 → 2.5.4
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 +13 -0
- package/dist/cli/base.js +5 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/utils/sharedStaticAssets.d.ts +2 -0
- package/dist/utils/sharedStaticAssets.js +274 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.5.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#807](https://github.com/generaltranslation/gt/pull/807) [`293a5a3`](https://github.com/generaltranslation/gt/commit/293a5a3ceba2321eed7b1271ca955331995f40a7) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Add handling of shared static assets
|
|
8
|
+
|
|
9
|
+
## 2.5.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`f98c504`](https://github.com/generaltranslation/gt/commit/f98c504f1e025024b3e1e5e16a0271e86ed095fa)]:
|
|
14
|
+
- generaltranslation@8.0.1
|
|
15
|
+
|
|
3
16
|
## 2.5.2
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/cli/base.js
CHANGED
|
@@ -21,6 +21,7 @@ import { getDownloaded, clearDownloaded } from '../state/recentDownloads.js';
|
|
|
21
21
|
import updateConfig from '../fs/config/updateConfig.js';
|
|
22
22
|
import { createLoadTranslationsFile } from '../fs/createLoadTranslationsFile.js';
|
|
23
23
|
import { saveLocalEdits } from '../api/saveLocalEdits.js';
|
|
24
|
+
import processSharedStaticAssets from '../utils/sharedStaticAssets.js';
|
|
24
25
|
export class BaseCLI {
|
|
25
26
|
library;
|
|
26
27
|
additionalModules;
|
|
@@ -79,6 +80,8 @@ export class BaseCLI {
|
|
|
79
80
|
}
|
|
80
81
|
async handleStage(initOptions) {
|
|
81
82
|
const settings = await generateSettings(initOptions);
|
|
83
|
+
// Preprocess shared static assets if configured (move + rewrite sources)
|
|
84
|
+
await processSharedStaticAssets(settings);
|
|
82
85
|
if (!settings.stageTranslations) {
|
|
83
86
|
// Update settings.stageTranslations to true
|
|
84
87
|
settings.stageTranslations = true;
|
|
@@ -91,6 +94,8 @@ export class BaseCLI {
|
|
|
91
94
|
}
|
|
92
95
|
async handleTranslate(initOptions) {
|
|
93
96
|
const settings = await generateSettings(initOptions);
|
|
97
|
+
// Preprocess shared static assets if configured (move + rewrite sources)
|
|
98
|
+
await processSharedStaticAssets(settings);
|
|
94
99
|
if (!settings.stageTranslations) {
|
|
95
100
|
const results = await handleStage(initOptions, settings, this.library, false);
|
|
96
101
|
if (results) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -134,6 +134,7 @@ export type Settings = {
|
|
|
134
134
|
modelProvider?: string;
|
|
135
135
|
parsingOptions: ParsingConfigOptions;
|
|
136
136
|
branchOptions: BranchOptions;
|
|
137
|
+
sharedStaticAssets?: SharedStaticAssetsConfig;
|
|
137
138
|
};
|
|
138
139
|
export type BranchOptions = {
|
|
139
140
|
currentBranch?: string;
|
|
@@ -163,6 +164,11 @@ export type AdditionalOptions = {
|
|
|
163
164
|
experimentalFlattenJsonFiles?: boolean;
|
|
164
165
|
baseDomain?: string;
|
|
165
166
|
};
|
|
167
|
+
export type SharedStaticAssetsConfig = {
|
|
168
|
+
include: string | string[];
|
|
169
|
+
outDir: string;
|
|
170
|
+
publicPath?: string;
|
|
171
|
+
};
|
|
166
172
|
export type JsonSchema = {
|
|
167
173
|
preset?: 'mintlify';
|
|
168
174
|
include?: string[];
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import remarkMdx from 'remark-mdx';
|
|
7
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
|
+
import remarkStringify from 'remark-stringify';
|
|
9
|
+
import { visit } from 'unist-util-visit';
|
|
10
|
+
import escapeHtmlInTextNodes from 'gt-remark';
|
|
11
|
+
function derivePublicPath(outDir, provided) {
|
|
12
|
+
if (provided)
|
|
13
|
+
return provided;
|
|
14
|
+
const norm = outDir.replace(/\\/g, '/');
|
|
15
|
+
if (norm.startsWith('public/'))
|
|
16
|
+
return '/' + norm.slice('public/'.length);
|
|
17
|
+
if (norm.startsWith('static/'))
|
|
18
|
+
return '/' + norm.slice('static/'.length);
|
|
19
|
+
if (norm.startsWith('/'))
|
|
20
|
+
return norm; // already absolute URL path
|
|
21
|
+
return '/' + path.basename(norm);
|
|
22
|
+
}
|
|
23
|
+
function toArray(val) {
|
|
24
|
+
if (!val)
|
|
25
|
+
return [];
|
|
26
|
+
return Array.isArray(val) ? val : [val];
|
|
27
|
+
}
|
|
28
|
+
async function ensureDir(dir) {
|
|
29
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
async function moveFile(src, dest) {
|
|
32
|
+
if (src === dest)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await ensureDir(path.dirname(dest));
|
|
36
|
+
await fs.promises.rename(src, dest);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// Fallback to copy+unlink for cross-device or existing files
|
|
40
|
+
if (err &&
|
|
41
|
+
(err.code === 'EXDEV' ||
|
|
42
|
+
err.code === 'EEXIST' ||
|
|
43
|
+
err.code === 'ENOTEMPTY')) {
|
|
44
|
+
const data = await fs.promises.readFile(src);
|
|
45
|
+
await ensureDir(path.dirname(dest));
|
|
46
|
+
await fs.promises.writeFile(dest, data);
|
|
47
|
+
try {
|
|
48
|
+
await fs.promises.unlink(src);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
else if (err && err.code === 'ENOENT') {
|
|
53
|
+
// already moved or missing; ignore
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function pathExists(p) {
|
|
62
|
+
try {
|
|
63
|
+
await fs.promises.stat(p);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function isDirEmpty(dir) {
|
|
71
|
+
try {
|
|
72
|
+
const entries = await fs.promises.readdir(dir);
|
|
73
|
+
return entries.length === 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function removeEmptyDirsUpwards(startDir, stopDir) {
|
|
80
|
+
let current = path.resolve(startDir);
|
|
81
|
+
const stop = path.resolve(stopDir);
|
|
82
|
+
while (current.startsWith(stop)) {
|
|
83
|
+
if (current === stop)
|
|
84
|
+
break;
|
|
85
|
+
const exists = await pathExists(current);
|
|
86
|
+
if (!exists)
|
|
87
|
+
break;
|
|
88
|
+
const empty = await isDirEmpty(current);
|
|
89
|
+
if (!empty)
|
|
90
|
+
break;
|
|
91
|
+
try {
|
|
92
|
+
await fs.promises.rmdir(current);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const parent = path.dirname(current);
|
|
98
|
+
if (parent === current)
|
|
99
|
+
break;
|
|
100
|
+
current = parent;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function stripQueryAndHash(url) {
|
|
104
|
+
const match = url.match(/^[^?#]+/);
|
|
105
|
+
const base = match ? match[0] : url;
|
|
106
|
+
const suffix = url.slice(base.length);
|
|
107
|
+
return { base, suffix };
|
|
108
|
+
}
|
|
109
|
+
function rewriteMdxContent(content, filePath, pathMap) {
|
|
110
|
+
let changed = false;
|
|
111
|
+
let ast;
|
|
112
|
+
try {
|
|
113
|
+
const processor = unified()
|
|
114
|
+
.use(remarkParse)
|
|
115
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
116
|
+
.use(remarkMdx);
|
|
117
|
+
ast = processor.runSync(processor.parse(content));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return { content, changed: false };
|
|
121
|
+
}
|
|
122
|
+
const fileDir = path.dirname(filePath);
|
|
123
|
+
// Helper to resolve and possibly rewrite a URL
|
|
124
|
+
const maybeRewrite = (url) => {
|
|
125
|
+
if (!url ||
|
|
126
|
+
/^(https?:)?\/\//i.test(url) ||
|
|
127
|
+
url.startsWith('data:') ||
|
|
128
|
+
url.startsWith('#') ||
|
|
129
|
+
url.startsWith('mailto:') ||
|
|
130
|
+
url.startsWith('tel:')) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const { base, suffix } = stripQueryAndHash(url);
|
|
134
|
+
// Only handle relative paths
|
|
135
|
+
if (base.startsWith('/'))
|
|
136
|
+
return null;
|
|
137
|
+
const abs = path.resolve(fileDir, base);
|
|
138
|
+
const normAbs = path.normalize(abs);
|
|
139
|
+
const mapped = pathMap.get(normAbs);
|
|
140
|
+
if (mapped) {
|
|
141
|
+
changed = true;
|
|
142
|
+
return mapped + suffix;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
visit(ast, (node) => {
|
|
147
|
+
// Markdown image: 
|
|
148
|
+
if (node.type === 'image' && typeof node.url === 'string') {
|
|
149
|
+
const newUrl = maybeRewrite(node.url);
|
|
150
|
+
if (newUrl)
|
|
151
|
+
node.url = newUrl;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Markdown link: [text](url) — useful for PDFs and other downloadable assets
|
|
155
|
+
if (node.type === 'link' && typeof node.url === 'string') {
|
|
156
|
+
const newUrl = maybeRewrite(node.url);
|
|
157
|
+
if (newUrl)
|
|
158
|
+
node.url = newUrl;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// MDX <img src="..." />
|
|
162
|
+
if ((node.type === 'mdxJsxFlowElement' ||
|
|
163
|
+
node.type === 'mdxJsxTextElement') &&
|
|
164
|
+
Array.isArray(node.attributes)) {
|
|
165
|
+
for (const attr of node.attributes) {
|
|
166
|
+
if (attr &&
|
|
167
|
+
attr.type === 'mdxJsxAttribute' &&
|
|
168
|
+
(attr.name === 'src' || attr.name === 'href') &&
|
|
169
|
+
typeof attr.value === 'string') {
|
|
170
|
+
const newUrl = maybeRewrite(attr.value);
|
|
171
|
+
if (newUrl)
|
|
172
|
+
attr.value = newUrl;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
const s = unified()
|
|
179
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
180
|
+
.use(remarkMdx)
|
|
181
|
+
.use(escapeHtmlInTextNodes)
|
|
182
|
+
.use(remarkStringify, {
|
|
183
|
+
handlers: {
|
|
184
|
+
text(node) {
|
|
185
|
+
return node.value;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const outTree = s.runSync(ast);
|
|
190
|
+
let out = s.stringify(outTree);
|
|
191
|
+
// Preserve trailing/leading newlines similar to localizeStaticUrls
|
|
192
|
+
if (out.endsWith('\n') && !content.endsWith('\n'))
|
|
193
|
+
out = out.slice(0, -1);
|
|
194
|
+
if (content.startsWith('\n') && !out.startsWith('\n'))
|
|
195
|
+
out = '\n' + out;
|
|
196
|
+
return { content: out, changed };
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
return { content, changed: false };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export default async function processSharedStaticAssets(settings) {
|
|
203
|
+
const cfg = settings.sharedStaticAssets;
|
|
204
|
+
if (!cfg)
|
|
205
|
+
return;
|
|
206
|
+
const cwd = process.cwd();
|
|
207
|
+
const include = toArray(cfg.include);
|
|
208
|
+
if (include.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
// Resolve assets
|
|
211
|
+
const assetPaths = new Set();
|
|
212
|
+
for (let pattern of include) {
|
|
213
|
+
// Treat leading '/' as repo-relative, not filesystem root
|
|
214
|
+
if (pattern.startsWith('/'))
|
|
215
|
+
pattern = pattern.slice(1);
|
|
216
|
+
const matches = fg.sync(path.resolve(cwd, pattern), { absolute: true });
|
|
217
|
+
for (const m of matches)
|
|
218
|
+
assetPaths.add(path.normalize(m));
|
|
219
|
+
}
|
|
220
|
+
if (assetPaths.size === 0)
|
|
221
|
+
return;
|
|
222
|
+
const outDirInput = cfg.outDir.startsWith('/')
|
|
223
|
+
? cfg.outDir.slice(1)
|
|
224
|
+
: cfg.outDir;
|
|
225
|
+
const outDirAbs = path.resolve(cwd, outDirInput);
|
|
226
|
+
const publicPath = derivePublicPath(outDirInput, cfg.publicPath);
|
|
227
|
+
// Map original absolute path -> public URL
|
|
228
|
+
const originalToPublic = new Map();
|
|
229
|
+
for (const abs of assetPaths) {
|
|
230
|
+
const relFromRoot = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
231
|
+
const publicUrl = (publicPath.endsWith('/') ? publicPath.slice(0, -1) : publicPath) +
|
|
232
|
+
'/' +
|
|
233
|
+
relFromRoot;
|
|
234
|
+
originalToPublic.set(path.normalize(abs), publicUrl);
|
|
235
|
+
}
|
|
236
|
+
// Move assets to outDir, preserving relative structure
|
|
237
|
+
for (const abs of assetPaths) {
|
|
238
|
+
const relFromRoot = path.relative(cwd, abs);
|
|
239
|
+
const destAbs = path.resolve(outDirAbs, relFromRoot);
|
|
240
|
+
// Skip if already in destination
|
|
241
|
+
if (path.normalize(abs) === path.normalize(destAbs))
|
|
242
|
+
continue;
|
|
243
|
+
// If destination exists, assume already moved
|
|
244
|
+
try {
|
|
245
|
+
const st = await fs.promises.stat(destAbs).catch(() => null);
|
|
246
|
+
if (st && st.isFile()) {
|
|
247
|
+
// Remove source if it still exists
|
|
248
|
+
await fs.promises.unlink(abs).catch(() => { });
|
|
249
|
+
await removeEmptyDirsUpwards(path.dirname(abs), cwd);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
await moveFile(abs, destAbs);
|
|
255
|
+
await removeEmptyDirsUpwards(path.dirname(abs), cwd);
|
|
256
|
+
}
|
|
257
|
+
// Rewrite references in default-locale files we send for translation
|
|
258
|
+
const resolved = settings.files?.resolvedPaths || {};
|
|
259
|
+
const mdFiles = [...(resolved.mdx || []), ...(resolved.md || [])];
|
|
260
|
+
await Promise.all(mdFiles.map(async (filePath) => {
|
|
261
|
+
// only rewrite existing files
|
|
262
|
+
const exists = await fs.promises
|
|
263
|
+
.stat(filePath)
|
|
264
|
+
.then(() => true)
|
|
265
|
+
.catch(() => false);
|
|
266
|
+
if (!exists)
|
|
267
|
+
return;
|
|
268
|
+
const orig = await fs.promises.readFile(filePath, 'utf8');
|
|
269
|
+
const { content: out, changed } = rewriteMdxContent(orig, filePath, originalToPublic);
|
|
270
|
+
if (changed) {
|
|
271
|
+
await fs.promises.writeFile(filePath, out, 'utf8');
|
|
272
|
+
}
|
|
273
|
+
}));
|
|
274
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtx-cli",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"unified": "^11.0.5",
|
|
94
94
|
"unist-util-visit": "^5.0.0",
|
|
95
95
|
"yaml": "^2.8.0",
|
|
96
|
-
"generaltranslation": "8.0.
|
|
96
|
+
"generaltranslation": "8.0.1"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
99
|
"@babel/types": "^7.28.4",
|