@firsthandjs/compiler 0.6.2 → 0.7.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.
@@ -178,7 +178,9 @@ function annotateComponent(path, state, options) {
178
178
  const name = declaredName(path);
179
179
  rewritePropsDestructuring(path, name, state);
180
180
  if (options.strictReactivity !== false) {
181
- checkKeptReads(path.get("arguments.0"), name);
181
+ const setupPath = path.get("arguments.0");
182
+ checkKeptReads(setupPath, name);
183
+ checkDecidedOnce(setupPath, name);
182
184
  }
183
185
  path.node.arguments = [
184
186
  setup,
@@ -213,6 +215,113 @@ Move the read into the part, handler, effect or computed that should re-read it
213
215
  }
214
216
  });
215
217
  }
218
+ function checkDecidedOnce(setup, name) {
219
+ const report = (at) => {
220
+ throw at.buildCodeFrameError(
221
+ `${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
+
223
+ Put the choice inside the markup, where it is a part that can run again:
224
+
225
+ return <>{open.value ? <A /> : <B />}</>;`
226
+ );
227
+ };
228
+ const check = (returned, at) => {
229
+ if (t.isConditionalExpression(returned)) {
230
+ if (!hasView(returned.consequent) && !hasView(returned.alternate)) {
231
+ return;
232
+ }
233
+ if (readsSignal(returned.test)) {
234
+ report(at);
235
+ }
236
+ return;
237
+ }
238
+ if (t.isLogicalExpression(returned) && returned.operator !== "??") {
239
+ if (!hasView(returned.right) && !hasView(returned.left)) {
240
+ return;
241
+ }
242
+ if (readsSignal(returned.left)) {
243
+ report(at);
244
+ }
245
+ }
246
+ };
247
+ const body = setup.get("body");
248
+ if (!body.isBlockStatement()) {
249
+ check(setup.node.body, body);
250
+ return;
251
+ }
252
+ body.traverse({
253
+ Function(nested) {
254
+ nested.skip();
255
+ },
256
+ ReturnStatement(statement) {
257
+ const returned = statement.node.argument;
258
+ if (returned !== null && returned !== void 0) {
259
+ check(returned, statement);
260
+ }
261
+ },
262
+ /**
263
+ * The same mistake spelled with a keyword, and the one that actually
264
+ * shipped: a route guard that returned `<Navigate />` early left the page
265
+ * it was meant to hide on the screen after a sign-out.
266
+ */
267
+ IfStatement(statement) {
268
+ if (!readsSignal(statement.node.test)) {
269
+ return;
270
+ }
271
+ if (returnsView(statement.node.consequent) || returnsView(statement.node.alternate)) {
272
+ report(statement.get("test"));
273
+ }
274
+ }
275
+ });
276
+ }
277
+ function returnsView(node) {
278
+ if (node === null || node === void 0) {
279
+ return false;
280
+ }
281
+ if (t.isReturnStatement(node)) {
282
+ return node.argument !== null && node.argument !== void 0 && hasView(node.argument);
283
+ }
284
+ if (t.isBlockStatement(node)) {
285
+ return node.body.some((inner) => returnsView(inner));
286
+ }
287
+ return false;
288
+ }
289
+ function hasView(node) {
290
+ if (t.isConditionalExpression(node)) {
291
+ return hasView(node.consequent) || hasView(node.alternate);
292
+ }
293
+ return t.isJSXElement(node) || t.isJSXFragment(node);
294
+ }
295
+ function readsSignal(node) {
296
+ let found = false;
297
+ const walk = (current) => {
298
+ if (current === null || current === void 0 || found) {
299
+ return;
300
+ }
301
+ if (t.isMemberExpression(current)) {
302
+ if (!current.computed && t.isIdentifier(current.property, { name: "value" })) {
303
+ found = true;
304
+ return;
305
+ }
306
+ walk(current.object);
307
+ return;
308
+ }
309
+ if (t.isUnaryExpression(current)) {
310
+ walk(current.argument);
311
+ return;
312
+ }
313
+ if (t.isBinaryExpression(current) || t.isLogicalExpression(current)) {
314
+ walk(current.left);
315
+ walk(current.right);
316
+ return;
317
+ }
318
+ if (t.isConditionalExpression(current)) {
319
+ walk(current.test);
320
+ }
321
+ };
322
+ walk(node);
323
+ return found;
324
+ }
216
325
  function readsOnly(node, propsName) {
217
326
  let read = false;
218
327
  const walk = (current) => {
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  firsthandPlugin,
4
4
  stableId,
5
5
  transform
6
- } from "./chunk-UYA7HB5G.js";
6
+ } from "./chunk-7DU5USN7.js";
7
7
  export {
8
8
  compileModule,
9
9
  firsthandPlugin,
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compileModule
3
- } from "./chunk-UYA7HB5G.js";
3
+ } from "./chunk-7DU5USN7.js";
4
4
 
5
5
  // packages/compiler/src/vite.ts
6
6
  function firsthand(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firsthandjs/compiler",
3
- "version": "0.6.2",
3
+ "version": "0.7.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",