pulse-js-framework 1.7.22 → 1.7.24

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/cli/release.js CHANGED
@@ -508,6 +508,46 @@ function buildCommitMessage(newVersion, title, changes) {
508
508
  return message;
509
509
  }
510
510
 
511
+ /**
512
+ * Verify that a git tag exists locally
513
+ */
514
+ function verifyTagExists(version) {
515
+ try {
516
+ const tags = execSync('git tag -l', { cwd: root, encoding: 'utf-8' });
517
+ return tags.split('\n').includes(`v${version}`);
518
+ } catch {
519
+ return false;
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Verify that a git tag exists on remote
525
+ */
526
+ function verifyTagOnRemote(version) {
527
+ try {
528
+ const remoteTags = execSync('git ls-remote --tags origin', { cwd: root, encoding: 'utf-8' });
529
+ return remoteTags.includes(`refs/tags/v${version}`);
530
+ } catch {
531
+ return false;
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Execute a git command with error handling
537
+ */
538
+ function execGitCommand(command, description) {
539
+ log.info(` Running: ${description}...`);
540
+ try {
541
+ execSync(command, { cwd: root, stdio: 'inherit' });
542
+ return { success: true };
543
+ } catch (error) {
544
+ log.error(` Failed: ${description}`);
545
+ log.error(` Command: ${command}`);
546
+ log.error(` Error: ${error.message}`);
547
+ return { success: false, error };
548
+ }
549
+ }
550
+
511
551
  /**
512
552
  * Execute git commands
513
553
  */
@@ -521,33 +561,142 @@ function gitCommitTagPush(newVersion, title, changes, dryRun = false) {
521
561
  log.info(` [dry-run] git tag -a v${newVersion} -m "Release v${newVersion}"`);
522
562
  log.info(' [dry-run] git push');
523
563
  log.info(' [dry-run] git push --tags');
524
- return;
564
+ return { success: true };
525
565
  }
526
566
 
527
567
  // git add
528
- log.info(' Running: git add -A...');
529
- execSync('git add -A', { cwd: root, stdio: 'inherit' });
568
+ let result = execGitCommand('git add -A', 'git add -A');
569
+ if (!result.success) {
570
+ return { success: false, stage: 'add', error: result.error };
571
+ }
530
572
 
531
573
  // git commit using temp file for cross-platform compatibility
532
- log.info(' Running: git commit...');
533
574
  const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
534
575
  writeFileSync(tempFile, commitMessage, 'utf-8');
535
576
  try {
536
- execSync(`git commit -F "${tempFile}"`, { cwd: root, stdio: 'inherit' });
577
+ result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
578
+ if (!result.success) {
579
+ return { success: false, stage: 'commit', error: result.error };
580
+ }
537
581
  } finally {
538
- unlinkSync(tempFile);
582
+ try {
583
+ unlinkSync(tempFile);
584
+ } catch {
585
+ // Ignore cleanup errors
586
+ }
539
587
  }
540
588
 
541
589
  // git tag
542
- log.info(` Running: git tag v${newVersion}...`);
543
- execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
590
+ result = execGitCommand(
591
+ `git tag -a v${newVersion} -m "Release v${newVersion}"`,
592
+ `git tag v${newVersion}`
593
+ );
594
+ if (!result.success) {
595
+ log.error('');
596
+ log.error('Tag creation failed. The commit was created but not tagged.');
597
+ log.error('You can manually create the tag with:');
598
+ log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
599
+ return { success: false, stage: 'tag', error: result.error };
600
+ }
601
+
602
+ // Verify tag was created
603
+ if (!verifyTagExists(newVersion)) {
604
+ log.error('');
605
+ log.error('Tag creation appeared to succeed but tag not found locally.');
606
+ log.error('You can manually create the tag with:');
607
+ log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
608
+ return { success: false, stage: 'tag-verify', error: new Error('Tag not found after creation') };
609
+ }
610
+ log.info(` Verified: tag v${newVersion} exists locally`);
544
611
 
545
612
  // git push
546
- log.info(' Running: git push...');
547
- execSync('git push', { cwd: root, stdio: 'inherit' });
613
+ result = execGitCommand('git push', 'git push');
614
+ if (!result.success) {
615
+ log.error('');
616
+ log.error('Push failed. Commit and tag were created locally.');
617
+ log.error('You can manually push with:');
618
+ log.error(' git push && git push --tags');
619
+ return { success: false, stage: 'push', error: result.error };
620
+ }
621
+
622
+ // git push --tags
623
+ result = execGitCommand('git push --tags', 'git push --tags');
624
+ if (!result.success) {
625
+ log.error('');
626
+ log.error('Tag push failed. The tag exists locally but not on remote.');
627
+ log.error('You can manually push tags with:');
628
+ log.error(' git push --tags');
629
+ return { success: false, stage: 'push-tags', error: result.error };
630
+ }
631
+
632
+ // Verify tag was pushed to remote
633
+ if (!verifyTagOnRemote(newVersion)) {
634
+ log.warn('');
635
+ log.warn('Tag push appeared to succeed but tag not found on remote.');
636
+ log.warn('You may need to manually verify or push with:');
637
+ log.warn(' git push --tags');
638
+ } else {
639
+ log.info(` Verified: tag v${newVersion} exists on remote`);
640
+ }
548
641
 
549
- log.info(' Running: git push --tags...');
550
- execSync('git push --tags', { cwd: root, stdio: 'inherit' });
642
+ return { success: true };
643
+ }
644
+
645
+ /**
646
+ * Execute git commands without pushing (--no-push mode)
647
+ */
648
+ function gitCommitTagNoPush(newVersion, title, changes) {
649
+ const commitMessage = buildCommitMessage(newVersion, title, changes);
650
+
651
+ // git add
652
+ let result = execGitCommand('git add -A', 'git add -A');
653
+ if (!result.success) {
654
+ return { success: false, stage: 'add', error: result.error };
655
+ }
656
+
657
+ // git commit using temp file for cross-platform compatibility
658
+ const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
659
+ writeFileSync(tempFile, commitMessage, 'utf-8');
660
+ try {
661
+ result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
662
+ if (!result.success) {
663
+ return { success: false, stage: 'commit', error: result.error };
664
+ }
665
+ } finally {
666
+ try {
667
+ unlinkSync(tempFile);
668
+ } catch {
669
+ // Ignore cleanup errors
670
+ }
671
+ }
672
+
673
+ // git tag
674
+ result = execGitCommand(
675
+ `git tag -a v${newVersion} -m "Release v${newVersion}"`,
676
+ `git tag v${newVersion}`
677
+ );
678
+ if (!result.success) {
679
+ log.error('');
680
+ log.error('Tag creation failed. The commit was created but not tagged.');
681
+ log.error('You can manually create the tag with:');
682
+ log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
683
+ return { success: false, stage: 'tag', error: result.error };
684
+ }
685
+
686
+ // Verify tag was created
687
+ if (!verifyTagExists(newVersion)) {
688
+ log.error('');
689
+ log.error('Tag creation appeared to succeed but tag not found locally.');
690
+ log.error('You can manually create the tag with:');
691
+ log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
692
+ return { success: false, stage: 'tag-verify', error: new Error('Tag not found after creation') };
693
+ }
694
+
695
+ log.info(` Verified: tag v${newVersion} exists locally`);
696
+ log.info(' Created commit and tag (--no-push specified)');
697
+ log.info(' To push later, run: git push && git push --tags');
698
+
699
+ return { success: true };
551
700
  }
552
701
 
553
702
  /**
@@ -811,25 +960,24 @@ export async function runRelease(args) {
811
960
  log.info('');
812
961
  log.info('Git operations...');
813
962
 
963
+ let gitResult = { success: true };
964
+
814
965
  if (!dryRun) {
815
966
  if (noPush) {
816
967
  // Only commit and tag, no push
817
- const commitMessage = buildCommitMessage(newVersion, title, changes);
818
- execSync('git add -A', { cwd: root, stdio: 'inherit' });
819
- const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
820
- writeFileSync(tempFile, commitMessage, 'utf-8');
821
- try {
822
- execSync(`git commit -F "${tempFile}"`, { cwd: root, stdio: 'inherit' });
823
- } finally {
824
- unlinkSync(tempFile);
825
- }
826
- execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
827
- log.info(' Created commit and tag (--no-push specified)');
968
+ gitResult = gitCommitTagNoPush(newVersion, title, changes);
828
969
  } else {
829
- gitCommitTagPush(newVersion, title, changes, false);
970
+ gitResult = gitCommitTagPush(newVersion, title, changes, false);
830
971
  }
831
972
  } else {
832
- gitCommitTagPush(newVersion, title, changes, true);
973
+ gitResult = gitCommitTagPush(newVersion, title, changes, true);
974
+ }
975
+
976
+ if (!gitResult.success) {
977
+ log.error('');
978
+ log.error(`Release failed at stage: ${gitResult.stage}`);
979
+ log.error('Please fix the issue and retry, or complete the release manually.');
980
+ process.exit(1);
833
981
  }
834
982
 
835
983
  log.info('');
package/compiler/lexer.js CHANGED
@@ -99,6 +99,7 @@ export const TokenType = {
99
99
  // Identifiers and selectors
100
100
  IDENT: 'IDENT',
101
101
  SELECTOR: 'SELECTOR', // CSS selector like .class, #id, tag.class#id
102
+ HEX_COLOR: 'HEX_COLOR', // CSS hex color like #fff, #667eea
102
103
 
103
104
  // Special
104
105
  INTERPOLATION_START: 'INTERPOLATION_START', // {
@@ -409,8 +410,9 @@ export class Lexer {
409
410
 
410
411
  /**
411
412
  * Read an identifier or keyword
413
+ * @param {boolean} forceIdent - If true, always return IDENT type (ignore keywords)
412
414
  */
413
- readIdentifier() {
415
+ readIdentifier(forceIdent = false) {
414
416
  const startLine = this.line;
415
417
  const startColumn = this.column;
416
418
  let value = '';
@@ -419,7 +421,7 @@ export class Lexer {
419
421
  value += this.advance();
420
422
  }
421
423
 
422
- const type = KEYWORDS[value] || TokenType.IDENT;
424
+ const type = forceIdent ? TokenType.IDENT : (KEYWORDS[value] || TokenType.IDENT);
423
425
  return new Token(type, value, startLine, startColumn);
424
426
  }
425
427
 
@@ -694,8 +696,13 @@ export class Lexer {
694
696
  continue;
695
697
  }
696
698
 
697
- // Hash outside selector
699
+ // Hash outside selector - check for hex color in style context
698
700
  if (char === '#') {
701
+ // In style context, check if this is a hex color
702
+ if (this.isStyleContext() && /[0-9a-fA-F]/.test(this.peek())) {
703
+ this.tokens.push(this.readHexColor());
704
+ continue;
705
+ }
699
706
  this.advance();
700
707
  this.tokens.push(new Token(TokenType.HASH, '#', startLine, startColumn));
701
708
  continue;
@@ -704,14 +711,29 @@ export class Lexer {
704
711
  // Identifiers, keywords, and selectors
705
712
  if (/[a-zA-Z_$]/.test(char)) {
706
713
  // First check if this is a keyword - keywords take precedence
714
+ // BUT in style context, most keywords should be treated as identifiers
715
+ // (e.g., 'style' in 'transform-style', 'in' in 'ease-in-out')
707
716
  const word = this.peekWord();
708
- if (KEYWORDS[word]) {
709
- this.tokens.push(this.readIdentifier());
717
+ const inStyle = this.isStyleContext();
718
+
719
+ // Keywords that should NEVER be treated as keywords in style context
720
+ // These appear in CSS property names and values
721
+ const cssReservedWords = new Set([
722
+ 'style', 'in', 'from', 'to', 'if', 'else', 'for', 'as', 'of',
723
+ 'true', 'false', 'null', 'export', 'import'
724
+ ]);
725
+
726
+ const shouldBeKeyword = KEYWORDS[word] && (!inStyle || !cssReservedWords.has(word));
727
+ // Force identifier if in style context and word is a CSS reserved word
728
+ const forceIdent = inStyle && cssReservedWords.has(word);
729
+
730
+ if (shouldBeKeyword) {
731
+ this.tokens.push(this.readIdentifier(false));
710
732
  } else if (this.isViewContext() && this.couldBeSelector()) {
711
733
  // Only treat as selector if not a keyword
712
734
  this.tokens.push(this.readSelector());
713
735
  } else {
714
- this.tokens.push(this.readIdentifier());
736
+ this.tokens.push(this.readIdentifier(forceIdent));
715
737
  }
716
738
  continue;
717
739
  }
@@ -725,6 +747,43 @@ export class Lexer {
725
747
  return this.tokens;
726
748
  }
727
749
 
750
+ /**
751
+ * Check if we're in a style context (inside style block)
752
+ */
753
+ isStyleContext() {
754
+ // Look back through tokens for 'style' keyword
755
+ for (let i = this.tokens.length - 1; i >= 0; i--) {
756
+ const token = this.tokens[i];
757
+ if (token.type === TokenType.STYLE) {
758
+ return true;
759
+ }
760
+ if (token.type === TokenType.STATE ||
761
+ token.type === TokenType.VIEW ||
762
+ token.type === TokenType.ACTIONS ||
763
+ token.type === TokenType.ROUTER ||
764
+ token.type === TokenType.STORE) {
765
+ return false;
766
+ }
767
+ }
768
+ return false;
769
+ }
770
+
771
+ /**
772
+ * Read a CSS hex color (#fff, #ffffff, #667eea, etc.)
773
+ */
774
+ readHexColor() {
775
+ const startLine = this.line;
776
+ const startColumn = this.column;
777
+ let value = this.advance(); // #
778
+
779
+ // Read hex characters (0-9, a-f, A-F)
780
+ while (!this.isEOF() && /[0-9a-fA-F]/.test(this.current())) {
781
+ value += this.advance();
782
+ }
783
+
784
+ return new Token(TokenType.HEX_COLOR, value, startLine, startColumn);
785
+ }
786
+
728
787
  /**
729
788
  * Check if we're in a view context where selectors are expected
730
789
  */
@@ -1564,35 +1564,70 @@ export class Parser {
1564
1564
  const selectorParts = [];
1565
1565
  let lastLine = this.current()?.line;
1566
1566
  let lastToken = null;
1567
+ let inAtRule = false; // Track if we're inside an @-rule like @media
1568
+ let inParens = 0; // Track parenthesis depth
1567
1569
 
1568
1570
  while (!this.is(TokenType.LBRACE) && !this.is(TokenType.EOF)) {
1569
1571
  const token = this.advance();
1570
1572
  const currentLine = token.line;
1571
1573
  const tokenValue = String(token.value);
1572
1574
 
1575
+ // Track @-rules (media queries, keyframes, etc.)
1576
+ if (tokenValue === '@') {
1577
+ inAtRule = true;
1578
+ }
1579
+
1580
+ // Track parenthesis depth for media queries
1581
+ if (tokenValue === '(') inParens++;
1582
+ if (tokenValue === ')') inParens--;
1583
+
1573
1584
  // Determine if we need a space before this token
1574
1585
  if (selectorParts.length > 0 && currentLine === lastLine) {
1575
1586
  const lastPart = selectorParts[selectorParts.length - 1];
1576
1587
 
1577
1588
  // Don't add space after these (they attach to what follows)
1578
- const noSpaceAfter = ['.', '#', ':', '[', '(', '>', '+', '~', '-'];
1589
+ const noSpaceAfter = new Set(['.', '#', '[', '(', '>', '+', '~', '-', '@', ':']);
1590
+
1579
1591
  // Don't add space before these (they attach to what precedes)
1580
- const noSpaceBefore = [':', ']', ')', ',', '.', '#', '-'];
1592
+ // In @media queries inside parens: "max-width:" should not have space before ":"
1593
+ const noSpaceBefore = new Set([']', ')', ',', '.', '#', '-', ':']);
1594
+
1595
+ // CSS units that should attach to numbers (no space before)
1596
+ const cssUnits = new Set(['px', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax', '%', 'fr', 's', 'ms', 'deg', 'rad', 'turn', 'grad', 'ex', 'ch', 'pt', 'pc', 'in', 'cm', 'mm', 'dvh', 'dvw', 'svh', 'svw', 'lvh', 'lvw']);
1581
1597
 
1582
1598
  // Special case: . or # after an identifier needs space (descendant selector)
1583
1599
  // e.g., ".school .date" - need space between "school" and "."
1584
1600
  const isDescendantSelector = (tokenValue === '.' || tokenValue === '#') &&
1585
- lastToken?.type === TokenType.IDENT;
1586
-
1587
- // Special case: hyphenated class names like .job-title
1588
- // Don't add space if current token is '-' and last token was IDENT
1601
+ lastToken?.type === TokenType.IDENT &&
1602
+ !inAtRule; // Don't add space in @media selectors
1603
+
1604
+ // Special case: hyphenated class/id names like .job-title, .card-3d, max-width
1605
+ // Check if we're continuing a class/id name - the last part should end with alphanumeric
1606
+ // that was started by . or # (no space in between)
1607
+ const lastPartJoined = selectorParts.join('');
1608
+ // Check if we're in the middle of a class/id name (last char is alphanumeric or -)
1609
+ // AND there's a . or # that started this name (not separated by space)
1610
+ const lastSegmentMatch = lastPartJoined.match(/[.#]([a-zA-Z0-9_-]*)$/);
1611
+ const inClassName = lastSegmentMatch && lastSegmentMatch[1].length > 0;
1612
+
1613
+ // Don't add space if current token is '-' and last token was IDENT or NUMBER
1589
1614
  // Or if last token was '-' (the next token should attach to it)
1590
- const isHyphenatedIdent = (tokenValue === '-' && lastToken?.type === TokenType.IDENT) ||
1591
- (lastToken?.type === TokenType.MINUS);
1592
-
1593
- const needsSpace = !noSpaceAfter.includes(lastPart) &&
1594
- !noSpaceBefore.includes(tokenValue) &&
1595
- !isHyphenatedIdent ||
1615
+ // Also handle .card-3d where we have NUMBER followed by IDENT (but only if in class name context)
1616
+ const isHyphenatedIdent = (tokenValue === '-' && (lastToken?.type === TokenType.IDENT || lastToken?.type === TokenType.NUMBER)) ||
1617
+ (lastToken?.type === TokenType.MINUS) ||
1618
+ (inClassName && lastToken?.type === TokenType.NUMBER && token.type === TokenType.IDENT);
1619
+
1620
+ // Special case: CSS units after numbers (768px, 1.5em)
1621
+ const isUnitAfterNumber = cssUnits.has(tokenValue) && lastToken?.type === TokenType.NUMBER;
1622
+
1623
+ // Special case: @-rule keywords (media, keyframes, etc.) should attach to @
1624
+ const isAtRuleKeyword = lastPart === '@' && /^[a-zA-Z]/.test(tokenValue);
1625
+
1626
+ const needsSpace = !noSpaceAfter.has(lastPart) &&
1627
+ !noSpaceBefore.has(tokenValue) &&
1628
+ !isHyphenatedIdent &&
1629
+ !isUnitAfterNumber &&
1630
+ !isAtRuleKeyword ||
1596
1631
  isDescendantSelector;
1597
1632
 
1598
1633
  if (needsSpace) {
@@ -1677,27 +1712,52 @@ export class Parser {
1677
1712
 
1678
1713
  /**
1679
1714
  * Parse style property
1715
+ * Handles CSS property names (including custom properties like --var-name)
1716
+ * and complex CSS values with proper spacing
1680
1717
  */
1681
1718
  parseStyleProperty() {
1719
+ // Parse property name (including custom properties with --)
1682
1720
  let name = '';
1721
+ let nameTokens = [];
1683
1722
  while (!this.is(TokenType.COLON) && !this.is(TokenType.EOF)) {
1684
- name += this.advance().value;
1723
+ nameTokens.push(this.advance());
1685
1724
  }
1686
- name = name.trim();
1725
+ // Join name tokens without spaces (property names don't have spaces)
1726
+ name = nameTokens.map(t => t.value).join('').trim();
1687
1727
 
1688
1728
  this.expect(TokenType.COLON);
1689
1729
 
1690
- // Tokens that should not have space after them in CSS values
1691
- const noSpaceAfter = new Set(['#', '(', '.', '/', 'rgba', 'rgb', 'hsl', 'hsla', 'var', 'calc', 'url', 'linear-gradient', 'radial-gradient']);
1692
- // Tokens that should not have space before them (units and punctuation)
1693
- const noSpaceBefore = new Set([')', ',', '%', 'px', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax', 'fr', 's', 'ms', '(', 'deg', 'rad', 'turn', 'grad', 'ex', 'ch', 'pt', 'pc', 'in', 'cm', 'mm']);
1694
-
1695
- let value = '';
1696
- let lastTokenValue = '';
1730
+ // CSS functions that should not have space before (
1731
+ const cssFunctions = new Set([
1732
+ 'rgba', 'rgb', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklch', 'oklab',
1733
+ 'var', 'calc', 'min', 'max', 'clamp', 'url', 'attr', 'env', 'counter', 'counters',
1734
+ 'linear-gradient', 'radial-gradient', 'conic-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient',
1735
+ 'translate', 'translateX', 'translateY', 'translateZ', 'translate3d',
1736
+ 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d',
1737
+ 'scale', 'scaleX', 'scaleY', 'scaleZ', 'scale3d',
1738
+ 'skew', 'skewX', 'skewY', 'matrix', 'matrix3d', 'perspective',
1739
+ 'cubic-bezier', 'steps', 'drop-shadow', 'blur', 'brightness', 'contrast',
1740
+ 'grayscale', 'hue-rotate', 'invert', 'opacity', 'saturate', 'sepia',
1741
+ 'minmax', 'repeat', 'fit-content', 'image', 'element', 'cross-fade',
1742
+ 'color-mix', 'light-dark'
1743
+ ]);
1744
+
1745
+ // CSS units that should attach to preceding number (no space before)
1746
+ const cssUnits = new Set([
1747
+ '%', 'px', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax', 'dvh', 'dvw', 'svh', 'svw', 'lvh', 'lvw',
1748
+ 'fr', 's', 'ms', 'deg', 'rad', 'turn', 'grad',
1749
+ 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh',
1750
+ 'pt', 'pc', 'in', 'cm', 'mm', 'Q',
1751
+ 'dpi', 'dpcm', 'dppx', 'x'
1752
+ ]);
1753
+
1754
+ // Tokens that should not have space before them
1755
+ const noSpaceBefore = new Set([')', ',', '(', ';']);
1756
+ cssUnits.forEach(u => noSpaceBefore.add(u));
1757
+
1758
+ // Collect value tokens
1759
+ let valueTokens = [];
1697
1760
  let lastTokenLine = this.current()?.line || 0;
1698
- let afterHash = false; // Track if we're collecting a hex color
1699
- let hexColorLength = 0; // Track hex color length (max 8 for RRGGBBAA)
1700
- let inCssVar = false; // Track if we're inside var(--...)
1701
1761
 
1702
1762
  while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
1703
1763
  const currentToken = this.current();
@@ -1707,64 +1767,165 @@ export class Parser {
1707
1767
  if (this.isPropertyStart() || this.isNestedRule()) {
1708
1768
  break;
1709
1769
  }
1770
+ lastTokenLine = currentToken.line;
1710
1771
  }
1711
- const token = this.advance();
1712
- // Use raw value if available to preserve original representation
1713
- // This is important for numbers that might be parsed as scientific notation
1714
- let tokenValue = token.raw || String(token.value);
1715
1772
 
1716
- // Track CSS var() context - no spaces in var(--name)
1717
- if (lastTokenValue === 'var' && tokenValue === '(') {
1773
+ valueTokens.push(this.advance());
1774
+ }
1775
+
1776
+ // Build value string with proper spacing
1777
+ let value = '';
1778
+ let inHexColor = false;
1779
+ let hexLength = 0;
1780
+ let parenDepth = 0;
1781
+ let inCssVar = false;
1782
+ let inCalc = false; // Track if we're inside calc(), min(), max(), clamp() where operators need spaces
1783
+ let calcDepth = 0; // Track nested calc depth
1784
+
1785
+ // Functions where arithmetic operators need spaces
1786
+ const mathFunctions = new Set(['calc', 'min', 'max', 'clamp']);
1787
+
1788
+ // Helper to check if a string is valid hex
1789
+ const isValidHex = (str) => /^[0-9a-fA-F]+$/.test(String(str));
1790
+
1791
+ for (let i = 0; i < valueTokens.length; i++) {
1792
+ const token = valueTokens[i];
1793
+ const tokenValue = token.raw || String(token.value);
1794
+ const prevToken = i > 0 ? valueTokens[i - 1] : null;
1795
+ const prevValue = prevToken ? (prevToken.raw || String(prevToken.value)) : '';
1796
+
1797
+ // Track parenthesis depth
1798
+ if (tokenValue === '(') parenDepth++;
1799
+ if (tokenValue === ')') parenDepth--;
1800
+
1801
+ // Track CSS var() context
1802
+ if (prevValue === 'var' && tokenValue === '(') {
1718
1803
  inCssVar = true;
1719
1804
  } else if (inCssVar && tokenValue === ')') {
1720
1805
  inCssVar = false;
1721
1806
  }
1722
1807
 
1723
- // For hex colors (#abc123), collect tokens without spacing after #
1724
- // Hex colors are 3, 4, 6, or 8 characters long
1725
- if (tokenValue === '#') {
1726
- afterHash = true;
1727
- hexColorLength = 0;
1728
- } else if (afterHash) {
1729
- // Check if this token is a valid hex color continuation
1808
+ // Track calc/min/max/clamp context - operators need spaces in these
1809
+ if (mathFunctions.has(prevValue) && tokenValue === '(') {
1810
+ inCalc = true;
1811
+ calcDepth = parenDepth;
1812
+ } else if (inCalc && tokenValue === ')' && parenDepth < calcDepth) {
1813
+ inCalc = false;
1814
+ }
1815
+
1816
+ // Handle HEX_COLOR token (from lexer) - it's already a complete hex color
1817
+ if (token.type === TokenType.HEX_COLOR) {
1818
+ // HEX_COLOR is already complete, no tracking needed
1819
+ inHexColor = false;
1820
+ }
1821
+ // Track hex colors for legacy cases - look for # followed by hex digits
1822
+ // Handle cases like #667eea being tokenized as # 667 eea
1823
+ // Valid hex colors are 3, 4, 6, or 8 chars long
1824
+ else if (tokenValue === '#') {
1825
+ inHexColor = true;
1826
+ hexLength = 0;
1827
+ } else if (inHexColor) {
1828
+ // Check if this token could be part of hex color
1829
+ // Numbers and identifiers that are valid hex chars continue the color
1730
1830
  const tokenStr = String(tokenValue);
1731
- const isHexChar = /^[0-9a-fA-F]+$/.test(tokenStr);
1732
- if (isHexChar) {
1733
- const newLength = hexColorLength + tokenStr.length;
1734
- // Valid hex color lengths are 3, 4, 6, or 8
1735
- // If adding this token would exceed a valid length, stop
1736
- if (hexColorLength >= 6 || newLength > 8) {
1737
- afterHash = false;
1738
- } else {
1739
- hexColorLength = newLength;
1831
+ if (isValidHex(tokenStr) && hexLength + tokenStr.length <= 8) {
1832
+ hexLength += tokenStr.length;
1833
+
1834
+ // Check if we should stop collecting hex color now
1835
+ // Stop if: we have 6+ chars, OR the next token is likely a CSS value (%, px, etc.)
1836
+ const nextToken = valueTokens[i + 1];
1837
+ const nextValue = nextToken ? String(nextToken.raw || nextToken.value) : '';
1838
+
1839
+ // CSS units/symbols that indicate the hex color is complete
1840
+ const cssValueIndicators = new Set(['%', 'px', 'em', 'rem', 'vh', 'vw', ',', ')', ' ', '']);
1841
+
1842
+ // End hex color if:
1843
+ // - We've reached 6 or 8 chars (complete hex)
1844
+ // - Next token is a CSS unit/punctuation (like %, px, comma, paren)
1845
+ // - Next token is empty (end of value)
1846
+ if (hexLength >= 6 || cssValueIndicators.has(nextValue) || nextToken?.type === TokenType.PERCENT || nextToken?.type === TokenType.COMMA || nextToken?.type === TokenType.RPAREN) {
1847
+ inHexColor = false; // Done collecting hex color
1740
1848
  }
1741
1849
  } else {
1742
- afterHash = false;
1850
+ // This token is not part of hex, end hex color collection
1851
+ inHexColor = false;
1743
1852
  }
1744
1853
  }
1745
1854
 
1746
- // Add space before this token unless:
1747
- // - It's the first token
1748
- // - Last token was in noSpaceAfter
1749
- // - This token is in noSpaceBefore
1750
- // - We're collecting a hex color (afterHash is true)
1751
- // - We're inside var(--...) and this is part of the variable name
1752
- // - Last was '-' and current is an identifier (hyphenated name)
1753
- const skipSpace = noSpaceAfter.has(String(lastTokenValue)) ||
1754
- noSpaceBefore.has(tokenValue) ||
1755
- afterHash ||
1756
- inCssVar ||
1757
- (lastTokenValue === '-' || lastTokenValue === '--') ||
1758
- (tokenValue === '-' && /^[a-zA-Z]/.test(String(this.current()?.value || '')));
1759
-
1760
- if (value.length > 0 && !skipSpace) {
1855
+ // Determine if we need space before this token
1856
+ let needsSpace = value.length > 0;
1857
+
1858
+ if (needsSpace) {
1859
+ // No space after # (hex color start)
1860
+ if (prevValue === '#') {
1861
+ needsSpace = false;
1862
+ }
1863
+ // No space after these
1864
+ else if (prevValue === '(' || prevValue === '.' || prevValue === '/' || prevValue === '@') {
1865
+ needsSpace = false;
1866
+ }
1867
+ // No space after ! for !important
1868
+ else if (prevValue === '!' && tokenValue === 'important') {
1869
+ needsSpace = false;
1870
+ }
1871
+ // No space after CSS functions before (
1872
+ else if (cssFunctions.has(prevValue) && tokenValue === '(') {
1873
+ needsSpace = false;
1874
+ }
1875
+ // No space before these
1876
+ else if (noSpaceBefore.has(tokenValue)) {
1877
+ needsSpace = false;
1878
+ }
1879
+ // No space in hex colors (continuing after #)
1880
+ else if (inHexColor && hexLength > 0) {
1881
+ needsSpace = false;
1882
+ }
1883
+ // No space in CSS var() content
1884
+ else if (inCssVar) {
1885
+ needsSpace = false;
1886
+ }
1887
+ // No space for hyphenated identifiers (ease-in-out, sans-serif, -apple-system)
1888
+ // BUT in calc(), min(), max(), clamp() - operators need spaces around them
1889
+ // Note: Some CSS keywords like 'in' are also Pulse keywords, so check token value too
1890
+ // Check if current token is '-' and should attach to previous identifier-like token
1891
+ else if (tokenValue === '-' && !inCalc && (prevToken?.type === TokenType.IDENT || prevToken?.type === TokenType.NUMBER || /^[a-zA-Z]/.test(prevValue))) {
1892
+ needsSpace = false;
1893
+ }
1894
+ // Check if current token follows a '-' (either prevValue is '-' or value ends with '-')
1895
+ // Include keywords that might appear in CSS values (in, from, to, etc.)
1896
+ // BUT in calc(), don't attach numbers to '-' (keep space for operators)
1897
+ else if (!inCalc && (prevValue === '-' || value.endsWith('-')) && (token.type === TokenType.IDENT || token.type === TokenType.NUMBER || /^[a-zA-Z]/.test(tokenValue))) {
1898
+ needsSpace = false;
1899
+ }
1900
+ // No space for -- (CSS custom property reference)
1901
+ else if (prevValue === '-' && tokenValue === '-') {
1902
+ needsSpace = false;
1903
+ }
1904
+ else if (prevValue === '--' || value.endsWith('--')) {
1905
+ needsSpace = false;
1906
+ }
1907
+ // CSS unit after number
1908
+ else if (cssUnits.has(tokenValue) && prevToken?.type === TokenType.NUMBER) {
1909
+ needsSpace = false;
1910
+ }
1911
+ // Handle cases like preserve-3d where identifier follows number in hyphenated value
1912
+ // Check if we're continuing a hyphenated identifier (value ends with number after hyphen)
1913
+ else if (token.type === TokenType.IDENT && prevToken?.type === TokenType.NUMBER) {
1914
+ // Check if the value looks like it's a hyphenated pattern: word-NUM + IDENT (e.g., preserve-3 + d, card-3 + d)
1915
+ const hyphenNumberPattern = /-\d+$/;
1916
+ if (hyphenNumberPattern.test(value)) {
1917
+ needsSpace = false;
1918
+ }
1919
+ }
1920
+ }
1921
+
1922
+ if (needsSpace) {
1761
1923
  value += ' ';
1762
- afterHash = false; // Space ends hex collection
1763
1924
  }
1764
1925
 
1765
1926
  value += tokenValue;
1766
- lastTokenValue = tokenValue;
1767
1927
  }
1928
+
1768
1929
  value = value.trim();
1769
1930
 
1770
1931
  if (this.is(TokenType.SEMICOLON)) {
@@ -2068,14 +2229,15 @@ export class Parser {
2068
2229
  */
2069
2230
  isPropertyStart() {
2070
2231
  // Check if it looks like: identifier (with possible hyphens) followed by :
2071
- // CSS properties can be: margin, margin-bottom, -webkit-transform, etc.
2072
- if (!this.is(TokenType.IDENT) && !this.is(TokenType.MINUS)) return false;
2232
+ // CSS properties can be: margin, margin-bottom, -webkit-transform, --custom-prop, etc.
2233
+ // Include MINUSMINUS for CSS custom properties (--var-name)
2234
+ if (!this.is(TokenType.IDENT) && !this.is(TokenType.MINUS) && !this.is(TokenType.MINUSMINUS)) return false;
2073
2235
 
2074
2236
  let i = 0;
2075
- // Skip over property name tokens (IDENT and MINUS for hyphenated names)
2237
+ // Skip over property name tokens (IDENT, MINUS, MINUSMINUS for hyphenated/custom props)
2076
2238
  while (this.peek(i)) {
2077
2239
  const token = this.peek(i);
2078
- if (token.type === TokenType.IDENT || token.type === TokenType.MINUS) {
2240
+ if (token.type === TokenType.IDENT || token.type === TokenType.MINUS || token.type === TokenType.MINUSMINUS) {
2079
2241
  i++;
2080
2242
  } else {
2081
2243
  break;
@@ -45,51 +45,125 @@ export function transformStyle(transformer, styleBlock) {
45
45
  return lines.join('\n');
46
46
  }
47
47
 
48
+ /**
49
+ * Check if a selector is an @-rule (media query, keyframes, etc.)
50
+ * @param {string} selector - CSS selector
51
+ * @returns {boolean}
52
+ */
53
+ function isAtRule(selector) {
54
+ return selector.trim().startsWith('@');
55
+ }
56
+
57
+ /**
58
+ * Check if a selector is @keyframes
59
+ * @param {string} selector - CSS selector
60
+ * @returns {boolean}
61
+ */
62
+ function isKeyframesRule(selector) {
63
+ return selector.trim().startsWith('@keyframes');
64
+ }
65
+
66
+ /**
67
+ * Check if a selector is a keyframe step (from, to, or percentage)
68
+ * @param {string} selector - CSS selector
69
+ * @returns {boolean}
70
+ */
71
+ function isKeyframeStep(selector) {
72
+ const trimmed = selector.trim();
73
+ return trimmed === 'from' || trimmed === 'to' || /^\d+%$/.test(trimmed);
74
+ }
75
+
48
76
  /**
49
77
  * Flatten nested CSS rules by combining selectors
50
78
  * Handles CSS nesting by prepending parent selector to nested rules
79
+ * Special handling for @-rules (media queries, keyframes, etc.)
51
80
  * @param {Object} transformer - Transformer instance
52
81
  * @param {Object} rule - CSS rule from AST
53
82
  * @param {string} parentSelector - Parent selector to prepend (empty for top-level)
54
83
  * @param {Array<string>} output - Array to collect flattened CSS rules
84
+ * @param {string} atRuleWrapper - Optional @-rule wrapper (e.g., "@media (max-width: 768px)")
85
+ * @param {boolean} inKeyframes - Whether we're inside @keyframes (don't scope keyframe steps)
55
86
  */
56
- export function flattenStyleRule(transformer, rule, parentSelector, output) {
87
+ export function flattenStyleRule(transformer, rule, parentSelector, output, atRuleWrapper = '', inKeyframes = false) {
88
+ const selector = rule.selector;
89
+
90
+ // Check if this is an @-rule
91
+ if (isAtRule(selector)) {
92
+ const isKeyframes = isKeyframesRule(selector);
93
+
94
+ // @keyframes should be output as a complete block, not flattened
95
+ if (isKeyframes) {
96
+ const lines = [];
97
+ lines.push(` ${selector} {`);
98
+
99
+ // Output all keyframe steps
100
+ for (const nested of rule.nestedRules) {
101
+ lines.push(` ${nested.selector} {`);
102
+ for (const prop of nested.properties) {
103
+ lines.push(` ${prop.name}: ${prop.value};`);
104
+ }
105
+ lines.push(' }');
106
+ }
107
+
108
+ lines.push(' }');
109
+ output.push(lines.join('\n'));
110
+ return;
111
+ }
112
+
113
+ // Other @-rules (@media, @supports) wrap their nested rules
114
+ for (const nested of rule.nestedRules) {
115
+ flattenStyleRule(transformer, nested, '', output, selector, false);
116
+ }
117
+ return;
118
+ }
119
+
57
120
  // Build the full selector by combining parent and current
58
- let fullSelector = rule.selector;
121
+ let fullSelector = selector;
59
122
 
60
123
  if (parentSelector) {
61
124
  // Handle & (parent reference) in nested selectors
62
- if (rule.selector.includes('&')) {
125
+ if (selector.includes('&')) {
63
126
  // Replace & with parent selector
64
- fullSelector = rule.selector.replace(/&/g, parentSelector);
127
+ fullSelector = selector.replace(/&/g, parentSelector);
65
128
  } else {
66
129
  // Combine parent and child with space (descendant combinator)
67
- fullSelector = `${parentSelector} ${rule.selector}`;
130
+ fullSelector = `${parentSelector} ${selector}`;
68
131
  }
69
132
  }
70
133
 
71
- // Apply scope to selector if enabled
134
+ // Apply scope to selector if enabled (but not for keyframe steps)
72
135
  let scopedSelector = fullSelector;
73
- if (transformer.scopeId) {
136
+ if (transformer.scopeId && !inKeyframes && !isKeyframeStep(selector)) {
74
137
  scopedSelector = scopeStyleSelector(transformer, fullSelector);
75
138
  }
76
139
 
77
140
  // Only output rule if it has properties
78
141
  if (rule.properties.length > 0) {
79
142
  const lines = [];
80
- lines.push(` ${scopedSelector} {`);
81
143
 
82
- for (const prop of rule.properties) {
83
- lines.push(` ${prop.name}: ${prop.value};`);
144
+ // If wrapped in an @-rule, output the wrapper
145
+ if (atRuleWrapper) {
146
+ lines.push(` ${atRuleWrapper} {`);
147
+ lines.push(` ${scopedSelector} {`);
148
+ for (const prop of rule.properties) {
149
+ lines.push(` ${prop.name}: ${prop.value};`);
150
+ }
151
+ lines.push(' }');
152
+ lines.push(' }');
153
+ } else {
154
+ lines.push(` ${scopedSelector} {`);
155
+ for (const prop of rule.properties) {
156
+ lines.push(` ${prop.name}: ${prop.value};`);
157
+ }
158
+ lines.push(' }');
84
159
  }
85
160
 
86
- lines.push(' }');
87
161
  output.push(lines.join('\n'));
88
162
  }
89
163
 
90
164
  // Recursively flatten nested rules with combined selector
91
165
  for (const nested of rule.nestedRules) {
92
- flattenStyleRule(transformer, nested, fullSelector, output);
166
+ flattenStyleRule(transformer, nested, fullSelector, output, atRuleWrapper, inKeyframes);
93
167
  }
94
168
  }
95
169
 
@@ -2,32 +2,42 @@
2
2
  * Pulse Vite Plugin
3
3
  *
4
4
  * Enables .pulse file support in Vite projects
5
+ * Extracts CSS to virtual .css modules so Vite's CSS pipeline handles them
6
+ * (prevents JS minifier from corrupting CSS in template literals)
5
7
  */
6
8
 
7
9
  import { compile } from '../compiler/index.js';
8
10
  import { existsSync } from 'fs';
9
11
  import { resolve, dirname } from 'path';
10
12
 
13
+ // Virtual module ID for extracted CSS (uses .css extension so Vite treats it as CSS)
14
+ const VIRTUAL_CSS_SUFFIX = '.pulse.css';
15
+
11
16
  /**
12
17
  * Create Pulse Vite plugin
13
18
  */
14
19
  export default function pulsePlugin(options = {}) {
15
20
  const {
16
- include = /\.pulse$/,
17
21
  exclude = /node_modules/,
18
22
  sourceMap = true
19
23
  } = options;
20
24
 
25
+ // Store extracted CSS for each .pulse module
26
+ const cssMap = new Map();
27
+
21
28
  return {
22
29
  name: 'vite-plugin-pulse',
23
30
  enforce: 'pre',
24
31
 
25
32
  /**
26
- * Resolve .pulse files and .js imports that map to .pulse files
27
- * The compiler transforms .pulse imports to .js, so we need to
28
- * resolve them back to .pulse for Vite to process them
33
+ * Resolve .pulse files and virtual CSS modules
29
34
  */
30
35
  resolveId(id, importer) {
36
+ // Handle virtual CSS module resolution
37
+ if (id.endsWith(VIRTUAL_CSS_SUFFIX)) {
38
+ return '\0' + id;
39
+ }
40
+
31
41
  // Direct .pulse imports - resolve to absolute path
32
42
  if (id.endsWith('.pulse') && importer) {
33
43
  const importerDir = dirname(importer);
@@ -38,7 +48,6 @@ export default function pulsePlugin(options = {}) {
38
48
  }
39
49
 
40
50
  // Check if a .js import has a corresponding .pulse file
41
- // This handles the compiler's transformation of .pulse -> .js imports
42
51
  if (id.endsWith('.js') && importer) {
43
52
  const pulseId = id.replace(/\.js$/, '.pulse');
44
53
  const importerDir = dirname(importer);
@@ -52,8 +61,22 @@ export default function pulsePlugin(options = {}) {
52
61
  return null;
53
62
  },
54
63
 
64
+ /**
65
+ * Load virtual CSS modules
66
+ */
67
+ load(id) {
68
+ // Virtual modules start with \0
69
+ if (id.startsWith('\0') && id.endsWith(VIRTUAL_CSS_SUFFIX)) {
70
+ const pulseId = id.slice(1, -VIRTUAL_CSS_SUFFIX.length + '.pulse'.length);
71
+ const css = cssMap.get(pulseId);
72
+ return css || '';
73
+ }
74
+ return null;
75
+ },
76
+
55
77
  /**
56
78
  * Transform .pulse files to JavaScript
79
+ * CSS is extracted to a virtual .css module that Vite processes separately
57
80
  */
58
81
  transform(code, id) {
59
82
  if (!id.endsWith('.pulse')) {
@@ -79,8 +102,27 @@ export default function pulsePlugin(options = {}) {
79
102
  return null;
80
103
  }
81
104
 
105
+ let outputCode = result.code;
106
+
107
+ // Extract CSS from compiled output and move to virtual CSS module
108
+ const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
109
+ if (stylesMatch) {
110
+ const css = stylesMatch[1];
111
+ const virtualCssId = id + '.css';
112
+
113
+ // Store CSS for the virtual module loader
114
+ cssMap.set(id, css);
115
+
116
+ // Replace inline style injection with CSS import
117
+ // Vite will process this through its CSS pipeline (not JS minifier)
118
+ outputCode = outputCode.replace(
119
+ /\/\/ Styles\nconst styles = `[\s\S]*?`;\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\nstyleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/,
120
+ `// Styles extracted to virtual CSS module\nimport "${virtualCssId}";`
121
+ );
122
+ }
123
+
82
124
  return {
83
- code: result.code,
125
+ code: outputCode,
84
126
  map: result.map || null
85
127
  };
86
128
  } catch (error) {
@@ -92,7 +134,7 @@ export default function pulsePlugin(options = {}) {
92
134
  /**
93
135
  * Handle hot module replacement
94
136
  */
95
- handleHotUpdate({ file, server, modules }) {
137
+ handleHotUpdate({ file, server }) {
96
138
  if (file.endsWith('.pulse')) {
97
139
  console.log(`[Pulse] HMR update: ${file}`);
98
140
 
@@ -102,8 +144,14 @@ export default function pulsePlugin(options = {}) {
102
144
  server.moduleGraph.invalidateModule(module);
103
145
  }
104
146
 
147
+ // Also invalidate the associated virtual CSS module
148
+ const virtualCssId = '\0' + file + '.css';
149
+ const cssModule = server.moduleGraph.getModuleById(virtualCssId);
150
+ if (cssModule) {
151
+ server.moduleGraph.invalidateModule(cssModule);
152
+ }
153
+
105
154
  // Send HMR update instead of full reload
106
- // The module will handle its own state preservation via hmrRuntime
107
155
  server.ws.send({
108
156
  type: 'update',
109
157
  updates: [{
@@ -123,7 +171,7 @@ export default function pulsePlugin(options = {}) {
123
171
  * Configure dev server
124
172
  */
125
173
  configureServer(server) {
126
- server.middlewares.use((req, res, next) => {
174
+ server.middlewares.use((_req, _res, next) => {
127
175
  // Add any custom middleware here
128
176
  next();
129
177
  });
@@ -133,11 +181,8 @@ export default function pulsePlugin(options = {}) {
133
181
  * Build hooks
134
182
  */
135
183
  buildStart() {
136
- console.log('[Pulse] Build started');
137
- },
138
-
139
- buildEnd() {
140
- console.log('[Pulse] Build completed');
184
+ // Clear CSS map on new build
185
+ cssMap.clear();
141
186
  }
142
187
  };
143
188
  }
@@ -191,9 +236,9 @@ export const utils = {
191
236
  },
192
237
 
193
238
  /**
194
- * Create a virtual module ID
239
+ * Get the virtual CSS module ID for a Pulse file
195
240
  */
196
- createVirtualId(id) {
197
- return `\0${id}`;
241
+ getVirtualCssId(id) {
242
+ return id + '.css';
198
243
  }
199
244
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.22",
3
+ "version": "1.7.24",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -109,9 +109,10 @@
109
109
  "LICENSE"
110
110
  ],
111
111
  "scripts": {
112
- "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr",
112
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:css-parsing && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr",
113
113
  "test:compiler": "node test/compiler.test.js",
114
114
  "test:sourcemap": "node test/sourcemap.test.js",
115
+ "test:css-parsing": "node test/css-parsing.test.js",
115
116
  "test:pulse": "node test/pulse.test.js",
116
117
  "test:dom": "node test/dom.test.js",
117
118
  "test:dom-element": "node test/dom-element.test.js",