vsn 1.0.0 → 1.0.2

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,24 +574,117 @@ 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 await 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
- context.rootScope.setPath?.(`self.${path}`, value);
593
+ context.rootScope.setPath?.(path, value);
588
594
  return value;
589
595
  }
596
+ if (this.target instanceof MemberExpression || this.target instanceof IndexExpression) {
597
+ const resolved = await this.resolveAssignmentTarget(context);
598
+ if (resolved?.scope?.setPath) {
599
+ resolved.scope.setPath(resolved.path, value);
600
+ return value;
601
+ }
602
+ }
590
603
  this.assignTarget(context, this.target, value);
591
604
  return value;
592
605
  }
606
+ async applyCompoundAssignment(context, value) {
607
+ if (!context.scope || !context.scope.setPath) {
608
+ return void 0;
609
+ }
610
+ const resolved = await this.resolveAssignmentTarget(context);
611
+ if (!resolved) {
612
+ throw new Error("Compound assignment requires a simple identifier or member path");
613
+ }
614
+ const { scope, path } = resolved;
615
+ const current = scope?.getPath ? scope.getPath(path) : void 0;
616
+ let result;
617
+ if (this.operator === "+=") {
618
+ result = current + value;
619
+ } else if (this.operator === "-=") {
620
+ result = current - value;
621
+ } else if (this.operator === "*=") {
622
+ result = current * value;
623
+ } else {
624
+ result = current / value;
625
+ }
626
+ scope?.setPath?.(path, result);
627
+ return result;
628
+ }
629
+ async resolveAssignmentTarget(context) {
630
+ if (this.target instanceof IdentifierExpression) {
631
+ const isRoot = this.target.name.startsWith("root.");
632
+ const rawPath = isRoot ? this.target.name.slice("root.".length) : this.target.name;
633
+ if (isRoot) {
634
+ return { scope: context.scope, path: `root.${rawPath}` };
635
+ }
636
+ return { scope: context.scope, path: rawPath };
637
+ }
638
+ if (this.target instanceof MemberExpression) {
639
+ const resolvedPath = this.target.getIdentifierPath();
640
+ if (!resolvedPath) {
641
+ return null;
642
+ }
643
+ const path = resolvedPath.path;
644
+ const isRoot = path.startsWith("root.");
645
+ const rawPath = isRoot ? path.slice("root.".length) : path;
646
+ if (isRoot) {
647
+ return { scope: context.scope, path: `root.${rawPath}` };
648
+ }
649
+ return { scope: context.scope, path: rawPath };
650
+ }
651
+ if (this.target instanceof IndexExpression) {
652
+ const path = await this.resolveIndexPath(context, this.target);
653
+ if (!path) {
654
+ return null;
655
+ }
656
+ const isRoot = path.startsWith("root.");
657
+ const rawPath = isRoot ? path.slice("root.".length) : path;
658
+ if (isRoot) {
659
+ return { scope: context.scope, path: `root.${rawPath}` };
660
+ }
661
+ return { scope: context.scope, path: rawPath };
662
+ }
663
+ return null;
664
+ }
665
+ async resolveIndexPath(context, expr) {
666
+ const base = await this.resolveTargetPath(context, expr.target);
667
+ if (!base) {
668
+ return null;
669
+ }
670
+ const indexValue = await expr.index.evaluate(context);
671
+ if (indexValue == null) {
672
+ return null;
673
+ }
674
+ return `${base}.${indexValue}`;
675
+ }
676
+ async resolveTargetPath(context, target) {
677
+ if (target instanceof IdentifierExpression) {
678
+ return target.name;
679
+ }
680
+ if (target instanceof MemberExpression) {
681
+ return target.getIdentifierPath()?.path ?? null;
682
+ }
683
+ if (target instanceof IndexExpression) {
684
+ return this.resolveIndexPath(context, target);
685
+ }
686
+ return null;
687
+ }
593
688
  assignTarget(context, target, value) {
594
689
  if (!context.scope || !context.scope.setPath) {
595
690
  return;
@@ -1460,11 +1555,46 @@ var Parser = class _Parser {
1460
1555
  this.stream.skipWhitespace();
1461
1556
  alias = this.stream.expect("Identifier" /* Identifier */).value;
1462
1557
  }
1558
+ const { flags, flagArgs } = this.parseUseFlags();
1463
1559
  this.stream.skipWhitespace();
1464
1560
  this.stream.expect("Semicolon" /* Semicolon */);
1465
- return new UseNode(name, alias);
1561
+ return new UseNode(name, alias, flags, flagArgs);
1466
1562
  });
1467
1563
  }
1564
+ parseUseFlags() {
1565
+ const flags = {};
1566
+ const flagArgs = {};
1567
+ while (true) {
1568
+ this.stream.skipWhitespace();
1569
+ if (this.stream.peek()?.type !== "Bang" /* Bang */) {
1570
+ break;
1571
+ }
1572
+ this.stream.next();
1573
+ const name = this.stream.expect("Identifier" /* Identifier */).value;
1574
+ if (name !== "wait") {
1575
+ throw new Error(`Unknown flag ${name}`);
1576
+ }
1577
+ flags.wait = true;
1578
+ if (this.stream.peek()?.type === "LParen" /* LParen */) {
1579
+ this.stream.next();
1580
+ this.stream.skipWhitespace();
1581
+ const timeoutToken = this.stream.expect("Number" /* Number */);
1582
+ const timeoutMs = Number(timeoutToken.value);
1583
+ let intervalMs;
1584
+ this.stream.skipWhitespace();
1585
+ if (this.stream.peek()?.type === "Comma" /* Comma */) {
1586
+ this.stream.next();
1587
+ this.stream.skipWhitespace();
1588
+ const intervalToken = this.stream.expect("Number" /* Number */);
1589
+ intervalMs = Number(intervalToken.value);
1590
+ this.stream.skipWhitespace();
1591
+ }
1592
+ this.stream.expect("RParen" /* RParen */);
1593
+ flagArgs.wait = { timeoutMs, ...intervalMs !== void 0 ? { intervalMs } : {} };
1594
+ }
1595
+ }
1596
+ return { flags, flagArgs };
1597
+ }
1468
1598
  wrapErrors(fn) {
1469
1599
  try {
1470
1600
  return fn();
@@ -1695,11 +1825,11 @@ ${caret}`;
1695
1825
  parseAssignment() {
1696
1826
  const target = this.parseAssignmentTarget();
1697
1827
  this.stream.skipWhitespace();
1698
- this.stream.expect("Equals" /* Equals */);
1828
+ const operator = this.parseAssignmentOperator();
1699
1829
  this.stream.skipWhitespace();
1700
1830
  const value = this.parseExpression();
1701
1831
  this.consumeStatementTerminator();
1702
- return new AssignmentNode(target, value);
1832
+ return new AssignmentNode(target, value, operator);
1703
1833
  }
1704
1834
  parseExpression() {
1705
1835
  return this.parsePipeExpression();
@@ -2281,7 +2411,14 @@ ${caret}`;
2281
2411
  return this.parseObjectPattern();
2282
2412
  }
2283
2413
  if (token.type === "Identifier" /* Identifier */) {
2284
- return new IdentifierExpression(this.parseIdentifierPath());
2414
+ const expr = this.parseCallExpression();
2415
+ if (expr instanceof CallExpression) {
2416
+ throw new Error("Invalid assignment target CallExpression");
2417
+ }
2418
+ if (expr instanceof IdentifierExpression || expr instanceof MemberExpression || expr instanceof IndexExpression) {
2419
+ return expr;
2420
+ }
2421
+ throw new Error("Invalid assignment target");
2285
2422
  }
2286
2423
  throw new Error(`Invalid assignment target ${token.type}`);
2287
2424
  }
@@ -2560,12 +2697,31 @@ ${caret}`;
2560
2697
  while (this.stream.peekNonWhitespace(index)?.type === "Dot" /* Dot */ && this.stream.peekNonWhitespace(index + 1)?.type === "Identifier" /* Identifier */) {
2561
2698
  index += 2;
2562
2699
  }
2563
- return this.stream.peekNonWhitespace(index)?.type === "Equals" /* Equals */;
2700
+ while (this.stream.peekNonWhitespace(index)?.type === "LBracket" /* LBracket */) {
2701
+ let depth = 0;
2702
+ while (true) {
2703
+ const token = this.stream.peekNonWhitespace(index);
2704
+ if (!token) {
2705
+ return false;
2706
+ }
2707
+ if (token.type === "LBracket" /* LBracket */) {
2708
+ depth += 1;
2709
+ } else if (token.type === "RBracket" /* RBracket */) {
2710
+ depth -= 1;
2711
+ if (depth === 0) {
2712
+ index += 1;
2713
+ break;
2714
+ }
2715
+ }
2716
+ index += 1;
2717
+ }
2718
+ }
2719
+ return this.isAssignmentOperatorStart(index);
2564
2720
  }
2565
2721
  if (first.type === "At" /* At */ || first.type === "Dollar" /* Dollar */) {
2566
2722
  const second = this.stream.peekNonWhitespace(1);
2567
2723
  const third = this.stream.peekNonWhitespace(2);
2568
- return second?.type === "Identifier" /* Identifier */ && third?.type === "Equals" /* Equals */;
2724
+ return second?.type === "Identifier" /* Identifier */ && this.isAssignmentOperatorStart(2);
2569
2725
  }
2570
2726
  if (first.type === "LBrace" /* LBrace */ || first.type === "LBracket" /* LBracket */) {
2571
2727
  const stack = [];
@@ -2580,7 +2736,7 @@ ${caret}`;
2580
2736
  } else if (token.type === "RBrace" /* RBrace */ || token.type === "RBracket" /* RBracket */) {
2581
2737
  stack.pop();
2582
2738
  if (stack.length === 0) {
2583
- return this.stream.peekNonWhitespace(index + 1)?.type === "Equals" /* Equals */;
2739
+ return this.isAssignmentOperatorStart(index + 1);
2584
2740
  }
2585
2741
  }
2586
2742
  index += 1;
@@ -2588,6 +2744,20 @@ ${caret}`;
2588
2744
  }
2589
2745
  return false;
2590
2746
  }
2747
+ isAssignmentOperatorStart(index) {
2748
+ const token = this.stream.peekNonWhitespace(index);
2749
+ if (!token) {
2750
+ return false;
2751
+ }
2752
+ if (token.type === "Equals" /* Equals */) {
2753
+ return true;
2754
+ }
2755
+ if (token.type === "Plus" /* Plus */ || token.type === "Minus" /* Minus */ || token.type === "Star" /* Star */ || token.type === "Slash" /* Slash */) {
2756
+ const next = this.stream.peekNonWhitespace(index + 1);
2757
+ return next?.type === "Equals" /* Equals */;
2758
+ }
2759
+ return false;
2760
+ }
2591
2761
  isCallStart() {
2592
2762
  const first = this.stream.peekNonWhitespace(0);
2593
2763
  if (!first || first.type !== "Identifier" /* Identifier */) {
@@ -2812,10 +2982,35 @@ ${caret}`;
2812
2982
  parseAssignmentExpression() {
2813
2983
  const target = this.parseAssignmentTarget();
2814
2984
  this.stream.skipWhitespace();
2815
- this.stream.expect("Equals" /* Equals */);
2985
+ const operator = this.parseAssignmentOperator();
2816
2986
  this.stream.skipWhitespace();
2817
2987
  const value = this.parseExpression();
2818
- return new AssignmentNode(target, value);
2988
+ return new AssignmentNode(target, value, operator);
2989
+ }
2990
+ parseAssignmentOperator() {
2991
+ const next = this.stream.peek();
2992
+ if (!next) {
2993
+ throw new Error("Expected assignment operator");
2994
+ }
2995
+ if (next.type === "Equals" /* Equals */) {
2996
+ this.stream.next();
2997
+ return "=";
2998
+ }
2999
+ if (next.type === "Plus" /* Plus */ || next.type === "Minus" /* Minus */ || next.type === "Star" /* Star */ || next.type === "Slash" /* Slash */) {
3000
+ const op = this.stream.next();
3001
+ this.stream.expect("Equals" /* Equals */);
3002
+ if (op.type === "Plus" /* Plus */) {
3003
+ return "+=";
3004
+ }
3005
+ if (op.type === "Minus" /* Minus */) {
3006
+ return "-=";
3007
+ }
3008
+ if (op.type === "Star" /* Star */) {
3009
+ return "*=";
3010
+ }
3011
+ return "/=";
3012
+ }
3013
+ throw new Error("Expected assignment operator");
2819
3014
  }
2820
3015
  parseTryBlock() {
2821
3016
  this.stream.expect("Try" /* Try */);
@@ -3318,6 +3513,7 @@ var Engine = class _Engine {
3318
3513
  ignoredAdded = /* @__PURE__ */ new WeakMap();
3319
3514
  diagnostics;
3320
3515
  logger;
3516
+ pendingUses = [];
3321
3517
  constructor(options = {}) {
3322
3518
  this.diagnostics = options.diagnostics ?? false;
3323
3519
  this.logger = options.logger ?? console;
@@ -3387,6 +3583,10 @@ var Engine = class _Engine {
3387
3583
  registerBehaviors(source) {
3388
3584
  const program = new Parser(source, { customFlags: new Set(this.flagHandlers.keys()) }).parseProgram();
3389
3585
  for (const use of program.uses) {
3586
+ if (use.flags?.wait) {
3587
+ this.pendingUses.push(this.waitForUseGlobal(use));
3588
+ continue;
3589
+ }
3390
3590
  const value = this.resolveGlobalPath(use.name);
3391
3591
  if (value === void 0) {
3392
3592
  console.warn(`vsn: global '${use.name}' not found`);
@@ -3436,6 +3636,52 @@ var Engine = class _Engine {
3436
3636
  }
3437
3637
  return value;
3438
3638
  }
3639
+ async waitForUses() {
3640
+ while (this.pendingUses.length > 0) {
3641
+ const pending = this.pendingUses;
3642
+ this.pendingUses = [];
3643
+ await Promise.all(pending);
3644
+ }
3645
+ }
3646
+ waitForUseGlobal(use) {
3647
+ const config = use.flagArgs?.wait ?? {};
3648
+ const timeoutMs = config.timeoutMs ?? 1e4;
3649
+ const initialDelayMs = config.intervalMs ?? 100;
3650
+ const maxDelayMs = 1e3;
3651
+ const existing = this.resolveGlobalPath(use.name);
3652
+ if (existing !== void 0) {
3653
+ this.registerGlobal(use.alias, existing);
3654
+ return Promise.resolve();
3655
+ }
3656
+ if (timeoutMs <= 0) {
3657
+ this.emitUseError(use.name, new Error(`vsn: global '${use.name}' not found`));
3658
+ return Promise.resolve();
3659
+ }
3660
+ return new Promise((resolve) => {
3661
+ let elapsedMs = 0;
3662
+ let delayMs = initialDelayMs;
3663
+ const check = () => {
3664
+ const value = this.resolveGlobalPath(use.name);
3665
+ if (value !== void 0) {
3666
+ this.registerGlobal(use.alias, value);
3667
+ resolve();
3668
+ return;
3669
+ }
3670
+ if (elapsedMs >= timeoutMs) {
3671
+ this.emitUseError(use.name, new Error(`vsn: global '${use.name}' not found`));
3672
+ resolve();
3673
+ return;
3674
+ }
3675
+ const scheduledDelay = Math.min(delayMs, timeoutMs - elapsedMs);
3676
+ setTimeout(() => {
3677
+ elapsedMs += scheduledDelay;
3678
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
3679
+ check();
3680
+ }, scheduledDelay);
3681
+ };
3682
+ check();
3683
+ });
3684
+ }
3439
3685
  getScope(element, parentScope) {
3440
3686
  const existing = this.scopes.get(element);
3441
3687
  if (existing) {
@@ -3559,6 +3805,7 @@ var Engine = class _Engine {
3559
3805
  }
3560
3806
  }
3561
3807
  async applyBehaviors(root) {
3808
+ await this.waitForUses();
3562
3809
  if (this.behaviorRegistry.length === 0) {
3563
3810
  return;
3564
3811
  }
@@ -3966,6 +4213,19 @@ var Engine = class _Engine {
3966
4213
  })
3967
4214
  );
3968
4215
  }
4216
+ emitUseError(name, error) {
4217
+ const selector = `use:${name}`;
4218
+ this.logger.warn?.("vsn:error", { error, selector });
4219
+ const target = globalThis.document;
4220
+ if (target && typeof target.dispatchEvent === "function") {
4221
+ target.dispatchEvent(
4222
+ new CustomEvent("vsn:error", {
4223
+ detail: { error, selector },
4224
+ bubbles: true
4225
+ })
4226
+ );
4227
+ }
4228
+ }
3969
4229
  attachOnHandler(element, config) {
3970
4230
  const options = this.getListenerOptions(config.modifiers);
3971
4231
  const listenerTarget = config.modifiers?.includes("outside") ? element.ownerDocument : element;