@firsthandjs/compiler 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface TransformOptions extends FirsthandPluginOptions {
19
19
  * inside this repository and fails in the first application that puts the
20
20
  * plugin into `vite.config.ts` — which is the one place it matters.
21
21
  */
22
- export interface SourceMap {
22
+ export type SourceMap = {
23
23
  version: number;
24
24
  mappings: string;
25
25
  names: string[];
@@ -27,9 +27,9 @@ export interface SourceMap {
27
27
  sourcesContent?: string[];
28
28
  sourceRoot?: string;
29
29
  file?: string;
30
- }
30
+ };
31
31
  /** A compiled module, and the map back to what was written. */
32
- export interface Compiled {
32
+ export type Compiled = {
33
33
  code: string;
34
34
  /**
35
35
  * A source map, when one was asked for.
@@ -40,7 +40,7 @@ export interface Compiled {
40
40
  * cannot, so the Vite plugin always asks.
41
41
  */
42
42
  map: SourceMap | null;
43
- }
43
+ };
44
44
  /**
45
45
  * Compiles one module, and maps it back.
46
46
  *
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAwB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAKlB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,QAAQ,CAqBpF;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAE9E"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAwB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAKlB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,QAAQ,CAqBpF;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAE9E"}
@@ -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,
@@ -13,18 +13,18 @@
13
13
  */
14
14
  import type { PluginObject } from '@babel/core';
15
15
  import * as t from '@babel/types';
16
- interface FirsthandState {
16
+ type FirsthandState = {
17
17
  imports: Map<string, t.Identifier>;
18
18
  templates: t.VariableDeclarator[];
19
19
  counter: number;
20
20
  moduleId: string;
21
- }
21
+ };
22
22
  declare module '@babel/core' {
23
23
  interface PluginPass {
24
24
  firsthand: FirsthandState;
25
25
  }
26
26
  }
27
- export interface FirsthandPluginOptions {
27
+ export type FirsthandPluginOptions = {
28
28
  /** Package name used when hashing stable component ids (ADR-0004). */
29
29
  packageName?: string;
30
30
  /**
@@ -49,7 +49,7 @@ export interface FirsthandPluginOptions {
49
49
  * production build emits nothing.
50
50
  */
51
51
  devtools?: boolean;
52
- }
52
+ };
53
53
  export default function firsthandPlugin(_api: unknown, options?: FirsthandPluginOptions): PluginObject;
54
54
  export {};
55
55
  //# sourceMappingURL=transform.d.ts.map
@@ -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,UAAU,cAAc;IACtB,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;AAED,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AAUD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,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;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"}
package/dist/vite.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type SourceMap } from './api.js';
2
2
  import type { FirsthandPluginOptions } from './transform.js';
3
3
  /** Minimal shape of the Vite plugin contract, so the package needs no Vite dependency. */
4
- export interface VitePluginLike {
4
+ export type VitePluginLike = {
5
5
  name: string;
6
6
  enforce: 'pre';
7
7
  configResolved(config: {
@@ -11,7 +11,7 @@ export interface VitePluginLike {
11
11
  code: string;
12
12
  map: SourceMap | null;
13
13
  } | null;
14
- }
14
+ };
15
15
  /**
16
16
  * Vite/Rollup plugin.
17
17
  *
@@ -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,WAAW,cAAc;IAC7B,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;AAED;;;;;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,wBAAgB,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CA0B9E"}
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.3",
3
+ "version": "0.7.1",
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",