jq79 0.5.1 → 0.5.2
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 +106 -6
package/package.json
CHANGED
package/src/jq79.ts
CHANGED
|
@@ -94,12 +94,95 @@ const compileExpr = (expr: string, params: string[]): Function | null => {
|
|
|
94
94
|
return fn
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// a template expression is re-evaluated constantly - once per effect run, once
|
|
98
|
+
// per interpolation, once per :each item - so a value that is briefly undefined
|
|
99
|
+
// mid-render has to fail quietly, and the catch below stays. A ReferenceError
|
|
100
|
+
// is the one failure worth a word: `with` resolves a name against the store and
|
|
101
|
+
// then globalThis, so a name that resolves nowhere is declared nowhere - a
|
|
102
|
+
// typo, a dropped prop, or the trap this was written for, a top-level
|
|
103
|
+
// `function` declaration, which transformSetupScript leaves as an ordinary
|
|
104
|
+
// lexical binding instead of a store property
|
|
105
|
+
//
|
|
106
|
+
// It is reported late rather than where it throws, because "declared nowhere"
|
|
107
|
+
// is not yet decidable at that moment: a factory script assigns its bindings to
|
|
108
|
+
// the store when it returns, so an async factory renders its whole template
|
|
109
|
+
// before any of its names exist. Reporting waits until no script is still
|
|
110
|
+
// running (pendingScripts), and then asks whether the name resolves *now*.
|
|
111
|
+
//
|
|
112
|
+
// The re-check is `name in scope` rather than a re-evaluation, because
|
|
113
|
+
// re-evaluating is not pure: `@click="count++ + missing"` increments before it
|
|
114
|
+
// throws, and running it again to see if it still throws would increment twice
|
|
115
|
+
// and notify. `in` walks the same scope chain (:each scopes are
|
|
116
|
+
// Object.create(scope), :with is a proxy over it) and evaluates nothing
|
|
117
|
+
const MISSING_NAME_RE = /^(?:([\w$]+) is not defined|Can't find variable: ([\w$]+))/
|
|
118
|
+
|
|
119
|
+
// the queue holds live scopes, so it is capped: a script that never settles
|
|
120
|
+
// would otherwise let it grow for the life of the page
|
|
121
|
+
const MAX_PENDING_REPORTS = 100
|
|
122
|
+
|
|
123
|
+
type PendingReport = { name: string; expr: string; scope: Record<string, any> }
|
|
124
|
+
|
|
125
|
+
const pendingReports = new Map<string, PendingReport>()
|
|
126
|
+
const reportedExprErrors = new Set<string>()
|
|
127
|
+
let pendingScripts = 0
|
|
128
|
+
let flushScheduled = false
|
|
129
|
+
|
|
130
|
+
const flushExprReports = () => {
|
|
131
|
+
flushScheduled = false
|
|
132
|
+
if (pendingScripts > 0) return // a script started meanwhile; its release re-schedules
|
|
133
|
+
pendingReports.forEach(({ name, expr, scope }, key) => {
|
|
134
|
+
if (name in scope) return // it arrived late - a factory's bindings, a prop
|
|
135
|
+
reportedExprErrors.add(key)
|
|
136
|
+
console.warn(
|
|
137
|
+
`jq79: ${name} is not defined - evaluating "${expr}". Template expressions ` +
|
|
138
|
+
`resolve against the component store: a top-level let/var/const in a :setup ` +
|
|
139
|
+
`script, a declared prop, or a global. Note a "function name() {}" declaration ` +
|
|
140
|
+
`is not on the store - write "const name = () => {}".`
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
pendingReports.clear()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const scheduleExprReportFlush = () => {
|
|
147
|
+
if (flushScheduled || pendingScripts > 0 || !pendingReports.size) return
|
|
148
|
+
flushScheduled = true
|
|
149
|
+
queueMicrotask(flushExprReports)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// scripts run before the template renders, so the counter is already up when
|
|
153
|
+
// the first evaluation fails. Both script modes settle through a promise;
|
|
154
|
+
// the factory's has to cover the merge, not just the module body
|
|
155
|
+
const trackScript = (settled: Promise<unknown>) => {
|
|
156
|
+
pendingScripts++
|
|
157
|
+
const release = () => {
|
|
158
|
+
pendingScripts--
|
|
159
|
+
scheduleExprReportFlush()
|
|
160
|
+
}
|
|
161
|
+
settled.then(release, release)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const reportExprError = (expr: string, scope: Record<string, any>, error: unknown) => {
|
|
165
|
+
if (!(error instanceof ReferenceError)) return
|
|
166
|
+
const match = MISSING_NAME_RE.exec(error.message)
|
|
167
|
+
const name = match?.[1] ?? match?.[2]
|
|
168
|
+
if (!name) return // an engine whose wording we don't know: stay quiet, as before
|
|
169
|
+
// keyed on name and expression, not on the expression alone, so two missing
|
|
170
|
+
// names in one expression stay distinguishable - and so a :each of 1000 items
|
|
171
|
+
// enqueues one entry rather than 1000
|
|
172
|
+
const key = `${name}|${expr}`
|
|
173
|
+
if (reportedExprErrors.has(key) || pendingReports.has(key)) return
|
|
174
|
+
if (pendingReports.size >= MAX_PENDING_REPORTS) return
|
|
175
|
+
pendingReports.set(key, { name, expr, scope })
|
|
176
|
+
scheduleExprReportFlush()
|
|
177
|
+
}
|
|
178
|
+
|
|
97
179
|
const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
|
|
98
180
|
const fn = compileExpr(expr, extras ? Object.keys(extras) : [])
|
|
99
181
|
if (!fn) return undefined
|
|
100
182
|
try {
|
|
101
183
|
return fn(scope, ...(extras ? Object.values(extras) : []))
|
|
102
|
-
} catch {
|
|
184
|
+
} catch (error) {
|
|
185
|
+
reportExprError(expr, scope, error)
|
|
103
186
|
return undefined
|
|
104
187
|
}
|
|
105
188
|
}
|
|
@@ -1682,6 +1765,7 @@ const runSetupScript = (code: string, scope: Record<string, any>, effect: (run:
|
|
|
1682
1765
|
`return (async () => { with ($scope) { ${code} } })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
1683
1766
|
)(scriptScope, effect, importer, ...Object.values(helpers))
|
|
1684
1767
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
1768
|
+
trackScript(result)
|
|
1685
1769
|
}
|
|
1686
1770
|
|
|
1687
1771
|
// puts a component's declared props on the store, before any script runs and
|
|
@@ -1838,11 +1922,16 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
1838
1922
|
|
|
1839
1923
|
const logError = (error: any) => console.error("jq79: error in factory script", error)
|
|
1840
1924
|
let invoked = false
|
|
1841
|
-
|
|
1842
|
-
|
|
1925
|
+
// what invoke() is still waiting on, memoized: it is called from both paths
|
|
1926
|
+
// below and does its work once, but the *second* caller is the one whose
|
|
1927
|
+
// promise is tracked - without this it would see `undefined` and count the
|
|
1928
|
+
// script as settled while an async factory's bindings are still on the way
|
|
1929
|
+
let merging: Promise<void> | undefined
|
|
1930
|
+
const invoke = (): Promise<void> | undefined => {
|
|
1931
|
+
if (invoked) return merging
|
|
1843
1932
|
invoked = true
|
|
1844
1933
|
const factory = $__exports.default
|
|
1845
|
-
if (typeof factory !== "function") return
|
|
1934
|
+
if (typeof factory !== "function") return undefined
|
|
1846
1935
|
const merge = (bindings: any) => {
|
|
1847
1936
|
if (bindings && typeof bindings === "object") Object.assign(scope, bindings)
|
|
1848
1937
|
}
|
|
@@ -1853,14 +1942,18 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
1853
1942
|
// the props it declared (copying, as destructuring does - $props is the
|
|
1854
1943
|
// live view for a primitive the parent reassigns later)
|
|
1855
1944
|
const returned = factory(scope, { $data: scope, $props: scope, $effect: effect, ...instanceHelpers })
|
|
1856
|
-
if (returned instanceof Promise) returned.then(merge).catch(logError)
|
|
1945
|
+
if (returned instanceof Promise) merging = returned.then(merge).catch(logError)
|
|
1857
1946
|
else merge(returned)
|
|
1858
1947
|
} catch (error) {
|
|
1859
1948
|
logError(error)
|
|
1860
1949
|
}
|
|
1950
|
+
return merging
|
|
1861
1951
|
}
|
|
1862
1952
|
|
|
1863
|
-
|
|
1953
|
+
// tracked through the merge, not just the module body: a factory's names
|
|
1954
|
+
// reach the store in `merge`, and a template expression that reads one before
|
|
1955
|
+
// then is not an authoring mistake (see reportExprError)
|
|
1956
|
+
trackScript(result.then(invoke, logError))
|
|
1864
1957
|
if ($__exports.done) invoke() // fully-sync body: factory runs before first render
|
|
1865
1958
|
}
|
|
1866
1959
|
|
|
@@ -2079,6 +2172,13 @@ export class Component79 {
|
|
|
2079
2172
|
// while its markers sit where its DOM actually is
|
|
2080
2173
|
hotReplace(src: string | ComponentParts): boolean {
|
|
2081
2174
|
const parts = typeof src === "string" ? parseComponentString(src) : src
|
|
2175
|
+
// the source just changed, so what was already said about it no longer
|
|
2176
|
+
// applies: without this the author fixes the typo, saves, and the next typo
|
|
2177
|
+
// in the same expression is deduped away against the old one. `compiled`
|
|
2178
|
+
// needs no such reset - it is keyed by expression text, so edited source is
|
|
2179
|
+
// a different key
|
|
2180
|
+
reportedExprErrors.clear()
|
|
2181
|
+
pendingReports.clear()
|
|
2082
2182
|
const marker = this.startMarker
|
|
2083
2183
|
const rendered = !!(marker && this.content)
|
|
2084
2184
|
|