@uxf/scripts 11.74.5 → 11.76.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/scripts",
3
- "version": "11.74.5",
3
+ "version": "11.76.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -11,19 +11,18 @@ module.exports = async () => {
11
11
  .option("o", { alias: "output", default: "i18n-pages.json" })
12
12
  .option("n", { alias: "defaultNamespace", array: true, default: ["common"] })
13
13
  .option("p", { alias: "pagesDirectory", default: "src/pages" })
14
- .option("e", { alias: "fileExtension", array: true, default: ["ts", "tsx"] })
15
14
  .option("h", { alias: "help" })
16
15
  .strict(false)
17
16
  .exitProcess(false);
18
17
 
19
18
  try {
20
- const { help, include, output, defaultNamespace, pagesDirectory, fileExtension } = cli.parse(argv.slice(2));
19
+ const { help, include, output, defaultNamespace, pagesDirectory } = cli.parse(argv.slice(2));
21
20
 
22
21
  if (Boolean(help)) {
23
22
  return 0;
24
23
  }
25
24
 
26
- await require("./index")(include, output, defaultNamespace, pagesDirectory, fileExtension);
25
+ await require("./index")(include, output, defaultNamespace, pagesDirectory);
27
26
  } catch (e) {
28
27
  console.error(e);
29
28
  return 1;
@@ -1,14 +1,134 @@
1
1
  #!/usr/bin/env node
2
2
  const madge = require("madge");
3
3
  const path = require("path");
4
- const fs = require("fs");
5
- const { readFileSync, readdirSync, writeFileSync } = require("fs");
4
+ const { readFileSync, readdirSync, writeFileSync, statSync, existsSync } = require("fs");
6
5
  const { findTFunctionNamespaces } = require("./utils/find-t-function-namespaces");
7
6
  const { findTransComponentNamespaces } = require("./utils/find-trans-component-namespaces");
8
7
  const join = require("node:path").join;
9
8
 
9
+ const UXF_PACKAGES_PATH = "node_modules/@uxf";
10
+ const FILE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "tsx", "d.ts", "mts", "cts", "d.mts", "d.cts"];
10
11
  const TS_CONFIG_PATH = path.resolve(process.cwd(), "tsconfig.json");
11
- const TS_CONFIG = fs.existsSync(TS_CONFIG_PATH) ? TS_CONFIG_PATH : undefined;
12
+ const TS_CONFIG = existsSync(TS_CONFIG_PATH) ? TS_CONFIG_PATH : undefined;
13
+
14
+ // Lazy-read tsconfig for paths alias resolution
15
+ let TS_PATHS_CACHE = null;
16
+ function getTsPaths() {
17
+ if (!TS_CONFIG) return null;
18
+ if (TS_PATHS_CACHE) return TS_PATHS_CACHE;
19
+ try {
20
+ const raw = JSON.parse(readFileSync(TS_CONFIG, "utf8"));
21
+ const compilerOptions = raw && raw.compilerOptions ? raw.compilerOptions : {};
22
+ const baseUrl = compilerOptions.baseUrl ? path.resolve(process.cwd(), compilerOptions.baseUrl) : process.cwd();
23
+ const paths = compilerOptions.paths || {};
24
+ TS_PATHS_CACHE = { baseUrl, paths };
25
+ return TS_PATHS_CACHE;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Check if a file is allowed to be scanned for translations.
33
+ * @param {string} file
34
+ * @returns {boolean}
35
+ */
36
+ function isAllowedFile(file) {
37
+ return !file.includes("node_modules") || file.includes(UXF_PACKAGES_PATH);
38
+ }
39
+
40
+ /**
41
+ * Resolve a module specifier (bare, relative, or absolute) to a real filesystem path if possible.
42
+ * Falls back to the original specifier when not resolvable.
43
+ * Only allows project files and node_modules/@uxf/* to avoid scanning the entire npm tree.
44
+ * @param {string} spec
45
+ * @returns {string}
46
+ */
47
+ function resolveModuleSpecifier(spec) {
48
+ try {
49
+ if (!spec || typeof spec !== "string") return spec;
50
+ if (existsSync(spec)) return spec;
51
+
52
+ const ts = getTsPaths();
53
+ if (ts?.paths) {
54
+ for (const [pattern, targets] of Object.entries(ts.paths)) {
55
+ const starIndex = pattern.indexOf("*");
56
+
57
+ if (starIndex !== -1) {
58
+ const prefix = pattern.slice(0, starIndex);
59
+ const suffix = pattern.slice(starIndex + 1);
60
+
61
+ if (spec.startsWith(prefix) && spec.endsWith(suffix)) {
62
+ const middle = spec.slice(prefix.length, spec.length - suffix.length);
63
+
64
+ for (const target of targets) {
65
+ const mapped = target.includes("*") ? target.replace("*", middle) : target;
66
+
67
+ const candidates = [
68
+ path.resolve(ts.baseUrl, mapped),
69
+ ...FILE_EXTENSIONS.map((e) => path.resolve(ts.baseUrl, mapped + e)),
70
+ ...FILE_EXTENSIONS.map((e) => path.join(ts.baseUrl, mapped, "index" + e)),
71
+ ].find(existsSync);
72
+
73
+ if (candidates) return candidates;
74
+ }
75
+ }
76
+ } else if (spec === pattern) {
77
+ const candidate = targets.map((t) => path.resolve(ts.baseUrl, t)).find(existsSync);
78
+ if (candidate) return candidate;
79
+ }
80
+ }
81
+ }
82
+
83
+ const resolved = require.resolve(spec, { paths: [process.cwd()] });
84
+ return isAllowedFile(resolved) ? resolved : spec;
85
+ } catch {
86
+ return spec;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Resolve import specifier in the context of a file path (handles relative imports correctly).
92
+ * @param {string} spec
93
+ * @param {string} fromFile
94
+ * @returns {string}
95
+ */
96
+ function resolveImportFrom(spec, fromFile) {
97
+ try {
98
+ if (!spec || typeof spec !== "string") return spec;
99
+
100
+ // Relative path from the file's directory
101
+ if (spec.startsWith(".")) {
102
+ const baseDir = path.dirname(fromFile);
103
+ let candidate = path.resolve(baseDir, spec);
104
+
105
+ // Try as file
106
+ for (const e of FILE_EXTENSIONS) {
107
+ const c = candidate + e;
108
+ if (existsSync(c)) return c;
109
+ }
110
+ // Try as directory index
111
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
112
+ for (const e of FILE_EXTENSIONS) {
113
+ const idx = path.join(candidate, "index" + e);
114
+ if (existsSync(idx)) return idx;
115
+ }
116
+ }
117
+
118
+ // Fallback to Node resolver with file dir as base
119
+ const resolved = require.resolve(spec, { paths: [baseDir] });
120
+ if (isAllowedFile(resolved)) {
121
+ return resolved;
122
+ }
123
+ return spec;
124
+ }
125
+
126
+ // Bare/absolute spec
127
+ return resolveModuleSpecifier(spec);
128
+ } catch (_e) {
129
+ return spec;
130
+ }
131
+ }
12
132
 
13
133
  function removeTrailingSlash(str) {
14
134
  return str.replace(/\/$/, "");
@@ -64,83 +184,101 @@ const filePathToRoute = (filePath) => {
64
184
  return removeTrailingSlash(route);
65
185
  };
66
186
 
67
- /**
68
- * Resolves re-exports to find the actual implementation files
69
- * @param {string} filePath - The file path to check for re-exports
70
- * @returns {string[]} - Array of resolved file paths
71
- */
72
- function resolveReexports(filePath) {
73
- if (filePath.includes("node_modules") && !filePath.includes("node_modules/@uxf/")) {
74
- return [];
75
- }
76
-
77
- const resolvedFiles = [];
187
+ async function getMadgeTree(entries, include) {
188
+ // Build madge options; allow traversing UXF packages in node_modules.
189
+ const options = {
190
+ tsConfig: TS_CONFIG,
191
+ fileExtensions: FILE_EXTENSIONS,
192
+ includeNpm: true,
193
+ dependencyFilter: (dependency) => {
194
+ return !dependency.includes("node_modules") || dependency.includes(UXF_PACKAGES_PATH);
195
+ },
196
+ };
78
197
 
79
- if (!fs.existsSync(filePath)) {
80
- return [filePath];
198
+ // If include is provided, limit traversal to those prefixes AND node_modules/@uxf.
199
+ if (Array.isArray(include) && include.length > 0) {
200
+ const allowedPrefixes = [...include, UXF_PACKAGES_PATH];
201
+ const searchDirs = new RegExp(`^(?!(${allowedPrefixes.join("|")}))`, "i");
202
+ options.excludeRegExp = [searchDirs];
81
203
  }
82
204
 
83
- try {
84
- const content = fs.readFileSync(filePath, "utf8");
205
+ const res = await madge(entries, options);
206
+ return res.obj();
207
+ }
85
208
 
86
- // Check if this is a re-export file with relative imports (contains export { } from or export * from "./file")
87
- // Absolute imports (export * from "@package/module") are already handled by madge
88
- const reexportMatches = content.match(/export\s*(?:\{[^}]*\}|\*)\s*from\s*['"](\.[^'"]+)['"]/g);
209
+ async function getFiles(entryPoint, tree) {
210
+ const filesOnPath = [];
89
211
 
90
- if (reexportMatches) {
91
- const fileDir = path.dirname(filePath);
212
+ // Always include the entry point file itself (for cases when page does not import anything with translations)
213
+ filesOnPath.push(entryPoint);
92
214
 
93
- for (const match of reexportMatches) {
94
- const pathMatch = match.match(/from\s*['"]([^'"]+)['"]/);
95
- if (pathMatch) {
96
- let relativePath = pathMatch[1];
215
+ getTree([entryPoint], tree, filesOnPath);
97
216
 
98
- // Resolve relative path
99
- let resolvedPath = path.resolve(fileDir, relativePath);
217
+ let flattenFilesOnPath = Array.from(new Set(filesOnPath.flat(Number.POSITIVE_INFINITY)));
100
218
 
101
- // Try different extensions if file doesn't exist
102
- const extensions = [".ts", ".tsx", ".js", ".jsx"];
103
- let foundFile = false;
219
+ // Helper: extract module specifiers from file content
220
+ const extractSpecs = (content) => {
221
+ const specs = new Set();
222
+ const reImportFrom = /import\s+[^'"\n;]*?from\s*['"]([^'"\n]+)['"]/g;
223
+ const reDynamicImport = /import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g;
224
+ const reRequire = /require\(\s*['"]([^'"\n]+)['"]\s*\)/g;
225
+ const reExportFrom = /export\s*(?:\{[^}]*\}|\*)\s*from\s*['"]([^'"\n]+)['"]/g;
226
+ for (const re of [reImportFrom, reDynamicImport, reRequire, reExportFrom]) {
227
+ for (const m of content.matchAll(re)) {
228
+ if (m && m[1]) specs.add(m[1]);
229
+ }
230
+ }
231
+ return Array.from(specs);
232
+ };
104
233
 
105
- for (const ext of extensions) {
106
- const pathWithExt = resolvedPath + ext;
107
- if (fs.existsSync(pathWithExt)) {
108
- resolvedFiles.push(pathWithExt);
109
- foundFile = true;
110
- break;
111
- }
234
+ // Resolve re-exports to get actual component files and traverse imports recursively
235
+ const resolvedFiles = [];
236
+ const queue = [];
237
+ const visited = new Set();
238
+
239
+ // Seed queue with initial files from madge tree
240
+ for (const file of flattenFilesOnPath) {
241
+ if (file && typeof file === "string") {
242
+ const fsPath = resolveModuleSpecifier(file);
243
+ if (typeof fsPath === "string") {
244
+ try {
245
+ if (isAllowedFile(fsPath) && statSync(fsPath).isFile()) {
246
+ queue.push(fsPath);
112
247
  }
248
+ } catch {}
249
+ }
250
+ }
251
+ }
113
252
 
114
- // If still not found, check if it's a directory with index file
115
- if (!foundFile && fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
116
- for (const ext of extensions) {
117
- const indexPath = path.join(resolvedPath, "index" + ext);
118
- if (fs.existsSync(indexPath)) {
119
- // Recursively resolve if the index file also has re-exports
120
- resolvedFiles.push(...resolveReexports(indexPath));
121
- foundFile = true;
122
- break;
253
+ while (queue.length > 0) {
254
+ const fsPath = queue.shift();
255
+ if (!fsPath || visited.has(fsPath)) continue;
256
+ visited.add(fsPath);
257
+ resolvedFiles.push(fsPath);
258
+
259
+ if (!existsSync(fsPath)) continue;
260
+
261
+ // Scan direct imports/requires/exports-from and follow them
262
+ try {
263
+ const content = readFileSync(fsPath, "utf8");
264
+ const specs = extractSpecs(content);
265
+ for (const spec of specs) {
266
+ const p = resolveImportFrom(spec, fsPath);
267
+ // Only follow project files and UXF packages (resolvers already filter others)
268
+ if (typeof p === "string" && (!p.includes("node_modules") || p.includes(UXF_PACKAGES_PATH))) {
269
+ if (!visited.has(p)) {
270
+ try {
271
+ if (isAllowedFile(fsPath) && statSync(p).isFile()) {
272
+ queue.push(p);
123
273
  }
124
- }
125
- }
126
-
127
- if (!foundFile) {
128
- console.warn(`Could not resolve re-export: ${relativePath} from ${filePath}`);
274
+ } catch {}
129
275
  }
130
276
  }
131
277
  }
132
-
133
- // If we found re-exports, don't include the original index file
134
- if (resolvedFiles.length > 0) {
135
- return resolvedFiles;
136
- }
137
- }
138
- } catch (error) {
139
- console.warn(`Error reading file ${filePath}:`, error.message);
278
+ } catch {}
140
279
  }
141
280
 
142
- // If no re-exports found or error occurred, return the original file
143
- return [filePath];
281
+ return Array.from(new Set(resolvedFiles));
144
282
  }
145
283
 
146
284
  /**
@@ -148,76 +286,47 @@ function resolveReexports(filePath) {
148
286
  * @param output string
149
287
  * @param defaultNamespaces string[]
150
288
  * @param pagesDirectory string
151
- * @param fileExtensions string[]
152
289
  */
153
- function main(include, output, defaultNamespaces, pagesDirectory, fileExtensions) {
290
+ async function main(include, output, defaultNamespaces, pagesDirectory) {
154
291
  const result = { "*": defaultNamespaces };
155
- // Negative lookahead – ignore searching for any files
156
- // that aren't part of our include list
157
- const searchDirs = new RegExp(`^(?!(${include.join("|")}))`, "i");
158
292
 
159
293
  const pages = walk(pagesDirectory).flat(Number.POSITIVE_INFINITY);
160
294
 
161
- madge(process.cwd(), {
162
- tsConfig: TS_CONFIG,
163
- excludeRegExp: [searchDirs],
164
- fileExtensions: fileExtensions,
165
- includeNpm: true,
166
- dependencyFilter: (dependency) => {
167
- return !dependency.includes("node_modules") || dependency.includes("node_modules/@uxf");
168
- },
169
- }).then((res) => {
170
- const tree = res.obj();
171
-
172
- for (const entryPoint of pages) {
173
- let namespaces = [];
174
- const filesOnPath = [];
175
-
176
- // Always include the entry point file itself (for cases when page does not import anything with translations)
177
- filesOnPath.push(entryPoint);
295
+ // Build a global dependency tree rooted at project to leverage madge resolution across aliases/packages
296
+ const tree = await getMadgeTree(process.cwd(), include);
178
297
 
179
- getTree([entryPoint], tree, filesOnPath);
298
+ for (const entryPoint of pages) {
299
+ let namespaces = [];
180
300
 
181
- let flattenFilesOnPath = Array.from(new Set(filesOnPath.flat(Number.POSITIVE_INFINITY)));
301
+ const uniqueResolvedFiles = await getFiles(entryPoint, tree);
182
302
 
183
- // Resolve re-exports to get actual component files
184
- const resolvedFiles = [];
185
- for (const file of flattenFilesOnPath) {
186
- if (file && typeof file === "string") {
187
- resolvedFiles.push(...resolveReexports(file));
188
- }
303
+ for (const file of uniqueResolvedFiles) {
304
+ if (!file || !existsSync(file)) {
305
+ continue;
189
306
  }
190
307
 
191
- const uniqueResolvedFiles = Array.from(new Set(resolvedFiles));
192
-
193
- for (const file of uniqueResolvedFiles) {
194
- if (!file || !fs.existsSync(file)) {
195
- continue;
196
- }
197
-
198
- try {
199
- const fileContent = readFileSync(file).toString();
200
- const fileNamespaces = findNamespaces(fileContent);
308
+ try {
309
+ const fileContent = readFileSync(file).toString();
310
+ const fileNamespaces = findNamespaces(fileContent);
201
311
 
202
- namespaces = [...namespaces, ...fileNamespaces];
203
- } catch (error) {
204
- console.warn(`Error reading file ${file}:`, error.message);
205
- }
312
+ namespaces = [...namespaces, ...fileNamespaces];
313
+ } catch (error) {
314
+ console.warn(`Error reading file ${file}:`, error.message);
206
315
  }
316
+ }
207
317
 
208
- namespaces = Array.from(new Set(namespaces)).sort();
318
+ namespaces = Array.from(new Set(namespaces)).sort();
209
319
 
210
- const page = filePathToRoute(entryPoint);
320
+ const page = filePathToRoute(entryPoint);
211
321
 
212
- if (namespaces.length > 0) {
213
- result[page] = namespaces;
214
- }
322
+ if (namespaces.length > 0) {
323
+ result[page] = namespaces;
215
324
  }
325
+ }
216
326
 
217
- writeFileSync(path.resolve(process.cwd(), output), JSON.stringify(result, null, 4));
327
+ writeFileSync(path.resolve(process.cwd(), output), JSON.stringify(result, null, 4));
218
328
 
219
- console.log("Namespaces generated!");
220
- });
329
+ console.log("Namespaces generated!");
221
330
  }
222
331
 
223
332
  module.exports = main;