@firsthandjs/compiler 0.7.1 → 0.8.0

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.
@@ -87,21 +87,43 @@ function eventName(attribute) {
87
87
  // packages/compiler/src/transform.ts
88
88
  var RUNTIME = "@firsthandjs/dom/internal";
89
89
  var CORE = "@firsthandjs/core";
90
+ var CHILD_THUNK = /* @__PURE__ */ Symbol("firsthand.childThunk");
91
+ var GENERATED = /* @__PURE__ */ Symbol("firsthand.generated");
92
+ function generated(arrow) {
93
+ arrow[GENERATED] = true;
94
+ return arrow;
95
+ }
96
+ function pushOnce(build, statement) {
97
+ build.once.push(statement);
98
+ }
99
+ function pushEach(build, statement) {
100
+ build.each.push(statement);
101
+ }
90
102
  function firsthandPlugin(_api, options = {}) {
91
103
  return {
92
104
  name: "firsthand",
93
105
  visitor: {
94
106
  Program: {
95
- enter(_path, state) {
107
+ enter(path, state) {
96
108
  state.firsthand = {
97
109
  imports: /* @__PURE__ */ new Map(),
98
110
  templates: [],
99
111
  counter: 0,
100
- moduleId: stableId(options.packageName ?? "app", state.filename ?? "module")
112
+ moduleId: stableId(options.packageName ?? "app", state.filename ?? "module"),
113
+ views: /* @__PURE__ */ new Map(),
114
+ viewNodes: /* @__PURE__ */ new Set(),
115
+ runs: /* @__PURE__ */ new Map()
101
116
  };
117
+ collectViews(path, state);
118
+ collectRuns(path, state);
102
119
  },
103
120
  exit(path, state) {
104
- const { imports, templates } = state.firsthand;
121
+ const { imports, templates, views } = state.firsthand;
122
+ for (const [, local] of views) {
123
+ path.node.body.push(
124
+ expressionStatement2(t.callExpression(runtime(state, "view"), [t.cloneNode(local)]))
125
+ );
126
+ }
105
127
  if (templates.length > 0) {
106
128
  path.node.body.unshift(t.variableDeclaration("const", templates));
107
129
  }
@@ -181,6 +203,7 @@ function annotateComponent(path, state, options) {
181
203
  const setupPath = path.get("arguments.0");
182
204
  checkKeptReads(setupPath, name);
183
205
  checkDecidedOnce(setupPath, name);
206
+ checkRunBody(setupPath, name);
184
207
  }
185
208
  path.node.arguments = [
186
209
  setup,
@@ -220,9 +243,13 @@ function checkDecidedOnce(setup, name) {
220
243
  throw at.buildCodeFrameError(
221
244
  `${name}: this view is chosen once, here, from a value that changes. A setup runs one time per instance, so the other branch will never appear.
222
245
 
223
- Put the choice inside the markup, where it is a part that can run again:
246
+ Either put the choice inside the markup, where it is a part:
247
+
248
+ return <>{open.value ? <A /> : <B />}</>;
249
+
250
+ or return a render function, which is a reactive scope of its own and may use ordinary control flow:
224
251
 
225
- return <>{open.value ? <A /> : <B />}</>;`
252
+ return () => (open.value ? <A /> : <B />);`
226
253
  );
227
254
  };
228
255
  const check = (returned, at) => {
@@ -274,6 +301,106 @@ Put the choice inside the markup, where it is a part that can run again:
274
301
  }
275
302
  });
276
303
  }
304
+ function checkRunBody(setup, name) {
305
+ const runs = [];
306
+ const body = setup.get("body");
307
+ body.traverse({
308
+ Function(nested) {
309
+ nested.skip();
310
+ },
311
+ ReturnStatement(statement) {
312
+ const argument = statement.get("argument");
313
+ if (argument.isArrowFunctionExpression() || argument.isFunctionExpression()) {
314
+ runs.push(argument);
315
+ }
316
+ }
317
+ });
318
+ for (const run of runs) {
319
+ checkNothingPersistent(run, name);
320
+ checkRepeatedMarkup(run, name);
321
+ }
322
+ }
323
+ var PERSISTENT = /* @__PURE__ */ new Map([
324
+ ["signal", "a signal"],
325
+ ["computed", "a computed"],
326
+ ["effect", "an effect"],
327
+ ["deepSignal", "a deep signal"],
328
+ ["useResource", "a resource"],
329
+ ["useAction", "an action"],
330
+ ["onCleanup", "a cleanup"],
331
+ ["provide", "a context value"]
332
+ ]);
333
+ function checkNothingPersistent(run, name) {
334
+ run.traverse({
335
+ Function(nested) {
336
+ nested.skip();
337
+ },
338
+ CallExpression(call) {
339
+ const callee = call.node.callee;
340
+ if (!t.isIdentifier(callee)) {
341
+ return;
342
+ }
343
+ const what = PERSISTENT.get(callee.name);
344
+ if (what === void 0) {
345
+ return;
346
+ }
347
+ const binding = call.scope.getBinding(callee.name);
348
+ if (binding === void 0 || !isFirsthandImport(binding)) {
349
+ return;
350
+ }
351
+ throw call.buildCodeFrameError(
352
+ `${name}: this render function makes ${what}, and it runs again whenever something it read changes \u2014 so this would be made again, and the one before it thrown away.
353
+
354
+ Move it into the setup, above the render function. Persistent things are made once, where the setup runs once.`
355
+ );
356
+ }
357
+ });
358
+ }
359
+ function checkRepeatedMarkup(run, name) {
360
+ const report = (at) => {
361
+ throw at.buildCodeFrameError(
362
+ `${name}: this markup is written once and appears many times, so where it stands cannot say which of them is which.
363
+
364
+ Give it a key:
365
+
366
+ {rows.map((row) => <Row key={row.id} row={row} />)}`
367
+ );
368
+ };
369
+ run.traverse({
370
+ Function(nested) {
371
+ nested.skip();
372
+ },
373
+ "ForStatement|ForOfStatement|ForInStatement|WhileStatement|DoWhileStatement"(loop) {
374
+ loop.traverse({
375
+ JSXElement(element) {
376
+ element.skip();
377
+ if (!hasKey(element.node)) {
378
+ report(element);
379
+ }
380
+ }
381
+ });
382
+ },
383
+ CallExpression(call) {
384
+ const callee = call.node.callee;
385
+ if (LIST_CALL in call.node || !t.isMemberExpression(callee) || callee.computed || !t.isIdentifier(callee.property, { name: "map" })) {
386
+ return;
387
+ }
388
+ call.traverse({
389
+ JSXElement(element) {
390
+ element.skip();
391
+ if (!hasKey(element.node)) {
392
+ report(element);
393
+ }
394
+ }
395
+ });
396
+ }
397
+ });
398
+ }
399
+ function hasKey(element) {
400
+ return element.openingElement.attributes.some(
401
+ (attribute) => t.isJSXAttribute(attribute) && attributeName(attribute) === "key"
402
+ );
403
+ }
277
404
  function returnsView(node) {
278
405
  if (node === null || node === void 0) {
279
406
  return false;
@@ -499,6 +626,228 @@ function compileNode(path, state) {
499
626
  }
500
627
  return compileTemplate(path, state);
501
628
  }
629
+ function collectViews(program, state) {
630
+ const { views, viewNodes } = state.firsthand;
631
+ const record = (path) => {
632
+ let fn = path.getFunctionParent();
633
+ while (fn !== null) {
634
+ viewNodes.add(fn.node);
635
+ const named = moduleLevelName(fn);
636
+ if (named !== null) {
637
+ views.set(named, t.identifier(named));
638
+ }
639
+ fn = fn.getFunctionParent();
640
+ }
641
+ };
642
+ program.traverse({
643
+ JSXElement: record,
644
+ JSXFragment: record
645
+ });
646
+ }
647
+ function moduleLevelName(fn) {
648
+ if (fn.isFunctionDeclaration()) {
649
+ const id = fn.node.id;
650
+ if (id === null || id === void 0) {
651
+ return null;
652
+ }
653
+ return t.isProgram(fn.parentPath.scope.block) ? id.name : null;
654
+ }
655
+ const declarator = fn.parentPath;
656
+ if (!declarator.isVariableDeclarator() || !t.isIdentifier(declarator.node.id)) {
657
+ return null;
658
+ }
659
+ return t.isProgram(declarator.scope.block) ? declarator.node.id.name : null;
660
+ }
661
+ function isLocalView(path, state) {
662
+ const name = path.node.openingElement.name;
663
+ if (!t.isJSXIdentifier(name)) {
664
+ return false;
665
+ }
666
+ const binding = path.scope.getBinding(name.name);
667
+ if (binding === void 0 || binding.kind === "module" || binding.constantViolations.length > 0) {
668
+ return false;
669
+ }
670
+ if (binding.path.isFunctionDeclaration()) {
671
+ return state.firsthand.viewNodes.has(binding.path.node);
672
+ }
673
+ if (!binding.path.isVariableDeclarator()) {
674
+ return false;
675
+ }
676
+ const init = binding.path.node.init;
677
+ if (!t.isArrowFunctionExpression(init) && !t.isFunctionExpression(init)) {
678
+ return false;
679
+ }
680
+ return state.firsthand.viewNodes.has(init);
681
+ }
682
+ function collectRuns(program, state) {
683
+ program.traverse({
684
+ CallExpression(call) {
685
+ if (!isComponentCall(call)) {
686
+ return;
687
+ }
688
+ const setup = call.get("arguments.0");
689
+ if (!setup.isArrowFunctionExpression() && !setup.isFunctionExpression()) {
690
+ return;
691
+ }
692
+ const found = [];
693
+ const body = setup.get("body");
694
+ if (!body.isBlockStatement()) {
695
+ if (body.isArrowFunctionExpression() || body.isFunctionExpression()) {
696
+ found.push(body);
697
+ }
698
+ } else {
699
+ body.traverse({
700
+ // A handler or a callback returns its own things; only what the
701
+ // setup itself hands back is the view.
702
+ Function(nested) {
703
+ nested.skip();
704
+ },
705
+ ReturnStatement(statement) {
706
+ const argument = statement.get("argument");
707
+ if (argument.isArrowFunctionExpression() || argument.isFunctionExpression()) {
708
+ found.push(argument);
709
+ }
710
+ }
711
+ });
712
+ }
713
+ if (found.length === 0) {
714
+ return;
715
+ }
716
+ setup.ensureBlock();
717
+ const block = setup.get("body");
718
+ for (const run of found) {
719
+ const store = setup.scope.generateUidIdentifier("store");
720
+ block.unshiftContainer(
721
+ "body",
722
+ t.variableDeclaration("const", [
723
+ t.variableDeclarator(
724
+ store,
725
+ t.callExpression(runtime(state, "store"), [t.stringLiteral(declaredName(call))])
726
+ )
727
+ ])
728
+ );
729
+ let index = 0;
730
+ state.firsthand.runs.set(run.node, {
731
+ node: run.node,
732
+ store,
733
+ next: () => index++
734
+ });
735
+ closeRun(run, store, state);
736
+ }
737
+ }
738
+ });
739
+ }
740
+ function closeRun(run, store, state) {
741
+ const body = run.get("body");
742
+ if (!body.isBlockStatement()) {
743
+ body.replaceWith(
744
+ t.callExpression(runtime(state, "ran"), [t.cloneNode(store), body.node])
745
+ );
746
+ return;
747
+ }
748
+ const returns = [];
749
+ body.traverse({
750
+ Function(nested) {
751
+ nested.skip();
752
+ },
753
+ ReturnStatement(statement) {
754
+ returns.push(statement);
755
+ }
756
+ });
757
+ for (const statement of returns) {
758
+ statement.node.argument = t.callExpression(runtime(state, "ran"), [
759
+ t.cloneNode(store),
760
+ statement.node.argument ?? t.identifier("undefined")
761
+ ]);
762
+ }
763
+ }
764
+ function isComponentCall(path) {
765
+ if (!t.isIdentifier(path.node.callee, { name: "component" })) {
766
+ return false;
767
+ }
768
+ const binding = path.scope.getBinding("component");
769
+ return binding !== void 0 && isFirsthandImport(binding);
770
+ }
771
+ function enclosingRun(path, state) {
772
+ let fn = path.getFunctionParent();
773
+ while (fn !== null && GENERATED in fn.node) {
774
+ fn = fn.getFunctionParent();
775
+ }
776
+ if (fn === null) {
777
+ return null;
778
+ }
779
+ return state.firsthand.runs.get(fn.node) ?? null;
780
+ }
781
+ function dependsOnRun(node, run, at) {
782
+ if (run === null) {
783
+ return false;
784
+ }
785
+ const names = /* @__PURE__ */ new Set();
786
+ collectNames(node, names);
787
+ for (const name of names) {
788
+ const binding = at.scope.getBinding(name);
789
+ if (binding === void 0) {
790
+ continue;
791
+ }
792
+ if (binding.scope.block === run.node || binding.path.findParent((parent) => parent.node === run.node) !== null) {
793
+ return true;
794
+ }
795
+ }
796
+ return false;
797
+ }
798
+ function collectNames(node, into) {
799
+ if (t.isIdentifier(node)) {
800
+ into.add(node.name);
801
+ return;
802
+ }
803
+ for (const key of t.VISITOR_KEYS[node.type]) {
804
+ if (t.isMemberExpression(node) && !node.computed && key === "property") {
805
+ continue;
806
+ }
807
+ if ((t.isObjectProperty(node) || t.isObjectMethod(node)) && !node.computed && key === "key") {
808
+ continue;
809
+ }
810
+ const value = node[key];
811
+ for (const child of Array.isArray(value) ? value : [value]) {
812
+ if (typeof child === "object" && child !== null) {
813
+ collectNames(child, into);
814
+ }
815
+ }
816
+ }
817
+ }
818
+ function canRetain(node, build) {
819
+ let possible = true;
820
+ const visit = (element) => {
821
+ for (const attribute of element.openingElement.attributes) {
822
+ if (t.isJSXSpreadAttribute(attribute)) {
823
+ if (dependsOnRun(attribute.argument, build.run, build.at)) {
824
+ possible = false;
825
+ }
826
+ continue;
827
+ }
828
+ const value = attribute.value;
829
+ if (attributeName(attribute) === "ref" && t.isJSXExpressionContainer(value) && !t.isJSXEmptyExpression(value.expression) && dependsOnRun(value.expression, build.run, build.at)) {
830
+ possible = false;
831
+ }
832
+ }
833
+ for (const child of element.children) {
834
+ if (t.isJSXElement(child)) {
835
+ if (!isComponentTag(child)) {
836
+ visit(child);
837
+ }
838
+ continue;
839
+ }
840
+ if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression) && isKeyedList(child.expression) && dependsOnRun(child.expression, build.run, build.at)) {
841
+ possible = false;
842
+ }
843
+ }
844
+ };
845
+ visit(node);
846
+ return possible;
847
+ }
848
+ function isKeyedList(node) {
849
+ return t.isCallExpression(node) && LIST_CALL in node;
850
+ }
502
851
  function isComponentTag(node) {
503
852
  const name = node.openingElement.name;
504
853
  if (t.isJSXMemberExpression(name)) {
@@ -517,7 +866,25 @@ function tagExpression(name) {
517
866
  }
518
867
  function compileComponent(path, state) {
519
868
  const node = path.node;
869
+ const run = enclosingRun(path, state);
520
870
  const properties = [];
871
+ const cells = [];
872
+ const throughCell = (value) => {
873
+ const holder = t.identifier(`_cell$${String(run.next())}`);
874
+ cells.push(
875
+ t.variableDeclaration("const", [
876
+ t.variableDeclarator(
877
+ holder,
878
+ t.callExpression(runtime(state, "cell"), [
879
+ t.cloneNode(run.store),
880
+ t.numericLiteral(run.next()),
881
+ value
882
+ ])
883
+ )
884
+ ])
885
+ );
886
+ return t.memberExpression(t.cloneNode(holder), t.identifier("value"));
887
+ };
521
888
  for (const attribute of node.openingElement.attributes) {
522
889
  if (t.isJSXSpreadAttribute(attribute)) {
523
890
  properties.push(t.spreadElement(attribute.argument));
@@ -528,9 +895,10 @@ function compileComponent(path, state) {
528
895
  if (value === null || isStaticValue(value)) {
529
896
  properties.push(t.objectProperty(propertyKey(name), value ?? t.booleanLiteral(true)));
530
897
  } else {
531
- properties.push(
532
- t.objectMethod("get", propertyKey(name), [], t.blockStatement([t.returnStatement(value)]))
533
- );
898
+ const held = dependsOnRun(value, run, path) ? throughCell(value) : value;
899
+ const read = t.returnStatement(held);
900
+ takePosition(read, value);
901
+ properties.push(t.objectMethod("get", propertyKey(name), [], t.blockStatement([read])));
534
902
  }
535
903
  }
536
904
  const children = compileChildren(node.children, state);
@@ -553,11 +921,57 @@ function compileComponent(path, state) {
553
921
  )
554
922
  );
555
923
  }
556
- return t.callExpression(runtime(state, "createComponent"), [
557
- // `isComponentTag` has already excluded namespaced names.
558
- tagExpression(node.openingElement.name),
559
- t.objectExpression(properties)
560
- ]);
924
+ const tag = tagExpression(node.openingElement.name);
925
+ const make = isLocalView(path, state) ? t.callExpression(tag, [t.objectExpression(properties)]) : t.callExpression(runtime(state, "createComponent"), [tag, t.objectExpression(properties)]);
926
+ if (run !== null) {
927
+ return keptChild(state, run, make, cells);
928
+ }
929
+ if (isLocalView(path, state)) {
930
+ const parent = path.parentPath;
931
+ if (parent.isArrowFunctionExpression() && parent.node.body === node && CHILD_THUNK in parent.node) {
932
+ return make;
933
+ }
934
+ return t.callExpression(runtime(state, "part"), [t.arrowFunctionExpression([], make)]);
935
+ }
936
+ return make;
937
+ }
938
+ function keptChild(state, run, make, cells) {
939
+ const slot = t.identifier(`_kept$${String(run.next())}`);
940
+ const held = t.identifier(`_own$${String(run.next())}`);
941
+ const made = () => t.memberExpression(t.cloneNode(slot), t.identifier("last"));
942
+ const body = [
943
+ ...cells,
944
+ t.variableDeclaration("const", [
945
+ t.variableDeclarator(
946
+ slot,
947
+ t.callExpression(runtime(state, "site"), [
948
+ t.cloneNode(run.store),
949
+ t.numericLiteral(run.next())
950
+ ])
951
+ )
952
+ ]),
953
+ t.ifStatement(
954
+ t.binaryExpression("===", made(), t.identifier("undefined")),
955
+ t.blockStatement([
956
+ t.variableDeclaration("const", [
957
+ t.variableDeclarator(
958
+ held,
959
+ t.callExpression(runtime(state, "open"), [t.cloneNode(run.store), t.cloneNode(slot)])
960
+ )
961
+ ]),
962
+ expressionStatement2(
963
+ t.assignmentExpression(
964
+ "=",
965
+ made(),
966
+ t.callExpression(runtime(state, "part"), [t.arrowFunctionExpression([], make)])
967
+ )
968
+ ),
969
+ expressionStatement2(t.callExpression(runtime(state, "close"), [t.cloneNode(held)]))
970
+ ])
971
+ ),
972
+ t.returnStatement(made())
973
+ ];
974
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body))), []);
561
975
  }
562
976
  function propertyKey(name) {
563
977
  return t.isValidIdentifier(name) ? t.identifier(name) : t.stringLiteral(name);
@@ -583,14 +997,34 @@ function isStaticValue(value) {
583
997
  return t.isStringLiteral(value) || t.isNumericLiteral(value) || t.isBooleanLiteral(value) || t.isNullLiteral(value);
584
998
  }
585
999
  function compileTemplate(path, state) {
1000
+ let names = 0;
1001
+ const statements = [];
586
1002
  const build = {
587
1003
  html: [],
588
- statements: [],
1004
+ statements,
1005
+ // Where a site is built once, these are three lists; where it is not, they
1006
+ // are one, and everything simply happens in the order it was written.
1007
+ once: statements,
1008
+ each: statements,
1009
+ run: null,
1010
+ at: path,
589
1011
  next: /* @__PURE__ */ (() => {
590
1012
  let n = 0;
591
1013
  return () => t.identifier(`_el$${String(++n)}`);
592
- })()
1014
+ })(),
1015
+ name: (prefix) => t.identifier(`${prefix}${String(++names)}`)
593
1016
  };
1017
+ const run = enclosingRun(path, state);
1018
+ if (run !== null) {
1019
+ build.run = run;
1020
+ if (canRetain(path.node, build)) {
1021
+ build.once = [];
1022
+ build.each = [];
1023
+ } else {
1024
+ build.run = null;
1025
+ }
1026
+ }
1027
+ const kept = build.run !== null;
594
1028
  const root = t.identifier("_el$");
595
1029
  emitElement(path.node, build, root, state);
596
1030
  const templateId = t.identifier(`_tmpl$${String(++state.firsthand.counter)}`);
@@ -604,14 +1038,107 @@ function compileTemplate(path, state) {
604
1038
  )
605
1039
  )
606
1040
  );
1041
+ if (!kept) {
1042
+ const body2 = [
1043
+ t.variableDeclaration("const", [
1044
+ t.variableDeclarator(root, t.callExpression(t.cloneNode(templateId), []))
1045
+ ]),
1046
+ ...build.statements,
1047
+ t.returnStatement(t.cloneNode(root))
1048
+ ];
1049
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body2))), []);
1050
+ }
1051
+ const slot = build.name("_site$");
1052
+ const fresh = build.name("_new$");
1053
+ const held = build.name("_own$");
1054
+ const owner = build.run;
607
1055
  const body = [
608
1056
  t.variableDeclaration("const", [
609
- t.variableDeclarator(root, t.callExpression(t.cloneNode(templateId), []))
1057
+ t.variableDeclarator(
1058
+ slot,
1059
+ t.callExpression(runtime(state, "site"), [
1060
+ t.cloneNode(owner.store),
1061
+ t.numericLiteral(owner.next())
1062
+ ])
1063
+ )
610
1064
  ]),
1065
+ t.variableDeclaration("let", [
1066
+ t.variableDeclarator(root, t.memberExpression(t.cloneNode(slot), t.identifier("node")))
1067
+ ]),
1068
+ t.variableDeclaration("const", [
1069
+ t.variableDeclarator(
1070
+ fresh,
1071
+ t.binaryExpression("===", t.cloneNode(root), t.identifier("undefined"))
1072
+ )
1073
+ ]),
1074
+ t.variableDeclaration("let", [t.variableDeclarator(held)]),
1075
+ t.ifStatement(
1076
+ t.cloneNode(fresh),
1077
+ t.blockStatement([
1078
+ // What this site makes belongs to the instance. A run's own scope is
1079
+ // cleared before it runs again, and a part left there would be
1080
+ // disposed by the very next run.
1081
+ expressionStatement2(
1082
+ t.assignmentExpression(
1083
+ "=",
1084
+ t.cloneNode(held),
1085
+ t.callExpression(runtime(state, "open"), [t.cloneNode(owner.store), t.cloneNode(slot)])
1086
+ )
1087
+ ),
1088
+ expressionStatement2(
1089
+ t.assignmentExpression(
1090
+ "=",
1091
+ t.cloneNode(root),
1092
+ t.assignmentExpression(
1093
+ "=",
1094
+ t.memberExpression(t.cloneNode(slot), t.identifier("node")),
1095
+ t.callExpression(t.cloneNode(templateId), [])
1096
+ )
1097
+ )
1098
+ )
1099
+ ])
1100
+ ),
611
1101
  ...build.statements,
612
- t.returnStatement(t.cloneNode(root))
1102
+ t.ifStatement(
1103
+ t.cloneNode(fresh),
1104
+ t.blockStatement([
1105
+ ...build.once,
1106
+ expressionStatement2(t.callExpression(runtime(state, "close"), [t.cloneNode(held)]))
1107
+ ])
1108
+ )
613
1109
  ];
614
- return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(body)), []);
1110
+ body.push(...build.each, t.returnStatement(t.cloneNode(root)));
1111
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body))), []);
1112
+ }
1113
+ function guardedWrite(build, state, value, write) {
1114
+ const run = build.run;
1115
+ const slot = build.name("_w$");
1116
+ const held = build.name("_x$");
1117
+ const last = () => t.memberExpression(t.cloneNode(slot), t.identifier("last"));
1118
+ pushEach(
1119
+ build,
1120
+ t.variableDeclaration("const", [
1121
+ t.variableDeclarator(
1122
+ slot,
1123
+ t.callExpression(runtime(state, "site"), [
1124
+ t.cloneNode(run.store),
1125
+ t.numericLiteral(run.next())
1126
+ ])
1127
+ ),
1128
+ t.variableDeclarator(held, value)
1129
+ ])
1130
+ );
1131
+ pushEach(
1132
+ build,
1133
+ t.ifStatement(
1134
+ t.binaryExpression("!==", last(), t.cloneNode(held)),
1135
+ t.blockStatement([
1136
+ expressionStatement2(t.assignmentExpression("=", last(), t.cloneNode(held))),
1137
+ expressionStatement2(t.callExpression(runtime(state, "wrote"), [t.cloneNode(run.store)])),
1138
+ expressionStatement2(write(t.cloneNode(held)))
1139
+ ])
1140
+ )
1141
+ );
615
1142
  }
616
1143
  function emitElement(node, build, self, state) {
617
1144
  const name = node.openingElement.name;
@@ -639,7 +1166,8 @@ function emitElement(node, build, self, state) {
639
1166
  function emitAttribute(attribute, build, self, state, deferred, tag) {
640
1167
  if (t.isJSXSpreadAttribute(attribute)) {
641
1168
  deferred.push(() => {
642
- build.statements.push(
1169
+ pushOnce(
1170
+ build,
643
1171
  expressionStatement2(
644
1172
  t.callExpression(runtime(state, "bind"), [
645
1173
  t.arrowFunctionExpression(
@@ -656,7 +1184,8 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
656
1184
  const value = attributeValue(attribute);
657
1185
  if (name === "ref") {
658
1186
  deferred.push(() => {
659
- build.statements.push(
1187
+ pushOnce(
1188
+ build,
660
1189
  expressionStatement2(t.callExpression(value, [t.cloneNode(self)]))
661
1190
  );
662
1191
  });
@@ -680,7 +1209,12 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
680
1209
  t.objectExpression([t.objectProperty(propertyKey(modifier), t.booleanLiteral(true))])
681
1210
  );
682
1211
  }
683
- build.statements.push(expressionStatement2(t.callExpression(runtime(state, "on"), args)));
1212
+ const attach = expressionStatement2(t.callExpression(runtime(state, "on"), args));
1213
+ if (dependsOnRun(value, build.run, build.at)) {
1214
+ pushEach(build, attach);
1215
+ } else {
1216
+ pushOnce(build, attach);
1217
+ }
684
1218
  });
685
1219
  return;
686
1220
  }
@@ -700,7 +1234,17 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
700
1234
  return;
701
1235
  }
702
1236
  deferred.push(() => {
703
- build.statements.push(
1237
+ if (dependsOnRun(value, build.run, build.at)) {
1238
+ guardedWrite(
1239
+ build,
1240
+ state,
1241
+ value,
1242
+ (held) => dynamicAttributeCall(name, held, self, state, tag)
1243
+ );
1244
+ return;
1245
+ }
1246
+ pushOnce(
1247
+ build,
704
1248
  expressionStatement2(
705
1249
  t.callExpression(runtime(state, "bind"), [
706
1250
  located(
@@ -886,18 +1430,39 @@ function emitChildren(children, build, self, state) {
886
1430
  continue;
887
1431
  }
888
1432
  const isLast = i === entries.length - 1;
1433
+ const expression = entry.expression;
1434
+ let marker = null;
1435
+ if (!isLast) {
1436
+ build.html.push("<!>");
1437
+ marker = t.cloneNode(reference());
1438
+ }
1439
+ if (entry.kind !== "list" && dependsOnRun(expression, build.run, build.at)) {
1440
+ const run = build.run;
1441
+ pushEach(
1442
+ build,
1443
+ expressionStatement2(
1444
+ t.callExpression(runtime(state, "writeChild"), [
1445
+ t.cloneNode(run.store),
1446
+ t.numericLiteral(run.next()),
1447
+ t.cloneNode(self),
1448
+ marker ?? t.nullLiteral(),
1449
+ expression
1450
+ ])
1451
+ )
1452
+ );
1453
+ index++;
1454
+ continue;
1455
+ }
889
1456
  const args = [
890
1457
  t.cloneNode(self),
891
1458
  // A list part is already a thunk that owns its rows; wrapping it would
892
1459
  // rebuild the whole list on every evaluation.
893
- entry.kind === "list" ? entry.expression : thunk(entry.expression)
1460
+ entry.kind === "list" ? expression : thunk(expression)
894
1461
  ];
895
- if (!isLast) {
896
- build.html.push("<!>");
897
- const id = reference();
898
- args.push(t.cloneNode(id));
1462
+ if (marker !== null) {
1463
+ args.push(marker);
899
1464
  }
900
- build.statements.push(expressionStatement2(t.callExpression(runtime(state, "insert"), args)));
1465
+ pushOnce(build, expressionStatement2(t.callExpression(runtime(state, "insert"), args)));
901
1466
  index++;
902
1467
  }
903
1468
  }
@@ -1006,9 +1571,7 @@ function compileChildren(children, state) {
1006
1571
  result.push(t.callExpression(runtime(state, "part"), [entry.expression]));
1007
1572
  } else {
1008
1573
  result.push(
1009
- t.callExpression(runtime(state, "part"), [
1010
- t.arrowFunctionExpression([], entry.expression)
1011
- ])
1574
+ t.callExpression(runtime(state, "part"), [thunk(entry.expression)])
1012
1575
  );
1013
1576
  }
1014
1577
  }
@@ -1016,12 +1579,16 @@ function compileChildren(children, state) {
1016
1579
  }
1017
1580
  function thunk(expression) {
1018
1581
  const arrow = t.arrowFunctionExpression([], expression);
1582
+ arrow[CHILD_THUNK] = true;
1583
+ takePosition(arrow, expression);
1584
+ return arrow;
1585
+ }
1586
+ function takePosition(target, expression) {
1019
1587
  const loc = expression.loc;
1020
1588
  if (loc !== null && loc !== void 0) {
1021
- arrow.loc = { ...loc, end: loc.start };
1589
+ target.loc = { ...loc, end: loc.start };
1022
1590
  expression.loc = null;
1023
1591
  }
1024
- return arrow;
1025
1592
  }
1026
1593
  function expressionStatement2(expression) {
1027
1594
  return located(t.expressionStatement(expression), expression);
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  firsthandPlugin,
4
4
  stableId,
5
5
  transform
6
- } from "./chunk-7DU5USN7.js";
6
+ } from "./chunk-NFKHWAYO.js";
7
7
  export {
8
8
  compileModule,
9
9
  firsthandPlugin,
@@ -18,6 +18,28 @@ type FirsthandState = {
18
18
  templates: t.VariableDeclarator[];
19
19
  counter: number;
20
20
  moduleId: string;
21
+ /** Module-level functions markup was compiled into, in source order. */
22
+ views: Map<string, t.Identifier>;
23
+ /** Every function markup was written inside, for resolving tags locally. */
24
+ viewNodes: Set<t.Node>;
25
+ /** Functions that run again as a whole, and where each keeps its sites. */
26
+ runs: Map<t.Node, RunContext>;
27
+ };
28
+ /**
29
+ * What a re-running function needs in order to keep its DOM.
30
+ *
31
+ * `store` is an array declared once per instance — in the setup, which runs
32
+ * once — and every site inside the run takes a numbered place in it. The index
33
+ * is fixed at compile time, so a site inside an `if` keeps its own place
34
+ * whether or not the branch was taken: nothing depends on the order the run
35
+ * happens to reach things in, which is the rule React needs for hooks and this
36
+ * does not.
37
+ */
38
+ type RunContext = {
39
+ /** The function whose body re-runs. Bindings inside it belong to one run. */
40
+ node: t.Node;
41
+ store: t.Identifier;
42
+ next: () => number;
21
43
  };
22
44
  declare module '@babel/core' {
23
45
  interface PluginPass {
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAelC,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AAUD,MAAM,MAAM,sBAAsB,GAAG;IACnC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAsDd"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAelC,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACjC,4EAA4E;IAC5E,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACvB,2EAA2E;IAC3E,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;GASG;AACH,KAAK,UAAU,GAAG;IAChB,6EAA6E;IAC7E,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACb,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC;IACpB,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAyBF,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AA2BD,MAAM,MAAM,sBAAsB,GAAG;IACnC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAmEd"}
package/dist/vite.d.ts CHANGED
@@ -18,5 +18,28 @@ export type VitePluginLike = {
18
18
  * Runs before the bundler's TypeScript step: Firsthand parses TS syntax but leaves
19
19
  * the annotations in place, so exactly one tool strips them.
20
20
  */
21
- export declare function firsthand(options?: FirsthandPluginOptions): VitePluginLike;
21
+ export type FirsthandViteOptions = FirsthandPluginOptions & {
22
+ /**
23
+ * Files to compile. Everything with a JSX extension, by default.
24
+ *
25
+ * Patterns are regular expressions rather than globs, so that this package
26
+ * keeps its promise of having no dependencies — and so that what matches is
27
+ * something you can read rather than something you have to guess at.
28
+ */
29
+ include?: readonly RegExp[];
30
+ /**
31
+ * Files to leave alone.
32
+ *
33
+ * This is how a project says *these are not mine*: a folder of React
34
+ * components kept during a migration, compiled by React's own transform.
35
+ * What this compiler does not compile, it does not claim — a component from
36
+ * an excluded file reaches the adapter like any other foreign one.
37
+ *
38
+ * ```ts
39
+ * firsthand({ exclude: [/\/legacy\//] })
40
+ * ```
41
+ */
42
+ exclude?: readonly RegExp[];
43
+ };
44
+ export declare function firsthand(options?: FirsthandViteOptions): VitePluginLike;
22
45
  //# sourceMappingURL=vite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,cAAc,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACrF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CA0B9E"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,cAAc,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACrF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG;IAC1D;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,GAAE,oBAAyB,GAAG,cAAc,CAkC5E"}
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compileModule
3
- } from "./chunk-7DU5USN7.js";
3
+ } from "./chunk-NFKHWAYO.js";
4
4
 
5
5
  // packages/compiler/src/vite.ts
6
6
  function firsthand(options = {}) {
@@ -16,6 +16,12 @@ function firsthand(options = {}) {
16
16
  if (!file.endsWith(".tsx") && !file.endsWith(".jsx")) {
17
17
  return null;
18
18
  }
19
+ if (options.exclude?.some((pattern) => pattern.test(file)) === true) {
20
+ return null;
21
+ }
22
+ if (options.include !== void 0 && !options.include.some((pattern) => pattern.test(file))) {
23
+ return null;
24
+ }
19
25
  return compileModule(code, {
20
26
  filename: file,
21
27
  typescript: file.endsWith(".tsx"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firsthandjs/compiler",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Build-time TSX transform for Firsthand: static markup into templates, dynamic expressions into DOM parts.",
5
5
  "license": "MIT",
6
6
  "type": "module",