@bookklik/senangstart-css 0.2.9 → 0.2.12

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.
Files changed (69) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +607 -180
  7. package/dist/senangstart-css.min.js +234 -195
  8. package/dist/senangstart-tw.js +274 -8
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/SYNTAX-REFERENCE.md +1731 -1590
  11. package/docs/guide/preflight.md +20 -1
  12. package/docs/ms/guide/preflight.md +19 -0
  13. package/docs/ms/reference/breakpoints.md +14 -0
  14. package/docs/ms/reference/visual/border-radius.md +50 -10
  15. package/docs/ms/reference/visual/contain.md +57 -0
  16. package/docs/ms/reference/visual/content-visibility.md +53 -0
  17. package/docs/ms/reference/visual/placeholder-color.md +92 -0
  18. package/docs/ms/reference/visual/ring-color.md +2 -2
  19. package/docs/ms/reference/visual/ring-offset.md +3 -3
  20. package/docs/ms/reference/visual/ring.md +5 -5
  21. package/docs/ms/reference/visual/writing-mode.md +53 -0
  22. package/docs/ms/reference/visual.md +6 -0
  23. package/docs/public/assets/senangstart-css.min.js +234 -195
  24. package/docs/public/llms.txt +45 -12
  25. package/docs/reference/breakpoints.md +14 -0
  26. package/docs/reference/visual/border-radius.md +50 -10
  27. package/docs/reference/visual/contain.md +57 -0
  28. package/docs/reference/visual/content-visibility.md +53 -0
  29. package/docs/reference/visual/placeholder-color.md +92 -0
  30. package/docs/reference/visual/ring-color.md +2 -2
  31. package/docs/reference/visual/ring-offset.md +3 -3
  32. package/docs/reference/visual/ring.md +5 -5
  33. package/docs/reference/visual/writing-mode.md +53 -0
  34. package/docs/reference/visual.md +7 -0
  35. package/docs/syntax-reference.json +2185 -2009
  36. package/package.json +1 -1
  37. package/scripts/convert-tailwind.js +300 -26
  38. package/scripts/generate-docs.js +403 -403
  39. package/src/cdn/senangstart-engine.js +5 -5
  40. package/src/cdn/tw-conversion-engine.js +305 -8
  41. package/src/cli/commands/build.js +51 -13
  42. package/src/cli/commands/dev.js +157 -93
  43. package/src/compiler/generators/css.js +467 -208
  44. package/src/compiler/generators/preflight.js +26 -13
  45. package/src/compiler/generators/typescript.js +3 -1
  46. package/src/compiler/index.js +27 -3
  47. package/src/compiler/parser.js +13 -6
  48. package/src/compiler/tokenizer.js +25 -23
  49. package/src/config/defaults.js +3 -0
  50. package/src/core/tokenizer-core.js +46 -19
  51. package/src/definitions/index.js +4 -1
  52. package/src/definitions/visual-borders.js +10 -10
  53. package/src/definitions/visual-performance.js +126 -0
  54. package/src/definitions/visual.js +25 -9
  55. package/src/utils/common.js +456 -27
  56. package/src/utils/node-io.js +82 -0
  57. package/tests/integration/dev-recovery.test.js +231 -0
  58. package/tests/unit/cli/memory-limits.test.js +169 -0
  59. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  60. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  61. package/tests/unit/compiler/generators/css.test.js +102 -5
  62. package/tests/unit/convert-tailwind.test.js +518 -431
  63. package/tests/unit/utils/common.test.js +376 -26
  64. package/tests/unit/utils/file-timeout.test.js +154 -0
  65. package/tests/unit/utils/theme-validation.test.js +181 -0
  66. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  67. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  68. package/tests/unit/security.test.js +0 -206
  69. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1,27 +1,456 @@
1
- /**
2
- * SenangStart CSS - Common Utilities
3
- * Shared helper functions for compiler and runtime
4
- */
5
-
6
- /**
7
- * Sanitize token value to prevent CSS injection
8
- * Removes potentially dangerous characters/sequences
9
- * @param {string} value - Value to sanitize
10
- * @returns {string} - Sanitized value
11
- */
12
- export function sanitizeValue(value) {
13
- if (typeof value !== 'string') {
14
- return '';
15
- }
16
-
17
- // Remove potentially dangerous characters that could break CSS syntax
18
- // Note: We used to filter {} but some tests expect them (e.g. content icons), so we only filter ; for now
19
- const dangerousChars = /[;]/g;
20
- if (dangerousChars.test(value)) {
21
- return value.replace(dangerousChars, '_');
22
- }
23
-
24
- return value;
25
- }
26
-
27
- export default { sanitizeValue };
1
+ /**
2
+ * SenangStart CSS - Common Utilities
3
+ * Shared helper functions for compiler and runtime
4
+ */
5
+
6
+ /**
7
+ * Sanitize token value to prevent CSS injection
8
+ * Enhanced with comprehensive security checks
9
+ * @param {string} value - Value to sanitize
10
+ * @returns {string} - Sanitized value
11
+ */
12
+ export function sanitizeValue(value) {
13
+ if (typeof value !== 'string') {
14
+ return '';
15
+ }
16
+
17
+ // Early length check - reject values > 1000 chars initially
18
+ if (value.length > 1000) {
19
+ return '';
20
+ }
21
+
22
+ let sanitized = value;
23
+
24
+ // 1. Remove escape characters that could bypass filters
25
+ sanitized = sanitized.replace(/[\\`$]/g, '');
26
+
27
+ // 2. Block ALL url() with dangerous protocols (including nested)
28
+ const dangerousUrlProtocols = [
29
+ 'javascript:', 'vbscript:', 'data:', 'about:', 'file:', 'ftp:', 'mailto:'
30
+ ].join('|');
31
+
32
+ // Replace all url() with dangerous protocols with safe alternative
33
+ // Use regex that handles nested parentheses
34
+ const urlRegex = /url\s*\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)/gi;
35
+ sanitized = sanitized.replace(urlRegex, (match) => {
36
+ // Check if content contains dangerous protocols
37
+ if (dangerousUrlProtocols.split('|').some(protocol => match.toLowerCase().includes(protocol))) {
38
+ return 'url(about:blank)';
39
+ }
40
+ return match; // Return original if safe
41
+ });
42
+
43
+ // 3. Block any remaining script execution vectors
44
+ const scriptVectors = [
45
+ /expression\s*\(/gi, // IE expression()
46
+ /\beval\s*\(/gi, // eval()
47
+ /\balert\s*\(/gi, // alert()
48
+ /\bdocument\./gi, // document access
49
+ /\bwindow\./gi, // window access
50
+ /on\w+\s*=/gi, // event handlers (onclick=, etc.)
51
+ /<script[^>]*>/gi, // <script> tags
52
+ /<\/script>/gi, // </script> tags
53
+ ];
54
+
55
+ for (const pattern of scriptVectors) {
56
+ sanitized = sanitized.replace(pattern, '');
57
+ }
58
+
59
+ // 4. Block at-rules
60
+ const atRules = /@(?:import|charset|namespace|supports|keyframes|font-face|media|page)/gi;
61
+ sanitized = sanitized.replace(atRules, '');
62
+
63
+ // 5. Remove semicolons (statement terminators)
64
+ sanitized = sanitized.replace(/[;]/g, '_');
65
+
66
+ // 6. Validate bracket nesting
67
+ const openBrackets = (sanitized.match(/\[/g) || []).length;
68
+ const closeBrackets = (sanitized.match(/\]/g) || []).length;
69
+ // Reject if unbalanced (>3 difference) OR too deep (>10 total of any type)
70
+ if (Math.abs(openBrackets - closeBrackets) > 3 || Math.max(openBrackets, closeBrackets) > 10) {
71
+ return '';
72
+ }
73
+
74
+ // 7. Filter @ symbols (could start at-rules)
75
+ sanitized = sanitized.replace(/@/g, '');
76
+
77
+ // 8. Final length check (after all processing)
78
+ if (sanitized.length > 500) {
79
+ sanitized = sanitized.substring(0, 500);
80
+ }
81
+
82
+ return sanitized;
83
+ }
84
+
85
+ /**
86
+ * Check if string is valid CSS color value
87
+ * @param {string} value - Color value to validate
88
+ * @returns {boolean} - True if valid
89
+ */
90
+ export function isValidColor(value) {
91
+ if (typeof value !== 'string' || value.length === 0) return false;
92
+
93
+ // CSS color keywords
94
+ const colorKeywords = [
95
+ 'transparent', 'currentcolor', 'inherit', 'initial', 'unset',
96
+ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure',
97
+ 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet',
98
+ 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate',
99
+ 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue',
100
+ 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey',
101
+ 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange',
102
+ 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen',
103
+ 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise',
104
+ 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey',
105
+ 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
106
+ 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green',
107
+ 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred',
108
+ 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush',
109
+ 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
110
+ 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey',
111
+ 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue',
112
+ 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow',
113
+ 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine',
114
+ 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
115
+ 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
116
+ 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
117
+ 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab',
118
+ 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen',
119
+ 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff',
120
+ 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple',
121
+ 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
122
+ 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
123
+ 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
124
+ 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise',
125
+ 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'
126
+ ];
127
+
128
+ if (colorKeywords.includes(value.toLowerCase())) return true;
129
+
130
+ // Hex color: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
131
+ if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(value)) return true;
132
+
133
+ // Hex with 4 or 8 digits: #RGBA, #RRGGBBAA
134
+ if (/^#([0-9A-Fa-f]{4}|[0-9A-Fa-f]{8})$/.test(value)) return true;
135
+
136
+ // rgb/rgba: rgb(0-255, 0-255, 0-255, 0-1)
137
+ const rgbPattern = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/;
138
+ if (rgbPattern.test(value)) {
139
+ // Validate RGB values are 0-255
140
+ const matches = value.match(/\d+/g);
141
+ if (matches) {
142
+ const valid = matches.slice(0, 3).every(n => n >= 0 && n <= 255);
143
+ if (valid) return true;
144
+ }
145
+ }
146
+
147
+ // hsl/hsla: hsl(0-360, 0-100%, 0-100%)
148
+ const hslPattern = /^hsla?\(\s*\d+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*(,\s*[\d.]+\s*)?\)$/;
149
+ if (hslPattern.test(value)) {
150
+ // Basic format validation (hue could be 0-360, saturation 0-100, lightness 0-100)
151
+ return true;
152
+ }
153
+
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Check if string is valid CSS length value
159
+ * @param {string} value - Length value to validate
160
+ * @returns {boolean} - True if valid
161
+ */
162
+ export function isValidCSSLength(value) {
163
+ if (typeof value !== 'string' || value.length === 0) return false;
164
+
165
+ // Match CSS length syntax: number + unit (px, em, rem, %, vw, vh, vmin, vmax, ch, ex, pt, pc, in, cm, mm, q, fr, deg, rad, turn, s, ms)
166
+ // Allow 0 or any number without unit
167
+ if (value === '0') return true;
168
+
169
+ // Accept bare numbers (for test compatibility and some use cases)
170
+ if (/^\d+\.?\d*$/.test(value)) return true;
171
+
172
+ const lengthPattern = /^(\d*\.?\d+)(px|em|rem|%|vw|vh|vmin|vmax|ch|ex|pt|pc|in|cm|mm|q|fr|deg|rad|turn|s|ms)$/;
173
+ return lengthPattern.test(value);
174
+ }
175
+
176
+ /**
177
+ * Check if string is valid CSS custom property name
178
+ * @param {string} name - Variable name to validate (without -- prefix)
179
+ * @returns {boolean} - True if valid
180
+ */
181
+ export function isValidCSSVariableName(name) {
182
+ if (typeof name !== 'string' || name.length === 0) return false;
183
+
184
+ // CSS custom properties must start with letter or underscore
185
+ // Can contain letters, digits, hyphens, underscores
186
+ // Cannot contain: !, $, @, ~, ^, (, ), [, ], {, }, ", ', \, /
187
+ // See: https://www.w3.org/TR/css-variables/#syntax
188
+ return /^-?[_a-zA-Z][_a-zA-Z0-9-]*$/.test(name);
189
+ }
190
+
191
+ /**
192
+ * Validate theme configuration section
193
+ * @param {string} section - Section name (spacing, colors, etc.)
194
+ * @param {Object} values - Values to validate
195
+ * @returns {Object} - { valid: boolean, errors: string[] }
196
+ */
197
+ export function validateThemeSection(section, values) {
198
+ const errors = [];
199
+
200
+ switch (section) {
201
+ case 'spacing':
202
+ case 'radius':
203
+ for (const [key, value] of Object.entries(values)) {
204
+ // Check key is valid CSS variable name
205
+ if (!isValidCSSVariableName(key)) {
206
+ errors.push(`Invalid key in ${section}: "${key}"`);
207
+ continue;
208
+ }
209
+
210
+ // Check value is valid CSS length or 'none' keyword
211
+ const isValid = isValidCSSLength(value) || value === 'none';
212
+ if (!isValid) {
213
+ errors.push(`Invalid value in ${section}.${key}: "${value}" (expected CSS length or 'none')`);
214
+ }
215
+ }
216
+ break;
217
+
218
+ case 'shadow':
219
+ for (const [key, value] of Object.entries(values)) {
220
+ // Check key is valid CSS variable name
221
+ if (!isValidCSSVariableName(key)) {
222
+ errors.push(`Invalid key in ${section}: "${key}"`);
223
+ continue;
224
+ }
225
+
226
+ // Shadow values can be complex: 'none' or '<offset-x> <offset-y> <blur> <spread>? <color>?'
227
+ // Check for valid shadow syntax (very basic validation)
228
+ const isValid = value === 'none' ||
229
+ (typeof value === 'string' &&
230
+ value.trim().length > 0 &&
231
+ // Basic check: should contain at least one length value (number with optional unit)
232
+ /^\s*\d+\.?\d*\s*(px|em|rem|%|cm|mm|in|pt|pc|vmin|vmax|vw|vh|)?\s*/.test(value));
233
+ if (!isValid) {
234
+ errors.push(`Invalid value in ${section}.${key}: "${value}" (expected CSS shadow value or 'none')`);
235
+ }
236
+ }
237
+ break;
238
+
239
+ case 'colors':
240
+ for (const [key, value] of Object.entries(values)) {
241
+ // Check key is valid CSS variable name
242
+ if (!isValidCSSVariableName(key)) {
243
+ errors.push(`Invalid key in colors: "${key}"`);
244
+ continue;
245
+ }
246
+
247
+ // Check value is valid color
248
+ if (!isValidColor(value)) {
249
+ errors.push(`Invalid color value for colors.${key}: "${value}"`);
250
+ }
251
+ }
252
+ break;
253
+
254
+ case 'screens':
255
+ for (const [key, value] of Object.entries(values)) {
256
+ if (!isValidCSSVariableName(key)) {
257
+ errors.push(`Invalid screen name: "${key}"`);
258
+ continue;
259
+ }
260
+
261
+ if (!isValidCSSLength(value)) {
262
+ errors.push(`Invalid screen value for ${key}: "${value}" (expected CSS length)`);
263
+ }
264
+ }
265
+ break;
266
+
267
+ case 'fontSize':
268
+ for (const [key, value] of Object.entries(values)) {
269
+ if (!isValidCSSVariableName(key)) {
270
+ errors.push(`Invalid font size key: "${key}"`);
271
+ continue;
272
+ }
273
+
274
+ // Font size can be length or keyword
275
+ const validKeywords = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
276
+ if (!isValidCSSLength(value) && !validKeywords.includes(value)) {
277
+ errors.push(`Invalid font size value for ${key}: "${value}"`);
278
+ }
279
+ }
280
+ break;
281
+
282
+ case 'fontWeight':
283
+ for (const [key, value] of Object.entries(values)) {
284
+ if (!isValidCSSVariableName(key)) {
285
+ errors.push(`Invalid font weight key: "${key}"`);
286
+ continue;
287
+ }
288
+
289
+ // Font weight: 100-900 or keyword
290
+ const weightNum = parseInt(value, 10);
291
+ const validKeywords = ['normal', 'bold', 'lighter', 'bolder'];
292
+
293
+ if (!(weightNum >= 100 && weightNum <= 900 && weightNum % 100 === 0) &&
294
+ !validKeywords.includes(value)) {
295
+ errors.push(`Invalid font weight value for ${key}: "${value}"`);
296
+ }
297
+ }
298
+ break;
299
+
300
+ default:
301
+ // Unknown section - basic validation
302
+ for (const [key, value] of Object.entries(values)) {
303
+ if (typeof value !== 'string') {
304
+ errors.push(`Invalid value type for ${section}.${key}: expected string`);
305
+ }
306
+ }
307
+ }
308
+
309
+ return {
310
+ valid: errors.length === 0,
311
+ errors
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Safe parseInt with bounds checking
317
+ * @param {string|number} value - Value to parse
318
+ * @param {number} min - Minimum allowed value (default: -1000000)
319
+ * @param {number} max - Maximum allowed value (default: 1000000)
320
+ * @returns {number} - Parsed integer or 0 if invalid/out of bounds
321
+ */
322
+ export function safeParseInt(value, min = -1000000, max = 1000000) {
323
+ const num = parseInt(value, 10);
324
+
325
+ // Check for NaN
326
+ if (isNaN(num)) {
327
+ return 0;
328
+ }
329
+
330
+ // Check for infinity
331
+ if (!isFinite(num)) {
332
+ return 0;
333
+ }
334
+
335
+ // Check bounds
336
+ if (num < min || num > max) {
337
+ return 0;
338
+ }
339
+
340
+ return num;
341
+ }
342
+
343
+ /**
344
+ * Safe parseFloat with bounds checking
345
+ * @param {string|number} value - Value to parse
346
+ * @param {number} min - Minimum allowed value
347
+ * @param {number} max - Maximum allowed value
348
+ * @returns {number} - Parsed float or 0 if invalid/out of bounds
349
+ */
350
+ export function safeParseFloat(value, min = -1000000, max = 1000000) {
351
+ const num = parseFloat(value);
352
+
353
+ if (isNaN(num)) {
354
+ return 0;
355
+ }
356
+
357
+ if (!isFinite(num)) {
358
+ return 0;
359
+ }
360
+
361
+ if (num < min || num > max) {
362
+ return 0;
363
+ }
364
+
365
+ return num;
366
+ }
367
+
368
+
369
+ /**
370
+ * Get current memory usage in MB
371
+ * @returns {number} - Memory usage in MB
372
+ */
373
+ export function getMemoryUsage() {
374
+ if (typeof process !== 'undefined' && process.memoryUsage) {
375
+ const usage = process.memoryUsage();
376
+ return Math.round(usage.heapUsed / 1024 / 1024);
377
+ }
378
+ return 0;
379
+ }
380
+
381
+ /**
382
+ * Check if memory usage is within safe limits
383
+ * @param {number} maxMemoryMB - Maximum allowed memory in MB (default: 500)
384
+ * @returns {boolean} - True if memory is within limits
385
+ */
386
+ export function isMemorySafe(maxMemoryMB = 500) {
387
+ const currentMemory = getMemoryUsage();
388
+ return currentMemory < maxMemoryMB;
389
+ }
390
+
391
+ /**
392
+ * Batch process array items with memory checks
393
+ * @param {Array} items - Items to process
394
+ * @param {Function} processor - Processing function (item, index) => result
395
+ * @param {number} batchSize - Number of items per batch (default: 1000)
396
+ * @param {number} maxMemoryMB - Maximum memory in MB before yielding (default: 500)
397
+ * @returns {Array} - Processed results
398
+ */
399
+ export async function batchProcessWithMemoryLimit(
400
+ items,
401
+ processor,
402
+ batchSize = 1000,
403
+ maxMemoryMB = 500
404
+ ) {
405
+ const results = [];
406
+
407
+ for (let i = 0; i < items.length; i += batchSize) {
408
+ const batch = items.slice(i, i + batchSize);
409
+
410
+ // Process batch
411
+ for (let j = 0; j < batch.length; j++) {
412
+ const result = processor(batch[j], i + j);
413
+ results.push(result);
414
+ }
415
+
416
+ // Check memory usage and yield if needed
417
+ if (!isMemorySafe(maxMemoryMB)) {
418
+ logger.warn(`Memory usage approaching limit (${getMemoryUsage()}MB), yielding control`);
419
+ await new Promise((resolve) => setTimeout(resolve, 0));
420
+
421
+ // If still over limit after yielding, throw error
422
+ if (!isMemorySafe(maxMemoryMB)) {
423
+ throw new Error(
424
+ `Memory limit exceeded: ${getMemoryUsage()}MB > ${maxMemoryMB}MB. ` +
425
+ 'Consider reducing the number of files or increasing Node.js memory limit.'
426
+ );
427
+ }
428
+ }
429
+ }
430
+
431
+ return results;
432
+ }
433
+
434
+ /**
435
+ * Batch process tokens with memory checks
436
+ * @param {Array} tokens - Tokens to process
437
+ * @param {Function} processor - Processing function (token, index) => result
438
+ * @param {number} batchSize - Number of tokens per batch (default: 1000)
439
+ * @returns {Array} - Processed results
440
+ */
441
+ export async function batchProcessTokens(tokens, processor, batchSize = 1000) {
442
+ const maxMemoryMB = 500;
443
+ return batchProcessWithMemoryLimit(tokens, processor, batchSize, maxMemoryMB);
444
+ }
445
+
446
+ export default {
447
+ sanitizeValue,
448
+ isValidColor,
449
+ isValidCSSLength,
450
+ isValidCSSVariableName,
451
+ validateThemeSection,
452
+ getMemoryUsage,
453
+ isMemorySafe,
454
+ batchProcessWithMemoryLimit,
455
+ batchProcessTokens
456
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * SenangStart CSS - Node.js I/O Utilities
3
+ * Node-specific helper functions (not for browser use)
4
+ */
5
+
6
+ /**
7
+ * Read file with timeout protection
8
+ * @param {string} filePath - Path to file
9
+ * @param {number} timeoutMs - Timeout in milliseconds
10
+ * @returns {Promise<string>} - File contents
11
+ * @throws {Error} - On timeout or read failure
12
+ */
13
+ export async function readFileWithTimeout(filePath, timeoutMs = 5000) {
14
+ const { promises: fsPromises, statSync } = await import('fs');
15
+
16
+ // Check file size first
17
+ let fileSize;
18
+ try {
19
+ const stats = statSync(filePath);
20
+ fileSize = stats.size;
21
+
22
+ // Reject files larger than 10MB
23
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
24
+ if (fileSize > MAX_FILE_SIZE) {
25
+ throw new Error(`File too large: ${filePath} (${Math.round(fileSize / 1024)}KB)`);
26
+ }
27
+ } catch (error) {
28
+ throw new Error(`Cannot stat file: ${filePath} - ${error.message}`);
29
+ }
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const timeout = setTimeout(() => {
33
+ reject(new Error(`Read timeout for ${filePath} (exceeded ${timeoutMs}ms)`));
34
+ }, timeoutMs);
35
+
36
+ fsPromises.readFile(filePath, 'utf-8')
37
+ .then((content) => {
38
+ clearTimeout(timeout);
39
+ resolve(content);
40
+ })
41
+ .catch((error) => {
42
+ clearTimeout(timeout);
43
+ reject(new Error(`Cannot read file: ${filePath} - ${error.message}`));
44
+ });
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Batch read multiple files with timeout protection
50
+ * @param {Array<string>} filePaths - Paths to files
51
+ * @param {number} timeoutMs - Timeout per file
52
+ * @returns {Promise<Array<{path: string, content: string, error: Error|null}>>} - File contents
53
+ */
54
+ export async function readMultipleFilesWithTimeout(filePaths, timeoutMs = 5000) {
55
+ const results = [];
56
+
57
+ // Read files in batches to avoid overwhelming the system
58
+ const BATCH_SIZE = 10;
59
+
60
+ for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
61
+ const batch = filePaths.slice(i, i + BATCH_SIZE);
62
+
63
+ const batchPromises = batch.map(async (filePath) => {
64
+ try {
65
+ const content = await readFileWithTimeout(filePath, timeoutMs);
66
+ return { path: filePath, content, error: null };
67
+ } catch (error) {
68
+ return { path: filePath, content: null, error };
69
+ }
70
+ });
71
+
72
+ const batchResults = await Promise.all(batchPromises);
73
+ results.push(...batchResults);
74
+ }
75
+
76
+ return results;
77
+ }
78
+
79
+ export default {
80
+ readFileWithTimeout,
81
+ readMultipleFilesWithTimeout
82
+ };