pulse-js-framework 1.4.2 → 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.
package/README.md CHANGED
@@ -14,6 +14,7 @@ A declarative DOM framework with CSS selector-based structure and reactive pulsa
14
14
  - **Lightweight** - Minimal footprint, maximum performance
15
15
  - **Router & Store** - Built-in SPA routing and state management
16
16
  - **Mobile Apps** - Build native Android & iOS apps (zero dependencies)
17
+ - **TypeScript Support** - Full type definitions for IDE autocomplete
17
18
 
18
19
  ## Installation
19
20
 
@@ -26,7 +27,7 @@ npm install pulse-js-framework
26
27
  ### Create a new project
27
28
 
28
29
  ```bash
29
- npx pulse create my-app
30
+ npx pulse-js-framework create my-app
30
31
  cd my-app
31
32
  npm install
32
33
  npm run dev
@@ -398,6 +399,27 @@ onNativeReady(({ platform }) => {
398
399
 
399
400
  **Available APIs:** Storage, Device Info, Network Status, Toast, Vibration, Clipboard, App Lifecycle
400
401
 
402
+ ## TypeScript Support
403
+
404
+ Pulse includes full TypeScript definitions for IDE autocomplete and type checking:
405
+
406
+ ```typescript
407
+ import { pulse, effect, computed, Pulse } from 'pulse-js-framework/runtime';
408
+ import { el, list, when } from 'pulse-js-framework/runtime';
409
+ import { createRouter, Router } from 'pulse-js-framework/runtime/router';
410
+ import { createStore, Store } from 'pulse-js-framework/runtime/store';
411
+
412
+ // Full autocomplete for all APIs
413
+ const count: Pulse<number> = pulse(0);
414
+ const doubled = computed(() => count.get() * 2);
415
+
416
+ effect(() => {
417
+ console.log(count.get()); // Type-safe access
418
+ });
419
+ ```
420
+
421
+ Types are automatically detected by IDEs (VS Code, WebStorm) without additional configuration.
422
+
401
423
  ## Examples
402
424
 
403
425
  - [Blog](examples/blog) - Full blog app with CRUD, categories, search, dark mode
package/cli/build.js CHANGED
@@ -120,7 +120,7 @@ function processDirectory(srcDir, outDir) {
120
120
 
121
121
  // Rewrite runtime imports
122
122
  content = content.replace(
123
- /from\s+['"]pulse-framework\/runtime['"]/g,
123
+ /from\s+['"]pulse-js-framework\/runtime['"]/g,
124
124
  "from './runtime.js'"
125
125
  );
126
126
 
@@ -168,7 +168,7 @@ ${readRuntimeFile('store.js')}
168
168
  */
169
169
  function readRuntimeFile(filename) {
170
170
  const paths = [
171
- join(process.cwd(), 'node_modules', 'pulse-framework', 'runtime', filename),
171
+ join(process.cwd(), 'node_modules', 'pulse-js-framework', 'runtime', filename),
172
172
  join(dirname(new URL(import.meta.url).pathname), '..', 'runtime', filename)
173
173
  ];
174
174
 
package/cli/dev.js CHANGED
@@ -79,7 +79,7 @@ export async function startDevServer(args) {
79
79
  try {
80
80
  const source = readFileSync(filePath, 'utf-8');
81
81
  const result = compile(source, {
82
- runtime: '/node_modules/pulse-framework/runtime/index.js'
82
+ runtime: '/node_modules/pulse-js-framework/runtime/index.js'
83
83
  });
84
84
 
85
85
  if (result.success) {
@@ -108,8 +108,8 @@ export async function startDevServer(args) {
108
108
  }
109
109
 
110
110
  // Handle node_modules
111
- if (pathname.startsWith('/node_modules/pulse-framework/')) {
112
- const modulePath = join(root, '..', 'pulse', pathname.replace('/node_modules/pulse-framework/', ''));
111
+ if (pathname.startsWith('/node_modules/pulse-js-framework/')) {
112
+ const modulePath = join(root, '..', 'pulse', pathname.replace('/node_modules/pulse-js-framework/', ''));
113
113
  if (existsSync(modulePath)) {
114
114
  const content = readFileSync(modulePath, 'utf-8');
115
115
  res.writeHead(200, { 'Content-Type': 'application/javascript' });
package/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync } from 'fs';
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
13
13
 
14
- const VERSION = '1.4.2';
14
+ const VERSION = '1.4.3';
15
15
 
16
16
  // Command handlers
17
17
  const commands = {
package/cli/mobile.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'fs';
7
- import { join, resolve, dirname } from 'path';
7
+ import { join, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { execSync } from 'child_process';
10
10
 
@@ -14,6 +14,38 @@ const __dirname = dirname(__filename);
14
14
  const MOBILE_DIR = 'mobile';
15
15
  const CONFIG_FILE = 'pulse.mobile.json';
16
16
 
17
+ // ============================================================================
18
+ // Helper functions
19
+ // ============================================================================
20
+
21
+ /** Create directory if it doesn't exist */
22
+ const mkdirp = (path) => !existsSync(path) && mkdirSync(path, { recursive: true });
23
+
24
+ /** Create multiple directories at once */
25
+ const mkdirs = (base, dirs) => dirs.forEach(d => mkdirp(join(base, d)));
26
+
27
+ /** Write file (auto-creates parent directories) */
28
+ const writeFile = (path, content) => {
29
+ mkdirp(dirname(path));
30
+ writeFileSync(path, content.trim());
31
+ };
32
+
33
+ /** Apply template variables to content */
34
+ const applyTemplate = (content, config) => content
35
+ .replace(/\{\{APP_NAME\}\}/g, config.name)
36
+ .replace(/\{\{DISPLAY_NAME\}\}/g, config.displayName)
37
+ .replace(/\{\{PACKAGE_ID\}\}/g, config.packageId)
38
+ .replace(/\{\{VERSION\}\}/g, config.version)
39
+ .replace(/\{\{MIN_SDK\}\}/g, String(config.android?.minSdkVersion || 24))
40
+ .replace(/\{\{TARGET_SDK\}\}/g, String(config.android?.targetSdkVersion || 34))
41
+ .replace(/\{\{COMPILE_SDK\}\}/g, String(config.android?.compileSdkVersion || 34))
42
+ .replace(/\{\{IOS_TARGET\}\}/g, config.ios?.deploymentTarget || '13.0');
43
+
44
+ /** Convert string to PascalCase */
45
+ const toPascalCase = (str) => str
46
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
47
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
48
+
17
49
  /**
18
50
  * Handle mobile subcommands
19
51
  */
@@ -419,67 +451,35 @@ function loadConfig(root) {
419
451
  return JSON.parse(readFileSync(configPath, 'utf-8'));
420
452
  }
421
453
 
422
- /**
423
- * Copy and process template files
424
- */
454
+ /** Copy and process template files */
425
455
  function copyAndProcessTemplate(src, dest, config) {
426
- if (!existsSync(src)) {
427
- return;
428
- }
429
-
430
- mkdirSync(dest, { recursive: true });
456
+ if (!existsSync(src)) return;
457
+ mkdirp(dest);
431
458
 
432
- const files = readdirSync(src, { withFileTypes: true });
433
-
434
- for (const file of files) {
459
+ for (const file of readdirSync(src, { withFileTypes: true })) {
435
460
  const srcPath = join(src, file.name);
436
461
  const destPath = join(dest, file.name);
437
462
 
438
463
  if (file.isDirectory()) {
439
464
  copyAndProcessTemplate(srcPath, destPath, config);
440
465
  } else {
441
- let content = readFileSync(srcPath, 'utf-8');
442
-
443
- // Replace template variables
444
- content = content
445
- .replace(/\{\{APP_NAME\}\}/g, config.name)
446
- .replace(/\{\{DISPLAY_NAME\}\}/g, config.displayName)
447
- .replace(/\{\{PACKAGE_ID\}\}/g, config.packageId)
448
- .replace(/\{\{VERSION\}\}/g, config.version)
449
- .replace(/\{\{MIN_SDK\}\}/g, String(config.android?.minSdkVersion || 24))
450
- .replace(/\{\{TARGET_SDK\}\}/g, String(config.android?.targetSdkVersion || 34))
451
- .replace(/\{\{COMPILE_SDK\}\}/g, String(config.android?.compileSdkVersion || 34))
452
- .replace(/\{\{IOS_TARGET\}\}/g, config.ios?.deploymentTarget || '13.0');
453
-
454
- writeFileSync(destPath, content);
466
+ writeFileSync(destPath, applyTemplate(readFileSync(srcPath, 'utf-8'), config));
455
467
  }
456
468
  }
457
469
  }
458
470
 
459
- /**
460
- * Create Android project from scratch
461
- */
471
+ /** Create Android project from scratch */
462
472
  function createAndroidProject(androidDir, config) {
463
473
  const packagePath = config.packageId.replace(/\./g, '/');
464
474
 
465
475
  // Create directory structure
466
- const dirs = [
476
+ mkdirs(androidDir, [
467
477
  'app/src/main/java/' + packagePath,
468
- 'app/src/main/res/layout',
469
- 'app/src/main/res/values',
470
- 'app/src/main/res/drawable',
471
- 'app/src/main/res/mipmap-hdpi',
472
- 'app/src/main/res/mipmap-mdpi',
473
- 'app/src/main/res/mipmap-xhdpi',
474
- 'app/src/main/res/mipmap-xxhdpi',
475
- 'app/src/main/res/mipmap-xxxhdpi',
476
- 'app/src/main/assets/www',
477
- 'gradle/wrapper'
478
- ];
479
-
480
- for (const dir of dirs) {
481
- mkdirSync(join(androidDir, dir), { recursive: true });
482
- }
478
+ 'app/src/main/res/layout', 'app/src/main/res/values', 'app/src/main/res/drawable',
479
+ 'app/src/main/res/mipmap-hdpi', 'app/src/main/res/mipmap-mdpi',
480
+ 'app/src/main/res/mipmap-xhdpi', 'app/src/main/res/mipmap-xxhdpi', 'app/src/main/res/mipmap-xxxhdpi',
481
+ 'app/src/main/assets/www', 'gradle/wrapper'
482
+ ]);
483
483
 
484
484
  // MainActivity.java
485
485
  writeFileSync(join(androidDir, 'app/src/main/java', packagePath, 'MainActivity.java'), `
@@ -906,21 +906,9 @@ gradle %*
906
906
  `.trim());
907
907
  }
908
908
 
909
- /**
910
- * Create iOS project from scratch
911
- */
909
+ /** Create iOS project from scratch */
912
910
  function createIOSProject(iosDir, config) {
913
- // Create directory structure
914
- const dirs = [
915
- 'PulseApp',
916
- 'PulseApp/www',
917
- 'PulseApp/Assets.xcassets',
918
- 'PulseApp.xcodeproj'
919
- ];
920
-
921
- for (const dir of dirs) {
922
- mkdirSync(join(iosDir, dir), { recursive: true });
923
- }
911
+ mkdirs(iosDir, ['PulseApp', 'PulseApp/www', 'PulseApp/Assets.xcassets', 'PulseApp.xcodeproj']);
924
912
 
925
913
  // AppDelegate.swift
926
914
  writeFileSync(join(iosDir, 'PulseApp/AppDelegate.swift'), `
@@ -1430,18 +1418,7 @@ function generateXcodeProject(config) {
1430
1418
  `;
1431
1419
  }
1432
1420
 
1433
- /**
1434
- * Convert string to PascalCase
1435
- */
1436
- function toPascalCase(str) {
1437
- return str
1438
- .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
1439
- .replace(/^(.)/, (_, char) => char.toUpperCase());
1440
- }
1441
-
1442
- /**
1443
- * Show mobile help
1444
- */
1421
+ /** Show mobile help */
1445
1422
  function showMobileHelp() {
1446
1423
  console.log(`
1447
1424
  Pulse Mobile - Zero-Dependency Mobile Platform
@@ -384,39 +384,36 @@ export class Parser {
384
384
  return new ASTNode(NodeType.Property, { name: name.value, value });
385
385
  }
386
386
 
387
+ /**
388
+ * Try to parse a literal token (STRING, NUMBER, TRUE, FALSE, NULL)
389
+ * Returns the AST node or null if not a literal
390
+ */
391
+ tryParseLiteral() {
392
+ const token = this.current();
393
+ if (!token) return null;
394
+
395
+ const literalMap = {
396
+ [TokenType.STRING]: () => new ASTNode(NodeType.Literal, { value: this.advance().value, raw: token.raw }),
397
+ [TokenType.NUMBER]: () => new ASTNode(NodeType.Literal, { value: this.advance().value }),
398
+ [TokenType.TRUE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: true })),
399
+ [TokenType.FALSE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: false })),
400
+ [TokenType.NULL]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: null }))
401
+ };
402
+
403
+ return literalMap[token.type]?.() || null;
404
+ }
405
+
387
406
  /**
388
407
  * Parse a value (literal, object, array, etc.)
389
408
  */
390
409
  parseValue() {
391
- if (this.is(TokenType.LBRACE)) {
392
- return this.parseObjectLiteral();
393
- }
394
- if (this.is(TokenType.LBRACKET)) {
395
- return this.parseArrayLiteral();
396
- }
397
- if (this.is(TokenType.STRING)) {
398
- const token = this.advance();
399
- return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
400
- }
401
- if (this.is(TokenType.NUMBER)) {
402
- const token = this.advance();
403
- return new ASTNode(NodeType.Literal, { value: token.value });
404
- }
405
- if (this.is(TokenType.TRUE)) {
406
- this.advance();
407
- return new ASTNode(NodeType.Literal, { value: true });
408
- }
409
- if (this.is(TokenType.FALSE)) {
410
- this.advance();
411
- return new ASTNode(NodeType.Literal, { value: false });
412
- }
413
- if (this.is(TokenType.NULL)) {
414
- this.advance();
415
- return new ASTNode(NodeType.Literal, { value: null });
416
- }
417
- if (this.is(TokenType.IDENT)) {
418
- return this.parseIdentifierOrExpression();
419
- }
410
+ if (this.is(TokenType.LBRACE)) return this.parseObjectLiteral();
411
+ if (this.is(TokenType.LBRACKET)) return this.parseArrayLiteral();
412
+
413
+ const literal = this.tryParseLiteral();
414
+ if (literal) return literal;
415
+
416
+ if (this.is(TokenType.IDENT)) return this.parseIdentifierOrExpression();
420
417
 
421
418
  throw new Error(
422
419
  `Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
@@ -612,32 +609,18 @@ export class Parser {
612
609
 
613
610
  let value;
614
611
  if (this.is(TokenType.LBRACE)) {
615
- // Expression prop: name={expression}
616
- this.advance(); // consume {
612
+ this.advance();
617
613
  value = this.parseExpression();
618
614
  this.expect(TokenType.RBRACE);
619
- } else if (this.is(TokenType.STRING)) {
620
- // String prop: name="value"
621
- const token = this.advance();
622
- value = new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
623
- } else if (this.is(TokenType.NUMBER)) {
624
- // Number prop: name=123
625
- const token = this.advance();
626
- value = new ASTNode(NodeType.Literal, { value: token.value });
627
- } else if (this.is(TokenType.TRUE)) {
628
- this.advance();
629
- value = new ASTNode(NodeType.Literal, { value: true });
630
- } else if (this.is(TokenType.FALSE)) {
631
- this.advance();
632
- value = new ASTNode(NodeType.Literal, { value: false });
633
- } else if (this.is(TokenType.NULL)) {
634
- this.advance();
635
- value = new ASTNode(NodeType.Literal, { value: null });
636
- } else if (this.is(TokenType.IDENT)) {
637
- // Identifier prop: name=someVar
638
- value = this.parseIdentifierOrExpression();
639
615
  } else {
640
- throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
616
+ value = this.tryParseLiteral();
617
+ if (!value) {
618
+ if (this.is(TokenType.IDENT)) {
619
+ value = this.parseIdentifierOrExpression();
620
+ } else {
621
+ throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
622
+ }
623
+ }
641
624
  }
642
625
 
643
626
  return new ASTNode(NodeType.Property, { name: name.value, value });
@@ -885,80 +868,39 @@ export class Parser {
885
868
  }
886
869
 
887
870
  /**
888
- * Parse OR expression
889
- */
890
- parseOrExpression() {
891
- let left = this.parseAndExpression();
892
-
893
- while (this.is(TokenType.OR)) {
894
- this.advance();
895
- const right = this.parseAndExpression();
896
- left = new ASTNode(NodeType.BinaryExpression, { operator: '||', left, right });
897
- }
898
-
899
- return left;
900
- }
901
-
902
- /**
903
- * Parse AND expression
871
+ * Binary operator precedence table (higher = binds tighter)
904
872
  */
905
- parseAndExpression() {
906
- let left = this.parseComparisonExpression();
907
-
908
- while (this.is(TokenType.AND)) {
909
- this.advance();
910
- const right = this.parseComparisonExpression();
911
- left = new ASTNode(NodeType.BinaryExpression, { operator: '&&', left, right });
912
- }
913
-
914
- return left;
915
- }
873
+ static BINARY_OPS = [
874
+ { ops: [TokenType.OR], name: 'or' },
875
+ { ops: [TokenType.AND], name: 'and' },
876
+ { ops: [TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
877
+ TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE], name: 'comparison' },
878
+ { ops: [TokenType.PLUS, TokenType.MINUS], name: 'additive' },
879
+ { ops: [TokenType.STAR, TokenType.SLASH, TokenType.PERCENT], name: 'multiplicative' }
880
+ ];
916
881
 
917
882
  /**
918
- * Parse comparison expression
883
+ * Generic binary expression parser using precedence climbing
919
884
  */
920
- parseComparisonExpression() {
921
- let left = this.parseAdditiveExpression();
922
-
923
- while (this.isAny(TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
924
- TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE)) {
925
- const operator = this.advance().value;
926
- const right = this.parseAdditiveExpression();
927
- left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
885
+ parseBinaryExpr(level = 0) {
886
+ if (level >= Parser.BINARY_OPS.length) {
887
+ return this.parseUnaryExpression();
928
888
  }
929
889
 
930
- return left;
931
- }
932
-
933
- /**
934
- * Parse additive expression
935
- */
936
- parseAdditiveExpression() {
937
- let left = this.parseMultiplicativeExpression();
890
+ let left = this.parseBinaryExpr(level + 1);
891
+ const { ops } = Parser.BINARY_OPS[level];
938
892
 
939
- while (this.isAny(TokenType.PLUS, TokenType.MINUS)) {
893
+ while (this.isAny(...ops)) {
940
894
  const operator = this.advance().value;
941
- const right = this.parseMultiplicativeExpression();
895
+ const right = this.parseBinaryExpr(level + 1);
942
896
  left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
943
897
  }
944
898
 
945
899
  return left;
946
900
  }
947
901
 
948
- /**
949
- * Parse multiplicative expression
950
- */
951
- parseMultiplicativeExpression() {
952
- let left = this.parseUnaryExpression();
953
-
954
- while (this.isAny(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
955
- const operator = this.advance().value;
956
- const right = this.parseUnaryExpression();
957
- left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
958
- }
959
-
960
- return left;
961
- }
902
+ /** Parse OR expression (entry point for binary expressions) */
903
+ parseOrExpression() { return this.parseBinaryExpr(0); }
962
904
 
963
905
  /**
964
906
  * Parse unary expression
@@ -1048,30 +990,9 @@ export class Parser {
1048
990
  return new ASTNode(NodeType.SpreadElement, { argument });
1049
991
  }
1050
992
 
1051
- if (this.is(TokenType.NUMBER)) {
1052
- const token = this.advance();
1053
- return new ASTNode(NodeType.Literal, { value: token.value });
1054
- }
1055
-
1056
- if (this.is(TokenType.STRING)) {
1057
- const token = this.advance();
1058
- return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
1059
- }
1060
-
1061
- if (this.is(TokenType.TRUE)) {
1062
- this.advance();
1063
- return new ASTNode(NodeType.Literal, { value: true });
1064
- }
1065
-
1066
- if (this.is(TokenType.FALSE)) {
1067
- this.advance();
1068
- return new ASTNode(NodeType.Literal, { value: false });
1069
- }
1070
-
1071
- if (this.is(TokenType.NULL)) {
1072
- this.advance();
1073
- return new ASTNode(NodeType.Literal, { value: null });
1074
- }
993
+ // Try parsing a literal (NUMBER, STRING, TRUE, FALSE, NULL)
994
+ const literal = this.tryParseLiteral();
995
+ if (literal) return literal;
1075
996
 
1076
997
  // In expressions, SELECTOR tokens should be treated as IDENT
1077
998
  // This happens when identifiers like 'selectedCategory' are followed by space in view context