pulse-js-framework 1.4.3 → 1.4.4

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.
@@ -11,12 +11,17 @@
11
11
 
12
12
  import { NodeType } from './parser.js';
13
13
 
14
- /**
15
- * Generate a unique scope ID for CSS scoping
16
- */
17
- function generateScopeId() {
18
- return 'p' + Math.random().toString(36).substring(2, 8);
19
- }
14
+ /** Generate a unique scope ID for CSS scoping */
15
+ const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 8);
16
+
17
+ // Token spacing constants (shared across methods)
18
+ const NO_SPACE_AFTER = new Set(['DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD', '.', '(', '[', '{', '!', '~', '...']);
19
+ const NO_SPACE_BEFORE = new Set(['DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA', 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET', '.', ')', ']', '}', ';', ',', '++', '--', '(', '[']);
20
+ const PUNCT_NO_SPACE_BEFORE = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
21
+ const PUNCT_NO_SPACE_AFTER = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
22
+ const STATEMENT_KEYWORDS = new Set(['let', 'const', 'var', 'return', 'if', 'else', 'for', 'while', 'switch', 'throw', 'try', 'catch', 'finally']);
23
+ const BUILTIN_FUNCTIONS = new Set(['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'alert', 'confirm', 'prompt', 'console', 'document', 'window', 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'fetch']);
24
+ const STATEMENT_TOKEN_TYPES = new Set(['IF', 'FOR', 'EACH']);
20
25
 
21
26
  /**
22
27
  * Transformer class
@@ -24,17 +29,12 @@ function generateScopeId() {
24
29
  export class Transformer {
25
30
  constructor(ast, options = {}) {
26
31
  this.ast = ast;
27
- this.options = {
28
- runtime: 'pulse-js-framework/runtime',
29
- minify: false,
30
- scopeStyles: true, // Enable CSS scoping by default
31
- ...options
32
- };
32
+ this.options = { runtime: 'pulse-js-framework/runtime', minify: false, scopeStyles: true, ...options };
33
33
  this.stateVars = new Set();
34
- this.propVars = new Set(); // Track prop names
35
- this.propDefaults = new Map(); // Track prop default values
34
+ this.propVars = new Set();
35
+ this.propDefaults = new Map();
36
36
  this.actionNames = new Set();
37
- this.importedComponents = new Map(); // Map of local name -> import info
37
+ this.importedComponents = new Map();
38
38
  this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
39
39
  }
40
40
 
@@ -283,30 +283,30 @@ export class Transformer {
283
283
  return lines.join('\n');
284
284
  }
285
285
 
286
- /**
287
- * Transform router guard body - handles store references
288
- */
286
+ /** Helper to emit token value with proper string/template handling */
287
+ emitToken(token) {
288
+ if (token.type === 'STRING') return token.raw || JSON.stringify(token.value);
289
+ if (token.type === 'TEMPLATE') return token.raw || ('`' + token.value + '`');
290
+ return token.value;
291
+ }
292
+
293
+ /** Helper to check if space needed between tokens */
294
+ needsSpace(token, nextToken) {
295
+ if (!nextToken) return false;
296
+ return !PUNCT_NO_SPACE_BEFORE.includes(nextToken.type) && !PUNCT_NO_SPACE_AFTER.includes(token.type);
297
+ }
298
+
299
+ /** Transform router guard body - handles store references */
289
300
  transformRouterGuardBody(tokens) {
290
301
  let code = '';
291
302
  for (let i = 0; i < tokens.length; i++) {
292
303
  const token = tokens[i];
293
- if (token.type === 'STRING') {
294
- code += token.raw || JSON.stringify(token.value);
295
- } else if (token.type === 'TEMPLATE') {
296
- code += token.raw || ('`' + token.value + '`');
297
- } else if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
298
- // Transform store.xxx to $store.xxx for accessing combined store
304
+ if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
299
305
  code += '$store';
300
306
  } else {
301
- code += token.value;
302
- }
303
- // Add space between tokens unless it's punctuation (before or after)
304
- const nextToken = tokens[i + 1];
305
- const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
306
- const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
307
- if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
308
- code += ' ';
307
+ code += this.emitToken(token);
309
308
  }
309
+ if (this.needsSpace(token, tokens[i + 1])) code += ' ';
310
310
  }
311
311
  return code.trim();
312
312
  }
@@ -374,49 +374,22 @@ export class Transformer {
374
374
  return lines.join('\n');
375
375
  }
376
376
 
377
- /**
378
- * Transform store action body (this.x = y -> store.x.set(y))
379
- */
377
+ /** Transform store action body (this.x = y -> store.x.set(y)) */
380
378
  transformStoreActionBody(tokens) {
381
379
  let code = '';
382
380
  for (let i = 0; i < tokens.length; i++) {
383
381
  const token = tokens[i];
384
- // Transform 'this' to 'store'
385
- if (token.value === 'this') {
386
- code += 'store';
387
- } else if (token.type === 'STRING') {
388
- code += token.raw || JSON.stringify(token.value);
389
- } else if (token.type === 'TEMPLATE') {
390
- code += token.raw || ('`' + token.value + '`');
391
- } else if (token.type === 'COLON') {
392
- // Handle colon with proper spacing (ternary operator)
393
- code += ' : ';
394
- } else {
395
- code += token.value;
396
- }
397
- // Add space between tokens unless it's punctuation (before or after)
398
- const nextToken = tokens[i + 1];
399
- const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
400
- const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
401
- if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
402
- code += ' ';
403
- }
382
+ if (token.value === 'this') code += 'store';
383
+ else if (token.type === 'COLON') code += ' : ';
384
+ else code += this.emitToken(token);
385
+ if (this.needsSpace(token, tokens[i + 1])) code += ' ';
404
386
  }
405
-
406
- // Post-process: store.x = y -> store.x.set(y)
407
- code = code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)');
408
-
409
- return code.trim();
387
+ return code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)').trim();
410
388
  }
411
389
 
412
- /**
413
- * Transform store getter body (this.x -> store.x.get())
414
- */
390
+ /** Transform store getter body (this.x -> store.x.get()) */
415
391
  transformStoreGetterBody(tokens) {
416
- let code = this.transformStoreActionBody(tokens);
417
- // Transform store.x reads to store.x.get() (but not store.x.set or store.x.get)
418
- code = code.replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
419
- return code;
392
+ return this.transformStoreActionBody(tokens).replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
420
393
  }
421
394
 
422
395
  /**
@@ -479,76 +452,20 @@ export class Transformer {
479
452
  let code = '';
480
453
  let lastToken = null;
481
454
  let lastNonSpaceToken = null;
482
- const statementKeywords = ['let', 'const', 'var', 'return', 'if', 'else', 'for', 'while', 'switch', 'throw', 'try', 'catch', 'finally'];
483
- const builtinFunctions = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'alert', 'confirm', 'prompt', 'console', 'document', 'window', 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'fetch'];
484
-
485
- // Tokens that should not have space after them
486
- const noSpaceAfterTypes = new Set(['DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD']);
487
- const noSpaceAfterValues = new Set(['.', '(', '[', '{', '!', '~', '...']);
488
-
489
- // Tokens that should not have space before them
490
- const noSpaceBeforeTypes = new Set(['DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA', 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET']);
491
- const noSpaceBeforeValues = new Set(['.', ')', ']', '}', ';', ',', '++', '--', '(', '[']);
492
-
493
- // Check if token is a statement starter that the regex won't handle
494
- // (i.e., not a state variable assignment - those are handled by regex)
495
- // Statement keywords that have their own token types
496
- // Note: ELSE is excluded because it follows IF and should not have semicolon before it
497
- const statementTokenTypes = new Set(['IF', 'FOR', 'EACH']);
498
455
 
499
456
  const needsManualSemicolon = (token, nextToken, lastNonSpace) => {
500
- if (!token) return false;
501
-
502
- // Don't add semicolon after 'new' keyword (e.g., new Date())
503
- if (lastNonSpace?.value === 'new') return false;
504
-
505
- // Statement keywords with dedicated token types (if, else, for, etc.)
506
- if (statementTokenTypes.has(token.type)) return true;
507
-
508
- // Only process IDENT tokens from here
457
+ if (!token || lastNonSpace?.value === 'new') return false;
458
+ if (STATEMENT_TOKEN_TYPES.has(token.type)) return true;
509
459
  if (token.type !== 'IDENT') return false;
510
-
511
- // Statement keywords (let, const, var, return, etc.)
512
- if (statementKeywords.includes(token.value)) return true;
513
-
514
- // State variable assignment (stateVar = value)
515
- // These need semicolons BEFORE them when following a statement end
516
- // The regex adds semicolons AFTER, but not before
517
- if (this.stateVars.has(token.value) && nextToken?.type === 'EQ') {
518
- return true;
519
- }
520
-
521
- // Builtin function call or action call (not state var assignment)
522
- if (nextToken?.type === 'LPAREN') {
523
- if (builtinFunctions.includes(token.value)) return true;
524
- if (this.actionNames.has(token.value)) return true;
525
- }
526
-
527
- // Builtin method chain (e.g., document.body.classList.toggle(...))
528
- if (nextToken?.type === 'DOT' && builtinFunctions.includes(token.value)) {
529
- return true;
530
- }
531
-
460
+ if (STATEMENT_KEYWORDS.has(token.value)) return true;
461
+ if (this.stateVars.has(token.value) && nextToken?.type === 'EQ') return true;
462
+ if (nextToken?.type === 'LPAREN' && (BUILTIN_FUNCTIONS.has(token.value) || this.actionNames.has(token.value))) return true;
463
+ if (nextToken?.type === 'DOT' && BUILTIN_FUNCTIONS.has(token.value)) return true;
532
464
  return false;
533
465
  };
534
466
 
535
- // Check if previous context indicates end of statement
536
- const afterStatementEnd = (lastNonSpace) => {
537
- if (!lastNonSpace) return false;
538
- return lastNonSpace.type === 'RBRACE' ||
539
- lastNonSpace.type === 'RPAREN' ||
540
- lastNonSpace.type === 'RBRACKET' ||
541
- lastNonSpace.type === 'SEMICOLON' ||
542
- lastNonSpace.type === 'STRING' ||
543
- lastNonSpace.type === 'NUMBER' ||
544
- lastNonSpace.type === 'TRUE' ||
545
- lastNonSpace.type === 'FALSE' ||
546
- lastNonSpace.type === 'NULL' ||
547
- lastNonSpace.value === 'null' ||
548
- lastNonSpace.value === 'true' ||
549
- lastNonSpace.value === 'false' ||
550
- lastNonSpace.type === 'IDENT'; // Any identifier can end a statement (variables, function results, etc.)
551
- };
467
+ const STATEMENT_END_TYPES = new Set(['RBRACE', 'RPAREN', 'RBRACKET', 'SEMICOLON', 'STRING', 'NUMBER', 'TRUE', 'FALSE', 'NULL', 'IDENT']);
468
+ const afterStatementEnd = (t) => t && STATEMENT_END_TYPES.has(t.type);
552
469
 
553
470
  let afterIfCondition = false; // Track if we just closed an if(...) condition
554
471
 
@@ -604,43 +521,24 @@ export class Transformer {
604
521
  }
605
522
 
606
523
  // Decide whether to add space after this token
607
- let addSpace = true;
608
-
609
- // No space after certain tokens
610
- if (noSpaceAfterTypes.has(token.type) || noSpaceAfterValues.has(token.value)) {
611
- addSpace = false;
612
- }
613
-
614
- // No space before certain tokens (look ahead)
615
- if (nextToken && (noSpaceBeforeTypes.has(nextToken.type) || noSpaceBeforeValues.has(nextToken.value))) {
616
- addSpace = false;
617
- }
618
-
619
- if (addSpace && nextToken) {
620
- code += ' ';
621
- }
524
+ const noSpaceAfter = NO_SPACE_AFTER.has(token.type) || NO_SPACE_AFTER.has(token.value);
525
+ const noSpaceBefore = nextToken && (NO_SPACE_BEFORE.has(nextToken.type) || NO_SPACE_BEFORE.has(nextToken.value));
526
+ if (!noSpaceAfter && !noSpaceBefore && nextToken) code += ' ';
622
527
 
623
528
  lastToken = token;
624
529
  lastNonSpaceToken = token;
625
530
  }
626
531
 
627
- // Build patterns for boundaries
532
+ // Build patterns for state variable transformation
628
533
  const stateVarPattern = [...this.stateVars].join('|');
629
- const funcPattern = [...this.actionNames, ...builtinFunctions].join('|');
534
+ const funcPattern = [...this.actionNames, ...BUILTIN_FUNCTIONS].join('|');
535
+ const keywordsPattern = [...STATEMENT_KEYWORDS].join('|');
630
536
 
631
- // Transform state access - order matters!
632
- // First, replace state var assignments with boundary detection
633
- // Use multiple passes to handle the case where replacements change the boundaries
537
+ // Transform state var assignments: stateVar = value -> stateVar.set(value)
634
538
  for (const stateVar of this.stateVars) {
635
- // Replace standalone state var assignments: stateVar = value -> stateVar.set(value)
636
- // Use negative lookahead (?!=) to avoid matching === or ==
637
- // Stop at: next state var assignment (original or already replaced), function call, statement keyword, or end
638
- const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${statementKeywords.join('|')})\\b|;|$`;
639
- const assignPattern = new RegExp(
640
- `\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`,
641
- 'g'
642
- );
643
- code = code.replace(assignPattern, (_match, value) => `${stateVar}.set(${value.trim()});`);
539
+ const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${keywordsPattern})\\b|;|$`;
540
+ const assignPattern = new RegExp(`\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`, 'g');
541
+ code = code.replace(assignPattern, (_, value) => `${stateVar}.set(${value.trim()});`);
644
542
  }
645
543
 
646
544
  // Clean up any double semicolons
@@ -700,44 +598,24 @@ export class Transformer {
700
598
  return lines.join('\n');
701
599
  }
702
600
 
703
- /**
704
- * Transform a view node (element, directive, slot, text)
705
- */
601
+ /** View node transformers lookup table */
602
+ static VIEW_NODE_HANDLERS = {
603
+ [NodeType.Element]: 'transformElement',
604
+ [NodeType.TextNode]: 'transformTextNode',
605
+ [NodeType.IfDirective]: 'transformIfDirective',
606
+ [NodeType.EachDirective]: 'transformEachDirective',
607
+ [NodeType.EventDirective]: 'transformEventDirective',
608
+ [NodeType.SlotElement]: 'transformSlot',
609
+ [NodeType.LinkDirective]: 'transformLinkDirective',
610
+ [NodeType.OutletDirective]: 'transformOutletDirective',
611
+ [NodeType.NavigateDirective]: 'transformNavigateDirective'
612
+ };
613
+
614
+ /** Transform a view node (element, directive, slot, text) */
706
615
  transformViewNode(node, indent = 0) {
707
- const pad = ' '.repeat(indent);
708
-
709
- switch (node.type) {
710
- case NodeType.Element:
711
- return this.transformElement(node, indent);
712
-
713
- case NodeType.TextNode:
714
- return this.transformTextNode(node, indent);
715
-
716
- case NodeType.IfDirective:
717
- return this.transformIfDirective(node, indent);
718
-
719
- case NodeType.EachDirective:
720
- return this.transformEachDirective(node, indent);
721
-
722
- case NodeType.EventDirective:
723
- return this.transformEventDirective(node, indent);
724
-
725
- case NodeType.SlotElement:
726
- return this.transformSlot(node, indent);
727
-
728
- // Router directives
729
- case NodeType.LinkDirective:
730
- return this.transformLinkDirective(node, indent);
731
-
732
- case NodeType.OutletDirective:
733
- return this.transformOutletDirective(node, indent);
734
-
735
- case NodeType.NavigateDirective:
736
- return this.transformNavigateDirective(node, indent);
737
-
738
- default:
739
- return `${pad}/* unknown node: ${node.type} */`;
740
- }
616
+ const handler = Transformer.VIEW_NODE_HANDLERS[node.type];
617
+ if (handler) return this[handler](node, indent);
618
+ return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
741
619
  }
742
620
 
743
621
  /**
package/package.json CHANGED
@@ -1,15 +1,30 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "types": "types/index.d.ts",
7
8
  "bin": {
8
9
  "pulse": "cli/index.js"
9
10
  },
10
11
  "exports": {
11
- ".": "./index.js",
12
- "./runtime": "./runtime/index.js",
12
+ ".": {
13
+ "types": "./types/index.d.ts",
14
+ "default": "./index.js"
15
+ },
16
+ "./runtime": {
17
+ "types": "./types/index.d.ts",
18
+ "default": "./runtime/index.js"
19
+ },
20
+ "./runtime/router": {
21
+ "types": "./types/router.d.ts",
22
+ "default": "./runtime/router.js"
23
+ },
24
+ "./runtime/store": {
25
+ "types": "./types/store.d.ts",
26
+ "default": "./runtime/store.js"
27
+ },
13
28
  "./runtime/*": "./runtime/*.js",
14
29
  "./compiler": "./compiler/index.js",
15
30
  "./vite": "./loader/vite-plugin.js"
@@ -21,14 +36,17 @@
21
36
  "compiler/",
22
37
  "loader/",
23
38
  "mobile/",
39
+ "types/",
24
40
  "README.md",
25
41
  "LICENSE"
26
42
  ],
27
43
  "scripts": {
28
- "test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:lint && npm run test:format && npm run test:analyze",
44
+ "test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:lint && npm run test:format && npm run test:analyze",
29
45
  "test:compiler": "node test/compiler.test.js",
30
46
  "test:pulse": "node test/pulse.test.js",
31
47
  "test:dom": "node test/dom.test.js",
48
+ "test:router": "node test/router.test.js",
49
+ "test:store": "node test/store.test.js",
32
50
  "test:lint": "node test/lint.test.js",
33
51
  "test:format": "node test/format.test.js",
34
52
  "test:analyze": "node test/analyze.test.js",