pulse-js-framework 1.7.32 → 1.7.37
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 +0 -0
- package/cli/release.js +159 -22
- package/compiler/parser.js +96 -10
- package/compiler/transformer/constants.js +1 -1
- package/compiler/transformer/expressions.js +69 -9
- package/loader/README.md +509 -0
- package/loader/esbuild-plugin.js +251 -0
- package/loader/parcel-plugin.js +216 -0
- package/loader/rollup-plugin.js +259 -0
- package/loader/swc-plugin.js +286 -0
- package/loader/vite-plugin.js +1 -1
- package/loader/webpack-loader.js +228 -0
- package/package.json +15 -2
- package/runtime/security.js +21 -0
- package/runtime/ssr.js +17 -2
- package/types/a11y.d.ts +186 -0
- package/types/devtools.d.ts +418 -0
- package/types/dom-adapter.d.ts +643 -0
- package/types/dom.d.ts +63 -0
- package/types/errors.d.ts +618 -0
- package/types/http.d.ts +426 -0
- package/types/logger.d.ts +12 -0
- package/types/native.d.ts +282 -0
- package/types/pulse.d.ts +70 -1
- package/types/security.d.ts +286 -0
- package/types/ssr.d.ts +263 -0
- package/types/utils.d.ts +85 -0
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
}
|
package/compiler/parser.js
CHANGED
|
@@ -100,7 +100,9 @@ export class Parser {
|
|
|
100
100
|
* Peek at token at offset
|
|
101
101
|
*/
|
|
102
102
|
peek(offset = 1) {
|
|
103
|
-
|
|
103
|
+
const index = this.pos + offset;
|
|
104
|
+
if (index < 0 || index >= this.tokens.length) return undefined;
|
|
105
|
+
return this.tokens[index];
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -1118,15 +1120,31 @@ export class Parser {
|
|
|
1118
1120
|
}
|
|
1119
1121
|
|
|
1120
1122
|
/**
|
|
1121
|
-
* Parse assignment expression (a = b)
|
|
1123
|
+
* Parse assignment expression (a = b, a += b, a -= b, etc.)
|
|
1122
1124
|
*/
|
|
1123
1125
|
parseAssignmentExpression() {
|
|
1124
1126
|
const left = this.parseConditionalExpression();
|
|
1125
1127
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
+
// Check for assignment operators
|
|
1129
|
+
const assignmentOps = [
|
|
1130
|
+
TokenType.EQ, // =
|
|
1131
|
+
TokenType.PLUS_ASSIGN, // +=
|
|
1132
|
+
TokenType.MINUS_ASSIGN, // -=
|
|
1133
|
+
TokenType.STAR_ASSIGN, // *=
|
|
1134
|
+
TokenType.SLASH_ASSIGN, // /=
|
|
1135
|
+
TokenType.AND_ASSIGN, // &&=
|
|
1136
|
+
TokenType.OR_ASSIGN, // ||=
|
|
1137
|
+
TokenType.NULLISH_ASSIGN // ??=
|
|
1138
|
+
];
|
|
1139
|
+
|
|
1140
|
+
if (this.isAny(...assignmentOps)) {
|
|
1141
|
+
const operator = this.advance().value;
|
|
1128
1142
|
const right = this.parseAssignmentExpression();
|
|
1129
|
-
return new ASTNode(NodeType.AssignmentExpression, {
|
|
1143
|
+
return new ASTNode(NodeType.AssignmentExpression, {
|
|
1144
|
+
left,
|
|
1145
|
+
right,
|
|
1146
|
+
operator
|
|
1147
|
+
});
|
|
1130
1148
|
}
|
|
1131
1149
|
|
|
1132
1150
|
return left;
|
|
@@ -1545,15 +1563,83 @@ export class Parser {
|
|
|
1545
1563
|
*/
|
|
1546
1564
|
parseStyleBlock() {
|
|
1547
1565
|
this.expect(TokenType.STYLE);
|
|
1548
|
-
this.expect(TokenType.LBRACE);
|
|
1566
|
+
const startBrace = this.expect(TokenType.LBRACE);
|
|
1549
1567
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1568
|
+
// Extract raw CSS content for preprocessor support
|
|
1569
|
+
// Instead of parsing token by token, collect all tokens until matching }
|
|
1570
|
+
const rawTokens = [];
|
|
1571
|
+
let braceDepth = 1; // We've already consumed the opening {
|
|
1572
|
+
const startPos = this.pos;
|
|
1573
|
+
|
|
1574
|
+
while (braceDepth > 0 && !this.is(TokenType.EOF)) {
|
|
1575
|
+
const token = this.current();
|
|
1576
|
+
if (token.type === TokenType.LBRACE) braceDepth++;
|
|
1577
|
+
if (token.type === TokenType.RBRACE) braceDepth--;
|
|
1578
|
+
|
|
1579
|
+
if (braceDepth > 0) {
|
|
1580
|
+
rawTokens.push(token);
|
|
1581
|
+
this.advance();
|
|
1582
|
+
}
|
|
1553
1583
|
}
|
|
1554
1584
|
|
|
1555
1585
|
this.expect(TokenType.RBRACE);
|
|
1556
|
-
|
|
1586
|
+
|
|
1587
|
+
// Reconstruct raw CSS from tokens for preprocessor
|
|
1588
|
+
const rawCSS = this.reconstructCSS(rawTokens);
|
|
1589
|
+
|
|
1590
|
+
// Try to parse as structured CSS (will work for plain CSS)
|
|
1591
|
+
// If parsing fails, fall back to raw mode for preprocessors
|
|
1592
|
+
let rules = [];
|
|
1593
|
+
let parseError = null;
|
|
1594
|
+
|
|
1595
|
+
// Reset to try parsing
|
|
1596
|
+
const savedPos = this.pos;
|
|
1597
|
+
this.pos = startPos;
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1601
|
+
rules.push(this.parseStyleRule());
|
|
1602
|
+
}
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
// Parsing failed - likely preprocessor syntax (LESS/SASS/Stylus)
|
|
1605
|
+
parseError = error;
|
|
1606
|
+
rules = []; // Clear any partial parse
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Restore position to after the closing }
|
|
1610
|
+
this.pos = savedPos;
|
|
1611
|
+
|
|
1612
|
+
return new ASTNode(NodeType.StyleBlock, {
|
|
1613
|
+
rules,
|
|
1614
|
+
raw: rawCSS,
|
|
1615
|
+
parseError: parseError ? parseError.message : null
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Reconstruct CSS from tokens, preserving formatting
|
|
1621
|
+
*/
|
|
1622
|
+
reconstructCSS(tokens) {
|
|
1623
|
+
if (!tokens.length) return '';
|
|
1624
|
+
|
|
1625
|
+
const lines = [];
|
|
1626
|
+
let currentLine = [];
|
|
1627
|
+
let lastLine = tokens[0].line;
|
|
1628
|
+
|
|
1629
|
+
for (const token of tokens) {
|
|
1630
|
+
if (token.line !== lastLine) {
|
|
1631
|
+
lines.push(currentLine.join(''));
|
|
1632
|
+
currentLine = [];
|
|
1633
|
+
lastLine = token.line;
|
|
1634
|
+
}
|
|
1635
|
+
currentLine.push(token.raw || token.value);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (currentLine.length > 0) {
|
|
1639
|
+
lines.push(currentLine.join(''));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
return lines.join('\n').trim();
|
|
1557
1643
|
}
|
|
1558
1644
|
|
|
1559
1645
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Generate a unique scope ID for CSS scoping */
|
|
8
|
-
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2,
|
|
8
|
+
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 12);
|
|
9
9
|
|
|
10
10
|
/** Token types that should not have space after them */
|
|
11
11
|
export const NO_SPACE_AFTER = new Set([
|
|
@@ -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.
|
|
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
|
|
90
|
-
return `${name}.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
// Compound assignment for non-state vars
|
|
145
|
+
return `(${left} ${operator} ${right})`;
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
case NodeType.ArrayLiteral: {
|
|
@@ -162,14 +182,54 @@ export function transformExpression(transformer, node) {
|
|
|
162
182
|
* @returns {string} Transformed expression string
|
|
163
183
|
*/
|
|
164
184
|
export function transformExpressionString(transformer, exprStr) {
|
|
165
|
-
//
|
|
185
|
+
// Transform state and prop vars in expression strings (interpolations, attribute bindings)
|
|
166
186
|
// Both are now reactive (useProp returns computed for uniform interface)
|
|
167
187
|
let result = exprStr;
|
|
168
188
|
|
|
169
|
-
//
|
|
189
|
+
// First, handle assignments to state vars: stateVar = expr -> stateVar.set(expr)
|
|
190
|
+
// This must happen before the generic .get() replacement to avoid generating
|
|
191
|
+
// invalid code like stateVar.get() = expr (LHS of assignment is not a reference)
|
|
192
|
+
for (const stateVar of transformer.stateVars) {
|
|
193
|
+
// Compound assignment: stateVar += expr -> stateVar.update(_v => _v + expr)
|
|
194
|
+
result = result.replace(
|
|
195
|
+
new RegExp(`\\b${stateVar}\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g'),
|
|
196
|
+
(_match, op) => {
|
|
197
|
+
const baseOp = op.slice(0, -1); // Remove trailing '='
|
|
198
|
+
return `${stateVar}.update(_v => _v ${baseOp} `;
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
// Close the .update() call - find the end of the expression after the replacement
|
|
202
|
+
// This is handled by the fact that the expression continues after the replacement text
|
|
203
|
+
// and the closing paren is added by wrapping logic below.
|
|
204
|
+
|
|
205
|
+
// Simple assignment: stateVar = expr -> stateVar.set(expr)
|
|
206
|
+
// Use negative lookbehind to skip compound assignments (already handled)
|
|
207
|
+
// Use negative lookahead to skip == and ===
|
|
208
|
+
result = result.replace(
|
|
209
|
+
new RegExp(`\\b${stateVar}\\s*=(?!=)`, 'g'),
|
|
210
|
+
`${stateVar}.set(`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If we inserted .set( or .update(, we need to close the parenthesis
|
|
215
|
+
// Find unclosed .set( and .update( calls and close them at end of expression
|
|
216
|
+
if (result.includes('.set(') || result.includes('.update(_v =>')) {
|
|
217
|
+
// For .update(_v => _v op expr), close with )
|
|
218
|
+
result = result.replace(
|
|
219
|
+
/\.update\(_v => _v [^\)]*$/,
|
|
220
|
+
(m) => m + ')'
|
|
221
|
+
);
|
|
222
|
+
// For .set(expr), close with )
|
|
223
|
+
result = result.replace(
|
|
224
|
+
/\.set\(([^)]*$)/,
|
|
225
|
+
(_m, expr) => `.set(${expr})`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Transform state var reads (not already transformed to .get/.set/.update)
|
|
170
230
|
for (const stateVar of transformer.stateVars) {
|
|
171
231
|
result = result.replace(
|
|
172
|
-
new RegExp(`\\b${stateVar}\\b`, 'g'),
|
|
232
|
+
new RegExp(`\\b${stateVar}\\b(?!\\.(?:get|set|update))`, 'g'),
|
|
173
233
|
`${stateVar}.get()`
|
|
174
234
|
);
|
|
175
235
|
}
|