@firsthandjs/compiler 0.7.1 → 0.9.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.
@@ -86,22 +86,49 @@ function eventName(attribute) {
86
86
 
87
87
  // packages/compiler/src/transform.ts
88
88
  var RUNTIME = "@firsthandjs/dom/internal";
89
+ var SERVER_RUNTIME = "@firsthandjs/server/internal";
89
90
  var CORE = "@firsthandjs/core";
91
+ var CHILD_THUNK = /* @__PURE__ */ Symbol("firsthand.childThunk");
92
+ var GENERATED = /* @__PURE__ */ Symbol("firsthand.generated");
93
+ function generated(arrow) {
94
+ arrow[GENERATED] = true;
95
+ return arrow;
96
+ }
97
+ function pushOnce(build, statement) {
98
+ build.once.push(statement);
99
+ }
100
+ function pushEach(build, statement) {
101
+ build.each.push(statement);
102
+ }
90
103
  function firsthandPlugin(_api, options = {}) {
91
104
  return {
92
105
  name: "firsthand",
93
106
  visitor: {
94
107
  Program: {
95
- enter(_path, state) {
108
+ enter(path, state) {
96
109
  state.firsthand = {
97
110
  imports: /* @__PURE__ */ new Map(),
98
111
  templates: [],
99
112
  counter: 0,
100
- moduleId: stableId(options.packageName ?? "app", state.filename ?? "module")
113
+ moduleId: stableId(options.packageName ?? "app", state.filename ?? "module"),
114
+ views: /* @__PURE__ */ new Map(),
115
+ viewNodes: /* @__PURE__ */ new Set(),
116
+ runs: /* @__PURE__ */ new Map(),
117
+ ssr: options.ssr === true,
118
+ hydratable: options.hydratable === true && options.ssr !== true
101
119
  };
120
+ collectViews(path, state);
121
+ if (options.ssr !== true) {
122
+ collectRuns(path, state);
123
+ }
102
124
  },
103
125
  exit(path, state) {
104
- const { imports, templates } = state.firsthand;
126
+ const { imports, templates, views } = state.firsthand;
127
+ for (const [, local] of views) {
128
+ path.node.body.push(
129
+ expressionStatement2(t.callExpression(runtime(state, "view"), [t.cloneNode(local)]))
130
+ );
131
+ }
105
132
  if (templates.length > 0) {
106
133
  path.node.body.unshift(t.variableDeclaration("const", templates));
107
134
  }
@@ -120,18 +147,22 @@ function firsthandPlugin(_api, options = {}) {
120
147
  // component's children, a dynamic expression) are re-visited later, by
121
148
  // which time they are no longer inside JSX.
122
149
  JSXElement(path, state) {
123
- rewriteKeyedMaps(path, state);
150
+ if (!state.firsthand.ssr) {
151
+ rewriteKeyedMaps(path, state);
152
+ }
124
153
  path.replaceWith(compileNode(path, state));
125
154
  },
126
155
  JSXFragment(path, state) {
127
- rewriteKeyedMaps(path, state);
156
+ if (!state.firsthand.ssr) {
157
+ rewriteKeyedMaps(path, state);
158
+ }
128
159
  path.replaceWith(compileNode(path, state));
129
160
  },
130
161
  CallExpression(path, state) {
131
162
  annotateComponent(path, state, options);
132
163
  },
133
164
  VariableDeclarator(path, state) {
134
- if (options.devtools === true) {
165
+ if (options.devtools === true && options.ssr !== true) {
135
166
  nameCell(path, state);
136
167
  }
137
168
  }
@@ -153,7 +184,10 @@ function groupBySource(imports) {
153
184
  }
154
185
  return grouped;
155
186
  }
156
- function runtime(state, name, source = RUNTIME) {
187
+ function runtime(state, name, source) {
188
+ return runtimeFrom(state, name, source ?? (state.firsthand.ssr ? SERVER_RUNTIME : RUNTIME));
189
+ }
190
+ function runtimeFrom(state, name, source) {
157
191
  const key = `${source}#${name}`;
158
192
  let local = state.firsthand.imports.get(key);
159
193
  if (local === void 0) {
@@ -181,6 +215,7 @@ function annotateComponent(path, state, options) {
181
215
  const setupPath = path.get("arguments.0");
182
216
  checkKeptReads(setupPath, name);
183
217
  checkDecidedOnce(setupPath, name);
218
+ checkRunBody(setupPath, name);
184
219
  }
185
220
  path.node.arguments = [
186
221
  setup,
@@ -220,9 +255,13 @@ function checkDecidedOnce(setup, name) {
220
255
  throw at.buildCodeFrameError(
221
256
  `${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
257
 
223
- Put the choice inside the markup, where it is a part that can run again:
258
+ Either put the choice inside the markup, where it is a part:
224
259
 
225
- return <>{open.value ? <A /> : <B />}</>;`
260
+ return <>{open.value ? <A /> : <B />}</>;
261
+
262
+ or return a render function, which is a reactive scope of its own and may use ordinary control flow:
263
+
264
+ return () => (open.value ? <A /> : <B />);`
226
265
  );
227
266
  };
228
267
  const check = (returned, at) => {
@@ -274,6 +313,106 @@ Put the choice inside the markup, where it is a part that can run again:
274
313
  }
275
314
  });
276
315
  }
316
+ function checkRunBody(setup, name) {
317
+ const runs = [];
318
+ const body = setup.get("body");
319
+ body.traverse({
320
+ Function(nested) {
321
+ nested.skip();
322
+ },
323
+ ReturnStatement(statement) {
324
+ const argument = statement.get("argument");
325
+ if (argument.isArrowFunctionExpression() || argument.isFunctionExpression()) {
326
+ runs.push(argument);
327
+ }
328
+ }
329
+ });
330
+ for (const run of runs) {
331
+ checkNothingPersistent(run, name);
332
+ checkRepeatedMarkup(run, name);
333
+ }
334
+ }
335
+ var PERSISTENT = /* @__PURE__ */ new Map([
336
+ ["signal", "a signal"],
337
+ ["computed", "a computed"],
338
+ ["effect", "an effect"],
339
+ ["deepSignal", "a deep signal"],
340
+ ["useResource", "a resource"],
341
+ ["useAction", "an action"],
342
+ ["onCleanup", "a cleanup"],
343
+ ["provide", "a context value"]
344
+ ]);
345
+ function checkNothingPersistent(run, name) {
346
+ run.traverse({
347
+ Function(nested) {
348
+ nested.skip();
349
+ },
350
+ CallExpression(call) {
351
+ const callee = call.node.callee;
352
+ if (!t.isIdentifier(callee)) {
353
+ return;
354
+ }
355
+ const what = PERSISTENT.get(callee.name);
356
+ if (what === void 0) {
357
+ return;
358
+ }
359
+ const binding = call.scope.getBinding(callee.name);
360
+ if (binding === void 0 || !isFirsthandImport(binding)) {
361
+ return;
362
+ }
363
+ throw call.buildCodeFrameError(
364
+ `${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.
365
+
366
+ Move it into the setup, above the render function. Persistent things are made once, where the setup runs once.`
367
+ );
368
+ }
369
+ });
370
+ }
371
+ function checkRepeatedMarkup(run, name) {
372
+ const report = (at) => {
373
+ throw at.buildCodeFrameError(
374
+ `${name}: this markup is written once and appears many times, so where it stands cannot say which of them is which.
375
+
376
+ Give it a key:
377
+
378
+ {rows.map((row) => <Row key={row.id} row={row} />)}`
379
+ );
380
+ };
381
+ run.traverse({
382
+ Function(nested) {
383
+ nested.skip();
384
+ },
385
+ "ForStatement|ForOfStatement|ForInStatement|WhileStatement|DoWhileStatement"(loop) {
386
+ loop.traverse({
387
+ JSXElement(element) {
388
+ element.skip();
389
+ if (!hasKey(element.node)) {
390
+ report(element);
391
+ }
392
+ }
393
+ });
394
+ },
395
+ CallExpression(call) {
396
+ const callee = call.node.callee;
397
+ if (LIST_CALL in call.node || !t.isMemberExpression(callee) || callee.computed || !t.isIdentifier(callee.property, { name: "map" })) {
398
+ return;
399
+ }
400
+ call.traverse({
401
+ JSXElement(element) {
402
+ element.skip();
403
+ if (!hasKey(element.node)) {
404
+ report(element);
405
+ }
406
+ }
407
+ });
408
+ }
409
+ });
410
+ }
411
+ function hasKey(element) {
412
+ return element.openingElement.attributes.some(
413
+ (attribute) => t.isJSXAttribute(attribute) && attributeName(attribute) === "key"
414
+ );
415
+ }
277
416
  function returnsView(node) {
278
417
  if (node === null || node === void 0) {
279
418
  return false;
@@ -497,7 +636,229 @@ function compileNode(path, state) {
497
636
  if (isComponentTag(node)) {
498
637
  return compileComponent(path, state);
499
638
  }
500
- return compileTemplate(path, state);
639
+ return state.firsthand.ssr ? compileMarkup(path, state) : compileTemplate(path, state);
640
+ }
641
+ function collectViews(program, state) {
642
+ const { views, viewNodes } = state.firsthand;
643
+ const record = (path) => {
644
+ let fn = path.getFunctionParent();
645
+ while (fn !== null) {
646
+ viewNodes.add(fn.node);
647
+ const named = moduleLevelName(fn);
648
+ if (named !== null) {
649
+ views.set(named, t.identifier(named));
650
+ }
651
+ fn = fn.getFunctionParent();
652
+ }
653
+ };
654
+ program.traverse({
655
+ JSXElement: record,
656
+ JSXFragment: record
657
+ });
658
+ }
659
+ function moduleLevelName(fn) {
660
+ if (fn.isFunctionDeclaration()) {
661
+ const id = fn.node.id;
662
+ if (id === null || id === void 0) {
663
+ return null;
664
+ }
665
+ return t.isProgram(fn.parentPath.scope.block) ? id.name : null;
666
+ }
667
+ const declarator = fn.parentPath;
668
+ if (!declarator.isVariableDeclarator() || !t.isIdentifier(declarator.node.id)) {
669
+ return null;
670
+ }
671
+ return t.isProgram(declarator.scope.block) ? declarator.node.id.name : null;
672
+ }
673
+ function isLocalView(path, state) {
674
+ const name = path.node.openingElement.name;
675
+ if (!t.isJSXIdentifier(name)) {
676
+ return false;
677
+ }
678
+ const binding = path.scope.getBinding(name.name);
679
+ if (binding === void 0 || binding.kind === "module" || binding.constantViolations.length > 0) {
680
+ return false;
681
+ }
682
+ if (binding.path.isFunctionDeclaration()) {
683
+ return state.firsthand.viewNodes.has(binding.path.node);
684
+ }
685
+ if (!binding.path.isVariableDeclarator()) {
686
+ return false;
687
+ }
688
+ const init = binding.path.node.init;
689
+ if (!t.isArrowFunctionExpression(init) && !t.isFunctionExpression(init)) {
690
+ return false;
691
+ }
692
+ return state.firsthand.viewNodes.has(init);
693
+ }
694
+ function collectRuns(program, state) {
695
+ program.traverse({
696
+ CallExpression(call) {
697
+ if (!isComponentCall(call)) {
698
+ return;
699
+ }
700
+ const setup = call.get("arguments.0");
701
+ if (!setup.isArrowFunctionExpression() && !setup.isFunctionExpression()) {
702
+ return;
703
+ }
704
+ const found = [];
705
+ const body = setup.get("body");
706
+ if (!body.isBlockStatement()) {
707
+ if (body.isArrowFunctionExpression() || body.isFunctionExpression()) {
708
+ found.push(body);
709
+ }
710
+ } else {
711
+ body.traverse({
712
+ // A handler or a callback returns its own things; only what the
713
+ // setup itself hands back is the view.
714
+ Function(nested) {
715
+ nested.skip();
716
+ },
717
+ ReturnStatement(statement) {
718
+ const argument = statement.get("argument");
719
+ if (argument.isArrowFunctionExpression() || argument.isFunctionExpression()) {
720
+ found.push(argument);
721
+ }
722
+ }
723
+ });
724
+ }
725
+ if (found.length === 0) {
726
+ return;
727
+ }
728
+ setup.ensureBlock();
729
+ const block = setup.get("body");
730
+ for (const run of found) {
731
+ const store = setup.scope.generateUidIdentifier("store");
732
+ block.unshiftContainer(
733
+ "body",
734
+ t.variableDeclaration("const", [
735
+ t.variableDeclarator(
736
+ store,
737
+ t.callExpression(runtime(state, "store"), [t.stringLiteral(declaredName(call))])
738
+ )
739
+ ])
740
+ );
741
+ let index = 0;
742
+ state.firsthand.runs.set(run.node, {
743
+ node: run.node,
744
+ store,
745
+ next: () => index++
746
+ });
747
+ closeRun(run, store, state);
748
+ }
749
+ }
750
+ });
751
+ }
752
+ function closeRun(run, store, state) {
753
+ const body = run.get("body");
754
+ if (!body.isBlockStatement()) {
755
+ body.replaceWith(
756
+ t.callExpression(runtime(state, "ran"), [t.cloneNode(store), body.node])
757
+ );
758
+ return;
759
+ }
760
+ const returns = [];
761
+ body.traverse({
762
+ Function(nested) {
763
+ nested.skip();
764
+ },
765
+ ReturnStatement(statement) {
766
+ returns.push(statement);
767
+ }
768
+ });
769
+ for (const statement of returns) {
770
+ statement.node.argument = t.callExpression(runtime(state, "ran"), [
771
+ t.cloneNode(store),
772
+ statement.node.argument ?? t.identifier("undefined")
773
+ ]);
774
+ }
775
+ }
776
+ function isComponentCall(path) {
777
+ if (!t.isIdentifier(path.node.callee, { name: "component" })) {
778
+ return false;
779
+ }
780
+ const binding = path.scope.getBinding("component");
781
+ return binding !== void 0 && isFirsthandImport(binding);
782
+ }
783
+ function enclosingRun(path, state) {
784
+ let fn = path.getFunctionParent();
785
+ while (fn !== null && GENERATED in fn.node) {
786
+ fn = fn.getFunctionParent();
787
+ }
788
+ if (fn === null) {
789
+ return null;
790
+ }
791
+ return state.firsthand.runs.get(fn.node) ?? null;
792
+ }
793
+ function dependsOnRun(node, run, at) {
794
+ if (run === null) {
795
+ return false;
796
+ }
797
+ const names = /* @__PURE__ */ new Set();
798
+ collectNames(node, names);
799
+ for (const name of names) {
800
+ const binding = at.scope.getBinding(name);
801
+ if (binding === void 0) {
802
+ continue;
803
+ }
804
+ if (binding.scope.block === run.node || binding.path.findParent((parent) => parent.node === run.node) !== null) {
805
+ return true;
806
+ }
807
+ }
808
+ return false;
809
+ }
810
+ function collectNames(node, into) {
811
+ if (t.isIdentifier(node)) {
812
+ into.add(node.name);
813
+ return;
814
+ }
815
+ for (const key of t.VISITOR_KEYS[node.type]) {
816
+ if (t.isMemberExpression(node) && !node.computed && key === "property") {
817
+ continue;
818
+ }
819
+ if ((t.isObjectProperty(node) || t.isObjectMethod(node)) && !node.computed && key === "key") {
820
+ continue;
821
+ }
822
+ const value = node[key];
823
+ for (const child of Array.isArray(value) ? value : [value]) {
824
+ if (typeof child === "object" && child !== null) {
825
+ collectNames(child, into);
826
+ }
827
+ }
828
+ }
829
+ }
830
+ function canRetain(node, build) {
831
+ let possible = true;
832
+ const visit = (element) => {
833
+ for (const attribute of element.openingElement.attributes) {
834
+ if (t.isJSXSpreadAttribute(attribute)) {
835
+ if (dependsOnRun(attribute.argument, build.run, build.at)) {
836
+ possible = false;
837
+ }
838
+ continue;
839
+ }
840
+ const value = attribute.value;
841
+ if (attributeName(attribute) === "ref" && t.isJSXExpressionContainer(value) && !t.isJSXEmptyExpression(value.expression) && dependsOnRun(value.expression, build.run, build.at)) {
842
+ possible = false;
843
+ }
844
+ }
845
+ for (const child of element.children) {
846
+ if (t.isJSXElement(child)) {
847
+ if (!isComponentTag(child)) {
848
+ visit(child);
849
+ }
850
+ continue;
851
+ }
852
+ if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression) && isKeyedList(child.expression) && dependsOnRun(child.expression, build.run, build.at)) {
853
+ possible = false;
854
+ }
855
+ }
856
+ };
857
+ visit(node);
858
+ return possible;
859
+ }
860
+ function isKeyedList(node) {
861
+ return t.isCallExpression(node) && LIST_CALL in node;
501
862
  }
502
863
  function isComponentTag(node) {
503
864
  const name = node.openingElement.name;
@@ -517,7 +878,25 @@ function tagExpression(name) {
517
878
  }
518
879
  function compileComponent(path, state) {
519
880
  const node = path.node;
881
+ const run = enclosingRun(path, state);
520
882
  const properties = [];
883
+ const cells = [];
884
+ const throughCell = (value) => {
885
+ const holder = t.identifier(`_cell$${String(run.next())}`);
886
+ cells.push(
887
+ t.variableDeclaration("const", [
888
+ t.variableDeclarator(
889
+ holder,
890
+ t.callExpression(runtime(state, "cell"), [
891
+ t.cloneNode(run.store),
892
+ t.numericLiteral(run.next()),
893
+ value
894
+ ])
895
+ )
896
+ ])
897
+ );
898
+ return t.memberExpression(t.cloneNode(holder), t.identifier("value"));
899
+ };
521
900
  for (const attribute of node.openingElement.attributes) {
522
901
  if (t.isJSXSpreadAttribute(attribute)) {
523
902
  properties.push(t.spreadElement(attribute.argument));
@@ -525,12 +904,20 @@ function compileComponent(path, state) {
525
904
  }
526
905
  const name = attributeName(attribute);
527
906
  const value = attributeValue(attribute);
907
+ if (state.firsthand.ssr && name === "key") {
908
+ continue;
909
+ }
528
910
  if (value === null || isStaticValue(value)) {
529
911
  properties.push(t.objectProperty(propertyKey(name), value ?? t.booleanLiteral(true)));
530
912
  } else {
531
- properties.push(
532
- t.objectMethod("get", propertyKey(name), [], t.blockStatement([t.returnStatement(value)]))
533
- );
913
+ const held = dependsOnRun(value, run, path) ? throughCell(value) : value;
914
+ if (state.firsthand.ssr && isPure(value)) {
915
+ properties.push(t.objectProperty(propertyKey(name), held));
916
+ } else {
917
+ const read = t.returnStatement(held);
918
+ takePosition(read, value);
919
+ properties.push(t.objectMethod("get", propertyKey(name), [], t.blockStatement([read])));
920
+ }
534
921
  }
535
922
  }
536
923
  const children = compileChildren(node.children, state);
@@ -553,11 +940,94 @@ function compileComponent(path, state) {
553
940
  )
554
941
  );
555
942
  }
556
- return t.callExpression(runtime(state, "createComponent"), [
557
- // `isComponentTag` has already excluded namespaced names.
558
- tagExpression(node.openingElement.name),
559
- t.objectExpression(properties)
560
- ]);
943
+ const tag = tagExpression(node.openingElement.name);
944
+ const make = isLocalView(path, state) ? t.callExpression(tag, [t.objectExpression(properties)]) : t.callExpression(runtime(state, "createComponent"), [tag, t.objectExpression(properties)]);
945
+ if (run !== null) {
946
+ return keptChild(state, run, make, cells);
947
+ }
948
+ if (isLocalView(path, state)) {
949
+ if (state.firsthand.ssr) {
950
+ return make;
951
+ }
952
+ const parent = path.parentPath;
953
+ if (parent.isArrowFunctionExpression() && parent.node.body === node && CHILD_THUNK in parent.node) {
954
+ return make;
955
+ }
956
+ return t.callExpression(runtime(state, "part"), [t.arrowFunctionExpression([], make)]);
957
+ }
958
+ return make;
959
+ }
960
+ function keptChild(state, run, make, cells) {
961
+ const slot = t.identifier(`_kept$${String(run.next())}`);
962
+ const held = t.identifier(`_own$${String(run.next())}`);
963
+ const made = () => t.memberExpression(t.cloneNode(slot), t.identifier("last"));
964
+ const body = [
965
+ ...cells,
966
+ t.variableDeclaration("const", [
967
+ t.variableDeclarator(
968
+ slot,
969
+ t.callExpression(runtime(state, "site"), [
970
+ t.cloneNode(run.store),
971
+ t.numericLiteral(run.next())
972
+ ])
973
+ )
974
+ ]),
975
+ t.ifStatement(
976
+ t.binaryExpression("===", made(), t.identifier("undefined")),
977
+ t.blockStatement([
978
+ t.variableDeclaration("const", [
979
+ t.variableDeclarator(
980
+ held,
981
+ t.callExpression(runtime(state, "open"), [t.cloneNode(run.store), t.cloneNode(slot)])
982
+ )
983
+ ]),
984
+ expressionStatement2(
985
+ t.assignmentExpression(
986
+ "=",
987
+ made(),
988
+ t.callExpression(runtime(state, "part"), [t.arrowFunctionExpression([], make)])
989
+ )
990
+ ),
991
+ expressionStatement2(t.callExpression(runtime(state, "close"), [t.cloneNode(held)]))
992
+ ])
993
+ ),
994
+ t.returnStatement(made())
995
+ ];
996
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body))), []);
997
+ }
998
+ function isPure(node) {
999
+ switch (node.type) {
1000
+ case "Identifier":
1001
+ case "ThisExpression":
1002
+ case "StringLiteral":
1003
+ case "NumericLiteral":
1004
+ case "BooleanLiteral":
1005
+ case "NullLiteral":
1006
+ case "BigIntLiteral":
1007
+ case "RegExpLiteral":
1008
+ return true;
1009
+ case "MemberExpression":
1010
+ case "OptionalMemberExpression":
1011
+ return isPure(node.object) && (!node.computed || isPure(node.property));
1012
+ case "UnaryExpression":
1013
+ return node.operator !== "delete" && isPure(node.argument);
1014
+ case "BinaryExpression":
1015
+ return isPure(node.left) && isPure(node.right);
1016
+ case "LogicalExpression":
1017
+ return isPure(node.left) && isPure(node.right);
1018
+ case "ConditionalExpression":
1019
+ return isPure(node.test) && isPure(node.consequent) && isPure(node.alternate);
1020
+ case "TemplateLiteral":
1021
+ return node.expressions.every((one) => t.isExpression(one) && isPure(one));
1022
+ case "ArrayExpression":
1023
+ return node.elements.every((one) => one === null || t.isExpression(one) && isPure(one));
1024
+ case "ObjectExpression":
1025
+ return node.properties.every(
1026
+ (one) => t.isObjectProperty(one) && !one.computed && t.isExpression(one.value) && isPure(one.value)
1027
+ );
1028
+ default:
1029
+ return false;
1030
+ }
561
1031
  }
562
1032
  function propertyKey(name) {
563
1033
  return t.isValidIdentifier(name) ? t.identifier(name) : t.stringLiteral(name);
@@ -582,15 +1052,149 @@ function attributeValue(attribute) {
582
1052
  function isStaticValue(value) {
583
1053
  return t.isStringLiteral(value) || t.isNumericLiteral(value) || t.isBooleanLiteral(value) || t.isNullLiteral(value);
584
1054
  }
1055
+ function pushText(markup, text) {
1056
+ markup.parts[markup.parts.length - 1] = markup.parts[markup.parts.length - 1] + text;
1057
+ }
1058
+ function pushHole(markup, value) {
1059
+ markup.values.push(value);
1060
+ markup.parts.push("");
1061
+ }
1062
+ function compileMarkup(path, state) {
1063
+ const markup = { parts: [""], values: [] };
1064
+ emitMarkupElement(path.node, markup, state);
1065
+ if (markup.values.length === 0) {
1066
+ return t.callExpression(runtime(state, "ssr"), [
1067
+ t.arrayExpression([t.stringLiteral(markup.parts[0])])
1068
+ ]);
1069
+ }
1070
+ return t.callExpression(runtime(state, "ssr"), [
1071
+ t.arrayExpression(markup.parts.map((part) => t.stringLiteral(part))),
1072
+ ...markup.values
1073
+ ]);
1074
+ }
1075
+ function emitMarkupElement(node, markup, state) {
1076
+ const name = node.openingElement.name;
1077
+ if (!t.isJSXIdentifier(name)) {
1078
+ const namespaced = name;
1079
+ throw new Error(
1080
+ `Namespaced element names are not supported: <${namespaced.namespace.name}:${namespaced.name.name}>. Write the element without a namespace; SVG children are resolved by the parser.`
1081
+ );
1082
+ }
1083
+ const tag = name.name;
1084
+ pushText(markup, `<${tag}`);
1085
+ for (const attribute of node.openingElement.attributes) {
1086
+ emitMarkupAttribute(attribute, markup, state);
1087
+ }
1088
+ pushText(markup, ">");
1089
+ emitMarkupChildren(node.children, markup, state);
1090
+ if (!VOID_ELEMENTS.has(tag)) {
1091
+ pushText(markup, `</${tag}>`);
1092
+ }
1093
+ }
1094
+ function emitMarkupAttribute(attribute, markup, state) {
1095
+ if (t.isJSXSpreadAttribute(attribute)) {
1096
+ pushHole(markup, t.callExpression(runtime(state, "spread"), [attribute.argument]));
1097
+ return;
1098
+ }
1099
+ const name = attributeName(attribute);
1100
+ const value = attributeValue(attribute);
1101
+ if (name === "ref" || name.startsWith("on") && name.length > 2 && /[A-Z:]/.test(name[2])) {
1102
+ return;
1103
+ }
1104
+ if (name === "key") {
1105
+ return;
1106
+ }
1107
+ if (value === null) {
1108
+ pushText(markup, ` ${name}=""`);
1109
+ return;
1110
+ }
1111
+ if (isStaticValue(value)) {
1112
+ if (t.isBooleanLiteral(value) && !value.value) {
1113
+ return;
1114
+ }
1115
+ if (t.isNullLiteral(value)) {
1116
+ return;
1117
+ }
1118
+ const literal = t.isStringLiteral(value) ? value.value : t.isNumericLiteral(value) ? String(value.value) : "";
1119
+ pushText(markup, ` ${name}="${escapeAttribute(literal)}"`);
1120
+ return;
1121
+ }
1122
+ pushHole(markup, markupAttributeCall(name, value, state));
1123
+ }
1124
+ function markupAttributeCall(name, value, state) {
1125
+ if (name.startsWith("prop:")) {
1126
+ return t.callExpression(runtime(state, "setProperty"), [t.stringLiteral(name.slice(5)), value]);
1127
+ }
1128
+ if (name.startsWith("attr:")) {
1129
+ return t.callExpression(runtime(state, "setAttribute"), [
1130
+ t.stringLiteral(name.slice(5)),
1131
+ value
1132
+ ]);
1133
+ }
1134
+ if (name === "class" || name === "className") {
1135
+ return t.callExpression(runtime(state, "setClass"), [value]);
1136
+ }
1137
+ if (name === "style") {
1138
+ return t.callExpression(runtime(state, "setStyle"), [value]);
1139
+ }
1140
+ if (BOOLEAN_PROPERTIES.has(name)) {
1141
+ return t.callExpression(runtime(state, "setBoolean"), [t.stringLiteral(name), value]);
1142
+ }
1143
+ if (DOM_PROPERTIES.has(name)) {
1144
+ return t.callExpression(runtime(state, "setProperty"), [t.stringLiteral(name), value]);
1145
+ }
1146
+ return t.callExpression(runtime(state, "setAttribute"), [t.stringLiteral(name), value]);
1147
+ }
1148
+ function emitMarkupChildren(children, markup, state) {
1149
+ const entries = planChildren(children);
1150
+ for (let i = 0; i < entries.length; i++) {
1151
+ const entry = entries[i];
1152
+ if (entry.kind === "text") {
1153
+ pushText(markup, escapeText(entry.text));
1154
+ continue;
1155
+ }
1156
+ if (entry.kind === "element") {
1157
+ emitMarkupElement(entry.element, markup, state);
1158
+ continue;
1159
+ }
1160
+ if (entries.length > 1) {
1161
+ pushText(markup, "<!--[-->");
1162
+ }
1163
+ pushHole(markup, t.callExpression(runtime(state, "child"), [entry.expression]));
1164
+ if (i !== entries.length - 1) {
1165
+ pushText(markup, "<!---->");
1166
+ }
1167
+ }
1168
+ }
585
1169
  function compileTemplate(path, state) {
1170
+ let names = 0;
1171
+ const statements = [];
586
1172
  const build = {
587
1173
  html: [],
588
- statements: [],
1174
+ statements,
1175
+ // Where a site is built once, these are three lists; where it is not, they
1176
+ // are one, and everything simply happens in the order it was written.
1177
+ once: statements,
1178
+ each: statements,
1179
+ run: null,
1180
+ at: path,
589
1181
  next: /* @__PURE__ */ (() => {
590
1182
  let n = 0;
591
1183
  return () => t.identifier(`_el$${String(++n)}`);
592
- })()
1184
+ })(),
1185
+ name: (prefix) => t.identifier(`${prefix}${String(++names)}`)
593
1186
  };
1187
+ const run = enclosingRun(path, state);
1188
+ if (run !== null) {
1189
+ build.run = run;
1190
+ if (canRetain(path.node, build)) {
1191
+ build.once = [];
1192
+ build.each = [];
1193
+ } else {
1194
+ build.run = null;
1195
+ }
1196
+ }
1197
+ const kept = build.run !== null;
594
1198
  const root = t.identifier("_el$");
595
1199
  emitElement(path.node, build, root, state);
596
1200
  const templateId = t.identifier(`_tmpl$${String(++state.firsthand.counter)}`);
@@ -604,14 +1208,107 @@ function compileTemplate(path, state) {
604
1208
  )
605
1209
  )
606
1210
  );
1211
+ if (!kept) {
1212
+ const body2 = [
1213
+ t.variableDeclaration("const", [
1214
+ t.variableDeclarator(root, t.callExpression(t.cloneNode(templateId), []))
1215
+ ]),
1216
+ ...build.statements,
1217
+ t.returnStatement(t.cloneNode(root))
1218
+ ];
1219
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body2))), []);
1220
+ }
1221
+ const slot = build.name("_site$");
1222
+ const fresh = build.name("_new$");
1223
+ const held = build.name("_own$");
1224
+ const owner = build.run;
607
1225
  const body = [
608
1226
  t.variableDeclaration("const", [
609
- t.variableDeclarator(root, t.callExpression(t.cloneNode(templateId), []))
1227
+ t.variableDeclarator(
1228
+ slot,
1229
+ t.callExpression(runtime(state, "site"), [
1230
+ t.cloneNode(owner.store),
1231
+ t.numericLiteral(owner.next())
1232
+ ])
1233
+ )
1234
+ ]),
1235
+ t.variableDeclaration("let", [
1236
+ t.variableDeclarator(root, t.memberExpression(t.cloneNode(slot), t.identifier("node")))
1237
+ ]),
1238
+ t.variableDeclaration("const", [
1239
+ t.variableDeclarator(
1240
+ fresh,
1241
+ t.binaryExpression("===", t.cloneNode(root), t.identifier("undefined"))
1242
+ )
610
1243
  ]),
1244
+ t.variableDeclaration("let", [t.variableDeclarator(held)]),
1245
+ t.ifStatement(
1246
+ t.cloneNode(fresh),
1247
+ t.blockStatement([
1248
+ // What this site makes belongs to the instance. A run's own scope is
1249
+ // cleared before it runs again, and a part left there would be
1250
+ // disposed by the very next run.
1251
+ expressionStatement2(
1252
+ t.assignmentExpression(
1253
+ "=",
1254
+ t.cloneNode(held),
1255
+ t.callExpression(runtime(state, "open"), [t.cloneNode(owner.store), t.cloneNode(slot)])
1256
+ )
1257
+ ),
1258
+ expressionStatement2(
1259
+ t.assignmentExpression(
1260
+ "=",
1261
+ t.cloneNode(root),
1262
+ t.assignmentExpression(
1263
+ "=",
1264
+ t.memberExpression(t.cloneNode(slot), t.identifier("node")),
1265
+ t.callExpression(t.cloneNode(templateId), [])
1266
+ )
1267
+ )
1268
+ )
1269
+ ])
1270
+ ),
611
1271
  ...build.statements,
612
- t.returnStatement(t.cloneNode(root))
1272
+ t.ifStatement(
1273
+ t.cloneNode(fresh),
1274
+ t.blockStatement([
1275
+ ...build.once,
1276
+ expressionStatement2(t.callExpression(runtime(state, "close"), [t.cloneNode(held)]))
1277
+ ])
1278
+ )
613
1279
  ];
614
- return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(body)), []);
1280
+ body.push(...build.each, t.returnStatement(t.cloneNode(root)));
1281
+ return t.callExpression(generated(t.arrowFunctionExpression([], t.blockStatement(body))), []);
1282
+ }
1283
+ function guardedWrite(build, state, value, write) {
1284
+ const run = build.run;
1285
+ const slot = build.name("_w$");
1286
+ const held = build.name("_x$");
1287
+ const last = () => t.memberExpression(t.cloneNode(slot), t.identifier("last"));
1288
+ pushEach(
1289
+ build,
1290
+ t.variableDeclaration("const", [
1291
+ t.variableDeclarator(
1292
+ slot,
1293
+ t.callExpression(runtime(state, "site"), [
1294
+ t.cloneNode(run.store),
1295
+ t.numericLiteral(run.next())
1296
+ ])
1297
+ ),
1298
+ t.variableDeclarator(held, value)
1299
+ ])
1300
+ );
1301
+ pushEach(
1302
+ build,
1303
+ t.ifStatement(
1304
+ t.binaryExpression("!==", last(), t.cloneNode(held)),
1305
+ t.blockStatement([
1306
+ expressionStatement2(t.assignmentExpression("=", last(), t.cloneNode(held))),
1307
+ expressionStatement2(t.callExpression(runtime(state, "wrote"), [t.cloneNode(run.store)])),
1308
+ expressionStatement2(write(t.cloneNode(held)))
1309
+ ])
1310
+ )
1311
+ );
615
1312
  }
616
1313
  function emitElement(node, build, self, state) {
617
1314
  const name = node.openingElement.name;
@@ -639,7 +1336,8 @@ function emitElement(node, build, self, state) {
639
1336
  function emitAttribute(attribute, build, self, state, deferred, tag) {
640
1337
  if (t.isJSXSpreadAttribute(attribute)) {
641
1338
  deferred.push(() => {
642
- build.statements.push(
1339
+ pushOnce(
1340
+ build,
643
1341
  expressionStatement2(
644
1342
  t.callExpression(runtime(state, "bind"), [
645
1343
  t.arrowFunctionExpression(
@@ -656,7 +1354,8 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
656
1354
  const value = attributeValue(attribute);
657
1355
  if (name === "ref") {
658
1356
  deferred.push(() => {
659
- build.statements.push(
1357
+ pushOnce(
1358
+ build,
660
1359
  expressionStatement2(t.callExpression(value, [t.cloneNode(self)]))
661
1360
  );
662
1361
  });
@@ -680,7 +1379,12 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
680
1379
  t.objectExpression([t.objectProperty(propertyKey(modifier), t.booleanLiteral(true))])
681
1380
  );
682
1381
  }
683
- build.statements.push(expressionStatement2(t.callExpression(runtime(state, "on"), args)));
1382
+ const attach = expressionStatement2(t.callExpression(runtime(state, "on"), args));
1383
+ if (dependsOnRun(value, build.run, build.at)) {
1384
+ pushEach(build, attach);
1385
+ } else {
1386
+ pushOnce(build, attach);
1387
+ }
684
1388
  });
685
1389
  return;
686
1390
  }
@@ -700,7 +1404,17 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
700
1404
  return;
701
1405
  }
702
1406
  deferred.push(() => {
703
- build.statements.push(
1407
+ if (dependsOnRun(value, build.run, build.at)) {
1408
+ guardedWrite(
1409
+ build,
1410
+ state,
1411
+ value,
1412
+ (held) => dynamicAttributeCall(name, held, self, state, tag)
1413
+ );
1414
+ return;
1415
+ }
1416
+ pushOnce(
1417
+ build,
704
1418
  expressionStatement2(
705
1419
  t.callExpression(runtime(state, "bind"), [
706
1420
  located(
@@ -860,7 +1574,7 @@ function emitChildren(children, build, self, state) {
860
1574
  const id = build.next();
861
1575
  build.statements.push(
862
1576
  t.variableDeclaration("const", [
863
- t.variableDeclarator(id, navigate(self, previous, previousIndex, index))
1577
+ t.variableDeclarator(id, navigate(self, previous, previousIndex, index, state))
864
1578
  ])
865
1579
  );
866
1580
  previous = id;
@@ -886,18 +1600,39 @@ function emitChildren(children, build, self, state) {
886
1600
  continue;
887
1601
  }
888
1602
  const isLast = i === entries.length - 1;
1603
+ const expression = entry.expression;
1604
+ let marker = null;
1605
+ if (!isLast) {
1606
+ build.html.push("<!>");
1607
+ marker = t.cloneNode(reference());
1608
+ }
1609
+ if (entry.kind !== "list" && dependsOnRun(expression, build.run, build.at)) {
1610
+ const run = build.run;
1611
+ pushEach(
1612
+ build,
1613
+ expressionStatement2(
1614
+ t.callExpression(runtime(state, "writeChild"), [
1615
+ t.cloneNode(run.store),
1616
+ t.numericLiteral(run.next()),
1617
+ t.cloneNode(self),
1618
+ marker ?? t.nullLiteral(),
1619
+ expression
1620
+ ])
1621
+ )
1622
+ );
1623
+ index++;
1624
+ continue;
1625
+ }
889
1626
  const args = [
890
1627
  t.cloneNode(self),
891
1628
  // A list part is already a thunk that owns its rows; wrapping it would
892
1629
  // rebuild the whole list on every evaluation.
893
- entry.kind === "list" ? entry.expression : thunk(entry.expression)
1630
+ entry.kind === "list" ? expression : thunk(expression)
894
1631
  ];
895
- if (!isLast) {
896
- build.html.push("<!>");
897
- const id = reference();
898
- args.push(t.cloneNode(id));
1632
+ if (marker !== null) {
1633
+ args.push(marker);
899
1634
  }
900
- build.statements.push(expressionStatement2(t.callExpression(runtime(state, "insert"), args)));
1635
+ pushOnce(build, expressionStatement2(t.callExpression(runtime(state, "insert"), args)));
901
1636
  index++;
902
1637
  }
903
1638
  }
@@ -925,18 +1660,21 @@ function needsReference(element) {
925
1660
  }
926
1661
  return false;
927
1662
  }
928
- function navigate(self, previous, previousIndex, index) {
1663
+ function navigate(self, previous, previousIndex, index, state) {
1664
+ const hydratable = state.firsthand.hydratable;
1665
+ const first = hydratable ? runtime(state, "first") : null;
1666
+ const next = hydratable ? runtime(state, "next") : null;
929
1667
  let expression;
930
1668
  let steps;
931
1669
  if (previous === null) {
932
- expression = t.memberExpression(t.cloneNode(self), t.identifier("firstChild"));
1670
+ expression = hydratable ? t.callExpression(first, [t.cloneNode(self)]) : t.memberExpression(t.cloneNode(self), t.identifier("firstChild"));
933
1671
  steps = index;
934
1672
  } else {
935
1673
  expression = t.cloneNode(previous);
936
1674
  steps = index - previousIndex;
937
1675
  }
938
1676
  for (let i = 0; i < steps; i++) {
939
- expression = t.memberExpression(expression, t.identifier("nextSibling"));
1677
+ expression = hydratable ? t.callExpression(next, [expression]) : t.memberExpression(expression, t.identifier("nextSibling"));
940
1678
  }
941
1679
  return expression;
942
1680
  }
@@ -1002,13 +1740,13 @@ function compileChildren(children, state) {
1002
1740
  result.push(t.stringLiteral(entry.text));
1003
1741
  } else if (entry.kind === "element") {
1004
1742
  result.push(entry.element);
1743
+ } else if (state.firsthand.ssr) {
1744
+ result.push(entry.expression);
1005
1745
  } else if (entry.kind === "list") {
1006
1746
  result.push(t.callExpression(runtime(state, "part"), [entry.expression]));
1007
1747
  } else {
1008
1748
  result.push(
1009
- t.callExpression(runtime(state, "part"), [
1010
- t.arrowFunctionExpression([], entry.expression)
1011
- ])
1749
+ t.callExpression(runtime(state, "part"), [thunk(entry.expression)])
1012
1750
  );
1013
1751
  }
1014
1752
  }
@@ -1016,12 +1754,16 @@ function compileChildren(children, state) {
1016
1754
  }
1017
1755
  function thunk(expression) {
1018
1756
  const arrow = t.arrowFunctionExpression([], expression);
1757
+ arrow[CHILD_THUNK] = true;
1758
+ takePosition(arrow, expression);
1759
+ return arrow;
1760
+ }
1761
+ function takePosition(target, expression) {
1019
1762
  const loc = expression.loc;
1020
1763
  if (loc !== null && loc !== void 0) {
1021
- arrow.loc = { ...loc, end: loc.start };
1764
+ target.loc = { ...loc, end: loc.start };
1022
1765
  expression.loc = null;
1023
1766
  }
1024
- return arrow;
1025
1767
  }
1026
1768
  function expressionStatement2(expression) {
1027
1769
  return located(t.expressionStatement(expression), expression);
@@ -1070,7 +1812,9 @@ function buildPlugins(options) {
1070
1812
  {
1071
1813
  packageName: options.packageName,
1072
1814
  strictReactivity: options.strictReactivity,
1073
- devtools: options.devtools
1815
+ devtools: options.devtools,
1816
+ ssr: options.ssr,
1817
+ hydratable: options.hydratable
1074
1818
  }
1075
1819
  ]);
1076
1820
  return plugins;
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-ACKKPAGI.js";
7
7
  export {
8
8
  compileModule,
9
9
  firsthandPlugin,
@@ -18,6 +18,32 @@ 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
+ /** Whether this module is being compiled for a server render. */
26
+ ssr: boolean;
27
+ /** Whether this module's output has to be able to adopt server markup. */
28
+ hydratable: boolean;
29
+ /** Functions that run again as a whole, and where each keeps its sites. */
30
+ runs: Map<t.Node, RunContext>;
31
+ };
32
+ /**
33
+ * What a re-running function needs in order to keep its DOM.
34
+ *
35
+ * `store` is an array declared once per instance — in the setup, which runs
36
+ * once — and every site inside the run takes a numbered place in it. The index
37
+ * is fixed at compile time, so a site inside an `if` keeps its own place
38
+ * whether or not the branch was taken: nothing depends on the order the run
39
+ * happens to reach things in, which is the rule React needs for hooks and this
40
+ * does not.
41
+ */
42
+ type RunContext = {
43
+ /** The function whose body re-runs. Bindings inside it belong to one run. */
44
+ node: t.Node;
45
+ store: t.Identifier;
46
+ next: () => number;
21
47
  };
22
48
  declare module '@babel/core' {
23
49
  interface PluginPass {
@@ -49,6 +75,26 @@ export type FirsthandPluginOptions = {
49
75
  * production build emits nothing.
50
76
  */
51
77
  devtools?: boolean;
78
+ /**
79
+ * Compile for a server render.
80
+ *
81
+ * The same source, emitted against `@firsthandjs/server/internal` instead of
82
+ * `@firsthandjs/dom/internal`: markup is built as a string rather than as
83
+ * nodes, and the things a server cannot do — listeners, refs, retained
84
+ * sites — are not emitted at all.
85
+ *
86
+ * The Vite plugin sets this from the bundler's own `ssr` flag, so an
87
+ * application configures nothing.
88
+ */
89
+ ssr?: boolean;
90
+ /**
91
+ * Emit navigation that can walk server markup.
92
+ *
93
+ * An application that hydrates needs it; one that does not should leave it
94
+ * off, because it turns two property reads per dynamic position into two
95
+ * calls. The Vite plugin sets it for a project that has a server build.
96
+ */
97
+ hydratable?: boolean;
52
98
  };
53
99
  export default function firsthandPlugin(_api: unknown, options?: FirsthandPluginOptions): PluginObject;
54
100
  export {};
@@ -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;AAgBlC,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,iEAAiE;IACjE,GAAG,EAAE,OAAO,CAAC;IACb,0EAA0E;IAC1E,UAAU,EAAE,OAAO,CAAC;IACpB,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;IACnB;;;;;;;;;;OAUG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAiFd"}
package/dist/vite.d.ts CHANGED
@@ -7,7 +7,9 @@ export type VitePluginLike = {
7
7
  configResolved(config: {
8
8
  command: string;
9
9
  }): void;
10
- transform(code: string, id: string): {
10
+ transform(code: string, id: string, options?: {
11
+ ssr?: boolean | undefined;
12
+ }): {
11
13
  code: string;
12
14
  map: SourceMap | null;
13
15
  } | null;
@@ -18,5 +20,28 @@ export type VitePluginLike = {
18
20
  * Runs before the bundler's TypeScript step: Firsthand parses TS syntax but leaves
19
21
  * the annotations in place, so exactly one tool strips them.
20
22
  */
21
- export declare function firsthand(options?: FirsthandPluginOptions): VitePluginLike;
23
+ export type FirsthandViteOptions = FirsthandPluginOptions & {
24
+ /**
25
+ * Files to compile. Everything with a JSX extension, by default.
26
+ *
27
+ * Patterns are regular expressions rather than globs, so that this package
28
+ * keeps its promise of having no dependencies — and so that what matches is
29
+ * something you can read rather than something you have to guess at.
30
+ */
31
+ include?: readonly RegExp[];
32
+ /**
33
+ * Files to leave alone.
34
+ *
35
+ * This is how a project says *these are not mine*: a folder of React
36
+ * components kept during a migration, compiled by React's own transform.
37
+ * What this compiler does not compile, it does not claim — a component from
38
+ * an excluded file reaches the adapter like any other foreign one.
39
+ *
40
+ * ```ts
41
+ * firsthand({ exclude: [/\/legacy\//] })
42
+ * ```
43
+ */
44
+ exclude?: readonly RegExp[];
45
+ };
46
+ export declare function firsthand(options?: FirsthandViteOptions): VitePluginLike;
22
47
  //# 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,CACP,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EAIV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,GACtC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACnD,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,CAsC5E"}
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compileModule
3
- } from "./chunk-7DU5USN7.js";
3
+ } from "./chunk-ACKKPAGI.js";
4
4
 
5
5
  // packages/compiler/src/vite.ts
6
6
  function firsthand(options = {}) {
@@ -11,17 +11,27 @@ function firsthand(options = {}) {
11
11
  configResolved(config) {
12
12
  serving = config.command === "serve";
13
13
  },
14
- transform(code, id) {
14
+ transform(code, id, hook) {
15
15
  const file = id.split("?")[0];
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"),
22
28
  devtools: serving,
23
29
  sourceMaps: true,
24
- ...options
30
+ ...options,
31
+ // The bundler already knows which build this is. An explicit `ssr`
32
+ // option still wins, because a project that compiles a module by hand
33
+ // has its own reasons.
34
+ ssr: options.ssr ?? hook?.ssr === true
25
35
  });
26
36
  }
27
37
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firsthandjs/compiler",
3
- "version": "0.7.1",
3
+ "version": "0.9.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",