jq79 0.5.4 → 0.5.6
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/jq79.cjs +12 -12
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.global.js +12 -12
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +12 -12
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +129 -7
package/package.json
CHANGED
package/src/jq79.ts
CHANGED
|
@@ -127,6 +127,36 @@ const reportedExprErrors = new Set<string>()
|
|
|
127
127
|
let pendingScripts = 0
|
|
128
128
|
let flushScheduled = false
|
|
129
129
|
|
|
130
|
+
// everything else an expression can throw - overwhelmingly a member access on
|
|
131
|
+
// an undefined value, `{{ game.is.loaded }}` over a game with no `is`. There is
|
|
132
|
+
// no pure question to ask about it the way there is for a missing name: `in`
|
|
133
|
+
// answers "declared anywhere?" without evaluating, and nothing answers "would
|
|
134
|
+
// this still throw?" without running the expression again, which is exactly
|
|
135
|
+
// what mustn't happen (`@click="count++ + missing"` increments twice).
|
|
136
|
+
//
|
|
137
|
+
// What is pure is watching the evaluations that were going to happen anyway. An
|
|
138
|
+
// expression whose value is merely late is re-evaluated when the value arrives -
|
|
139
|
+
// the reads that did land registered as dependencies, so the write wakes the
|
|
140
|
+
// effect - and it succeeds then. So transient means "succeeds later", and the
|
|
141
|
+
// re-check is to clear the entry on the next successful evaluation and report
|
|
142
|
+
// only what is still queued once the page has gone quiet.
|
|
143
|
+
//
|
|
144
|
+
// Quiet is a timer rather than the microtask the other queue flushes on,
|
|
145
|
+
// because a value from fetch() arrives many tasks later and clearing on success
|
|
146
|
+
// exists to let it. A second covers that and still lands the warning inside the
|
|
147
|
+
// page load. It is a guess, and the cost is a slower fetch warning about correct
|
|
148
|
+
// code with nothing to take it back - accepted because the silence it replaces
|
|
149
|
+
// hid a render that was actively wrong
|
|
150
|
+
const FAILED_EXPR_DELAY = 1000
|
|
151
|
+
|
|
152
|
+
// keyed by expression text alone, not `message|expr`: the expression is what the
|
|
153
|
+
// author edits, and one broken path throwing two messages is still one bug. The
|
|
154
|
+
// entries hold no scope, only strings, so this queue cannot retain a live
|
|
155
|
+
// component the way pendingReports can
|
|
156
|
+
const failedExprs = new Map<string, string>()
|
|
157
|
+
const reportedFailedExprs = new Set<string>()
|
|
158
|
+
let failedFlushTimer: ReturnType<typeof setTimeout> | undefined
|
|
159
|
+
|
|
130
160
|
const flushExprReports = () => {
|
|
131
161
|
flushScheduled = false
|
|
132
162
|
if (pendingScripts > 0) return // a script started meanwhile; its release re-schedules
|
|
@@ -149,6 +179,25 @@ const scheduleExprReportFlush = () => {
|
|
|
149
179
|
queueMicrotask(flushExprReports)
|
|
150
180
|
}
|
|
151
181
|
|
|
182
|
+
const flushFailedExprs = () => {
|
|
183
|
+
failedFlushTimer = undefined
|
|
184
|
+
if (pendingScripts > 0) return // a script started meanwhile; its release re-schedules
|
|
185
|
+
failedExprs.forEach((message, expr) => {
|
|
186
|
+
reportedFailedExprs.add(expr)
|
|
187
|
+
console.warn(
|
|
188
|
+
`jq79: ${message} - evaluating "${expr}". It rendered as nothing and was ` +
|
|
189
|
+
`still failing a moment later, so the value is probably never coming: check ` +
|
|
190
|
+
`the path. If it does arrive asynchronously, guard it - "a?.b", or :if on the element.`
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
failedExprs.clear()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scheduleFailedExprFlush = () => {
|
|
197
|
+
if (failedFlushTimer !== undefined || pendingScripts > 0 || !failedExprs.size) return
|
|
198
|
+
failedFlushTimer = setTimeout(flushFailedExprs, FAILED_EXPR_DELAY)
|
|
199
|
+
}
|
|
200
|
+
|
|
152
201
|
// scripts run before the template renders, so the counter is already up when
|
|
153
202
|
// the first evaluation fails. Both script modes settle through a promise;
|
|
154
203
|
// the factory's has to cover the merge, not just the module body
|
|
@@ -157,12 +206,20 @@ const trackScript = (settled: Promise<unknown>) => {
|
|
|
157
206
|
const release = () => {
|
|
158
207
|
pendingScripts--
|
|
159
208
|
scheduleExprReportFlush()
|
|
209
|
+
scheduleFailedExprFlush()
|
|
160
210
|
}
|
|
161
211
|
settled.then(release, release)
|
|
162
212
|
}
|
|
163
213
|
|
|
214
|
+
const reportFailedExpr = (expr: string, error: unknown) => {
|
|
215
|
+
if (reportedFailedExprs.has(expr) || failedExprs.has(expr)) return
|
|
216
|
+
if (failedExprs.size >= MAX_PENDING_REPORTS) return
|
|
217
|
+
failedExprs.set(expr, (error as Error)?.message || String(error))
|
|
218
|
+
scheduleFailedExprFlush()
|
|
219
|
+
}
|
|
220
|
+
|
|
164
221
|
const reportExprError = (expr: string, scope: Record<string, any>, error: unknown) => {
|
|
165
|
-
if (!(error instanceof ReferenceError)) return
|
|
222
|
+
if (!(error instanceof ReferenceError)) return reportFailedExpr(expr, error)
|
|
166
223
|
const match = MISSING_NAME_RE.exec(error.message)
|
|
167
224
|
const name = match?.[1] ?? match?.[2]
|
|
168
225
|
if (!name) return // an engine whose wording we don't know: stay quiet, as before
|
|
@@ -176,17 +233,52 @@ const reportExprError = (expr: string, scope: Record<string, any>, error: unknow
|
|
|
176
233
|
scheduleExprReportFlush()
|
|
177
234
|
}
|
|
178
235
|
|
|
179
|
-
const
|
|
236
|
+
const runExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
|
|
180
237
|
const fn = compileExpr(expr, extras ? Object.keys(extras) : [])
|
|
181
|
-
if (!fn) return undefined
|
|
238
|
+
if (!fn) return undefined // a syntax error: compileExpr cached the failure, and it stays undefined
|
|
239
|
+
return fn(scope, ...(extras ? Object.values(extras) : []))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
|
|
182
243
|
try {
|
|
183
|
-
|
|
244
|
+
const value = runExpr(expr, scope, extras)
|
|
245
|
+
// the re-check for the failed-expression queue: this run is the proof that
|
|
246
|
+
// the earlier throw was a value arriving late. Guarded on size so the
|
|
247
|
+
// normal page - where nothing has ever thrown - pays one comparison
|
|
248
|
+
if (failedExprs.size) failedExprs.delete(expr)
|
|
249
|
+
return value
|
|
184
250
|
} catch (error) {
|
|
185
251
|
reportExprError(expr, scope, error)
|
|
186
252
|
return undefined
|
|
187
253
|
}
|
|
188
254
|
}
|
|
189
255
|
|
|
256
|
+
// the same evaluation for an @event attribute, which swallows far less. Every
|
|
257
|
+
// word of the reason evalExpr catches is about rendering: an expression is
|
|
258
|
+
// re-evaluated per effect run, per interpolation, per :each item, so a value
|
|
259
|
+
// that is briefly undefined mid-render has to render empty rather than tear the
|
|
260
|
+
// render down. A handler runs in an event listener - not in an effect, once,
|
|
261
|
+
// when the user clicked - so there is no transient failure to absorb, only a
|
|
262
|
+
// bug to report, and an exception belongs in the console with its stack. That
|
|
263
|
+
// is what `@click="save"` has always done (the call happens outside the try,
|
|
264
|
+
// on the returned function); this is what makes `@click="save()"` and
|
|
265
|
+
// `@click="count++"` behave the same rather than the other way round.
|
|
266
|
+
//
|
|
267
|
+
// ReferenceError is the exception, for the reason it always is: it is not yet
|
|
268
|
+
// decidable at throw time. A factory assigns its names to the store when it
|
|
269
|
+
// returns, so a click while one is still in flight throws for a name that is
|
|
270
|
+
// about to exist - reportExprError re-checks after the scripts settle and stays
|
|
271
|
+
// quiet if it arrived, which throwing here would replace with a false alarm
|
|
272
|
+
const evalHandler = (expr: string, scope: Record<string, any>, extras: Record<string, any>): any => {
|
|
273
|
+
try {
|
|
274
|
+
return runExpr(expr, scope, extras)
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!(error instanceof ReferenceError)) throw error
|
|
277
|
+
reportExprError(expr, scope, error)
|
|
278
|
+
return undefined
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
190
282
|
// [\s\S] rather than `.` so an expression can span lines, like the ones in
|
|
191
283
|
// directive attributes (which reach evalExpr wrapped in parens either way)
|
|
192
284
|
const interpolate = (template: string, scope: Record<string, any>): string =>
|
|
@@ -225,7 +317,7 @@ const bindEvent = (el: Element, attr: string, expr: string, scope: Record<string
|
|
|
225
317
|
if (mods.has("prevent")) event.preventDefault()
|
|
226
318
|
if (mods.has("stop")) event.stopPropagation()
|
|
227
319
|
|
|
228
|
-
const handler =
|
|
320
|
+
const handler = evalHandler(expr, scope, { $event: event })
|
|
229
321
|
if (typeof handler === "function") handler.call(el, event)
|
|
230
322
|
}, { once: mods.has("once"), capture: mods.has("capture") })
|
|
231
323
|
}
|
|
@@ -256,7 +348,7 @@ const wireTagEvent = (instance: Component79, attr: string, expr: string, scope:
|
|
|
256
348
|
// and the creation effect's definition guard no-ops a spurious wake - but
|
|
257
349
|
// "what a handler reads is nobody's dependency" shouldn't hinge on either
|
|
258
350
|
untracked(() => {
|
|
259
|
-
const handler =
|
|
351
|
+
const handler = evalHandler(expr, scope, { $event: event })
|
|
260
352
|
if (typeof handler === "function") handler(event)
|
|
261
353
|
})
|
|
262
354
|
}
|
|
@@ -1839,7 +1931,35 @@ const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
|
|
|
1839
1931
|
const setupSignature = (script: TagBlock): PropDecl[] | null => {
|
|
1840
1932
|
const pattern = script.attrs[":setup"]
|
|
1841
1933
|
if (pattern === undefined) return null
|
|
1842
|
-
|
|
1934
|
+
if (pattern.trim() === "") return []
|
|
1935
|
+
const props = parsePropsPattern(pattern)
|
|
1936
|
+
if (!props) warnUnreadableSignature(script, pattern)
|
|
1937
|
+
return props
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// script blocks already warned about, keyed by the block itself - parsed once
|
|
1941
|
+
// and shared by every instance of a definition, the same reason warnUndeclared
|
|
1942
|
+
// keys on the template node. It matters for a smaller reason here too:
|
|
1943
|
+
// setupSignature is called three times per render (both declared-name passes
|
|
1944
|
+
// and the script loop's declareProps), so even one mount would say it thrice
|
|
1945
|
+
const signatureWarned = new WeakSet<TagBlock>()
|
|
1946
|
+
|
|
1947
|
+
// a value that isn't a props pattern reads as "declared no signature", which is
|
|
1948
|
+
// the most permissive mode there is - so a typo doesn't fail, it quietly opts
|
|
1949
|
+
// the component out of the contract it was trying to write. That is now the
|
|
1950
|
+
// only accidental route left to permissive: a bare :setup is closed and `_` is
|
|
1951
|
+
// the opt-out you have to ask for, so the mode nothing lands in by accident is
|
|
1952
|
+
// still reachable by getting it wrong. Both of parsePropsPattern's nulls count
|
|
1953
|
+
// - not-an-object (",{ a }", "props") and unbalanced ("{ a, b") - and only `_`
|
|
1954
|
+
// is exempt, because intent is the sole thing separating it from the typos
|
|
1955
|
+
const warnUnreadableSignature = (script: TagBlock, pattern: string) => {
|
|
1956
|
+
if (pattern.trim() === "_" || signatureWarned.has(script)) return
|
|
1957
|
+
signatureWarned.add(script)
|
|
1958
|
+
console.warn(
|
|
1959
|
+
`jq79: :setup="${pattern}" is not a props pattern, so this component declares no ` +
|
|
1960
|
+
`signature and takes whatever a parent passes - write the props it takes ` +
|
|
1961
|
+
`("{ a, b }"), a bare :setup for none, or "_" to stay open on purpose`
|
|
1962
|
+
)
|
|
1843
1963
|
}
|
|
1844
1964
|
|
|
1845
1965
|
// every prop name a component's scripts declare, across both script modes.
|
|
@@ -2218,6 +2338,8 @@ export class Component79 {
|
|
|
2218
2338
|
// a different key
|
|
2219
2339
|
reportedExprErrors.clear()
|
|
2220
2340
|
pendingReports.clear()
|
|
2341
|
+
reportedFailedExprs.clear()
|
|
2342
|
+
failedExprs.clear()
|
|
2221
2343
|
const marker = this.startMarker
|
|
2222
2344
|
const rendered = !!(marker && this.content)
|
|
2223
2345
|
|