@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
@@ -93,11 +93,11 @@ import { defaultConfig, mergeConfig } from '../config/defaults.js';
93
93
  function compileCSS(domTokens, config) {
94
94
  // 1. Convert raw DOM tokens (Sets) to parsed token array
95
95
  // tokenizeAll expects { layout: Set, ... } which matches domTokens structure
96
- const tokens = tokenizeAll(domTokens);
97
-
98
- // 2. Generate CSS using the core generator
99
- // generateCSS expects array of tokens and config
100
- return generateCSS(tokens, config);
96
+ const tokens = tokenizeAll(domTokens);
97
+
98
+ // 2. Generate CSS using the core generator
99
+ // generateCSS expects array of tokens and config
100
+ return generateCSS(tokens, config);
101
101
  }
102
102
 
103
103
  // ============================================
@@ -108,6 +108,45 @@ const fontSizeScale = {
108
108
  "9xl": "hero", // 8rem → hero
109
109
  };
110
110
 
111
+ // Line height scale mapping Tailwind values to SenangStart semantic values
112
+ // Engine native values: none(1), tight(1.25), snug(1.375), normal(1.5), relaxed(1.625), loose(2)
113
+ const lineHeightScale = {
114
+ none: "none", // line-height: 1
115
+ tight: "tight", // line-height: 1.25
116
+ snug: "snug", // line-height: 1.375
117
+ normal: "normal", // line-height: 1.5
118
+ relaxed: "relaxed", // line-height: 1.625
119
+ loose: "loose" // line-height: 2
120
+ };
121
+
122
+ // Letter spacing scale mapping Tailwind values to SenangStart semantic values
123
+ // Engine native values: tighter(-0.05em), tight(-0.025em), normal(0), wide(0.025em), wider(0.05em), widest(0.1em)
124
+ const letterSpacingScale = {
125
+ tighter: "tighter", // letter-spacing: -0.05em
126
+ tight: "tight", // letter-spacing: -0.025em
127
+ normal: "normal", // letter-spacing: 0
128
+ wide: "wide", // letter-spacing: 0.025em
129
+ wider: "wider", // letter-spacing: 0.05em
130
+ widest: "widest" // letter-spacing: 0.1em
131
+ };
132
+
133
+ // Z-index scale mapping Tailwind values to SenangStart semantic values
134
+ // Engine native values: base(0), low(10), mid(50), high(100), top(9999)
135
+ const zIndexScale = {
136
+ 0: "base", // z-index: 0
137
+ 10: "low", // z-index: 10
138
+ 20: "low", // z-index: 20
139
+ 30: "low", // z-index: 30
140
+ 40: "low", // z-index: 40
141
+ 50: "mid", // z-index: 50
142
+ 60: "high", // z-index: 60
143
+ 70: "high", // z-index: 70
144
+ 80: "high", // z-index: 80
145
+ 90: "high", // z-index: 90
146
+ 100: "high", // z-index: 100
147
+ auto: "auto" // z-index: auto
148
+ };
149
+
111
150
  // Fraction scale mapping Tailwind fractions to SenangStart semantic values
112
151
  // Used for positioning (left-1/2) and transforms (translate-x-1/2)
113
152
  const fractionScale = {
@@ -181,23 +220,108 @@ const layoutMappings = {
181
220
  };
182
221
 
183
222
  const visualKeywords = {
223
+ // Font style
184
224
  italic: "italic",
185
225
  "not-italic": "not-italic",
226
+
227
+ // Font smoothing
186
228
  antialiased: "antialiased",
229
+ "subpixel-antialiased": "subpixel-antialiased",
230
+
231
+ // Text transform
187
232
  uppercase: "uppercase",
188
233
  lowercase: "lowercase",
189
234
  capitalize: "capitalize",
190
235
  "normal-case": "normal-case",
236
+
237
+ // Text decoration
191
238
  underline: "underline",
239
+ overline: "overline",
192
240
  "line-through": "line-through",
193
241
  "no-underline": "no-underline",
242
+
243
+ // Text decoration style
244
+ "decoration-solid": "decoration-solid",
245
+ "decoration-double": "decoration-double",
246
+ "decoration-dotted": "decoration-dotted",
247
+ "decoration-dashed": "decoration-dashed",
248
+ "decoration-wavy": "decoration-wavy",
249
+
250
+ // Text overflow
194
251
  truncate: "truncate",
195
- "cursor-pointer": "cursor:pointer",
252
+ "text-ellipsis": "text-ellipsis",
253
+ "text-clip": "text-clip",
254
+
255
+ // Text wrap
256
+ "text-wrap": "text-wrap",
257
+ "text-nowrap": "text-nowrap",
258
+ "text-balance": "text-balance",
259
+ "text-pretty": "text-pretty",
260
+
261
+ // Whitespace
262
+ "whitespace-normal": "whitespace-normal",
263
+ "whitespace-nowrap": "whitespace-nowrap",
264
+ "whitespace-pre": "whitespace-pre",
265
+ "whitespace-pre-line": "whitespace-pre-line",
266
+ "whitespace-pre-wrap": "whitespace-pre-wrap",
267
+ "whitespace-break-spaces": "whitespace-break-spaces",
268
+
269
+ // Word break
270
+ "break-normal": "break-normal",
271
+ "break-words": "break-words",
272
+ "break-all": "break-all",
273
+ "break-keep": "break-keep",
274
+
275
+ // Hyphens
276
+ "hyphens-none": "hyphens-none",
277
+ "hyphens-manual": "hyphens-manual",
278
+ "hyphens-auto": "hyphens-auto",
279
+
280
+ // List style
281
+ "list-none": "list-none",
282
+ "list-disc": "list-disc",
283
+ "list-decimal": "list-decimal",
284
+ "list-inside": "list-inside",
285
+ "list-outside": "list-outside",
286
+
287
+ // Cursor
288
+ "cursor-auto": "cursor:auto",
196
289
  "cursor-default": "cursor:default",
290
+ "cursor-pointer": "cursor:pointer",
291
+ "cursor-wait": "cursor:wait",
292
+ "cursor-text": "cursor:text",
293
+ "cursor-move": "cursor:move",
197
294
  "cursor-not-allowed": "cursor:not-allowed",
295
+ "cursor-grab": "cursor:grab",
296
+ "cursor-grabbing": "cursor:grabbing",
297
+
298
+ // User select
198
299
  "select-none": "select:none",
199
300
  "select-text": "select:text",
200
301
  "select-all": "select:all",
302
+ "select-auto": "select:auto",
303
+
304
+ // Pointer events
305
+ "pointer-events-none": "pointer-events:none",
306
+ "pointer-events-auto": "pointer-events:auto",
307
+
308
+ // Appearance
309
+ "appearance-none": "appearance:none",
310
+ "appearance-auto": "appearance:auto",
311
+
312
+ // 3D Transforms
313
+ perspective: "perspective",
314
+ "perspective-origin": "perspective-origin",
315
+ "transform-style": "transform-style",
316
+ "backface-visibility": "backface",
317
+ mask: "mask",
318
+ "mask-image": "mask-image",
319
+ "mask-mode": "mask-mode",
320
+ "mask-origin": "mask-origin",
321
+ "mask-position": "mask-position",
322
+ "mask-repeat": "mask-repeat",
323
+ "mask-size": "mask-size",
324
+ "mask-type": "mask-type"
201
325
  };
202
326
 
203
327
  // ============================
@@ -206,7 +330,7 @@ const visualKeywords = {
206
330
 
207
331
  function getSpacing(value, exact) {
208
332
  // Check if it's already an arbitrary value with brackets
209
- if (value.startsWith('[') && value.endsWith(']')) {
333
+ if (value && value.startsWith('[') && value.endsWith(']')) {
210
334
  return value; // Return as-is, don't double-wrap
211
335
  }
212
336
  if (exact) {
@@ -343,6 +467,20 @@ function convertClass(twClass, exact) {
343
467
  return attachExtra({ cat: "visual", val: prefix + "text-size:" + size });
344
468
  }
345
469
 
470
+ // Line height
471
+ const leadingMatch = baseClass.match(/^leading-(\[.+\]|none|tight|snug|normal|relaxed|loose)$/);
472
+ if (leadingMatch) {
473
+ const val = leadingMatch[1];
474
+ return attachExtra({ cat: "visual", val: prefix + "leading:" + (lineHeightScale[val] || val) });
475
+ }
476
+
477
+ // Letter spacing
478
+ const trackingMatch = baseClass.match(/^tracking-(\[.+\]|tighter|tight|normal|wide|wider|widest)$/);
479
+ if (trackingMatch) {
480
+ const val = trackingMatch[1];
481
+ return attachExtra({ cat: "visual", val: prefix + "tracking:" + (letterSpacingScale[val] || val) });
482
+ }
483
+
346
484
  // Background color
347
485
  const bgMatch = baseClass.match(
348
486
  /^bg-((?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)(?:-\d+)?|transparent|current|inherit)$/
@@ -410,7 +548,7 @@ function convertClass(twClass, exact) {
410
548
  const side = marginMatch[1] ? "-" + marginMatch[1] : "";
411
549
  let val = getSpacing(marginMatch[2], exact);
412
550
 
413
- if (isNeg) {
551
+ if (isNeg && val) {
414
552
  if (val.startsWith('[') && val.endsWith(']')) {
415
553
  // Handle arbitrary value: [10px] -> [-10px]
416
554
  const inner = val.slice(1, -1);
@@ -508,14 +646,14 @@ function convertClass(twClass, exact) {
508
646
  if (positionMatch) {
509
647
  const prop = positionMatch[1];
510
648
  let val = positionMatch[2];
511
- // Handle 0 specially
512
- if (val === '0') {
513
- val = 'none';
514
- } else if (val.startsWith('[') && val.endsWith(']')) {
649
+ if (val && val.startsWith('[') && val.endsWith(']')) {
515
650
  // Keep arbitrary values as-is
516
651
  } else if (fractionScale[val]) {
517
652
  // Map fractions to semantic names (1/2 → half, etc.)
518
653
  val = fractionScale[val];
654
+ } else if (val === '0') {
655
+ // Keep 0 as-is for positioning (CSS: top: 0, not top: none)
656
+ val = '0';
519
657
  } else {
520
658
  val = getSpacing(val, exact);
521
659
  }
@@ -530,7 +668,7 @@ function convertClass(twClass, exact) {
530
668
  let val = translateMatch[3];
531
669
 
532
670
  // Map fractions and values
533
- if (val.startsWith('[') && val.endsWith(']')) {
671
+ if (val && val.startsWith('[') && val.endsWith(']')) {
534
672
  // Keep arbitrary values as-is, but handle negative
535
673
  if (isNeg) {
536
674
  const inner = val.slice(1, -1);
@@ -560,6 +698,33 @@ function convertClass(twClass, exact) {
560
698
  return attachExtra({ cat: "layout", val: prefix + "order:" + orderMatch[1] });
561
699
  }
562
700
 
701
+ // Z-index
702
+ const zIndexMatch = baseClass.match(/^-?z-(\d+|auto)$/);
703
+ if (zIndexMatch) {
704
+ const isNeg = baseClass.startsWith("-");
705
+ const val = zIndexMatch[1];
706
+ let zIndexVal = zIndexScale[val] || val;
707
+ if (isNeg) {
708
+ zIndexVal = `-${zIndexVal}`;
709
+ }
710
+ return attachExtra({ cat: "layout", val: prefix + "z:" + zIndexVal });
711
+ }
712
+
713
+ // Flex basis
714
+ const basisMatch = baseClass.match(/^basis-(\[.+\]|\d+\.?\d*|auto|full|1\/2|1\/3|2\/3|1\/4|2\/4|3\/4)$/);
715
+ if (basisMatch) {
716
+ let val = basisMatch[1];
717
+ if (val.startsWith('[') && val.endsWith(']')) {
718
+ // Keep arbitrary values as-is
719
+ } else if (fractionScale[val]) {
720
+ // Map fractions to semantic names (1/2 → half, etc.)
721
+ val = fractionScale[val];
722
+ } else if (val === '0') {
723
+ val = '0';
724
+ }
725
+ return attachExtra({ cat: "layout", val: prefix + "basis:" + val });
726
+ }
727
+
563
728
  // Grid columns
564
729
  const gridColsMatch = baseClass.match(/^grid-cols-(\d+|none)$/);
565
730
  if (gridColsMatch) {
@@ -750,6 +915,138 @@ function convertClass(twClass, exact) {
750
915
  });
751
916
  }
752
917
 
918
+ // Border style
919
+ const borderStyleMatch = baseClass.match(/^border-(solid|dashed|dotted|double|none)$/);
920
+ if (borderStyleMatch) {
921
+ return attachExtra({
922
+ cat: "visual",
923
+ val: prefix + "border-style:" + borderStyleMatch[1],
924
+ });
925
+ }
926
+
927
+ // Filter utilities
928
+ // Blur
929
+ const blurMatch = baseClass.match(/^blur-(0|sm|md|lg|xl|2xl|3xl)$/);
930
+ if (blurMatch) {
931
+ const blurScale = {
932
+ '0': 'none',
933
+ 'sm': 'tiny',
934
+ 'md': 'small',
935
+ 'lg': 'medium',
936
+ 'xl': 'big',
937
+ '2xl': 'giant',
938
+ '3xl': 'vast'
939
+ };
940
+ return attachExtra({
941
+ cat: "visual",
942
+ val: prefix + "blur:" + blurScale[blurMatch[1]],
943
+ });
944
+ }
945
+
946
+ // Brightness
947
+ const brightnessMatch = baseClass.match(/^brightness-(0|50|75|90|95|100|105|110|125|150|200)$/);
948
+ if (brightnessMatch) {
949
+ const brightnessScale = {
950
+ '0': 'dim',
951
+ '50': 'dim',
952
+ '75': 'dark',
953
+ '90': 'dark',
954
+ '95': 'dark',
955
+ '100': 'normal',
956
+ '105': 'bright',
957
+ '110': 'bright',
958
+ '125': 'vivid',
959
+ '150': 'vivid',
960
+ '200': 'vivid'
961
+ };
962
+ return attachExtra({
963
+ cat: "visual",
964
+ val: prefix + "brightness:" + brightnessScale[brightnessMatch[1]],
965
+ });
966
+ }
967
+
968
+ // Contrast
969
+ const contrastMatch = baseClass.match(/^contrast-(0|50|75|100|125|150|200)$/);
970
+ if (contrastMatch) {
971
+ const contrastScale = {
972
+ '0': 'low',
973
+ '50': 'low',
974
+ '75': 'reduced',
975
+ '100': 'normal',
976
+ '125': 'high',
977
+ '150': 'high',
978
+ '200': 'max'
979
+ };
980
+ return attachExtra({
981
+ cat: "visual",
982
+ val: prefix + "contrast:" + contrastScale[contrastMatch[1]],
983
+ });
984
+ }
985
+
986
+ // Grayscale
987
+ const grayscaleMatch = baseClass.match(/^grayscale(0)?$/);
988
+ if (grayscaleMatch) {
989
+ const val = grayscaleMatch[1] === '0' ? 'none' : 'full';
990
+ return attachExtra({
991
+ cat: "visual",
992
+ val: prefix + "grayscale:" + val,
993
+ });
994
+ }
995
+
996
+ // Hue rotate
997
+ const hueRotateMatch = baseClass.match(/^hue-rotate-(0|15|30|60|90|180)$/);
998
+ if (hueRotateMatch) {
999
+ return attachExtra({
1000
+ cat: "visual",
1001
+ val: prefix + "hue-rotate:" + hueRotateMatch[1],
1002
+ });
1003
+ }
1004
+
1005
+ // Invert
1006
+ const invertMatch = baseClass.match(/^invert(0)?$/);
1007
+ if (invertMatch) {
1008
+ const val = invertMatch[1] === '0' ? 'none' : 'full';
1009
+ return attachExtra({
1010
+ cat: "visual",
1011
+ val: prefix + "invert:" + val,
1012
+ });
1013
+ }
1014
+
1015
+ // Saturate
1016
+ const saturateMatch = baseClass.match(/^saturate-(0|50|100|150|200)$/);
1017
+ if (saturateMatch) {
1018
+ const saturateScale = {
1019
+ '0': 'none',
1020
+ '50': 'low',
1021
+ '100': 'normal',
1022
+ '150': 'high',
1023
+ '200': 'vivid'
1024
+ };
1025
+ return attachExtra({
1026
+ cat: "visual",
1027
+ val: prefix + "saturate:" + saturateScale[saturateMatch[1]],
1028
+ });
1029
+ }
1030
+
1031
+ // Sepia
1032
+ const sepiaMatch = baseClass.match(/^sepia(0)?$/);
1033
+ if (sepiaMatch) {
1034
+ const val = sepiaMatch[1] === '0' ? 'none' : 'full';
1035
+ return attachExtra({
1036
+ cat: "visual",
1037
+ val: prefix + "sepia:" + val,
1038
+ });
1039
+ }
1040
+
1041
+ // Animation utilities
1042
+ const animateMatch = baseClass.match(/^animate-(none|spin|ping|pulse|bounce)$/);
1043
+ if (animateMatch) {
1044
+ return attachExtra({
1045
+ cat: "visual",
1046
+ val: prefix + "animate:" + animateMatch[1],
1047
+ });
1048
+ }
1049
+
753
1050
  return null;
754
1051
  }
755
1052
 
@@ -3,15 +3,17 @@
3
3
  * One-time compilation of CSS from source files
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
6
+ import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { defaultConfig, mergeConfig } from '../../config/defaults.js';
9
9
  import { parseSource } from '../../compiler/parser.js';
10
- import { tokenizeAll } from '../../compiler/tokenizer.js';
10
+ import { tokenizeAll, tokenizeAllWithBatching } from '../../compiler/tokenizer.js';
11
11
  import { generateCSS, minifyCSS } from '../../compiler/generators/css.js';
12
12
  import { generateAIContext } from '../../compiler/generators/ai-context.js';
13
13
  import { generateTypeScript } from '../../compiler/generators/typescript.js';
14
14
  import logger from '../../utils/logger.js';
15
+ import { validateThemeValue, getMemoryUsage } from '../../utils/common.js';
16
+ import { readMultipleFilesWithTimeout } from '../../utils/node-io.js';
15
17
 
16
18
  /**
17
19
  * Find files matching content patterns
@@ -118,31 +120,67 @@ export async function build(options = {}) {
118
120
  }
119
121
 
120
122
  logger.info(`Found ${files.length} source files`);
121
-
122
- // Parse all files
123
+
124
+ // Parse all files with timeout protection
123
125
  const allTokens = {
124
126
  layout: new Set(),
125
127
  space: new Set(),
126
128
  visual: new Set()
127
129
  };
128
-
129
- for (const filePath of files) {
130
+
131
+ let failedFiles = 0;
132
+ const fileReadResults = await readMultipleFilesWithTimeout(files, 5000);
133
+
134
+ for (const { path: filePath, content, error } of fileReadResults) {
135
+ if (error) {
136
+ logger.warn(`Skipping ${filePath}: ${error.message}`);
137
+ failedFiles++;
138
+ continue;
139
+ }
140
+
130
141
  try {
131
- const content = readFileSync(filePath, 'utf-8');
132
142
  const parsed = parseSource(content);
133
-
143
+
134
144
  parsed.layout.forEach(t => allTokens.layout.add(t));
135
145
  parsed.space.forEach(t => allTokens.space.add(t));
136
146
  parsed.visual.forEach(t => allTokens.visual.add(t));
137
147
  } catch (e) {
138
- logger.warn(`Could not parse ${filePath}`);
148
+ logger.warn(`Could not parse ${filePath}: ${e.message}`);
149
+ failedFiles++;
139
150
  }
140
151
  }
141
-
152
+
153
+ if (failedFiles > 0) {
154
+ logger.warn(`${failedFiles} file(s) failed to process`);
155
+ }
156
+
157
+ // Calculate total token count
158
+ const totalTokens = allTokens.layout.size + allTokens.space.size + allTokens.visual.size;
159
+ logger.info(`Found ${totalTokens} unique token values`);
160
+
161
+ // Check memory usage and decide whether to use batch processing
162
+ const currentMemory = getMemoryUsage();
163
+ const useBatching = totalTokens > 10000 || currentMemory > 200;
164
+
142
165
  // Tokenize
143
- const tokens = tokenizeAll(allTokens);
144
-
145
- logger.info(`Extracted ${tokens.length} unique tokens`);
166
+ let tokens;
167
+ if (useBatching) {
168
+ logger.info('Using batch processing for memory protection');
169
+ tokens = await tokenizeAllWithBatching(allTokens, 1000);
170
+ } else {
171
+ tokens = tokenizeAll(allTokens);
172
+ }
173
+
174
+ logger.info(`Generated ${tokens.length} tokens`);
175
+
176
+ // Check for invalid tokens
177
+ const invalidTokens = tokens.filter(token => token.error);
178
+ if (invalidTokens.length > 0) {
179
+ logger.warn(`${invalidTokens.length} error(s) found in source:`);
180
+ for (const token of invalidTokens) {
181
+ logger.warn(` • ${token.raw} (${token.attrType}): ${token.error}`);
182
+ }
183
+ }
146
184
 
147
185
  // Generate CSS
148
186
  let css = generateCSS(tokens, config);