@zohodesk/client_build_tool 0.0.1-0.exp.0.0.4 → 0.0.1-0.exp.0.0.9

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.
@@ -36,7 +36,7 @@ function getI18nAssetsForChunkAsArrayStr({
36
36
  chunkSplitEnable,
37
37
  i18nFileNameTemplate
38
38
  }) {
39
- if (!chunkSplitEnable) {
39
+ if (!chunkSplitEnable || !i18nStore) {
40
40
  // NOTE: we have used lang variable inside
41
41
  // NOTE: below code for full i18n for now it is not implemented
42
42
  // if (!chunkSplitEnable) {
@@ -71,6 +71,11 @@ function getI18nAssetForChunkAsStr({
71
71
  i18nStore,
72
72
  i18nFileNameTemplate
73
73
  }) {
74
+ // Handle case where i18nStore is undefined (when i18nChunkSplit is disabled or not initialized)
75
+ if (!i18nStore || !i18nStore.isChunkHasI18n) {
76
+ return '';
77
+ }
78
+
74
79
  if (!i18nStore.isChunkHasI18n(chunk)) {
75
80
  return '';
76
81
  }
@@ -4,98 +4,122 @@ const path = require('path');
4
4
 
5
5
  const fs = require('fs');
6
6
 
7
- let allI18nDataFromPropertiesCache = null;
7
+ const {
8
+ parseProperties
9
+ } = require('../utils/propertiesParser'); // Improved caching with proper cleanup
8
10
 
9
- function loadJSResourcesOnce(options) {
10
- if (allI18nDataFromPropertiesCache) {
11
- return allI18nDataFromPropertiesCache;
11
+
12
+ class ConfigCache {
13
+ constructor() {
14
+ this.i18nDataCache = new Map();
15
+ this.maxCacheSize = 10; // Limit cache size
16
+ }
17
+
18
+ getI18nData(filePath) {
19
+ return this.i18nDataCache.get(filePath);
20
+ }
21
+
22
+ setI18nData(filePath, data) {
23
+ if (this.i18nDataCache.size >= this.maxCacheSize) {
24
+ // Clear oldest entries
25
+ const firstKey = this.i18nDataCache.keys().next().value;
26
+ this.i18nDataCache.delete(firstKey);
27
+ }
28
+
29
+ this.i18nDataCache.set(filePath, data);
30
+ }
31
+
32
+ clear() {
33
+ this.i18nDataCache.clear();
12
34
  }
13
35
 
36
+ }
37
+
38
+ const configCache = new ConfigCache();
39
+
40
+ function loadJSResourcesOnce(options, webpackContext) {
14
41
  if (!options.i18nIndexing || !options.i18nIndexing.enable) {
15
42
  throw new Error('i18nIdReplaceLoader requires i18nIndexing to be enabled');
16
43
  }
17
44
 
18
45
  if (!options.i18nIndexing.jsResourcePath) {
19
46
  throw new Error('Missing required jsResourcePath in i18nIndexing options');
20
- }
47
+ } // Use webpack context instead of process.cwd() for better reliability
21
48
 
22
- const propertiesFilePath = path.resolve(process.cwd(), options.i18nIndexing.jsResourcePath);
49
+
50
+ const contextPath = webpackContext || process.cwd();
51
+ const propertiesFilePath = path.isAbsolute(options.i18nIndexing.jsResourcePath) ? options.i18nIndexing.jsResourcePath : path.resolve(contextPath, options.i18nIndexing.jsResourcePath); // Check cache first
52
+
53
+ const cached = configCache.getI18nData(propertiesFilePath);
54
+
55
+ if (cached) {
56
+ return cached;
57
+ }
23
58
 
24
59
  if (!fs.existsSync(propertiesFilePath)) {
25
60
  throw new Error(`JSResource file not found at: ${propertiesFilePath}`);
26
61
  }
27
62
 
28
- const i18nData = {};
29
-
30
63
  try {
31
64
  const data = fs.readFileSync(propertiesFilePath, {
32
65
  encoding: 'utf-8'
33
66
  });
34
- const lines = data.split(/\r?\n/);
35
- lines.forEach(line => {
36
- const trimmedLine = line.trim();
37
-
38
- if (trimmedLine.startsWith('#') || trimmedLine.startsWith('!') || trimmedLine === '') {
39
- return;
40
- }
41
-
42
- let separatorIndex = -1;
43
-
44
- for (let i = 0; i < trimmedLine.length; i++) {
45
- if ((trimmedLine[i] === '=' || trimmedLine[i] === ':') && (i === 0 || trimmedLine[i - 1] !== '\\')) {
46
- separatorIndex = i;
47
- break;
48
- }
49
- }
50
-
51
- if (separatorIndex > 0) {
52
- let key = trimmedLine.substring(0, separatorIndex).trim();
53
- const value = trimmedLine.substring(separatorIndex + 1).trim();
54
- key = key.replace(/\\ /g, ' ');
55
-
56
- if (key) {
57
- i18nData[key] = value;
58
- }
59
- }
60
- });
67
+ const i18nData = parseProperties(data);
68
+
69
+ if (Object.keys(i18nData).length === 0) {
70
+ throw new Error(`No i18n data found in JSResource file: ${propertiesFilePath}`);
71
+ } // Cache the result
72
+
73
+
74
+ configCache.setI18nData(propertiesFilePath, i18nData);
75
+ return i18nData;
61
76
  } catch (err) {
62
77
  throw new Error(`Error reading JSResource file ${propertiesFilePath}: ${err.message}`);
63
78
  }
64
-
65
- if (Object.keys(i18nData).length === 0) {
66
- throw new Error(`No i18n data found in JSResource file: ${propertiesFilePath}`);
67
- }
68
-
69
- allI18nDataFromPropertiesCache = i18nData;
70
- return allI18nDataFromPropertiesCache;
71
79
  }
72
80
 
73
- function i18nIdReplaceLoaderConfig(options) {
81
+ function i18nIdReplaceLoaderConfig(options, webpackContext) {
82
+ // Validate required options
74
83
  if (!options.i18nIndexing || !options.i18nIndexing.enable) {
75
84
  throw new Error('i18nIdReplaceLoader requires i18nIndexing to be enabled');
76
85
  }
77
86
 
78
87
  if (!options.i18nIndexing.numericMapPath) {
79
88
  throw new Error('Missing required numericMapPath in i18nIndexing options');
80
- }
89
+ } // Load i18n data with proper context
90
+
81
91
 
82
- const allI18nData = loadJSResourcesOnce(options);
92
+ const allI18nData = loadJSResourcesOnce(options, webpackContext);
93
+
94
+ const i18nKeyReplaceLoaderPath = require.resolve('../loaders/i18nIdReplaceLoader.js'); // Enhanced loader options with better defaults
83
95
 
84
- const i18nKeyReplaceLoaderPath = require.resolve('../loaders/i18nIdReplaceLoader.js');
85
96
 
86
97
  const loaderOptions = {
87
98
  allI18nData: allI18nData,
88
99
  sourceMaps: !!(options.devtool && options.devtool.includes('source-map')),
89
100
  isDebug: options.mode === 'development',
90
101
  useNumericIndexing: true,
91
- numericMapPath: options.i18nIndexing.numericMapPath
102
+ numericMapPath: options.i18nIndexing.numericMapPath,
103
+ // Additional configurable options
104
+ retainLines: options.i18nIndexing.retainLines || false,
105
+ preserveComments: options.i18nIndexing.preserveComments !== false,
106
+ compact: options.mode === 'production',
107
+ minified: options.mode === 'production',
108
+ // Allow custom babel plugins
109
+ babelPlugins: options.i18nIndexing.babelPlugins || undefined
92
110
  };
93
111
  return {
94
112
  loader: i18nKeyReplaceLoaderPath,
95
113
  options: loaderOptions
96
114
  };
115
+ } // Export cache for potential cleanup
116
+
117
+
118
+ function clearCache() {
119
+ configCache.clear();
97
120
  }
98
121
 
99
122
  module.exports = {
100
- i18nIdReplaceLoaderConfig
123
+ i18nIdReplaceLoaderConfig,
124
+ clearCache
101
125
  };
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
- const fs = require('fs');
3
+ const fs = require('fs').promises;
4
+
5
+ const fsSync = require('fs');
4
6
 
5
7
  const path = require('path');
6
8
 
@@ -24,41 +26,72 @@ try {
24
26
  throw new Error('[i18nIdReplaceLoader] Required dependency not found: ' + e.message);
25
27
  }
26
28
 
27
- const LOADER_PREFIX = '[i18nIdReplaceLoader]';
28
- let numericIdMapDataCache = null;
29
- let mapLoadAttemptedForPath = {};
29
+ const LOADER_PREFIX = '[i18nIdReplaceLoader]'; // Improved caching with proper cleanup
30
30
 
31
- function loadNumericIdMap(loaderContext, mapPath) {
32
- if (!mapPath) {
33
- throw new Error(`${LOADER_PREFIX} Numeric map path not provided in loader options.`);
31
+ class LoaderCache {
32
+ constructor() {
33
+ this.numericMapCache = new Map();
34
+ this.astCache = new Map();
35
+ this.maxCacheSize = 100; // Prevent memory leaks
36
+ }
37
+
38
+ getNumericMap(mapPath) {
39
+ return this.numericMapCache.get(mapPath);
40
+ }
41
+
42
+ setNumericMap(mapPath, data) {
43
+ if (this.numericMapCache.size >= this.maxCacheSize) {
44
+ // Clear oldest entries
45
+ const firstKey = this.numericMapCache.keys().next().value;
46
+ this.numericMapCache.delete(firstKey);
47
+ }
48
+
49
+ this.numericMapCache.set(mapPath, data);
50
+ }
51
+
52
+ clear() {
53
+ this.numericMapCache.clear();
54
+ this.astCache.clear();
34
55
  }
35
56
 
36
- const absoluteMapPath = path.isAbsolute(mapPath) ? mapPath : path.resolve(loaderContext.rootContext || process.cwd(), mapPath);
57
+ }
58
+
59
+ const loaderCache = new LoaderCache();
37
60
 
38
- if (numericIdMapDataCache && mapLoadAttemptedForPath[absoluteMapPath]) {
39
- return numericIdMapDataCache;
61
+ async function loadNumericIdMap(loaderContext, mapPath) {
62
+ if (!mapPath) {
63
+ throw new Error(`${LOADER_PREFIX} Numeric map path not provided in loader options.`);
40
64
  }
41
65
 
42
- mapLoadAttemptedForPath[absoluteMapPath] = true;
66
+ const absoluteMapPath = path.isAbsolute(mapPath) ? mapPath : path.resolve(loaderContext.rootContext || process.cwd(), mapPath); // Check cache first
67
+
68
+ const cached = loaderCache.getNumericMap(absoluteMapPath);
43
69
 
44
- if (!fs.existsSync(absoluteMapPath)) {
45
- throw new Error(`${LOADER_PREFIX} Pre-generated i18n numeric map file NOT FOUND at: ${absoluteMapPath}.`);
70
+ if (cached) {
71
+ return cached;
46
72
  }
47
73
 
48
74
  try {
49
- const fileContent = fs.readFileSync(absoluteMapPath, 'utf-8');
50
- const parsedData = JSON.parse(fileContent);
75
+ // Check if file exists
76
+ if (!fsSync.existsSync(absoluteMapPath)) {
77
+ throw new Error(`Pre-generated i18n numeric map file NOT FOUND at: ${absoluteMapPath}`);
78
+ } // Read file asynchronously for better performance
79
+
80
+
81
+ const fileContent = await fs.readFile(absoluteMapPath, 'utf-8');
82
+ const parsedData = JSON.parse(fileContent); // Validate map structure
51
83
 
52
84
  if (!parsedData || !parsedData.originalKeyToNumericId || typeof parsedData.originalKeyToNumericId !== 'object') {
53
- throw new Error(`${LOADER_PREFIX} Pre-generated map file (${absoluteMapPath}) is invalid or does not contain 'originalKeyToNumericId'.`);
85
+ throw new Error(`Pre-generated map file (${absoluteMapPath}) is invalid or does not contain 'originalKeyToNumericId'`);
54
86
  }
55
87
 
56
- numericIdMapDataCache = parsedData.originalKeyToNumericId;
88
+ const numericIdMap = parsedData.originalKeyToNumericId; // Cache the result
89
+
90
+ loaderCache.setNumericMap(absoluteMapPath, numericIdMap);
91
+ return numericIdMap;
57
92
  } catch (err) {
58
93
  throw new Error(`${LOADER_PREFIX} Error loading or parsing pre-generated i18n map from ${absoluteMapPath}: ${err.message}`);
59
94
  }
60
-
61
- return numericIdMapDataCache;
62
95
  }
63
96
 
64
97
  module.exports = function i18nIdReplaceLoader(source, map, meta) {
@@ -66,103 +99,131 @@ module.exports = function i18nIdReplaceLoader(source, map, meta) {
66
99
  this.cacheable && this.cacheable();
67
100
  const options = getOptions(this) || {};
68
101
  const callback = this.async();
69
- const loaderContext = this;
102
+ const loaderContext = this; // Validate required options
70
103
 
71
104
  if (!options.allI18nData || typeof options.allI18nData !== 'object' || Object.keys(options.allI18nData).length === 0) {
72
- throw new Error(`${LOADER_PREFIX} [${resourcePath}] 'allI18nData' option is missing or empty.`);
105
+ return callback(new Error(`${LOADER_PREFIX} [${resourcePath}] 'allI18nData' option is missing or empty.`));
73
106
  }
74
107
 
75
108
  if (!options.useNumericIndexing) {
76
- throw new Error(`${LOADER_PREFIX} [${resourcePath}] 'useNumericIndexing' must be enabled.`);
77
- }
109
+ return callback(new Error(`${LOADER_PREFIX} [${resourcePath}] 'useNumericIndexing' must be enabled.`));
110
+ } // Load numeric map asynchronously
111
+
112
+
113
+ loadNumericIdMap(this, options.numericMapPath).then(numericIdMap => {
114
+ try {
115
+ // Configure parser with better defaults and configurability
116
+ const parserOptions = {
117
+ sourceType: 'module',
118
+ plugins: options.babelPlugins || ['jsx', 'typescript', 'classProperties', 'optionalChaining', 'nullishCoalescingOperator', 'objectRestSpread', 'dynamicImport', 'decorators-legacy', 'asyncGenerators', 'bigInt', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'functionBind', 'importMeta', 'numericSeparator', 'optionalCatchBinding', 'throwExpressions', 'topLevelAwait'],
119
+ attachComment: true,
120
+ sourceFilename: resourcePath,
121
+ allowImportExportEverywhere: true,
122
+ allowAwaitOutsideFunction: true,
123
+ allowReturnOutsideFunction: true,
124
+ ranges: false,
125
+ tokens: false
126
+ };
127
+ const astFile = parser.parse(source, parserOptions);
128
+ const astProgram = astFile.program;
129
+ const comments = astFile.comments || [];
130
+ const {
131
+ literalKeys,
132
+ commentKeys
133
+ } = collectAndCategorizeUsedI18nKeys(astProgram, comments, options.allI18nData, options.isDebug); // Store keys in module build info for plugin consumption
134
+
135
+ if (this._module) {
136
+ if (!this._module.buildInfo) {
137
+ this._module.buildInfo = {};
138
+ }
78
139
 
79
- const numericIdMap = loadNumericIdMap(this, options.numericMapPath);
140
+ if (literalKeys.size > 0) {
141
+ this._module.buildInfo.loaderIdentifiedLiteralI18nKeys = Array.from(literalKeys);
142
+ }
80
143
 
81
- try {
82
- const parserOptions = {
83
- sourceType: 'module',
84
- plugins: ['jsx', 'typescript', 'classProperties', 'optionalChaining', 'nullishCoalescingOperator', 'objectRestSpread', 'dynamicImport'],
85
- attachComment: true,
86
- sourceFilename: resourcePath
87
- };
88
- const astFile = parser.parse(source, parserOptions);
89
- const astProgram = astFile.program;
90
- const comments = astFile.comments || [];
91
- const {
92
- literalKeys,
93
- commentKeys
94
- } = collectAndCategorizeUsedI18nKeys(astProgram, comments, options.allI18nData, options.isDebug);
95
-
96
- if (this._module) {
97
- if (!this._module.buildInfo) {
98
- this._module.buildInfo = {};
99
- }
144
+ if (commentKeys.size > 0) {
145
+ this._module.buildInfo.loaderIdentifiedCommentI18nKeys = Array.from(commentKeys);
146
+ }
147
+ } // Early return if no replacements needed
100
148
 
101
- if (literalKeys.size > 0) {
102
- this._module.buildInfo.loaderIdentifiedLiteralI18nKeys = Array.from(literalKeys);
103
- }
104
149
 
105
- if (commentKeys.size > 0) {
106
- this._module.buildInfo.loaderIdentifiedCommentI18nKeys = Array.from(commentKeys);
150
+ if (literalKeys.size === 0) {
151
+ return callback(null, source, map);
107
152
  }
108
- }
109
-
110
- if (literalKeys.size === 0) {
111
- return callback(null, source, map);
112
- }
113
153
 
114
- let replacementMade = false;
115
- walk(astProgram, {
116
- enter: function (node, parent, prop, index) {
117
- const walkerControl = this;
154
+ let replacementMade = false;
155
+ let replacementCount = 0; // Process AST and replace string literals with numeric IDs
118
156
 
119
- if ((node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string') {
120
- const originalValue = node.value;
157
+ walk(astProgram, {
158
+ enter: function (node, parent, prop, index) {
159
+ const walkerControl = this;
121
160
 
122
- if (literalKeys.has(originalValue)) {
123
- const numericId = numericIdMap[originalValue];
161
+ if ((node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string') {
162
+ const originalValue = node.value;
124
163
 
125
- if (numericId !== undefined) {
126
- const numericLiteralNode = {
127
- type: 'NumericLiteral',
128
- value: numericId
129
- };
130
- let replacementNode = numericLiteralNode;
164
+ if (literalKeys.has(originalValue)) {
165
+ const numericId = numericIdMap[originalValue];
131
166
 
132
- if (parent && parent.type === 'JSXAttribute' && parent.value === node) {
133
- replacementNode = {
134
- type: 'JSXExpressionContainer',
135
- expression: numericLiteralNode
167
+ if (numericId !== undefined) {
168
+ const numericLiteralNode = {
169
+ type: 'NumericLiteral',
170
+ value: numericId,
171
+ raw: String(numericId)
136
172
  };
173
+ let replacementNode = numericLiteralNode; // Handle JSX attributes specially
174
+
175
+ if (parent && parent.type === 'JSXAttribute' && parent.value === node) {
176
+ replacementNode = {
177
+ type: 'JSXExpressionContainer',
178
+ expression: numericLiteralNode
179
+ };
180
+ }
181
+
182
+ walkerControl.replace(replacementNode);
183
+ replacementMade = true;
184
+ replacementCount++;
137
185
  }
138
-
139
- walkerControl.replace(replacementNode);
140
- replacementMade = true;
141
186
  }
142
187
  }
143
188
  }
189
+ }); // Generate output only if replacements were made
190
+
191
+ if (replacementMade) {
192
+ const generateOptions = {
193
+ sourceMaps: !!options.sourceMaps,
194
+ sourceFileName: resourcePath,
195
+ retainLines: options.retainLines || false,
196
+ comments: options.preserveComments !== false,
197
+ compact: options.compact || false,
198
+ minified: options.minified || false
199
+ };
200
+ const output = generator(astFile, generateOptions, source); // Debug logging if enabled
201
+
202
+ if (options.isDebug) {
203
+ console.log(`${LOADER_PREFIX} [${resourcePath}] Replaced ${replacementCount} i18n keys with numeric IDs`);
204
+ }
205
+
206
+ callback(null, output.code, options.sourceMaps && output.map ? output.map : map);
207
+ } else {
208
+ callback(null, source, map);
144
209
  }
145
- });
146
-
147
- if (replacementMade) {
148
- const generateOptions = {
149
- sourceMaps: !!options.sourceMaps,
150
- sourceFileName: resourcePath,
151
- retainLines: false,
152
- comments: true
153
- };
154
- const output = generator(astFile, generateOptions, source);
155
- callback(null, output.code, options.sourceMaps && output.map ? output.map : map);
156
- } else {
157
- callback(null, source, map);
158
- }
159
- } catch (err) {
160
- const detailedError = new Error(`${LOADER_PREFIX} [${resourcePath}] AST Processing Error: ${err.message} (Stack: ${err.stack})`);
210
+ } catch (err) {
211
+ // Enhanced error handling with better context
212
+ const detailedError = new Error(`${LOADER_PREFIX} [${resourcePath}] AST Processing Error: ${err.message}`);
161
213
 
162
- if (err.loc && err.loc.line) {
163
- detailedError.message += ` at line ${err.loc.line}, column ${err.loc.column}`;
164
- }
214
+ if (err.loc && err.loc.line) {
215
+ detailedError.message += ` at line ${err.loc.line}, column ${err.loc.column}`;
216
+ } // Add stack trace in debug mode
165
217
 
166
- callback(detailedError);
167
- }
218
+
219
+ if (options.isDebug && err.stack) {
220
+ detailedError.message += `\nStack: ${err.stack}`;
221
+ }
222
+
223
+ callback(detailedError);
224
+ }
225
+ }).catch(err => {
226
+ // Handle async errors from map loading
227
+ callback(new Error(`${LOADER_PREFIX} [${resourcePath}] Failed to load numeric map: ${err.message}`));
228
+ });
168
229
  };
@@ -7,7 +7,7 @@ exports.configI18nNumericIndexPlugin = configI18nNumericIndexPlugin;
7
7
 
8
8
  var {
9
9
  I18nNumericIndexPlugin
10
- } = require("@zohodesk/client_build_tool/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexPlugin");
10
+ } = require("../custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexPlugin");
11
11
 
12
12
  var {
13
13
  I18nNumericIndexHtmlInjectorPlugin
@@ -36,6 +36,9 @@ function configI18nSplitPlugin(options) {
36
36
  publicPath: i18nPublicPath,
37
37
  i18nManifestFileName: (0, _nameTemplates.nameTemplates)('i18nmanifest', options),
38
38
  // template: (object, locale) => `window.loadI18n(${JSON.stringify(object)}, ${JSON.stringify(locale)})`,
39
- propertiesFolder: i18nChunkSplit.propertiesFolder
39
+ propertiesFolder: i18nChunkSplit.propertiesFolder,
40
+ // NEW OPTIONS FOR NUMERIC INDEXING
41
+ useNumericIndexing: i18nChunkSplit.useNumericIndexing,
42
+ numericMapPath: i18nChunkSplit.numericMapPath
40
43
  });
41
44
  }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Shared properties file parsing utility
4
+ * Handles consistent parsing across all i18n tools
5
+ */
6
+ // Decode Unicode escape sequences (for values only)
7
+
8
+ function decodeUnicodeEscapes(str) {
9
+ if (typeof str !== 'string') {
10
+ return str;
11
+ }
12
+
13
+ return str.replace(/\\u([0-9a-fA-F]{4})/g, (match, hex) => {
14
+ return String.fromCharCode(parseInt(hex, 16));
15
+ });
16
+ }
17
+ /**
18
+ * Parse properties file content into key-value pairs
19
+ * @param {string} content - Properties file content
20
+ * @returns {Object} Parsed key-value pairs
21
+ */
22
+
23
+
24
+ function parseProperties(content) {
25
+ const lines = content.split(/\r?\n/);
26
+ const data = {};
27
+ lines.forEach(line => {
28
+ const trimmedLine = line.trim();
29
+
30
+ if (trimmedLine.startsWith('#') || trimmedLine.startsWith('!') || trimmedLine === '') {
31
+ return;
32
+ } // Find unescaped separator (= or :)
33
+
34
+
35
+ let separatorIndex = -1;
36
+
37
+ for (let i = 0; i < trimmedLine.length; i++) {
38
+ if ((trimmedLine[i] === '=' || trimmedLine[i] === ':') && (i === 0 || trimmedLine[i - 1] !== '\\')) {
39
+ separatorIndex = i;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (separatorIndex > 0) {
45
+ let key = trimmedLine.substring(0, separatorIndex).trim();
46
+ let value = trimmedLine.substring(separatorIndex + 1).trim();
47
+
48
+ if (key) {
49
+ // Handle escaped spaces in keys only
50
+ key = key.replace(/\\ /g, ' '); // Decode Unicode escape sequences ONLY in values, not keys
51
+
52
+ value = decodeUnicodeEscapes(value);
53
+ data[key] = value;
54
+ }
55
+ }
56
+ });
57
+ return data;
58
+ }
59
+ /**
60
+ * Parse properties file content into a Set of keys only
61
+ * @param {string} content - Properties file content
62
+ * @returns {Set<string>} Set of keys
63
+ */
64
+
65
+
66
+ function parsePropertiesToKeySet(content) {
67
+ const lines = content.split(/\r?\n/);
68
+ const keys = new Set();
69
+ lines.forEach(line => {
70
+ const trimmedLine = line.trim();
71
+
72
+ if (trimmedLine.startsWith('#') || trimmedLine.startsWith('!') || trimmedLine === '') {
73
+ return;
74
+ } // Find unescaped separator (= or :)
75
+
76
+
77
+ let separatorIndex = -1;
78
+
79
+ for (let i = 0; i < trimmedLine.length; i++) {
80
+ if ((trimmedLine[i] === '=' || trimmedLine[i] === ':') && (i === 0 || trimmedLine[i - 1] !== '\\')) {
81
+ separatorIndex = i;
82
+ break;
83
+ }
84
+ }
85
+
86
+ if (separatorIndex > 0) {
87
+ let key = trimmedLine.substring(0, separatorIndex).trim();
88
+
89
+ if (key) {
90
+ // Handle escaped spaces in keys only
91
+ key = key.replace(/\\ /g, ' ');
92
+ keys.add(key);
93
+ }
94
+ }
95
+ });
96
+ return keys;
97
+ }
98
+
99
+ module.exports = {
100
+ parseProperties,
101
+ parsePropertiesToKeySet,
102
+ decodeUnicodeEscapes
103
+ };