jq79 0.5.3 → 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/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 +86 -26
package/package.json
CHANGED
package/src/jq79.ts
CHANGED
|
@@ -176,17 +176,47 @@ const reportExprError = (expr: string, scope: Record<string, any>, error: unknow
|
|
|
176
176
|
scheduleExprReportFlush()
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const
|
|
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 => {
|
|
182
186
|
try {
|
|
183
|
-
return
|
|
187
|
+
return runExpr(expr, scope, extras)
|
|
184
188
|
} catch (error) {
|
|
185
189
|
reportExprError(expr, scope, error)
|
|
186
190
|
return undefined
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
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 => {
|
|
211
|
+
try {
|
|
212
|
+
return runExpr(expr, scope, extras)
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!(error instanceof ReferenceError)) throw error
|
|
215
|
+
reportExprError(expr, scope, error)
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
// [\s\S] rather than `.` so an expression can span lines, like the ones in
|
|
191
221
|
// directive attributes (which reach evalExpr wrapped in parens either way)
|
|
192
222
|
const interpolate = (template: string, scope: Record<string, any>): string =>
|
|
@@ -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 =
|
|
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 =
|
|
289
|
+
const handler = evalHandler(expr, scope, { $event: event })
|
|
260
290
|
if (typeof handler === "function") handler(event)
|
|
261
291
|
})
|
|
262
292
|
}
|
|
@@ -1688,31 +1718,33 @@ const importResource = (url: string): Promise<any> =>
|
|
|
1688
1718
|
// fetch() against the document; a component in a subdirectory gets a 404 from
|
|
1689
1719
|
// the first and the page's directory from the second.
|
|
1690
1720
|
//
|
|
1691
|
-
//
|
|
1692
|
-
//
|
|
1693
|
-
//
|
|
1694
|
-
//
|
|
1721
|
+
// What comes back is always a fully absolute URL, and that is the load-bearing
|
|
1722
|
+
// part rather than a detail of formatting. A *path* would be resolved by that
|
|
1723
|
+
// same native import() against the library module's ORIGIN - and the library
|
|
1724
|
+
// is the one file on the page most likely to come from somewhere else:
|
|
1725
|
+
//
|
|
1726
|
+
// page http://localhost:8024/craft/app.html
|
|
1727
|
+
// jq79 https://jgermade.github.io/jq79/jq79.js
|
|
1728
|
+
// "/craft/services/x.js" -> https://jgermade.github.io/craft/services/x.js
|
|
1729
|
+
//
|
|
1730
|
+
// which is a CORS error naming a host the app never mentioned. Only an
|
|
1731
|
+
// absolute URL means the same thing to both branches.
|
|
1732
|
+
//
|
|
1733
|
+
// For the same reason a *root-absolute* specifier is resolved too, not passed
|
|
1734
|
+
// through: `/x.js` means the page's root to whoever wrote it, and the page is
|
|
1735
|
+
// the only base under which the two branches agree. Bare specifiers ("lodash")
|
|
1736
|
+
// are the exception that stays untouched - they belong to the import map or
|
|
1737
|
+
// the bundler, and resolving one would quietly turn it into a path.
|
|
1695
1738
|
//
|
|
1696
1739
|
// The base is absolutized first, the way hotKey does and for the same reason:
|
|
1697
1740
|
// 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
|
-
|
|
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
|
|
1741
|
+
// parent), and a relative URL cannot be a base
|
|
1742
|
+
const RESOLVABLE_SPECIFIER_RE = /^(?:\.\.?\/|\/|[a-z][a-z0-9+.-]*:)/i
|
|
1743
|
+
|
|
1706
1744
|
const resolveSpecifier = (spec: string, filename: string | undefined): string => {
|
|
1707
|
-
if (
|
|
1745
|
+
if (!RESOLVABLE_SPECIFIER_RE.test(spec)) return spec
|
|
1708
1746
|
try {
|
|
1709
|
-
|
|
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
|
|
1747
|
+
return new URL(spec, new URL(filename ?? "", document.baseURI)).href
|
|
1716
1748
|
} catch {
|
|
1717
1749
|
return spec
|
|
1718
1750
|
}
|
|
@@ -1837,7 +1869,35 @@ const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
|
|
|
1837
1869
|
const setupSignature = (script: TagBlock): PropDecl[] | null => {
|
|
1838
1870
|
const pattern = script.attrs[":setup"]
|
|
1839
1871
|
if (pattern === undefined) return null
|
|
1840
|
-
|
|
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
|
+
)
|
|
1841
1901
|
}
|
|
1842
1902
|
|
|
1843
1903
|
// every prop name a component's scripts declare, across both script modes.
|