i18ntk 2.3.1 → 2.3.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/README.md +9 -6
- package/main/manage/commands/AnalyzeCommand.js +64 -18
- package/main/manage/index.js +15 -4
- package/package.json +7 -4
- package/ui-locales/en.json +1 -1
- package/utils/config-manager.js +140 -28
- package/utils/npm-version-warning.js +142 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# i18ntk v2.3.
|
|
1
|
+
# i18ntk v2.3.4
|
|
2
2
|
|
|
3
3
|
Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
|
|
4
4
|
|
|
@@ -9,12 +9,15 @@ Zero-dependency internationalization toolkit for setup, scanning, analysis, vali
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.npmjs.com/package/i18ntk)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://socket.dev/npm/package/i18ntk/overview/2.3.4)
|
|
13
13
|
|
|
14
14
|
## Upgrade Notice
|
|
15
15
|
|
|
16
|
-
Versions earlier than `2.3.
|
|
17
|
-
They are considered unsupported for production use. Upgrade to `2.3.
|
|
16
|
+
Versions earlier than `2.3.4` may contain known stability and security issues.
|
|
17
|
+
They are considered unsupported for production use. Upgrade to `2.3.4` or newer.
|
|
18
|
+
The CLI now checks npm registry metadata at startup and warns when your installed version is out of date.
|
|
19
|
+
Set `I18NTK_DISABLE_UPDATE_CHECK=true` to disable this warning in restricted/offline environments.
|
|
20
|
+
Set `I18NTK_DISABLE_AUTOSAVE=1` in server/runtime environments to keep config in memory and skip disk writes.
|
|
18
21
|
|
|
19
22
|
## What i18ntk Does
|
|
20
23
|
|
|
@@ -151,7 +154,7 @@ Example `.i18ntk-config`:
|
|
|
151
154
|
|
|
152
155
|
```json
|
|
153
156
|
{
|
|
154
|
-
"version": "2.3.
|
|
157
|
+
"version": "2.3.4",
|
|
155
158
|
"sourceDir": "./locales",
|
|
156
159
|
"i18nDir": "./locales",
|
|
157
160
|
"outputDir": "./i18ntk-reports",
|
|
@@ -174,7 +177,7 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
|
|
|
174
177
|
- [Runtime API Guide](docs/runtime.md)
|
|
175
178
|
- [Scanner Guide](docs/scanner-guide.md)
|
|
176
179
|
- [Environment Variables](docs/environment-variables.md)
|
|
177
|
-
- [Migration Guide v2.3.
|
|
180
|
+
- [Migration Guide v2.3.4](docs/migration-guide-v2.3.4.md)
|
|
178
181
|
- [Optimization Prompt](docs/development/package-optimization-prompt.md)
|
|
179
182
|
|
|
180
183
|
## License
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* Contains embedded business logic from I18nAnalyzer.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const fs = require('fs');
|
|
10
11
|
const path = require('path');
|
|
11
12
|
const cliHelper = require('../../../utils/cli-helper');
|
|
12
13
|
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
|
-
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
|
|
14
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir } = require('../../../utils/config-helper');
|
|
14
15
|
const SecurityUtils = require('../../../utils/security');
|
|
15
16
|
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
17
|
const AdminAuth = require('../../../utils/admin-auth');
|
|
@@ -21,7 +22,7 @@ loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
|
|
|
21
22
|
|
|
22
23
|
const PROJECT_ROOT = process.cwd();
|
|
23
24
|
|
|
24
|
-
class AnalyzeCommand {
|
|
25
|
+
class AnalyzeCommand {
|
|
25
26
|
constructor(config = {}, ui = null) {
|
|
26
27
|
this.config = config;
|
|
27
28
|
this.ui = ui;
|
|
@@ -44,11 +45,11 @@ class AnalyzeCommand {
|
|
|
44
45
|
this.safeClose = safeClose;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
* Initialize the analyzer with configuration
|
|
49
|
-
*/
|
|
50
|
-
async initialize() {
|
|
51
|
-
try {
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the analyzer with configuration
|
|
50
|
+
*/
|
|
51
|
+
async initialize() {
|
|
52
|
+
try {
|
|
52
53
|
const args = this.parseArgs();
|
|
53
54
|
if (args.help) {
|
|
54
55
|
displayHelp('i18ntk-analyze', {
|
|
@@ -78,17 +79,62 @@ class AnalyzeCommand {
|
|
|
78
79
|
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
|
|
79
80
|
|
|
80
81
|
this.sourceDir = this.config.sourceDir;
|
|
81
|
-
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
82
|
-
this.outputDir = this.config.outputDir;
|
|
83
|
-
|
|
84
|
-
// Validate source directory exists
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
83
|
+
this.outputDir = this.config.outputDir;
|
|
84
|
+
|
|
85
|
+
// Validate source directory exists and is readable/writable
|
|
86
|
+
this.validateSourceDirWithFallback(this.sourceDir);
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
SecurityUtils.logSecurityEvent('Analyze command initialization failed', 'error', {
|
|
90
|
+
component: 'AnalyzeCommand',
|
|
91
|
+
error: error.message
|
|
92
|
+
});
|
|
93
|
+
console.error(`Fatal analysis error: ${error.message}`);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate source directory with fallback logic if the shared helper is unavailable.
|
|
100
|
+
* This prevents runtime crashes from missing imports or module regressions.
|
|
101
|
+
* @param {string} sourceDir
|
|
102
|
+
*/
|
|
103
|
+
validateSourceDirWithFallback(sourceDir) {
|
|
104
|
+
if (!sourceDir || typeof sourceDir !== 'string') {
|
|
105
|
+
throw new Error('Source directory is missing or invalid');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof validateSourceDir === 'function') {
|
|
109
|
+
validateSourceDir(sourceDir, 'i18ntk-analyze');
|
|
110
|
+
} else {
|
|
111
|
+
// Graceful fallback: create directory if needed.
|
|
112
|
+
const created = SecurityUtils.safeMkdirSync(sourceDir, process.cwd(), { recursive: true });
|
|
113
|
+
if (created) {
|
|
114
|
+
SecurityUtils.logSecurityEvent('Fallback source directory creation executed', 'warn', {
|
|
115
|
+
component: 'AnalyzeCommand',
|
|
116
|
+
sourceDir,
|
|
117
|
+
reason: 'validateSourceDir helper unavailable'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const safePath = SecurityUtils.validatePath(sourceDir, process.cwd());
|
|
123
|
+
if (!safePath) {
|
|
124
|
+
throw new Error(`Source directory path is unsafe: ${sourceDir}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stat = SecurityUtils.safeStatSync(safePath, process.cwd());
|
|
128
|
+
if (!stat || !stat.isDirectory()) {
|
|
129
|
+
throw new Error(`Source directory does not exist or is not a directory: ${safePath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
fs.accessSync(safePath, fs.constants.R_OK | fs.constants.W_OK);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`Insufficient permissions for source directory: ${safePath}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
92
138
|
|
|
93
139
|
// Initialize readline interface (deprecated - use cliHelper directly)
|
|
94
140
|
initReadline() {
|
package/main/manage/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const { showFrameworkWarningOnce } = require('../../utils/cli-helper');
|
|
|
24
24
|
const { createPrompt, isInteractive } = require('../../utils/prompt-helper');
|
|
25
25
|
const { loadTranslations, t, refreshLanguageFromSettings} = require('../../utils/i18n-helper');
|
|
26
26
|
const cliHelper = require('../../utils/cli-helper');
|
|
27
|
+
const { printUpgradeWarningIfOutdated } = require('../../utils/npm-version-warning');
|
|
27
28
|
const { blue } = require('../../utils/colors-new');
|
|
28
29
|
const { loadConfig, saveConfig, ensureConfigDefaults } = require('../../utils/config');
|
|
29
30
|
const SettingsCLI = require('../../settings/settings-cli');
|
|
@@ -315,10 +316,20 @@ class I18nManager {
|
|
|
315
316
|
// Show help immediately without any setup/auth (useful for CI/uninitialized projects)
|
|
316
317
|
if (args.help) {
|
|
317
318
|
this.showHelp();
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Warn users when their installed CLI version is behind npm latest.
|
|
323
|
+
try {
|
|
324
|
+
await printUpgradeWarningIfOutdated({
|
|
325
|
+
packageName: pkg.name,
|
|
326
|
+
currentVersion: pkg.version
|
|
327
|
+
});
|
|
328
|
+
} catch {
|
|
329
|
+
// Keep startup resilient if registry access is unavailable.
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let startupTimeout = setTimeout(() => {
|
|
322
333
|
console.error('❌ CLI startup timeout - something is hanging');
|
|
323
334
|
process.exit(1);
|
|
324
335
|
}, 10000); // 10 second timeout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "🚀 The fastest internationalization toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive translation management. Perfect for startups to enterprises.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"i18n",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
},
|
|
228
228
|
"preferGlobal": true,
|
|
229
229
|
"versionInfo": {
|
|
230
|
-
"version": "2.3.
|
|
230
|
+
"version": "2.3.4",
|
|
231
231
|
"releaseDate": "12/04/2026",
|
|
232
232
|
"lastUpdated": "12/04/2026",
|
|
233
233
|
"maintainer": "Vlad Noskov",
|
|
@@ -238,7 +238,10 @@
|
|
|
238
238
|
"HOTFIX: Removed deprecated package-path fallback that caused production build warnings for non-exported subpaths.",
|
|
239
239
|
"CRITICAL FIX: Resolved sizing and usage-analysis regressions in v2 command flow.",
|
|
240
240
|
"PACKAGING: Reduced publish footprint by removing internal development scripts and legacy fixed-file artifacts.",
|
|
241
|
-
"SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.
|
|
241
|
+
"SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.4 versions.",
|
|
242
|
+
"CONFIG: Added cross-process file locking for .i18ntk-config writes to prevent production rename races.",
|
|
243
|
+
"CONFIG: Made autosave runtime-safe with non-throwing save failures and I18NTK_DISABLE_AUTOSAVE support.",
|
|
244
|
+
"CLI: Added npm registry version check with upgrade warning for out-of-date installs.",
|
|
242
245
|
"I18N: Completed internal UI locale parity and actionable untranslated-key cleanup across supported languages."
|
|
243
246
|
],
|
|
244
247
|
"breakingChanges": [],
|
|
@@ -264,7 +267,7 @@
|
|
|
264
267
|
"spring-boot": ">=2.5.0",
|
|
265
268
|
"laravel": ">=8.0.0"
|
|
266
269
|
},
|
|
267
|
-
"supportPolicy": "Versions earlier than 2.3.
|
|
270
|
+
"supportPolicy": "Versions earlier than 2.3.4 may be unstable or insecure. Upgrade to 2.3.4 or newer."
|
|
268
271
|
},
|
|
269
272
|
"_comment": "This package is zero-dependency and uses only native Node.js modules"
|
|
270
273
|
}
|
package/ui-locales/en.json
CHANGED
|
@@ -476,7 +476,7 @@
|
|
|
476
476
|
"report_saved_to": "Report saved to: {reportPath}",
|
|
477
477
|
"considerReviewingTranslations": "Consider reviewing ${lang} translations - they are ${data.percentageDifference}% longer than baseline",
|
|
478
478
|
"csv_report_saved_to": "CSV report saved to: {csvPath}",
|
|
479
|
-
"human_report_saved": "
|
|
479
|
+
"human_report_saved": "Readable report saved: {reportPath}",
|
|
480
480
|
"folder_summary_title": "Folder Summary",
|
|
481
481
|
"folder_summary_table_header": "Language Size(KB) Keys Avg Length Total Chars",
|
|
482
482
|
"folder_summary_row": "{lang} {sizeKB} {totalKeys} {avgLength} {totalChars}",
|
package/utils/config-manager.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const SecurityUtils = require('./security');
|
|
5
6
|
|
|
6
7
|
// Determine package directory and user project root
|
|
@@ -9,8 +10,13 @@ const userProjectRoot = process.cwd();
|
|
|
9
10
|
|
|
10
11
|
// Always use current working directory for settings to support test environments
|
|
11
12
|
// This ensures config works correctly when tests change the working directory
|
|
12
|
-
const PROJECT_CONFIG_PATH = path.join(process.cwd(), '.i18ntk-config');
|
|
13
|
-
const PROJECT_SETTINGS_DIR = path.dirname(PROJECT_CONFIG_PATH);
|
|
13
|
+
const PROJECT_CONFIG_PATH = path.join(process.cwd(), '.i18ntk-config');
|
|
14
|
+
const PROJECT_SETTINGS_DIR = path.dirname(PROJECT_CONFIG_PATH);
|
|
15
|
+
const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
|
|
16
|
+
const CONFIG_LOCK_TIMEOUT_MS = 5000;
|
|
17
|
+
const CONFIG_LOCK_STALE_MS = 15000;
|
|
18
|
+
const CONFIG_LOCK_RETRY_MS = 50;
|
|
19
|
+
let autosaveDisabledWarned = false;
|
|
14
20
|
|
|
15
21
|
// Setup tracking file
|
|
16
22
|
const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
|
|
@@ -244,10 +250,69 @@ const DEFAULT_CONFIG = {
|
|
|
244
250
|
|
|
245
251
|
// Environment variable support has been removed in favor of exclusive .i18ntk-config configuration
|
|
246
252
|
|
|
247
|
-
let currentConfig = null;
|
|
248
|
-
let configLoadInProgress = false;
|
|
249
|
-
let recursionDepth = 0;
|
|
250
|
-
const MAX_RECURSION_DEPTH = 15; // Increased to handle legitimate sequential calls
|
|
253
|
+
let currentConfig = null;
|
|
254
|
+
let configLoadInProgress = false;
|
|
255
|
+
let recursionDepth = 0;
|
|
256
|
+
const MAX_RECURSION_DEPTH = 15; // Increased to handle legitimate sequential calls
|
|
257
|
+
let configSaveQueue = Promise.resolve();
|
|
258
|
+
|
|
259
|
+
function sleep(ms) {
|
|
260
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
|
|
264
|
+
const start = Date.now();
|
|
265
|
+
let lockHandle = null;
|
|
266
|
+
|
|
267
|
+
while (!lockHandle) {
|
|
268
|
+
try {
|
|
269
|
+
lockHandle = await fs.promises.open(CONFIG_LOCK_PATH, 'wx');
|
|
270
|
+
await lockHandle.writeFile(String(process.pid), 'utf8');
|
|
271
|
+
break;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!error || error.code !== 'EEXIST') {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Recover from stale lock files left by crashed processes.
|
|
278
|
+
try {
|
|
279
|
+
const stats = await fs.promises.stat(CONFIG_LOCK_PATH);
|
|
280
|
+
if (Date.now() - stats.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
281
|
+
await fs.promises.unlink(CONFIG_LOCK_PATH);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
} catch (_) {
|
|
285
|
+
// Lock may have been released between exists check and stat/unlink.
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (Date.now() - start >= timeoutMs) {
|
|
289
|
+
throw new Error(`Timed out waiting for config lock after ${timeoutMs}ms`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await sleep(CONFIG_LOCK_RETRY_MS);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return async function releaseConfigLock() {
|
|
297
|
+
try {
|
|
298
|
+
if (lockHandle) {
|
|
299
|
+
await lockHandle.close();
|
|
300
|
+
}
|
|
301
|
+
} catch (_) {
|
|
302
|
+
// Best-effort close only.
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await fs.promises.unlink(CONFIG_LOCK_PATH);
|
|
306
|
+
} catch (_) {
|
|
307
|
+
// Best-effort cleanup only.
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isAutosaveDisabled() {
|
|
313
|
+
const flag = String(process.env.I18NTK_DISABLE_AUTOSAVE || '').trim().toLowerCase();
|
|
314
|
+
return flag === '1' || flag === 'true' || flag === 'yes';
|
|
315
|
+
}
|
|
251
316
|
|
|
252
317
|
function clone(obj) {
|
|
253
318
|
return JSON.parse(JSON.stringify(obj));
|
|
@@ -424,28 +489,71 @@ function loadConfig() {
|
|
|
424
489
|
|
|
425
490
|
async function saveConfig(cfg = currentConfig) {
|
|
426
491
|
if (!cfg || typeof cfg !== 'object') return;
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
// Ensure settings directory exists
|
|
430
|
-
if (!SecurityUtils.safeExistsSync(PROJECT_SETTINGS_DIR)) {
|
|
431
|
-
fs.mkdirSync(PROJECT_SETTINGS_DIR, { recursive: true });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const serialized = JSON.stringify(cfg, null, 2);
|
|
435
|
-
if (typeof serialized !== 'string' || serialized.length === 0) {
|
|
436
|
-
throw new Error('Cannot save empty configuration payload');
|
|
437
|
-
}
|
|
438
492
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await fs.promises.writeFile(tempPath, serialized, 'utf8');
|
|
442
|
-
await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
|
|
493
|
+
// Runtime/server safety valve: allow disabling disk writes entirely.
|
|
494
|
+
if (isAutosaveDisabled()) {
|
|
443
495
|
currentConfig = cfg;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
496
|
+
if (!autosaveDisabledWarned) {
|
|
497
|
+
autosaveDisabledWarned = true;
|
|
498
|
+
console.warn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
configSaveQueue = configSaveQueue.then(async () => {
|
|
504
|
+
let tempPath = null;
|
|
505
|
+
let releaseLock = null;
|
|
506
|
+
try {
|
|
507
|
+
// Ensure settings directory exists before any lock/file operations.
|
|
508
|
+
await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
|
|
509
|
+
|
|
510
|
+
releaseLock = await acquireConfigLock();
|
|
511
|
+
|
|
512
|
+
const serialized = JSON.stringify(cfg, null, 2);
|
|
513
|
+
if (typeof serialized !== 'string' || serialized.length === 0) {
|
|
514
|
+
throw new Error('Cannot save empty configuration payload');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Use a unique temp file to avoid concurrent writer races.
|
|
518
|
+
const nonce = `${process.pid}.${Date.now()}.${crypto.randomUUID()}`;
|
|
519
|
+
tempPath = `${PROJECT_CONFIG_PATH}.${nonce}.tmp`;
|
|
520
|
+
await fs.promises.writeFile(tempPath, serialized, 'utf8');
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
|
|
524
|
+
} catch (renameError) {
|
|
525
|
+
// If destination dir disappeared between checks, recreate and retry once.
|
|
526
|
+
if (renameError && renameError.code === 'ENOENT') {
|
|
527
|
+
await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
|
|
528
|
+
await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
|
|
529
|
+
} else {
|
|
530
|
+
throw renameError;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
currentConfig = cfg;
|
|
535
|
+
return true;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error('[i18ntk] Error saving configuration:', error.message);
|
|
538
|
+
return false;
|
|
539
|
+
} finally {
|
|
540
|
+
if (releaseLock) {
|
|
541
|
+
await releaseLock();
|
|
542
|
+
}
|
|
543
|
+
if (tempPath) {
|
|
544
|
+
try {
|
|
545
|
+
if (SecurityUtils.safeExistsSync(tempPath, path.dirname(tempPath))) {
|
|
546
|
+
fs.unlinkSync(tempPath);
|
|
547
|
+
}
|
|
548
|
+
} catch (_) {
|
|
549
|
+
// Best-effort temp cleanup only.
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return configSaveQueue;
|
|
556
|
+
}
|
|
449
557
|
|
|
450
558
|
function getConfig() {
|
|
451
559
|
// Check for recursion
|
|
@@ -486,7 +594,9 @@ function getConfig() {
|
|
|
486
594
|
throw new Error('Invalid legacy configuration JSON');
|
|
487
595
|
}
|
|
488
596
|
const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
|
|
489
|
-
saveConfig(migratedConfig)
|
|
597
|
+
saveConfig(migratedConfig).catch((err) => {
|
|
598
|
+
console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
|
|
599
|
+
});
|
|
490
600
|
currentConfig = migratedConfig;
|
|
491
601
|
|
|
492
602
|
// Clean up legacy config
|
|
@@ -504,7 +614,9 @@ function getConfig() {
|
|
|
504
614
|
|
|
505
615
|
// Use package defaults for new installation
|
|
506
616
|
console.log('📦 Initializing with default configuration...');
|
|
507
|
-
saveConfig(DEFAULT_CONFIG)
|
|
617
|
+
saveConfig(DEFAULT_CONFIG).catch((err) => {
|
|
618
|
+
console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
|
|
619
|
+
});
|
|
508
620
|
currentConfig = DEFAULT_CONFIG;
|
|
509
621
|
return resolvePaths(DEFAULT_CONFIG);
|
|
510
622
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const { compareVersions } = require('./version-utils');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 1800;
|
|
5
|
+
const NPM_REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
6
|
+
|
|
7
|
+
function isSemverLike(version) {
|
|
8
|
+
return typeof version === 'string' && /^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$/.test(version.trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fetchPackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
12
|
+
const safePackageName = encodeURIComponent(packageName);
|
|
13
|
+
const requestUrl = `${NPM_REGISTRY_BASE}/${safePackageName}`;
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let settled = false;
|
|
17
|
+
|
|
18
|
+
const finish = (value) => {
|
|
19
|
+
if (!settled) {
|
|
20
|
+
settled = true;
|
|
21
|
+
resolve(value);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const req = https.get(
|
|
26
|
+
requestUrl,
|
|
27
|
+
{
|
|
28
|
+
timeout: timeoutMs,
|
|
29
|
+
headers: {
|
|
30
|
+
Accept: 'application/json',
|
|
31
|
+
'User-Agent': 'i18ntk-version-check'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
(res) => {
|
|
35
|
+
if (res.statusCode !== 200) {
|
|
36
|
+
res.resume();
|
|
37
|
+
finish(null);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let raw = '';
|
|
42
|
+
res.on('data', (chunk) => {
|
|
43
|
+
raw += chunk;
|
|
44
|
+
});
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
try {
|
|
47
|
+
finish(JSON.parse(raw));
|
|
48
|
+
} catch {
|
|
49
|
+
finish(null);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
req.on('timeout', () => {
|
|
56
|
+
req.destroy();
|
|
57
|
+
finish(null);
|
|
58
|
+
});
|
|
59
|
+
req.on('error', () => finish(null));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getOutdatedStatus(currentVersion, metadata) {
|
|
64
|
+
if (!isSemverLike(currentVersion) || !metadata || !metadata.versions) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const distTags = metadata['dist-tags'] || {};
|
|
69
|
+
const taggedLatest = distTags.latest;
|
|
70
|
+
const allPublishedVersions = Object.keys(metadata.versions).filter(isSemverLike);
|
|
71
|
+
|
|
72
|
+
if (allPublishedVersions.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const latestVersion = isSemverLike(taggedLatest)
|
|
77
|
+
? taggedLatest
|
|
78
|
+
: allPublishedVersions.sort(compareVersions).at(-1);
|
|
79
|
+
|
|
80
|
+
if (!latestVersion || !isSemverLike(latestVersion)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const currentMeta = metadata.versions[currentVersion] || null;
|
|
85
|
+
const isCurrentDeprecated = Boolean(currentMeta && currentMeta.deprecated);
|
|
86
|
+
const isOutdated = compareVersions(currentVersion, latestVersion) < 0;
|
|
87
|
+
|
|
88
|
+
const newerStableVersions = allPublishedVersions.filter((version) => (
|
|
89
|
+
compareVersions(version, currentVersion) > 0 &&
|
|
90
|
+
compareVersions(version, latestVersion) <= 0
|
|
91
|
+
));
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
latestVersion,
|
|
95
|
+
isOutdated,
|
|
96
|
+
isCurrentDeprecated,
|
|
97
|
+
newerStableCount: newerStableVersions.length
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkNpmOutdated({ packageName, currentVersion, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
102
|
+
const metadata = await fetchPackageMetadata(packageName, timeoutMs);
|
|
103
|
+
return getOutdatedStatus(currentVersion, metadata);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function printUpgradeWarningIfOutdated({
|
|
107
|
+
packageName,
|
|
108
|
+
currentVersion,
|
|
109
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
110
|
+
}) {
|
|
111
|
+
if (process.env.I18NTK_DISABLE_UPDATE_CHECK === 'true') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const status = await checkNpmOutdated({ packageName, currentVersion, timeoutMs });
|
|
116
|
+
if (!status) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (status.isCurrentDeprecated) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`\n⚠️ Installed ${packageName}@${currentVersion} is deprecated on npm. ` +
|
|
123
|
+
`Upgrade to ${packageName}@${status.latestVersion}:\n` +
|
|
124
|
+
` npm install -g ${packageName}@latest`
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (status.isOutdated) {
|
|
130
|
+
const suffix = status.newerStableCount === 1 ? '' : 's';
|
|
131
|
+
console.warn(
|
|
132
|
+
`\n⚠️ Update available for ${packageName}: ${currentVersion} -> ${status.latestVersion} ` +
|
|
133
|
+
`(${status.newerStableCount} newer release${suffix}).\n` +
|
|
134
|
+
` Run: npm install -g ${packageName}@latest`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
checkNpmOutdated,
|
|
141
|
+
printUpgradeWarningIfOutdated
|
|
142
|
+
};
|