pulse-js-framework 1.7.32 → 1.7.33

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/index.js CHANGED
File without changes
package/cli/release.js CHANGED
@@ -532,6 +532,92 @@ function verifyTagOnRemote(version) {
532
532
  }
533
533
  }
534
534
 
535
+ /**
536
+ * Check if gh CLI is available
537
+ */
538
+ function isGhCliAvailable() {
539
+ try {
540
+ execSync('gh --version', { cwd: root, stdio: 'ignore' });
541
+ return true;
542
+ } catch {
543
+ return false;
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Create GitHub release using gh CLI
549
+ */
550
+ function createGitHubRelease(version, title, changes) {
551
+ if (!isGhCliAvailable()) {
552
+ log.warn(' gh CLI not available, skipping GitHub release creation');
553
+ log.info(' Install gh CLI: https://cli.github.com/');
554
+ return { success: false, reason: 'gh-not-installed' };
555
+ }
556
+
557
+ // Build release notes
558
+ let notes = '';
559
+
560
+ if (title) {
561
+ notes += `## ${title}\n\n`;
562
+ }
563
+
564
+ if (changes.added?.length > 0) {
565
+ notes += `### ✨ Added\n\n`;
566
+ for (const item of changes.added) {
567
+ notes += `- ${item}\n`;
568
+ }
569
+ notes += '\n';
570
+ }
571
+
572
+ if (changes.changed?.length > 0) {
573
+ notes += `### 🔄 Changed\n\n`;
574
+ for (const item of changes.changed) {
575
+ notes += `- ${item}\n`;
576
+ }
577
+ notes += '\n';
578
+ }
579
+
580
+ if (changes.fixed?.length > 0) {
581
+ notes += `### 🐛 Fixed\n\n`;
582
+ for (const item of changes.fixed) {
583
+ notes += `- ${item}\n`;
584
+ }
585
+ notes += '\n';
586
+ }
587
+
588
+ if (changes.removed?.length > 0) {
589
+ notes += `### 🗑️ Removed\n\n`;
590
+ for (const item of changes.removed) {
591
+ notes += `- ${item}\n`;
592
+ }
593
+ notes += '\n';
594
+ }
595
+
596
+ // Write notes to temp file
597
+ const tempFile = join(tmpdir(), `pulse-release-notes-${Date.now()}.md`);
598
+ writeFileSync(tempFile, notes, 'utf-8');
599
+
600
+ try {
601
+ const releaseTitle = title ? `v${version} - ${title}` : `v${version}`;
602
+ const command = `gh release create v${version} --title "${releaseTitle}" --notes-file "${tempFile}"`;
603
+
604
+ log.info(` Creating GitHub release v${version}...`);
605
+ execSync(command, { cwd: root, stdio: 'inherit' });
606
+
607
+ log.info(` GitHub release created successfully`);
608
+ return { success: true };
609
+ } catch (error) {
610
+ log.error(` Failed to create GitHub release: ${error.message}`);
611
+ return { success: false, error };
612
+ } finally {
613
+ try {
614
+ unlinkSync(tempFile);
615
+ } catch {
616
+ // Ignore cleanup errors
617
+ }
618
+ }
619
+ }
620
+
535
621
  /**
536
622
  * Execute a git command with error handling
537
623
  */
@@ -619,24 +705,53 @@ function gitCommitTagPush(newVersion, title, changes, dryRun = false) {
619
705
  return { success: false, stage: 'push', error: result.error };
620
706
  }
621
707
 
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');
708
+ // git push --tags (use gh CLI if available for better error handling)
709
+ if (isGhCliAvailable()) {
710
+ // Use git push directly as gh doesn't have a push command
711
+ result = execGitCommand('git push --tags', 'git push --tags');
712
+ if (!result.success) {
713
+ log.error('');
714
+ log.error('Tag push failed. The tag exists locally but not on remote.');
715
+ log.error('You can manually push tags with:');
716
+ log.error(' git push --tags');
717
+ return { success: false, stage: 'push-tags', error: result.error };
718
+ }
719
+
720
+ // Verify tag was pushed using gh
721
+ try {
722
+ execSync(`gh release view v${newVersion}`, { cwd: root, stdio: 'ignore' });
723
+ log.info(` Verified: tag v${newVersion} exists on remote (via gh)`);
724
+ } catch {
725
+ // Tag pushed but no release yet, which is fine
726
+ if (verifyTagOnRemote(newVersion)) {
727
+ log.info(` Verified: tag v${newVersion} exists on remote`);
728
+ } else {
729
+ log.warn('');
730
+ log.warn('Tag push appeared to succeed but tag not found on remote.');
731
+ log.warn('You may need to manually verify or push with:');
732
+ log.warn(' git push --tags');
733
+ }
734
+ }
638
735
  } else {
639
- log.info(` Verified: tag v${newVersion} exists on remote`);
736
+ // Fallback to standard git push --tags
737
+ result = execGitCommand('git push --tags', 'git push --tags');
738
+ if (!result.success) {
739
+ log.error('');
740
+ log.error('Tag push failed. The tag exists locally but not on remote.');
741
+ log.error('You can manually push tags with:');
742
+ log.error(' git push --tags');
743
+ return { success: false, stage: 'push-tags', error: result.error };
744
+ }
745
+
746
+ // Verify tag was pushed to remote
747
+ if (!verifyTagOnRemote(newVersion)) {
748
+ log.warn('');
749
+ log.warn('Tag push appeared to succeed but tag not found on remote.');
750
+ log.warn('You may need to manually verify or push with:');
751
+ log.warn(' git push --tags');
752
+ } else {
753
+ log.info(` Verified: tag v${newVersion} exists on remote`);
754
+ }
640
755
  }
641
756
 
642
757
  return { success: true };
@@ -714,6 +829,7 @@ Types:
714
829
  Options:
715
830
  --dry-run Show what would be done without making changes
716
831
  --no-push Create commit and tag but don't push
832
+ --no-gh-release Skip creating GitHub release (only create tag)
717
833
  --title <text> Release title (e.g., "Performance Improvements")
718
834
  --skip-prompt Use empty changelog (for automated releases)
719
835
  --skip-docs-test Skip documentation validation before release
@@ -733,6 +849,7 @@ Examples:
733
849
  pulse release patch --title "Security" --fixed "XSS vulnerability,SQL injection" -y
734
850
  pulse release patch --title "New API" --added "Feature A,Feature B" --fixed "Bug X" -y
735
851
  pulse release patch --discord-webhook "https://discord.com/api/webhooks/..."
852
+ pulse release patch --no-gh-release # Create tag but skip GitHub release
736
853
  `);
737
854
  }
738
855
 
@@ -750,6 +867,7 @@ export async function runRelease(args) {
750
867
  // Parse options
751
868
  const dryRun = args.includes('--dry-run');
752
869
  const noPush = args.includes('--no-push');
870
+ const noGhRelease = args.includes('--no-gh-release');
753
871
  const skipPrompt = args.includes('--skip-prompt');
754
872
  const fromCommits = args.includes('--from-commits') || args.includes('--fc');
755
873
  const autoConfirm = args.includes('--yes') || args.includes('-y');
@@ -984,8 +1102,32 @@ export async function runRelease(args) {
984
1102
  log.info(`Release v${newVersion} complete!`);
985
1103
  log.info('');
986
1104
 
1105
+ // Create GitHub release using gh CLI
1106
+ if (!dryRun && !noPush && !noGhRelease && hasChanges) {
1107
+ log.info('Creating GitHub release...');
1108
+ const ghResult = createGitHubRelease(newVersion, title, changes);
1109
+
1110
+ if (!ghResult.success) {
1111
+ if (ghResult.reason === 'gh-not-installed') {
1112
+ log.info('Manual step:');
1113
+ log.info(` Create GitHub release: https://github.com/vincenthirtz/pulse-js-framework/releases/new?tag=v${newVersion}`);
1114
+ } else {
1115
+ log.warn('GitHub release creation failed, but release was successful.');
1116
+ log.info('Manual step:');
1117
+ log.info(` Create GitHub release: https://github.com/vincenthirtz/pulse-js-framework/releases/new?tag=v${newVersion}`);
1118
+ }
1119
+ }
1120
+ } else if (dryRun) {
1121
+ log.info(' [dry-run] Would create GitHub release using gh CLI');
1122
+ } else if (noGhRelease) {
1123
+ log.info('Skipping GitHub release creation (--no-gh-release)');
1124
+ log.info('Manual step:');
1125
+ log.info(` Create GitHub release: https://github.com/vincenthirtz/pulse-js-framework/releases/new?tag=v${newVersion}`);
1126
+ }
1127
+
987
1128
  // Send Discord notification if webhook URL provided
988
1129
  if (discordWebhook && !dryRun) {
1130
+ log.info('');
989
1131
  log.info('Sending Discord notification...');
990
1132
  try {
991
1133
  await sendDiscordNotification(discordWebhook, newVersion, title, changes);
@@ -996,9 +1138,4 @@ export async function runRelease(args) {
996
1138
  } else if (discordWebhook && dryRun) {
997
1139
  log.info(' [dry-run] Would send Discord notification to webhook');
998
1140
  }
999
-
1000
- if (!dryRun && !noPush) {
1001
- log.info('Next step:');
1002
- log.info(` Create GitHub release: https://github.com/vincenthirtz/pulse-js-framework/releases/new?tag=v${newVersion}`);
1003
- }
1004
1141
  }
@@ -1118,15 +1118,31 @@ export class Parser {
1118
1118
  }
1119
1119
 
1120
1120
  /**
1121
- * Parse assignment expression (a = b)
1121
+ * Parse assignment expression (a = b, a += b, a -= b, etc.)
1122
1122
  */
1123
1123
  parseAssignmentExpression() {
1124
1124
  const left = this.parseConditionalExpression();
1125
1125
 
1126
- if (this.is(TokenType.EQ)) {
1127
- this.advance();
1126
+ // Check for assignment operators
1127
+ const assignmentOps = [
1128
+ TokenType.EQ, // =
1129
+ TokenType.PLUS_ASSIGN, // +=
1130
+ TokenType.MINUS_ASSIGN, // -=
1131
+ TokenType.STAR_ASSIGN, // *=
1132
+ TokenType.SLASH_ASSIGN, // /=
1133
+ TokenType.AND_ASSIGN, // &&=
1134
+ TokenType.OR_ASSIGN, // ||=
1135
+ TokenType.NULLISH_ASSIGN // ??=
1136
+ ];
1137
+
1138
+ if (this.isAny(...assignmentOps)) {
1139
+ const operator = this.advance().value;
1128
1140
  const right = this.parseAssignmentExpression();
1129
- return new ASTNode(NodeType.AssignmentExpression, { left, right });
1141
+ return new ASTNode(NodeType.AssignmentExpression, {
1142
+ left,
1143
+ right,
1144
+ operator
1145
+ });
1130
1146
  }
1131
1147
 
1132
1148
  return left;
@@ -1545,15 +1561,83 @@ export class Parser {
1545
1561
  */
1546
1562
  parseStyleBlock() {
1547
1563
  this.expect(TokenType.STYLE);
1548
- this.expect(TokenType.LBRACE);
1564
+ const startBrace = this.expect(TokenType.LBRACE);
1549
1565
 
1550
- const rules = [];
1551
- while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
1552
- rules.push(this.parseStyleRule());
1566
+ // Extract raw CSS content for preprocessor support
1567
+ // Instead of parsing token by token, collect all tokens until matching }
1568
+ const rawTokens = [];
1569
+ let braceDepth = 1; // We've already consumed the opening {
1570
+ const startPos = this.pos;
1571
+
1572
+ while (braceDepth > 0 && !this.is(TokenType.EOF)) {
1573
+ const token = this.current();
1574
+ if (token.type === TokenType.LBRACE) braceDepth++;
1575
+ if (token.type === TokenType.RBRACE) braceDepth--;
1576
+
1577
+ if (braceDepth > 0) {
1578
+ rawTokens.push(token);
1579
+ this.advance();
1580
+ }
1553
1581
  }
1554
1582
 
1555
1583
  this.expect(TokenType.RBRACE);
1556
- return new ASTNode(NodeType.StyleBlock, { rules });
1584
+
1585
+ // Reconstruct raw CSS from tokens for preprocessor
1586
+ const rawCSS = this.reconstructCSS(rawTokens);
1587
+
1588
+ // Try to parse as structured CSS (will work for plain CSS)
1589
+ // If parsing fails, fall back to raw mode for preprocessors
1590
+ let rules = [];
1591
+ let parseError = null;
1592
+
1593
+ // Reset to try parsing
1594
+ const savedPos = this.pos;
1595
+ this.pos = startPos;
1596
+
1597
+ try {
1598
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
1599
+ rules.push(this.parseStyleRule());
1600
+ }
1601
+ } catch (error) {
1602
+ // Parsing failed - likely preprocessor syntax (LESS/SASS/Stylus)
1603
+ parseError = error;
1604
+ rules = []; // Clear any partial parse
1605
+ }
1606
+
1607
+ // Restore position to after the closing }
1608
+ this.pos = savedPos;
1609
+
1610
+ return new ASTNode(NodeType.StyleBlock, {
1611
+ rules,
1612
+ raw: rawCSS,
1613
+ parseError: parseError ? parseError.message : null
1614
+ });
1615
+ }
1616
+
1617
+ /**
1618
+ * Reconstruct CSS from tokens, preserving formatting
1619
+ */
1620
+ reconstructCSS(tokens) {
1621
+ if (!tokens.length) return '';
1622
+
1623
+ const lines = [];
1624
+ let currentLine = [];
1625
+ let lastLine = tokens[0].line;
1626
+
1627
+ for (const token of tokens) {
1628
+ if (token.line !== lastLine) {
1629
+ lines.push(currentLine.join(''));
1630
+ currentLine = [];
1631
+ lastLine = token.line;
1632
+ }
1633
+ currentLine.push(token.raw || token.value);
1634
+ }
1635
+
1636
+ if (currentLine.length > 0) {
1637
+ lines.push(currentLine.join(''));
1638
+ }
1639
+
1640
+ return lines.join('\n').trim();
1557
1641
  }
1558
1642
 
1559
1643
  /**
@@ -82,12 +82,12 @@ export function transformExpression(transformer, node) {
82
82
 
83
83
  case NodeType.UpdateExpression: {
84
84
  const argument = transformExpression(transformer, node.argument);
85
- // For state variables, convert x++ to x.set(x.get() + 1)
85
+ // For state variables, convert x++ to x.update(x => x + 1)
86
86
  if (node.argument.type === NodeType.Identifier &&
87
87
  transformer.stateVars.has(node.argument.name)) {
88
88
  const name = node.argument.name;
89
- const delta = node.operator === '++' ? 1 : -1;
90
- return `${name}.set(${name}.get() + ${delta})`;
89
+ const op = node.operator === '++' ? '+' : '-';
90
+ return `${name}.update(_${name} => _${name} ${op} 1)`;
91
91
  }
92
92
  return node.prefix
93
93
  ? `${node.operator}${argument}`
@@ -117,12 +117,32 @@ export function transformExpression(transformer, node) {
117
117
  case NodeType.AssignmentExpression: {
118
118
  const left = transformExpression(transformer, node.left);
119
119
  const right = transformExpression(transformer, node.right);
120
- // For state variables, convert to .set()
120
+ const operator = node.operator || '=';
121
+
122
+ // For state variables, convert to .set() or .update()
121
123
  if (node.left.type === NodeType.Identifier &&
122
124
  transformer.stateVars.has(node.left.name)) {
123
- return `${node.left.name}.set(${right})`;
125
+ const varName = node.left.name;
126
+
127
+ // Compound assignment operators (+=, -=, *=, /=, &&=, ||=, ??=)
128
+ if (operator !== '=') {
129
+ // Convert compound assignment to update
130
+ // a += b => a.update(_a => _a + b)
131
+ // a -= b => a.update(_a => _a - b)
132
+ const baseOp = operator.slice(0, -1); // Remove trailing '='
133
+ return `${varName}.update(_${varName} => _${varName} ${baseOp} ${right})`;
134
+ }
135
+
136
+ // Simple assignment: a = b => a.set(b)
137
+ return `${varName}.set(${right})`;
138
+ }
139
+
140
+ // Regular assignment (non-state vars)
141
+ if (operator === '=') {
142
+ return `(${left} = ${right})`;
124
143
  }
125
- return `(${left} = ${right})`;
144
+ // Compound assignment for non-state vars
145
+ return `(${left} ${operator} ${right})`;
126
146
  }
127
147
 
128
148
  case NodeType.ArrayLiteral: {