vsn 1.0.0 → 1.0.1

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/dist/index.cjs CHANGED
@@ -513,10 +513,12 @@ var ProgramNode = class extends BaseNode {
513
513
  }
514
514
  };
515
515
  var UseNode = class extends BaseNode {
516
- constructor(name, alias) {
516
+ constructor(name, alias, flags = {}, flagArgs = {}) {
517
517
  super("Use");
518
518
  this.name = name;
519
519
  this.alias = alias;
520
+ this.flags = flags;
521
+ this.flagArgs = flagArgs;
520
522
  }
521
523
  };
522
524
  var BlockNode = class extends BaseNode {
@@ -572,16 +574,20 @@ var OnBlockNode = class extends BaseNode {
572
574
  }
573
575
  };
574
576
  var AssignmentNode = class extends BaseNode {
575
- constructor(target, value) {
577
+ constructor(target, value, operator = "=") {
576
578
  super("Assignment");
577
579
  this.target = target;
578
580
  this.value = value;
581
+ this.operator = operator;
579
582
  }
580
583
  async evaluate(context) {
581
584
  if (!context.scope || !context.scope.setPath) {
582
585
  return void 0;
583
586
  }
584
587
  const value = await this.value.evaluate(context);
588
+ if (this.operator !== "=") {
589
+ return this.applyCompoundAssignment(context, value);
590
+ }
585
591
  if (this.target instanceof IdentifierExpression && this.target.name.startsWith("root.") && context.rootScope) {
586
592
  const path = this.target.name.slice("root.".length);
587
593
  context.rootScope.setPath?.(`self.${path}`, value);
@@ -590,6 +596,31 @@ var AssignmentNode = class extends BaseNode {
590
596
  this.assignTarget(context, this.target, value);
591
597
  return value;
592
598
  }
599
+ applyCompoundAssignment(context, value) {
600
+ if (!context.scope || !context.scope.setPath) {
601
+ return void 0;
602
+ }
603
+ if (!(this.target instanceof IdentifierExpression)) {
604
+ throw new Error("Compound assignment requires a simple identifier");
605
+ }
606
+ const isRoot = this.target.name.startsWith("root.");
607
+ const scope = isRoot && context.rootScope ? context.rootScope : context.scope;
608
+ const rawPath = isRoot ? this.target.name.slice("root.".length) : this.target.name;
609
+ const path = isRoot ? `self.${rawPath}` : rawPath;
610
+ const current = scope?.getPath ? scope.getPath(path) : void 0;
611
+ let result;
612
+ if (this.operator === "+=") {
613
+ result = current + value;
614
+ } else if (this.operator === "-=") {
615
+ result = current - value;
616
+ } else if (this.operator === "*=") {
617
+ result = current * value;
618
+ } else {
619
+ result = current / value;
620
+ }
621
+ scope?.setPath?.(path, result);
622
+ return result;
623
+ }
593
624
  assignTarget(context, target, value) {
594
625
  if (!context.scope || !context.scope.setPath) {
595
626
  return;
@@ -1460,11 +1491,46 @@ var Parser = class _Parser {
1460
1491
  this.stream.skipWhitespace();
1461
1492
  alias = this.stream.expect("Identifier" /* Identifier */).value;
1462
1493
  }
1494
+ const { flags, flagArgs } = this.parseUseFlags();
1463
1495
  this.stream.skipWhitespace();
1464
1496
  this.stream.expect("Semicolon" /* Semicolon */);
1465
- return new UseNode(name, alias);
1497
+ return new UseNode(name, alias, flags, flagArgs);
1466
1498
  });
1467
1499
  }
1500
+ parseUseFlags() {
1501
+ const flags = {};
1502
+ const flagArgs = {};
1503
+ while (true) {
1504
+ this.stream.skipWhitespace();
1505
+ if (this.stream.peek()?.type !== "Bang" /* Bang */) {
1506
+ break;
1507
+ }
1508
+ this.stream.next();
1509
+ const name = this.stream.expect("Identifier" /* Identifier */).value;
1510
+ if (name !== "wait") {
1511
+ throw new Error(`Unknown flag ${name}`);
1512
+ }
1513
+ flags.wait = true;
1514
+ if (this.stream.peek()?.type === "LParen" /* LParen */) {
1515
+ this.stream.next();
1516
+ this.stream.skipWhitespace();
1517
+ const timeoutToken = this.stream.expect("Number" /* Number */);
1518
+ const timeoutMs = Number(timeoutToken.value);
1519
+ let intervalMs;
1520
+ this.stream.skipWhitespace();
1521
+ if (this.stream.peek()?.type === "Comma" /* Comma */) {
1522
+ this.stream.next();
1523
+ this.stream.skipWhitespace();
1524
+ const intervalToken = this.stream.expect("Number" /* Number */);
1525
+ intervalMs = Number(intervalToken.value);
1526
+ this.stream.skipWhitespace();
1527
+ }
1528
+ this.stream.expect("RParen" /* RParen */);
1529
+ flagArgs.wait = { timeoutMs, ...intervalMs !== void 0 ? { intervalMs } : {} };
1530
+ }
1531
+ }
1532
+ return { flags, flagArgs };
1533
+ }
1468
1534
  wrapErrors(fn) {
1469
1535
  try {
1470
1536
  return fn();
@@ -1695,11 +1761,11 @@ ${caret}`;
1695
1761
  parseAssignment() {
1696
1762
  const target = this.parseAssignmentTarget();
1697
1763
  this.stream.skipWhitespace();
1698
- this.stream.expect("Equals" /* Equals */);
1764
+ const operator = this.parseAssignmentOperator();
1699
1765
  this.stream.skipWhitespace();
1700
1766
  const value = this.parseExpression();
1701
1767
  this.consumeStatementTerminator();
1702
- return new AssignmentNode(target, value);
1768
+ return new AssignmentNode(target, value, operator);
1703
1769
  }
1704
1770
  parseExpression() {
1705
1771
  return this.parsePipeExpression();
@@ -2560,12 +2626,12 @@ ${caret}`;
2560
2626
  while (this.stream.peekNonWhitespace(index)?.type === "Dot" /* Dot */ && this.stream.peekNonWhitespace(index + 1)?.type === "Identifier" /* Identifier */) {
2561
2627
  index += 2;
2562
2628
  }
2563
- return this.stream.peekNonWhitespace(index)?.type === "Equals" /* Equals */;
2629
+ return this.isAssignmentOperatorStart(index);
2564
2630
  }
2565
2631
  if (first.type === "At" /* At */ || first.type === "Dollar" /* Dollar */) {
2566
2632
  const second = this.stream.peekNonWhitespace(1);
2567
2633
  const third = this.stream.peekNonWhitespace(2);
2568
- return second?.type === "Identifier" /* Identifier */ && third?.type === "Equals" /* Equals */;
2634
+ return second?.type === "Identifier" /* Identifier */ && this.isAssignmentOperatorStart(2);
2569
2635
  }
2570
2636
  if (first.type === "LBrace" /* LBrace */ || first.type === "LBracket" /* LBracket */) {
2571
2637
  const stack = [];
@@ -2580,7 +2646,7 @@ ${caret}`;
2580
2646
  } else if (token.type === "RBrace" /* RBrace */ || token.type === "RBracket" /* RBracket */) {
2581
2647
  stack.pop();
2582
2648
  if (stack.length === 0) {
2583
- return this.stream.peekNonWhitespace(index + 1)?.type === "Equals" /* Equals */;
2649
+ return this.isAssignmentOperatorStart(index + 1);
2584
2650
  }
2585
2651
  }
2586
2652
  index += 1;
@@ -2588,6 +2654,20 @@ ${caret}`;
2588
2654
  }
2589
2655
  return false;
2590
2656
  }
2657
+ isAssignmentOperatorStart(index) {
2658
+ const token = this.stream.peekNonWhitespace(index);
2659
+ if (!token) {
2660
+ return false;
2661
+ }
2662
+ if (token.type === "Equals" /* Equals */) {
2663
+ return true;
2664
+ }
2665
+ if (token.type === "Plus" /* Plus */ || token.type === "Minus" /* Minus */ || token.type === "Star" /* Star */ || token.type === "Slash" /* Slash */) {
2666
+ const next = this.stream.peekNonWhitespace(index + 1);
2667
+ return next?.type === "Equals" /* Equals */;
2668
+ }
2669
+ return false;
2670
+ }
2591
2671
  isCallStart() {
2592
2672
  const first = this.stream.peekNonWhitespace(0);
2593
2673
  if (!first || first.type !== "Identifier" /* Identifier */) {
@@ -2812,10 +2892,35 @@ ${caret}`;
2812
2892
  parseAssignmentExpression() {
2813
2893
  const target = this.parseAssignmentTarget();
2814
2894
  this.stream.skipWhitespace();
2815
- this.stream.expect("Equals" /* Equals */);
2895
+ const operator = this.parseAssignmentOperator();
2816
2896
  this.stream.skipWhitespace();
2817
2897
  const value = this.parseExpression();
2818
- return new AssignmentNode(target, value);
2898
+ return new AssignmentNode(target, value, operator);
2899
+ }
2900
+ parseAssignmentOperator() {
2901
+ const next = this.stream.peek();
2902
+ if (!next) {
2903
+ throw new Error("Expected assignment operator");
2904
+ }
2905
+ if (next.type === "Equals" /* Equals */) {
2906
+ this.stream.next();
2907
+ return "=";
2908
+ }
2909
+ if (next.type === "Plus" /* Plus */ || next.type === "Minus" /* Minus */ || next.type === "Star" /* Star */ || next.type === "Slash" /* Slash */) {
2910
+ const op = this.stream.next();
2911
+ this.stream.expect("Equals" /* Equals */);
2912
+ if (op.type === "Plus" /* Plus */) {
2913
+ return "+=";
2914
+ }
2915
+ if (op.type === "Minus" /* Minus */) {
2916
+ return "-=";
2917
+ }
2918
+ if (op.type === "Star" /* Star */) {
2919
+ return "*=";
2920
+ }
2921
+ return "/=";
2922
+ }
2923
+ throw new Error("Expected assignment operator");
2819
2924
  }
2820
2925
  parseTryBlock() {
2821
2926
  this.stream.expect("Try" /* Try */);
@@ -3318,6 +3423,7 @@ var Engine = class _Engine {
3318
3423
  ignoredAdded = /* @__PURE__ */ new WeakMap();
3319
3424
  diagnostics;
3320
3425
  logger;
3426
+ pendingUses = [];
3321
3427
  constructor(options = {}) {
3322
3428
  this.diagnostics = options.diagnostics ?? false;
3323
3429
  this.logger = options.logger ?? console;
@@ -3387,6 +3493,10 @@ var Engine = class _Engine {
3387
3493
  registerBehaviors(source) {
3388
3494
  const program = new Parser(source, { customFlags: new Set(this.flagHandlers.keys()) }).parseProgram();
3389
3495
  for (const use of program.uses) {
3496
+ if (use.flags?.wait) {
3497
+ this.pendingUses.push(this.waitForUseGlobal(use));
3498
+ continue;
3499
+ }
3390
3500
  const value = this.resolveGlobalPath(use.name);
3391
3501
  if (value === void 0) {
3392
3502
  console.warn(`vsn: global '${use.name}' not found`);
@@ -3436,6 +3546,52 @@ var Engine = class _Engine {
3436
3546
  }
3437
3547
  return value;
3438
3548
  }
3549
+ async waitForUses() {
3550
+ while (this.pendingUses.length > 0) {
3551
+ const pending = this.pendingUses;
3552
+ this.pendingUses = [];
3553
+ await Promise.all(pending);
3554
+ }
3555
+ }
3556
+ waitForUseGlobal(use) {
3557
+ const config = use.flagArgs?.wait ?? {};
3558
+ const timeoutMs = config.timeoutMs ?? 1e4;
3559
+ const initialDelayMs = config.intervalMs ?? 100;
3560
+ const maxDelayMs = 1e3;
3561
+ const existing = this.resolveGlobalPath(use.name);
3562
+ if (existing !== void 0) {
3563
+ this.registerGlobal(use.alias, existing);
3564
+ return Promise.resolve();
3565
+ }
3566
+ if (timeoutMs <= 0) {
3567
+ this.emitUseError(use.name, new Error(`vsn: global '${use.name}' not found`));
3568
+ return Promise.resolve();
3569
+ }
3570
+ return new Promise((resolve) => {
3571
+ let elapsedMs = 0;
3572
+ let delayMs = initialDelayMs;
3573
+ const check = () => {
3574
+ const value = this.resolveGlobalPath(use.name);
3575
+ if (value !== void 0) {
3576
+ this.registerGlobal(use.alias, value);
3577
+ resolve();
3578
+ return;
3579
+ }
3580
+ if (elapsedMs >= timeoutMs) {
3581
+ this.emitUseError(use.name, new Error(`vsn: global '${use.name}' not found`));
3582
+ resolve();
3583
+ return;
3584
+ }
3585
+ const scheduledDelay = Math.min(delayMs, timeoutMs - elapsedMs);
3586
+ setTimeout(() => {
3587
+ elapsedMs += scheduledDelay;
3588
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
3589
+ check();
3590
+ }, scheduledDelay);
3591
+ };
3592
+ check();
3593
+ });
3594
+ }
3439
3595
  getScope(element, parentScope) {
3440
3596
  const existing = this.scopes.get(element);
3441
3597
  if (existing) {
@@ -3559,6 +3715,7 @@ var Engine = class _Engine {
3559
3715
  }
3560
3716
  }
3561
3717
  async applyBehaviors(root) {
3718
+ await this.waitForUses();
3562
3719
  if (this.behaviorRegistry.length === 0) {
3563
3720
  return;
3564
3721
  }
@@ -3966,6 +4123,19 @@ var Engine = class _Engine {
3966
4123
  })
3967
4124
  );
3968
4125
  }
4126
+ emitUseError(name, error) {
4127
+ const selector = `use:${name}`;
4128
+ this.logger.warn?.("vsn:error", { error, selector });
4129
+ const target = globalThis.document;
4130
+ if (target && typeof target.dispatchEvent === "function") {
4131
+ target.dispatchEvent(
4132
+ new CustomEvent("vsn:error", {
4133
+ detail: { error, selector },
4134
+ bubbles: true
4135
+ })
4136
+ );
4137
+ }
4138
+ }
3969
4139
  attachOnHandler(element, config) {
3970
4140
  const options = this.getListenerOptions(config.modifiers);
3971
4141
  const listenerTarget = config.modifiers?.includes("outside") ? element.ownerDocument : element;