jq79 0.5.1 → 0.5.3
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 +150 -8
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
|
}
|
|
@@ -1598,6 +1681,43 @@ const componentPartsFrom = (elements: Element[], hashSource: string): ComponentP
|
|
|
1598
1681
|
const importResource = (url: string): Promise<any> =>
|
|
1599
1682
|
/\.html?([?#]|$)/.test(url) ? fetchComponent(url) : import(url)
|
|
1600
1683
|
|
|
1684
|
+
// a relative specifier means "next to the file that wrote it", so it is
|
|
1685
|
+
// resolved against the component's own URL before importResource sees it -
|
|
1686
|
+
// neither of that function's two branches would otherwise land there. The
|
|
1687
|
+
// native import() inside it resolves against *this module* (dist/jq79.js), and
|
|
1688
|
+
// fetch() against the document; a component in a subdirectory gets a 404 from
|
|
1689
|
+
// the first and the page's directory from the second.
|
|
1690
|
+
//
|
|
1691
|
+
// Only `./` and `../` are resolved. A bare specifier ("lodash") belongs to the
|
|
1692
|
+
// import map or the native resolver, and resolving it would quietly turn it
|
|
1693
|
+
// into a path; anything already absolute means one thing under any base. That
|
|
1694
|
+
// keeps this the browser's rule rather than a third one of our own.
|
|
1695
|
+
//
|
|
1696
|
+
// The base is absolutized first, the way hotKey does and for the same reason:
|
|
1697
|
+
// the filename may itself be relative ("./card.html", from an import() in a
|
|
1698
|
+
// parent), and a relative URL cannot be a base.
|
|
1699
|
+
//
|
|
1700
|
+
// What comes back is a path whenever it lands on the page's own origin, and a
|
|
1701
|
+
// full URL only when it doesn't (a component served from a CDN resolves its
|
|
1702
|
+
// siblings on that CDN). Both are the same request, but the path keeps the
|
|
1703
|
+
// value the shape everything downstream already sees - what hotKey keys on,
|
|
1704
|
+
// what devtools shows as the script's name - so only the cross-origin case,
|
|
1705
|
+
// which has no path form, introduces a new one
|
|
1706
|
+
const resolveSpecifier = (spec: string, filename: string | undefined): string => {
|
|
1707
|
+
if (!/^\.\.?\//.test(spec)) return spec
|
|
1708
|
+
try {
|
|
1709
|
+
const page = new URL(document.baseURI)
|
|
1710
|
+
const url = new URL(spec, new URL(filename ?? "", page))
|
|
1711
|
+
// "null" is what an opaque origin (file:, blob:) reports, for both sides
|
|
1712
|
+
// and for anything else - it says the origins are unknown, not equal
|
|
1713
|
+
return url.origin !== "null" && url.origin === page.origin
|
|
1714
|
+
? `${url.pathname}${url.search}${url.hash}`
|
|
1715
|
+
: url.href
|
|
1716
|
+
} catch {
|
|
1717
|
+
return spec
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1601
1721
|
// ---------------------------------------------------------------------------
|
|
1602
1722
|
// naming scripts for devtools
|
|
1603
1723
|
//
|
|
@@ -1682,6 +1802,7 @@ const runSetupScript = (code: string, scope: Record<string, any>, effect: (run:
|
|
|
1682
1802
|
`return (async () => { with ($scope) { ${code} } })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
1683
1803
|
)(scriptScope, effect, importer, ...Object.values(helpers))
|
|
1684
1804
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
1805
|
+
trackScript(result)
|
|
1685
1806
|
}
|
|
1686
1807
|
|
|
1687
1808
|
// puts a component's declared props on the store, before any script runs and
|
|
@@ -1838,11 +1959,16 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
1838
1959
|
|
|
1839
1960
|
const logError = (error: any) => console.error("jq79: error in factory script", error)
|
|
1840
1961
|
let invoked = false
|
|
1841
|
-
|
|
1842
|
-
|
|
1962
|
+
// what invoke() is still waiting on, memoized: it is called from both paths
|
|
1963
|
+
// below and does its work once, but the *second* caller is the one whose
|
|
1964
|
+
// promise is tracked - without this it would see `undefined` and count the
|
|
1965
|
+
// script as settled while an async factory's bindings are still on the way
|
|
1966
|
+
let merging: Promise<void> | undefined
|
|
1967
|
+
const invoke = (): Promise<void> | undefined => {
|
|
1968
|
+
if (invoked) return merging
|
|
1843
1969
|
invoked = true
|
|
1844
1970
|
const factory = $__exports.default
|
|
1845
|
-
if (typeof factory !== "function") return
|
|
1971
|
+
if (typeof factory !== "function") return undefined
|
|
1846
1972
|
const merge = (bindings: any) => {
|
|
1847
1973
|
if (bindings && typeof bindings === "object") Object.assign(scope, bindings)
|
|
1848
1974
|
}
|
|
@@ -1853,14 +1979,18 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
1853
1979
|
// the props it declared (copying, as destructuring does - $props is the
|
|
1854
1980
|
// live view for a primitive the parent reassigns later)
|
|
1855
1981
|
const returned = factory(scope, { $data: scope, $props: scope, $effect: effect, ...instanceHelpers })
|
|
1856
|
-
if (returned instanceof Promise) returned.then(merge).catch(logError)
|
|
1982
|
+
if (returned instanceof Promise) merging = returned.then(merge).catch(logError)
|
|
1857
1983
|
else merge(returned)
|
|
1858
1984
|
} catch (error) {
|
|
1859
1985
|
logError(error)
|
|
1860
1986
|
}
|
|
1987
|
+
return merging
|
|
1861
1988
|
}
|
|
1862
1989
|
|
|
1863
|
-
|
|
1990
|
+
// tracked through the merge, not just the module body: a factory's names
|
|
1991
|
+
// reach the store in `merge`, and a template expression that reads one before
|
|
1992
|
+
// then is not an authoring mistake (see reportExprError)
|
|
1993
|
+
trackScript(result.then(invoke, logError))
|
|
1864
1994
|
if ($__exports.done) invoke() // fully-sync body: factory runs before first render
|
|
1865
1995
|
}
|
|
1866
1996
|
|
|
@@ -2079,6 +2209,13 @@ export class Component79 {
|
|
|
2079
2209
|
// while its markers sit where its DOM actually is
|
|
2080
2210
|
hotReplace(src: string | ComponentParts): boolean {
|
|
2081
2211
|
const parts = typeof src === "string" ? parseComponentString(src) : src
|
|
2212
|
+
// the source just changed, so what was already said about it no longer
|
|
2213
|
+
// applies: without this the author fixes the typo, saves, and the next typo
|
|
2214
|
+
// in the same expression is deduped away against the old one. `compiled`
|
|
2215
|
+
// needs no such reset - it is keyed by expression text, so edited source is
|
|
2216
|
+
// a different key
|
|
2217
|
+
reportedExprErrors.clear()
|
|
2218
|
+
pendingReports.clear()
|
|
2082
2219
|
const marker = this.startMarker
|
|
2083
2220
|
const rendered = !!(marker && this.content)
|
|
2084
2221
|
|
|
@@ -2253,10 +2390,15 @@ export class Component79 {
|
|
|
2253
2390
|
|
|
2254
2391
|
// import() calls whose specifier was pre-resolved by a bundler (the
|
|
2255
2392
|
// modules map) get the bundled module; everything else falls back to the
|
|
2256
|
-
// runtime importResource (fetch for .html, native import otherwise)
|
|
2393
|
+
// runtime importResource (fetch for .html, native import otherwise),
|
|
2394
|
+
// relative to this component's file. The map is keyed by the literal
|
|
2395
|
+
// specifier the script wrote, so it is consulted *before* resolution -
|
|
2396
|
+
// what the bundler hoisted and what the source says are the same string
|
|
2257
2397
|
const modules = this.modules
|
|
2258
2398
|
const $import = (url: string): Promise<any> =>
|
|
2259
|
-
modules && url in modules
|
|
2399
|
+
modules && url in modules
|
|
2400
|
+
? Promise.resolve(modules[url])
|
|
2401
|
+
: importResource(resolveSpecifier(url, this.filename))
|
|
2260
2402
|
|
|
2261
2403
|
// the writeback half of :model, from the child's side: one argument is the
|
|
2262
2404
|
// value for the default model (the bare :model), two are a name and a
|