@firsthandjs/compiler 0.1.0 → 0.3.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.
@@ -128,7 +128,7 @@ function firsthandPlugin(_api, options = {}) {
128
128
  path.replaceWith(compileNode(path, state));
129
129
  },
130
130
  CallExpression(path, state) {
131
- annotateComponent(path, state);
131
+ annotateComponent(path, state, options);
132
132
  }
133
133
  }
134
134
  };
@@ -157,7 +157,7 @@ function runtime(state, name, source = RUNTIME) {
157
157
  }
158
158
  return t.cloneNode(local);
159
159
  }
160
- function annotateComponent(path, state) {
160
+ function annotateComponent(path, state, options) {
161
161
  const callee = path.node.callee;
162
162
  if (!t.isIdentifier(callee, { name: "component" }) || path.node.arguments.length > 2) {
163
163
  return;
@@ -172,6 +172,9 @@ function annotateComponent(path, state) {
172
172
  }
173
173
  const name = declaredName(path);
174
174
  rewritePropsDestructuring(path, name, state);
175
+ if (options.strictReactivity === true) {
176
+ checkKeptReads(path.get("arguments.0"), name);
177
+ }
175
178
  path.node.arguments = [
176
179
  setup,
177
180
  path.node.arguments[1] ?? t.identifier("undefined"),
@@ -179,6 +182,64 @@ function annotateComponent(path, state) {
179
182
  t.stringLiteral(name)
180
183
  ];
181
184
  }
185
+ function checkKeptReads(setup, name) {
186
+ const parameter = setup.node.params[0];
187
+ const propsName = t.isIdentifier(parameter) ? parameter.name : null;
188
+ const body = setup.get("body");
189
+ if (!body.isBlockStatement()) {
190
+ return;
191
+ }
192
+ body.traverse({
193
+ Function(nested) {
194
+ nested.skip();
195
+ },
196
+ VariableDeclarator(declarator) {
197
+ const init = declarator.node.init;
198
+ if (init === null || init === void 0 || !readsOnly(init, propsName)) {
199
+ return;
200
+ }
201
+ const target = declarator.node.id;
202
+ const label = t.isIdentifier(target) ? `\`${target.name}\`` : "This value";
203
+ throw declarator.buildCodeFrameError(
204
+ `${name}: ${label} is read once, here, and then kept. A setup runs one time per instance, so this value will not change again.
205
+
206
+ Move the read into the part, handler, effect or computed that should re-read it \u2014 or wrap it in snapshot(() => \u2026) if reading once is what you meant.`
207
+ );
208
+ }
209
+ });
210
+ }
211
+ function readsOnly(node, propsName) {
212
+ let read = false;
213
+ const walk = (current) => {
214
+ if (t.isIdentifier(current) || t.isLiteral(current)) {
215
+ if (t.isTemplateLiteral(current)) {
216
+ return current.expressions.every((part) => walk(part));
217
+ }
218
+ return true;
219
+ }
220
+ if (t.isMemberExpression(current)) {
221
+ const property = current.property;
222
+ if (!current.computed && t.isIdentifier(property, { name: "value" })) {
223
+ read = true;
224
+ }
225
+ if (propsName !== null && t.isIdentifier(current.object, { name: propsName })) {
226
+ read = true;
227
+ }
228
+ return walk(current.object) && (!current.computed || walk(property));
229
+ }
230
+ if (t.isUnaryExpression(current)) {
231
+ return walk(current.argument);
232
+ }
233
+ if (t.isBinaryExpression(current) || t.isLogicalExpression(current)) {
234
+ return walk(current.left) && walk(current.right);
235
+ }
236
+ if (t.isConditionalExpression(current)) {
237
+ return walk(current.test) && walk(current.consequent) && walk(current.alternate);
238
+ }
239
+ return false;
240
+ };
241
+ return walk(node) && read;
242
+ }
182
243
  function isFirsthandImport(binding) {
183
244
  const parent = binding.path.parentPath;
184
245
  if (!parent.isImportDeclaration()) {
@@ -830,7 +891,10 @@ function buildPlugins(options) {
830
891
  plugins.push([typescriptSyntax, { isTSX: true }]);
831
892
  }
832
893
  plugins.push(jsxSyntax);
833
- plugins.push([firsthandPlugin, { packageName: options.packageName }]);
894
+ plugins.push([
895
+ firsthandPlugin,
896
+ { packageName: options.packageName, strictReactivity: options.strictReactivity }
897
+ ]);
834
898
  return plugins;
835
899
  }
836
900
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  firsthandPlugin,
3
3
  stableId,
4
4
  transform
5
- } from "./chunk-HHKCDDN2.js";
5
+ } from "./chunk-WW26NJL4.js";
6
6
  export {
7
7
  firsthandPlugin,
8
8
  stableId,
@@ -27,6 +27,14 @@ declare module '@babel/core' {
27
27
  export interface FirsthandPluginOptions {
28
28
  /** Package name used when hashing stable component ids (ADR-0004). */
29
29
  packageName?: string;
30
+ /**
31
+ * Refuse to compile a value that is read once in a setup and then kept.
32
+ *
33
+ * Off by default: reading once is legal and often deliberate. On, it is a
34
+ * build error rather than a warning, because the cases it can see are the
35
+ * unambiguous ones — see `checkKeptReads` (ADR-0019).
36
+ */
37
+ strictReactivity?: boolean;
30
38
  }
31
39
  export default function firsthandPlugin(_api: unknown, options?: FirsthandPluginOptions): PluginObject;
32
40
  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,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;CACtB;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAgDd"}
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;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAgDd"}
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  transform
3
- } from "./chunk-HHKCDDN2.js";
3
+ } from "./chunk-WW26NJL4.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.1.0",
3
+ "version": "0.3.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",