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 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) {
@@ -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,2 @@
1
+ import type { Settings } from '../types/index.js';
2
+ export default function processSharedStaticAssets(settings: Settings): Promise<void>;
@@ -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: ![alt](url)
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.2",
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.0"
96
+ "generaltranslation": "8.0.1"
97
97
  },
98
98
  "devDependencies": {
99
99
  "@babel/types": "^7.28.4",