jq79 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jq79",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
5
5
  "keywords": [
6
6
  "reactive",
package/src/jq79.ts CHANGED
@@ -176,12 +176,42 @@ const reportExprError = (expr: string, scope: Record<string, any>, error: unknow
176
176
  scheduleExprReportFlush()
177
177
  }
178
178
 
179
- const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
179
+ const runExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
180
180
  const fn = compileExpr(expr, extras ? Object.keys(extras) : [])
181
- if (!fn) return undefined
181
+ if (!fn) return undefined // a syntax error: compileExpr cached the failure, and it stays undefined
182
+ return fn(scope, ...(extras ? Object.values(extras) : []))
183
+ }
184
+
185
+ const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
186
+ try {
187
+ return runExpr(expr, scope, extras)
188
+ } catch (error) {
189
+ reportExprError(expr, scope, error)
190
+ return undefined
191
+ }
192
+ }
193
+
194
+ // the same evaluation for an @event attribute, which swallows far less. Every
195
+ // word of the reason evalExpr catches is about rendering: an expression is
196
+ // re-evaluated per effect run, per interpolation, per :each item, so a value
197
+ // that is briefly undefined mid-render has to render empty rather than tear the
198
+ // render down. A handler runs in an event listener - not in an effect, once,
199
+ // when the user clicked - so there is no transient failure to absorb, only a
200
+ // bug to report, and an exception belongs in the console with its stack. That
201
+ // is what `@click="save"` has always done (the call happens outside the try,
202
+ // on the returned function); this is what makes `@click="save()"` and
203
+ // `@click="count++"` behave the same rather than the other way round.
204
+ //
205
+ // ReferenceError is the exception, for the reason it always is: it is not yet
206
+ // decidable at throw time. A factory assigns its names to the store when it
207
+ // returns, so a click while one is still in flight throws for a name that is
208
+ // about to exist - reportExprError re-checks after the scripts settle and stays
209
+ // quiet if it arrived, which throwing here would replace with a false alarm
210
+ const evalHandler = (expr: string, scope: Record<string, any>, extras: Record<string, any>): any => {
182
211
  try {
183
- return fn(scope, ...(extras ? Object.values(extras) : []))
212
+ return runExpr(expr, scope, extras)
184
213
  } catch (error) {
214
+ if (!(error instanceof ReferenceError)) throw error
185
215
  reportExprError(expr, scope, error)
186
216
  return undefined
187
217
  }
@@ -225,7 +255,7 @@ const bindEvent = (el: Element, attr: string, expr: string, scope: Record<string
225
255
  if (mods.has("prevent")) event.preventDefault()
226
256
  if (mods.has("stop")) event.stopPropagation()
227
257
 
228
- const handler = evalExpr(expr, scope, { $event: event })
258
+ const handler = evalHandler(expr, scope, { $event: event })
229
259
  if (typeof handler === "function") handler.call(el, event)
230
260
  }, { once: mods.has("once"), capture: mods.has("capture") })
231
261
  }
@@ -256,7 +286,7 @@ const wireTagEvent = (instance: Component79, attr: string, expr: string, scope:
256
286
  // and the creation effect's definition guard no-ops a spurious wake - but
257
287
  // "what a handler reads is nobody's dependency" shouldn't hinge on either
258
288
  untracked(() => {
259
- const handler = evalExpr(expr, scope, { $event: event })
289
+ const handler = evalHandler(expr, scope, { $event: event })
260
290
  if (typeof handler === "function") handler(event)
261
291
  })
262
292
  }
@@ -1839,7 +1869,35 @@ const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
1839
1869
  const setupSignature = (script: TagBlock): PropDecl[] | null => {
1840
1870
  const pattern = script.attrs[":setup"]
1841
1871
  if (pattern === undefined) return null
1842
- return pattern.trim() === "" ? [] : parsePropsPattern(pattern)
1872
+ if (pattern.trim() === "") return []
1873
+ const props = parsePropsPattern(pattern)
1874
+ if (!props) warnUnreadableSignature(script, pattern)
1875
+ return props
1876
+ }
1877
+
1878
+ // script blocks already warned about, keyed by the block itself - parsed once
1879
+ // and shared by every instance of a definition, the same reason warnUndeclared
1880
+ // keys on the template node. It matters for a smaller reason here too:
1881
+ // setupSignature is called three times per render (both declared-name passes
1882
+ // and the script loop's declareProps), so even one mount would say it thrice
1883
+ const signatureWarned = new WeakSet<TagBlock>()
1884
+
1885
+ // a value that isn't a props pattern reads as "declared no signature", which is
1886
+ // the most permissive mode there is - so a typo doesn't fail, it quietly opts
1887
+ // the component out of the contract it was trying to write. That is now the
1888
+ // only accidental route left to permissive: a bare :setup is closed and `_` is
1889
+ // the opt-out you have to ask for, so the mode nothing lands in by accident is
1890
+ // still reachable by getting it wrong. Both of parsePropsPattern's nulls count
1891
+ // - not-an-object (",{ a }", "props") and unbalanced ("{ a, b") - and only `_`
1892
+ // is exempt, because intent is the sole thing separating it from the typos
1893
+ const warnUnreadableSignature = (script: TagBlock, pattern: string) => {
1894
+ if (pattern.trim() === "_" || signatureWarned.has(script)) return
1895
+ signatureWarned.add(script)
1896
+ console.warn(
1897
+ `jq79: :setup="${pattern}" is not a props pattern, so this component declares no ` +
1898
+ `signature and takes whatever a parent passes - write the props it takes ` +
1899
+ `("{ a, b }"), a bare :setup for none, or "_" to stay open on purpose`
1900
+ )
1843
1901
  }
1844
1902
 
1845
1903
  // every prop name a component's scripts declare, across both script modes.