i18ntk 2.0.3 → 2.1.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.
@@ -8,8 +8,9 @@
8
8
  * - Optional: prune extra keys not present in EN
9
9
  */
10
10
 
11
- const fs = require('fs');
12
- const path = require('path');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const SecurityUtils = require('../utils/security');
13
14
 
14
15
  const argv = Object.fromEntries(
15
16
  process.argv.slice(2).map(a => {
@@ -37,11 +38,11 @@ const KEY_PATTERNS = [
37
38
  const EXCLUDE_DIRS = new Set(['node_modules', '.git', path.basename(I18N_DIR)]);
38
39
 
39
40
  function readUTF8(p) { try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; } }
40
- function writeJSON(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8'); }
41
+ function writeJSON(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', path.dirname(p), 'utf8'); }
41
42
  function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
42
43
  function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
43
44
 
44
- function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']) {
45
+ function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']) {
45
46
  const out = [];
46
47
  (function walk(d) {
47
48
  for (const name of fs.readdirSync(d)) {
@@ -54,22 +55,42 @@ function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '
54
55
  } catch {}
55
56
  }
56
57
  })(dir);
57
- return out;
58
- }
59
-
60
- function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
61
- const content = readUTF8(file);
62
- if (!content) return [];
63
- const keys = [];
64
- for (const re of patterns) {
65
- re.lastIndex = 0;
66
- let m; let guard = 0;
67
- while ((m = re.exec(content)) && guard++ < 10000) {
68
- if (m[1]) keys.push(m[1]);
69
- }
70
- }
71
- return keys;
72
- }
58
+ return out;
59
+ }
60
+
61
+ function normalizeKeyCandidate(rawKey) {
62
+ if (rawKey === null || rawKey === undefined) return null;
63
+ let key = String(rawKey).trim();
64
+ if (!key) return null;
65
+
66
+ key = key.replace(/\$\{[^}]+\}/g, '*');
67
+
68
+ if (/[\r\n\t]/.test(key)) return null;
69
+ if (/\s/.test(key)) return null;
70
+ if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
71
+ if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
72
+ if (key.startsWith('.') || key.endsWith('.') || key.includes('..')) return null;
73
+ if (key === '*' || key.includes('*')) return null;
74
+
75
+ return key;
76
+ }
77
+
78
+ function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
79
+ const content = readUTF8(file);
80
+ if (!content) return [];
81
+ const keys = [];
82
+ for (const re of patterns) {
83
+ re.lastIndex = 0;
84
+ let m; let guard = 0;
85
+ while ((m = re.exec(content)) && guard++ < 10000) {
86
+ if (m[1]) {
87
+ const normalized = normalizeKeyCandidate(m[1]);
88
+ if (normalized) keys.push(normalized);
89
+ }
90
+ }
91
+ }
92
+ return keys;
93
+ }
73
94
 
74
95
  function flatten(obj, prefix = '') {
75
96
  const out = {};
@@ -48,12 +48,12 @@ const COUNTRY_CODES = { de: 'DE', es: 'ES', fr: 'FR', ru: 'RU', ja: 'JA', zh: 'Z
48
48
  function readUTF8(p) {
49
49
  try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; }
50
50
  }
51
- function writeJSON(p, obj) {
52
- fs.mkdirSync(path.dirname(p), { recursive: true });
53
- SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
54
- }
55
- function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
56
- function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
51
+ function writeJSON(p, obj) {
52
+ fs.mkdirSync(path.dirname(p), { recursive: true });
53
+ SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', path.dirname(p), 'utf8');
54
+ }
55
+ function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
56
+ function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
57
57
 
58
58
  function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']) {
59
59
  const out = [];
@@ -67,23 +67,43 @@ function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '
67
67
  else if (st.isFile() && exts.includes(path.extname(name))) out.push(full);
68
68
  } catch {}
69
69
  }
70
- })(dir);
71
- return out;
72
- }
73
-
74
- function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
75
- const content = readUTF8(file);
76
- if (!content) return [];
77
- const keys = [];
78
- for (const re of patterns) {
79
- re.lastIndex = 0;
80
- let m; let guard = 0;
81
- while ((m = re.exec(content)) && guard++ < 10000) {
82
- if (m[1]) keys.push(m[1]);
83
- }
84
- }
85
- return keys;
86
- }
70
+ })(dir);
71
+ return out;
72
+ }
73
+
74
+ function normalizeKeyCandidate(rawKey) {
75
+ if (rawKey === null || rawKey === undefined) return null;
76
+ let key = String(rawKey).trim();
77
+ if (!key) return null;
78
+
79
+ key = key.replace(/\$\{[^}]+\}/g, '*');
80
+
81
+ if (/[\r\n\t]/.test(key)) return null;
82
+ if (/\s/.test(key)) return null;
83
+ if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
84
+ if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
85
+ if (key.startsWith('.') || key.endsWith('.') || key.includes('..')) return null;
86
+ if (key === '*' || key.includes('*')) return null;
87
+
88
+ return key;
89
+ }
90
+
91
+ function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
92
+ const content = readUTF8(file);
93
+ if (!content) return [];
94
+ const keys = [];
95
+ for (const re of patterns) {
96
+ re.lastIndex = 0;
97
+ let m; let guard = 0;
98
+ while ((m = re.exec(content)) && guard++ < 10000) {
99
+ if (m[1]) {
100
+ const normalized = normalizeKeyCandidate(m[1]);
101
+ if (normalized) keys.push(normalized);
102
+ }
103
+ }
104
+ }
105
+ return keys;
106
+ }
87
107
 
88
108
  function flatten(obj, prefix = '') {
89
109
  const out = {};
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DEVELOPMENT-ONLY Prepublish Script
5
+ * This script is for development use only and should NEVER be included in production packages
6
+ * It provides additional development validation and cleanup functionality
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const SecurityUtils = require('../utils/security');
12
+
13
+ class DevelopmentPrepublishCleaner {
14
+ constructor() {
15
+ this.projectRoot = path.join(__dirname, '..');
16
+ this.directories = [
17
+ 'scripts/debug/logs',
18
+ 'scripts/debug/reports',
19
+ 'settings/backups',
20
+ 'i18ntk-reports',
21
+ 'reports',
22
+ 'tests/temp'
23
+ ];
24
+ this.files = [
25
+ 'settings/.i18n-admin-config.json',
26
+ 'test-*.json',
27
+ 'debug-*.log',
28
+ 'npm-debug.log',
29
+ 'yarn-error.log',
30
+ '.env',
31
+ '.env.test'
32
+ ];
33
+
34
+ // Essential files that must exist for development
35
+ this.essentialFiles = [
36
+ 'package.json',
37
+ 'main/manage/index.js',
38
+ 'main/i18ntk-init.js',
39
+ 'main/i18ntk-analyze.js',
40
+ 'main/i18ntk-validate.js',
41
+ 'utils/security.js'
42
+ ];
43
+ }
44
+
45
+ log(message) {
46
+ console.log(`[Prepublish-Dev] ${message}`);
47
+ }
48
+
49
+ async clean() {
50
+ this.log('Starting development pre-publish validation...');
51
+
52
+ // Validate essential files exist
53
+ await this.validateEssentialFiles();
54
+
55
+ // Clean directories
56
+ for (const dir of this.directories) {
57
+ await this.cleanDirectory(path.join(this.projectRoot, dir));
58
+ }
59
+
60
+ // Clean files
61
+ for (const file of this.files) {
62
+ await this.cleanFile(file);
63
+ }
64
+
65
+ // Development-specific validations
66
+ await this.devValidations();
67
+
68
+ this.log('Development pre-publish validation completed successfully!');
69
+ }
70
+
71
+ async cleanDirectory(dirPath) {
72
+ const validatedPath = SecurityUtils.validatePath(dirPath, this.projectRoot);
73
+ if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const files = SecurityUtils.safeReaddirSync(validatedPath, this.projectRoot);
79
+ let deletedCount = 0;
80
+
81
+ for (const file of files) {
82
+ const filePath = path.join(validatedPath, file);
83
+ const validatedFilePath = SecurityUtils.validatePath(filePath, this.projectRoot);
84
+ if (!validatedFilePath) continue;
85
+
86
+ const stat = SecurityUtils.safeStatSync(validatedFilePath, this.projectRoot);
87
+ if (!stat) continue;
88
+
89
+ if (stat.isFile()) {
90
+ if (SecurityUtils.safeExistsSync(validatedFilePath)) {
91
+ fs.unlinkSync(validatedFilePath);
92
+ deletedCount++;
93
+ }
94
+ } else if (stat.isDirectory()) {
95
+ // Recursively clean subdirectories
96
+ await this.cleanDirectory(validatedFilePath);
97
+ // Remove empty directories
98
+ try {
99
+ fs.rmdirSync(validatedFilePath);
100
+ } catch (e) {
101
+ // Directory not empty, skip
102
+ }
103
+ }
104
+ }
105
+
106
+ if (deletedCount > 0) {
107
+ this.log(`Cleaned ${deletedCount} files from ${path.relative(this.projectRoot, validatedPath)}`);
108
+ }
109
+ } catch (error) {
110
+ this.log(`Warning: Could not clean ${dirPath}: ${error.message}`);
111
+ }
112
+ }
113
+
114
+ async cleanFile(pattern) {
115
+ const searchPath = path.join(this.projectRoot, pattern);
116
+ const validatedSearchPath = SecurityUtils.validatePath(searchPath, this.projectRoot);
117
+ if (!validatedSearchPath) return;
118
+
119
+ if (pattern.includes('*')) {
120
+ // Handle glob patterns
121
+ const dir = path.dirname(validatedSearchPath);
122
+ const filenamePattern = path.basename(validatedSearchPath);
123
+ const validatedDir = SecurityUtils.validatePath(dir, this.projectRoot);
124
+ if (!validatedDir) return;
125
+
126
+ if (SecurityUtils.safeExistsSync(validatedDir)) {
127
+ const files = SecurityUtils.safeReaddirSync(validatedDir, this.projectRoot);
128
+ const regex = new RegExp(filenamePattern.replace('*', '.*'));
129
+
130
+ for (const file of files) {
131
+ if (regex.test(file)) {
132
+ const filePath = path.join(validatedDir, file);
133
+ const validatedFilePath = SecurityUtils.validatePath(filePath, this.projectRoot);
134
+ if (validatedFilePath && SecurityUtils.safeExistsSync(validatedFilePath)) {
135
+ fs.unlinkSync(validatedFilePath);
136
+ this.log(`Deleted ${path.relative(this.projectRoot, validatedFilePath)}`);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ } else {
142
+ // Handle exact files
143
+ const validatedFilePath = SecurityUtils.validatePath(searchPath, this.projectRoot);
144
+ if (validatedFilePath && SecurityUtils.safeExistsSync(validatedFilePath)) {
145
+ fs.unlinkSync(validatedFilePath);
146
+ this.log(`Deleted ${path.relative(this.projectRoot, validatedFilePath)}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ async validateEssentialFiles() {
152
+ this.log('Validating essential development files...');
153
+
154
+ let missingFiles = [];
155
+ for (const file of this.essentialFiles) {
156
+ const filePath = path.join(this.projectRoot, file);
157
+ const validatedPath = SecurityUtils.validatePath(filePath, this.projectRoot);
158
+ if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
159
+ missingFiles.push(file);
160
+ } else {
161
+ const stat = SecurityUtils.safeStatSync(validatedPath, this.projectRoot);
162
+ if (!stat || !stat.isFile()) {
163
+ this.log(`❌ ${file} is not a file`);
164
+ process.exit(1);
165
+ }
166
+ }
167
+ }
168
+
169
+ if (missingFiles.length > 0) {
170
+ this.log(`❌ Missing essential files: ${missingFiles.join(', ')}`);
171
+ process.exit(1);
172
+ }
173
+
174
+ this.log('✅ All essential development files present');
175
+ }
176
+
177
+ async devValidations() {
178
+ this.log('Running development-specific validations...');
179
+
180
+ // Check for test files
181
+ const testFiles = [
182
+ 'tests/security.test.js',
183
+ 'tests/config-system.test.js',
184
+ 'tests/comprehensive-test.js'
185
+ ];
186
+
187
+ for (const testFile of testFiles) {
188
+ const testPath = path.join(this.projectRoot, testFile);
189
+ const validatedPath = SecurityUtils.validatePath(testPath, this.projectRoot);
190
+ if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
191
+ this.log(`⚠️ Missing test file: ${testFile}`);
192
+ }
193
+ }
194
+
195
+ // Check for development tools
196
+ const devTools = [
197
+ '.vscode/',
198
+ '.idea/',
199
+ 'node_modules/'
200
+ ];
201
+
202
+ for (const tool of devTools) {
203
+ const toolPath = path.join(this.projectRoot, tool);
204
+ const validatedPath = SecurityUtils.validatePath(toolPath, this.projectRoot);
205
+ if (validatedPath && SecurityUtils.safeExistsSync(validatedPath)) {
206
+ this.log(`ℹ️ Development tool found: ${tool}`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ // Run if called directly
213
+ if (require.main === module) {
214
+ const cleaner = new DevelopmentPrepublishCleaner();
215
+ cleaner.clean().catch(error => {
216
+ console.error('Error during development cleanup:', error);
217
+ process.exit(1);
218
+ });
219
+ }
220
+
221
+ module.exports = DevelopmentPrepublishCleaner;