pulse-js-framework 1.7.31 → 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/build.js CHANGED
@@ -7,9 +7,13 @@
7
7
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs';
8
8
  import { join, extname, relative, dirname } from 'path';
9
9
  import { compile } from '../compiler/index.js';
10
+ import { preprocessStylesSync, isSassAvailable, getSassVersion } from '../compiler/preprocessor.js';
10
11
  import { log } from './logger.js';
11
12
  import { createTimer, createProgressBar, formatDuration, createSpinner } from './utils/cli-ui.js';
12
13
 
14
+ // SASS availability (checked once at build start)
15
+ let sassAvailable = false;
16
+
13
17
  /**
14
18
  * Build project for production
15
19
  */
@@ -34,6 +38,13 @@ export async function buildProject(args) {
34
38
 
35
39
  log.info('Building with Pulse compiler...\n');
36
40
 
41
+ // Check for SASS availability
42
+ sassAvailable = isSassAvailable();
43
+ if (sassAvailable) {
44
+ const version = getSassVersion();
45
+ log.info(` SASS support enabled (sass ${version || 'unknown'})`);
46
+ }
47
+
37
48
  // Create output directory
38
49
  if (!existsSync(outDir)) {
39
50
  mkdirSync(outDir, { recursive: true });
@@ -140,8 +151,30 @@ function processDirectory(srcDir, outDir, progress = null) {
140
151
  });
141
152
 
142
153
  if (result.success) {
154
+ let code = result.code;
155
+
156
+ // Preprocess SASS/SCSS in style blocks if sass is available
157
+ if (sassAvailable) {
158
+ const stylesMatch = code.match(/const styles = `([\s\S]*?)`;/);
159
+ if (stylesMatch) {
160
+ try {
161
+ const preprocessed = preprocessStylesSync(stylesMatch[1], {
162
+ filename: srcPath,
163
+ loadPaths: [dirname(srcPath)],
164
+ compressed: true
165
+ });
166
+
167
+ if (preprocessed.wasSass) {
168
+ code = code.replace(stylesMatch[0], `const styles = \`${preprocessed.css}\`;`);
169
+ }
170
+ } catch (sassError) {
171
+ log.warn(` SASS warning in ${file}: ${sassError.message}`);
172
+ }
173
+ }
174
+ }
175
+
143
176
  const outPath = join(outDir, file.replace('.pulse', '.js'));
144
- writeFileSync(outPath, result.code);
177
+ writeFileSync(outPath, code);
145
178
  } else {
146
179
  log.error(` Error compiling ${file}:`);
147
180
  for (const error of result.errors) {
package/cli/dev.js CHANGED
@@ -6,10 +6,14 @@
6
6
 
7
7
  import { createServer } from 'http';
8
8
  import { readFileSync, existsSync, statSync, watch } from 'fs';
9
- import { join, extname, resolve } from 'path';
9
+ import { join, extname, resolve, dirname } from 'path';
10
10
  import { compile } from '../compiler/index.js';
11
+ import { preprocessStylesSync, isSassAvailable, getSassVersion } from '../compiler/preprocessor.js';
11
12
  import { log } from './logger.js';
12
13
 
14
+ // SASS availability (checked once at server start)
15
+ let sassAvailable = false;
16
+
13
17
  const MIME_TYPES = {
14
18
  '.html': 'text/html',
15
19
  '.js': 'application/javascript',
@@ -87,6 +91,13 @@ export async function startDevServer(args) {
87
91
  // Vite not available, use built-in server
88
92
  }
89
93
 
94
+ // Check for SASS availability
95
+ sassAvailable = isSassAvailable();
96
+ if (sassAvailable) {
97
+ const version = getSassVersion();
98
+ log.info(`SASS support enabled (sass ${version || 'unknown'})`);
99
+ }
100
+
90
101
  // Built-in development server
91
102
  const server = createServer(async (req, res) => {
92
103
  const url = new URL(req.url, `http://localhost:${port}`);
@@ -128,11 +139,31 @@ export async function startDevServer(args) {
128
139
  });
129
140
 
130
141
  if (result.success) {
142
+ let code = result.code;
143
+
144
+ // Preprocess SASS/SCSS if available
145
+ if (sassAvailable) {
146
+ const stylesMatch = code.match(/const styles = `([\s\S]*?)`;/);
147
+ if (stylesMatch) {
148
+ try {
149
+ const preprocessed = preprocessStylesSync(stylesMatch[1], {
150
+ filename: filePath,
151
+ loadPaths: [dirname(filePath)]
152
+ });
153
+ if (preprocessed.wasSass) {
154
+ code = code.replace(stylesMatch[0], `const styles = \`${preprocessed.css}\`;`);
155
+ }
156
+ } catch (sassError) {
157
+ console.warn(`[Pulse] SASS warning: ${sassError.message}`);
158
+ }
159
+ }
160
+ }
161
+
131
162
  res.writeHead(200, {
132
163
  'Content-Type': 'application/javascript',
133
164
  'Cache-Control': 'no-cache, no-store, must-revalidate'
134
165
  });
135
- res.end(result.code);
166
+ res.end(code);
136
167
  } else {
137
168
  const errorDetails = result.errors.map(e => `${e.message} at line ${e.line || '?'}:${e.column || '?'}`).join('\n');
138
169
  console.error(`[Pulse] Compilation error in ${filePath}:`, result.errors);
@@ -173,11 +204,31 @@ export async function startDevServer(args) {
173
204
  });
174
205
 
175
206
  if (result.success) {
207
+ let code = result.code;
208
+
209
+ // Preprocess SASS/SCSS if available
210
+ if (sassAvailable) {
211
+ const stylesMatch = code.match(/const styles = `([\s\S]*?)`;/);
212
+ if (stylesMatch) {
213
+ try {
214
+ const preprocessed = preprocessStylesSync(stylesMatch[1], {
215
+ filename: pulseFilePath,
216
+ loadPaths: [dirname(pulseFilePath)]
217
+ });
218
+ if (preprocessed.wasSass) {
219
+ code = code.replace(stylesMatch[0], `const styles = \`${preprocessed.css}\`;`);
220
+ }
221
+ } catch (sassError) {
222
+ console.warn(`[Pulse] SASS warning: ${sassError.message}`);
223
+ }
224
+ }
225
+ }
226
+
176
227
  res.writeHead(200, {
177
228
  'Content-Type': 'application/javascript',
178
229
  'Cache-Control': 'no-cache, no-store, must-revalidate'
179
230
  });
180
- res.end(result.code);
231
+ res.end(code);
181
232
  } else {
182
233
  res.writeHead(500, { 'Content-Type': 'text/plain' });
183
234
  res.end(`Compilation error: ${result.errors.map(e => e.message).join('\n')}`);
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
  /**