pulse-js-framework 1.7.23 → 1.7.25

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('');
@@ -25,10 +25,14 @@ export function transformExpression(transformer, node) {
25
25
 
26
26
  switch (node.type) {
27
27
  case NodeType.Identifier:
28
+ // Props take precedence over state (props are destructured in render scope)
29
+ if (transformer.propVars.has(node.name)) {
30
+ return node.name;
31
+ }
28
32
  if (transformer.stateVars.has(node.name)) {
29
33
  return `${node.name}.get()`;
30
34
  }
31
- // Props are accessed directly (already destructured)
35
+ // Other identifiers (actions, imports, etc.) accessed directly
32
36
  return node.name;
33
37
 
34
38
  case NodeType.Literal:
@@ -153,8 +157,13 @@ export function transformExpression(transformer, node) {
153
157
  */
154
158
  export function transformExpressionString(transformer, exprStr) {
155
159
  // Simple transformation: wrap state vars with .get()
160
+ // Props take precedence - don't wrap props with .get()
156
161
  let result = exprStr;
157
162
  for (const stateVar of transformer.stateVars) {
163
+ // Skip if this var name is also a prop (props shadow state in render scope)
164
+ if (transformer.propVars.has(stateVar)) {
165
+ continue;
166
+ }
158
167
  result = result.replace(
159
168
  new RegExp(`\\b${stateVar}\\b`, 'g'),
160
169
  `${stateVar}.get()`
@@ -298,32 +298,127 @@ export function transformFocusTrapDirective(transformer, node, indent) {
298
298
  return `{ ${optionsCode} }`;
299
299
  }
300
300
 
301
+ /**
302
+ * Parse a balanced expression starting from an opening brace
303
+ * Handles nested braces and string literals correctly
304
+ * @param {string} str - The string to parse
305
+ * @param {number} start - Index of the opening brace
306
+ * @returns {Object} { expr: string, end: number } or null if invalid
307
+ */
308
+ function parseBalancedExpression(str, start) {
309
+ if (str[start] !== '{') return null;
310
+
311
+ let depth = 0;
312
+ let inString = false;
313
+ let stringChar = '';
314
+ let i = start;
315
+
316
+ while (i < str.length) {
317
+ const char = str[i];
318
+ const prevChar = i > 0 ? str[i - 1] : '';
319
+
320
+ // Handle string literals
321
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
322
+ inString = true;
323
+ stringChar = char;
324
+ } else if (inString && char === stringChar && prevChar !== '\\') {
325
+ inString = false;
326
+ stringChar = '';
327
+ }
328
+
329
+ // Count braces only outside strings
330
+ if (!inString) {
331
+ if (char === '{') {
332
+ depth++;
333
+ } else if (char === '}') {
334
+ depth--;
335
+ if (depth === 0) {
336
+ // Found the matching closing brace
337
+ return {
338
+ expr: str.slice(start + 1, i),
339
+ end: i
340
+ };
341
+ }
342
+ }
343
+ }
344
+
345
+ i++;
346
+ }
347
+
348
+ return null; // Unbalanced braces
349
+ }
350
+
301
351
  /**
302
352
  * Extract dynamic attributes from a selector
303
353
  * Returns { cleanSelector, dynamicAttrs } where dynamicAttrs is an array of { name, expr }
354
+ * Handles complex expressions including ternaries, nested braces, and string literals
304
355
  * @param {string} selector - CSS selector with potential dynamic attributes
305
356
  * @returns {Object} { cleanSelector, dynamicAttrs }
306
357
  */
307
358
  function extractDynamicAttributes(selector) {
308
359
  const dynamicAttrs = [];
309
- // Match attributes with {expression} values: [name={expr}] or [name="{expr}"]
310
- const attrPattern = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*\{([^}]+)\}\]/g;
311
- const attrPatternQuoted = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"\{([^}]+)\}"\]/g;
360
+ let cleanSelector = '';
361
+ let i = 0;
362
+
363
+ while (i < selector.length) {
364
+ // Look for attribute start: [
365
+ if (selector[i] === '[') {
366
+ i++; // Skip [
367
+
368
+ // Parse attribute name
369
+ let attrName = '';
370
+ while (i < selector.length && /[a-zA-Z0-9-]/.test(selector[i])) {
371
+ attrName += selector[i];
372
+ i++;
373
+ }
312
374
 
313
- let cleanSelector = selector;
375
+ // Skip whitespace
376
+ while (i < selector.length && /\s/.test(selector[i])) i++;
314
377
 
315
- // Extract unquoted dynamic attributes: [value={expr}]
316
- let match;
317
- while ((match = attrPattern.exec(selector)) !== null) {
318
- dynamicAttrs.push({ name: match[1], expr: match[2] });
319
- }
320
- cleanSelector = cleanSelector.replace(attrPattern, '');
378
+ // Check for =
379
+ if (i < selector.length && selector[i] === '=') {
380
+ i++; // Skip =
381
+
382
+ // Skip whitespace
383
+ while (i < selector.length && /\s/.test(selector[i])) i++;
321
384
 
322
- // Extract quoted dynamic attributes: [value="{expr}"]
323
- while ((match = attrPatternQuoted.exec(selector)) !== null) {
324
- dynamicAttrs.push({ name: match[1], expr: match[2] });
385
+ // Check for optional quote
386
+ const hasQuote = selector[i] === '"';
387
+ if (hasQuote) i++;
388
+
389
+ // Check for dynamic expression {
390
+ if (selector[i] === '{') {
391
+ const result = parseBalancedExpression(selector, i);
392
+ if (result) {
393
+ dynamicAttrs.push({ name: attrName, expr: result.expr });
394
+ i = result.end + 1; // Skip past closing }
395
+
396
+ // Skip optional closing quote
397
+ if (hasQuote && selector[i] === '"') i++;
398
+
399
+ // Skip closing ]
400
+ if (selector[i] === ']') i++;
401
+
402
+ // Don't add this attribute to cleanSelector
403
+ continue;
404
+ }
405
+ }
406
+ }
407
+
408
+ // Not a dynamic attribute, copy everything from [ to ]
409
+ let bracketDepth = 1;
410
+ cleanSelector += '[';
411
+ while (i < selector.length && bracketDepth > 0) {
412
+ if (selector[i] === '[') bracketDepth++;
413
+ else if (selector[i] === ']') bracketDepth--;
414
+ cleanSelector += selector[i];
415
+ i++;
416
+ }
417
+ } else {
418
+ cleanSelector += selector[i];
419
+ i++;
420
+ }
325
421
  }
326
- cleanSelector = cleanSelector.replace(attrPatternQuoted, '');
327
422
 
328
423
  return { cleanSelector, dynamicAttrs };
329
424
  }
@@ -577,6 +672,18 @@ export function transformComponentCall(transformer, node, indent) {
577
672
  * @param {number} indent - Indentation level
578
673
  * @returns {string} JavaScript code
579
674
  */
675
+ /**
676
+ * Escape a string for use in a template literal
677
+ * @param {string} str - String to escape
678
+ * @returns {string} Escaped string
679
+ */
680
+ function escapeTemplateString(str) {
681
+ return str
682
+ .replace(/\\/g, '\\\\') // Escape backslashes first
683
+ .replace(/`/g, '\\`') // Escape backticks
684
+ .replace(/\$/g, '\\$'); // Escape dollar signs to prevent ${} interpretation
685
+ }
686
+
580
687
  export function transformTextNode(transformer, node, indent) {
581
688
  const pad = ' '.repeat(indent);
582
689
  const parts = node.parts;
@@ -589,7 +696,8 @@ export function transformTextNode(transformer, node, indent) {
589
696
  // Has interpolations - use text() with a function
590
697
  const textParts = parts.map(part => {
591
698
  if (typeof part === 'string') {
592
- return JSON.stringify(part);
699
+ // Escape for template literal (not JSON.stringify which adds quotes)
700
+ return escapeTemplateString(part);
593
701
  }
594
702
  // Interpolation
595
703
  const expr = transformExpressionString(transformer, part.expression);
@@ -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.23",
3
+ "version": "1.7.25",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",