jq79 0.5.6 → 0.5.8
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 +13 -12
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +2 -0
- package/dist/jq79.global.js +13 -12
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +13 -12
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +196 -88
package/package.json
CHANGED
package/src/jq79.ts
CHANGED
|
@@ -128,34 +128,23 @@ let pendingScripts = 0
|
|
|
128
128
|
let flushScheduled = false
|
|
129
129
|
|
|
130
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`.
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// what mustn't happen (`@click="count++ + missing"` increments twice).
|
|
131
|
+
// an undefined value, `{{ game.is.loaded }}` over a game with no `is`. Unlike a
|
|
132
|
+
// missing name it needs no deferral and gets none: the engine already caught a
|
|
133
|
+
// real exception and wrote the message, so there is nothing left to decide and
|
|
134
|
+
// it is reported where it throws, as an error rather than a warning.
|
|
136
135
|
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// only what is still queued once the page has gone quiet.
|
|
136
|
+
// That means a value still on its way is reported too - `{{ user.name }}` over
|
|
137
|
+
// a user that a fetch will fill renders empty and says so, once. It is a
|
|
138
|
+
// deliberate trade against the silence it replaces, which hid a render that was
|
|
139
|
+
// actively wrong (a thrown `:disabled` is falsy, so the button rendered
|
|
140
|
+
// *enabled*). The fix is the one the message names: `user?.name`, or `:if`.
|
|
143
141
|
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
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>()
|
|
142
|
+
// Deduped by expression text alone. A `:each` over 1000 rows throws 1000 times
|
|
143
|
+
// per render and the Set is what keeps that to one line; keying finer - by
|
|
144
|
+
// message too, as the missing-name queue does - would let one broken path
|
|
145
|
+
// report once per distinct message. There is no queue behind it because there
|
|
146
|
+
// is nothing to re-check, and so nothing that could retain a live scope
|
|
157
147
|
const reportedFailedExprs = new Set<string>()
|
|
158
|
-
let failedFlushTimer: ReturnType<typeof setTimeout> | undefined
|
|
159
148
|
|
|
160
149
|
const flushExprReports = () => {
|
|
161
150
|
flushScheduled = false
|
|
@@ -179,25 +168,6 @@ const scheduleExprReportFlush = () => {
|
|
|
179
168
|
queueMicrotask(flushExprReports)
|
|
180
169
|
}
|
|
181
170
|
|
|
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
|
-
|
|
201
171
|
// scripts run before the template renders, so the counter is already up when
|
|
202
172
|
// the first evaluation fails. Both script modes settle through a promise;
|
|
203
173
|
// the factory's has to cover the merge, not just the module body
|
|
@@ -206,16 +176,22 @@ const trackScript = (settled: Promise<unknown>) => {
|
|
|
206
176
|
const release = () => {
|
|
207
177
|
pendingScripts--
|
|
208
178
|
scheduleExprReportFlush()
|
|
209
|
-
scheduleFailedExprFlush()
|
|
210
179
|
}
|
|
211
180
|
settled.then(release, release)
|
|
212
181
|
}
|
|
213
182
|
|
|
183
|
+
// console.error, not warn: a name that resolves nowhere is a warning because
|
|
184
|
+
// the runtime can only say the name is absent, while this one caught a real
|
|
185
|
+
// exception - it has a message the engine wrote, and the expression rendered as
|
|
186
|
+
// nothing instead of doing what it says
|
|
214
187
|
const reportFailedExpr = (expr: string, error: unknown) => {
|
|
215
|
-
if (reportedFailedExprs.has(expr)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
188
|
+
if (reportedFailedExprs.has(expr)) return
|
|
189
|
+
reportedFailedExprs.add(expr)
|
|
190
|
+
const message = (error as Error)?.message || String(error)
|
|
191
|
+
console.error(
|
|
192
|
+
`jq79: ${message} - evaluating "${expr}". The expression rendered as nothing. ` +
|
|
193
|
+
`If the value arrives later, guard it - "a?.b", or :if on the element.`
|
|
194
|
+
)
|
|
219
195
|
}
|
|
220
196
|
|
|
221
197
|
const reportExprError = (expr: string, scope: Record<string, any>, error: unknown) => {
|
|
@@ -241,12 +217,7 @@ const runExpr = (expr: string, scope: Record<string, any>, extras?: Record<strin
|
|
|
241
217
|
|
|
242
218
|
const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
|
|
243
219
|
try {
|
|
244
|
-
|
|
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
|
|
220
|
+
return runExpr(expr, scope, extras)
|
|
250
221
|
} catch (error) {
|
|
251
222
|
reportExprError(expr, scope, error)
|
|
252
223
|
return undefined
|
|
@@ -1835,6 +1806,11 @@ const resolveSpecifier = (spec: string, filename: string | undefined): string =>
|
|
|
1835
1806
|
// or devtools shows only one of them)
|
|
1836
1807
|
type ScriptLocation = { filename?: string; index?: number }
|
|
1837
1808
|
|
|
1809
|
+
// what running a script tells its caller: the promise it settles through, and
|
|
1810
|
+
// whether it already finished on this stack. `sync` is the fast path the render
|
|
1811
|
+
// gate is built on - see runSetupScript
|
|
1812
|
+
type ScriptRun = { settled: Promise<unknown>; sync: boolean }
|
|
1813
|
+
|
|
1838
1814
|
// nothing to name an inline component's scripts after, so they stay anonymous
|
|
1839
1815
|
const sourceUrlComment = (filename: string | undefined, index: number): string =>
|
|
1840
1816
|
filename ? `\n//# sourceURL=${filename}?jq79-script=${index}` : ""
|
|
@@ -1880,23 +1856,36 @@ const releaseStyle = (content: string) => {
|
|
|
1880
1856
|
// normally. get/set are deliberately not trapped: they default-forward to
|
|
1881
1857
|
// `scope` (the reactive proxy), preserving tracking and notify.
|
|
1882
1858
|
// The body is wrapped in an async IIFE so top-level `await` works: everything
|
|
1883
|
-
// up to the first await runs synchronously
|
|
1884
|
-
//
|
|
1885
|
-
|
|
1859
|
+
// up to the first await runs synchronously, and later assignments update the
|
|
1860
|
+
// DOM reactively when they happen.
|
|
1861
|
+
//
|
|
1862
|
+
// Returns whether the body ran to completion synchronously, plus the promise it
|
|
1863
|
+
// settles through. renderWith needs the *synchronous* answer - a script that
|
|
1864
|
+
// finished in this turn cannot hold anything up, so the template can render on
|
|
1865
|
+
// this stack exactly as it always has (see the render gate). Asking the promise
|
|
1866
|
+
// instead would defer every render by a microtask, including the overwhelmingly
|
|
1867
|
+
// common case of a script with no await in it at all.
|
|
1868
|
+
//
|
|
1869
|
+
// The flag is set on the code's last line, after the `with` block rather than
|
|
1870
|
+
// inside it, so the scope proxy never sees the name - and appended, so the
|
|
1871
|
+
// author's line numbers (which sourceUrlComment maps for devtools) don't shift
|
|
1872
|
+
const runSetupScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}): ScriptRun => {
|
|
1886
1873
|
// instanceHelpers are per-component-instance additions (e.g. $emit, which
|
|
1887
1874
|
// is bound to this instance's DOM position)
|
|
1888
1875
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
1889
1876
|
const scriptScope = new Proxy(scope, {
|
|
1890
1877
|
has: (target, key) =>
|
|
1891
|
-
key !== "$__effect" && key !== "$__import" &&
|
|
1878
|
+
key !== "$__effect" && key !== "$__import" && key !== "$__state" &&
|
|
1892
1879
|
(Reflect.has(target, key) || !(key in globalThis) && !(key in helpers)),
|
|
1893
1880
|
})
|
|
1881
|
+
const state: { done?: boolean } = {}
|
|
1894
1882
|
const result: Promise<void> = new Function(
|
|
1895
|
-
"$scope", "$__effect", "$__import", ...Object.keys(helpers),
|
|
1896
|
-
`return (async () => { with ($scope) { ${code} } })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
1897
|
-
)(scriptScope, effect, importer, ...Object.values(helpers))
|
|
1883
|
+
"$scope", "$__effect", "$__import", "$__state", ...Object.keys(helpers),
|
|
1884
|
+
`return (async () => { with ($scope) { ${code} }\n;$__state.done = true })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
1885
|
+
)(scriptScope, effect, importer, state, ...Object.values(helpers))
|
|
1898
1886
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
1899
1887
|
trackScript(result)
|
|
1888
|
+
return { settled: result, sync: state.done === true }
|
|
1900
1889
|
}
|
|
1901
1890
|
|
|
1902
1891
|
// puts a component's declared props on the store, before any script runs and
|
|
@@ -2071,7 +2060,7 @@ const interopDefault = (mod: any) => (mod && mod.default !== undefined ? mod.def
|
|
|
2071
2060
|
// synchronous body invokes the factory before the first render, matching
|
|
2072
2061
|
// setup-script timing; bodies with top-level await (static imports included)
|
|
2073
2062
|
// resolve later and the template updates reactively
|
|
2074
|
-
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}) => {
|
|
2063
|
+
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}): ScriptRun => {
|
|
2075
2064
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
2076
2065
|
const $__exports: { default?: (props: Record<string, any>, ctx: Record<string, any>) => any; done?: boolean } = {}
|
|
2077
2066
|
const result: Promise<void> = new Function(
|
|
@@ -2112,8 +2101,14 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
2112
2101
|
// tracked through the merge, not just the module body: a factory's names
|
|
2113
2102
|
// reach the store in `merge`, and a template expression that reads one before
|
|
2114
2103
|
// then is not an authoring mistake (see reportExprError)
|
|
2115
|
-
|
|
2104
|
+
const settled = result.then(invoke, logError)
|
|
2105
|
+
trackScript(settled)
|
|
2116
2106
|
if ($__exports.done) invoke() // fully-sync body: factory runs before first render
|
|
2107
|
+
// sync only if the bindings are already on the store: a factory whose body
|
|
2108
|
+
// finished but whose *factory* returned a promise (an async factory, or one
|
|
2109
|
+
// that awaits $mounted()) still has names on the way, and the render gate
|
|
2110
|
+
// must treat it as pending rather than race its merge
|
|
2111
|
+
return { settled, sync: $__exports.done === true && merging === undefined }
|
|
2117
2112
|
}
|
|
2118
2113
|
|
|
2119
2114
|
// ---------------------------------------------------------------------------
|
|
@@ -2212,6 +2207,32 @@ export const enableHotReload = (): void => {
|
|
|
2212
2207
|
|
|
2213
2208
|
type EmitListener = (event: CustomEvent, payload: any) => void
|
|
2214
2209
|
|
|
2210
|
+
// how long a first render may sit behind its scripts before the console says so
|
|
2211
|
+
const STUCK_RENDER_DELAY = 3000
|
|
2212
|
+
|
|
2213
|
+
// a script that neither returns nor calls $mounted() holds the template
|
|
2214
|
+
// forever, and the failure looks like nothing at all: no error, no markup, a
|
|
2215
|
+
// component indistinguishable from one nobody mounted. So the wait is loud
|
|
2216
|
+
// after a few seconds - and it keeps waiting, because rendering on a timer
|
|
2217
|
+
// would make the moment of the first render depend on the machine it runs on.
|
|
2218
|
+
//
|
|
2219
|
+
// Armed only on the deferred path, so a page of synchronous components creates
|
|
2220
|
+
// no timers at all
|
|
2221
|
+
const warnIfStuck = (component: Component79, gates: Promise<void>[]) => {
|
|
2222
|
+
const timer = setTimeout(() => {
|
|
2223
|
+
console.warn(
|
|
2224
|
+
`jq79: ${component.name ? `<${component.name}>` : "a component"}${component.filename ? ` (${component.filename})` : ""} ` +
|
|
2225
|
+
`has been waiting ${STUCK_RENDER_DELAY / 1000}s for a :setup script and has rendered nothing. ` +
|
|
2226
|
+
"The template waits until every script returns or calls $mounted() - add an " +
|
|
2227
|
+
"await $mounted() above the slow part to render first and fill in after."
|
|
2228
|
+
)
|
|
2229
|
+
}, STUCK_RENDER_DELAY)
|
|
2230
|
+
// unref where it exists (node/vitest): a pending timer must not be what keeps
|
|
2231
|
+
// a process alive. Browsers have no such notion and no such need
|
|
2232
|
+
;(timer as any)?.unref?.()
|
|
2233
|
+
Promise.all(gates).then(() => clearTimeout(timer))
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2215
2236
|
const fetchComponent = async (url: string): Promise<Component79> => {
|
|
2216
2237
|
const response = await fetch(url)
|
|
2217
2238
|
if (!response.ok) throw new Error(`failed to fetch component from ${url}: ${response.status}`)
|
|
@@ -2282,6 +2303,11 @@ export class Component79 {
|
|
|
2282
2303
|
private mountRoot: Element | ShadowRoot | DocumentFragment | null = null
|
|
2283
2304
|
// settles the $mounted() promise handed to this render generation's scripts
|
|
2284
2305
|
private resolveMounted: (() => void) | null = null
|
|
2306
|
+
// whether this generation's template has been built. A render held back by a
|
|
2307
|
+
// script (see the gate in renderWith) has markers but no nodes, and $mounted()
|
|
2308
|
+
// must not resolve on attach alone - a script awaiting it would wake to an
|
|
2309
|
+
// empty component and find nothing to query
|
|
2310
|
+
private renderDone = false
|
|
2285
2311
|
// instance-level listeners for $emit events, registered with on(). Kept
|
|
2286
2312
|
// outside the render generation so they survive re-render and destroy()
|
|
2287
2313
|
private emitListeners = new Map<string, Set<EmitListener>>()
|
|
@@ -2339,7 +2365,6 @@ export class Component79 {
|
|
|
2339
2365
|
reportedExprErrors.clear()
|
|
2340
2366
|
pendingReports.clear()
|
|
2341
2367
|
reportedFailedExprs.clear()
|
|
2342
|
-
failedExprs.clear()
|
|
2343
2368
|
const marker = this.startMarker
|
|
2344
2369
|
const rendered = !!(marker && this.content)
|
|
2345
2370
|
|
|
@@ -2375,7 +2400,7 @@ export class Component79 {
|
|
|
2375
2400
|
if (shadow) this.styleEls.forEach(el => parent.insertBefore(el, before))
|
|
2376
2401
|
parent.insertBefore(this.content!, before)
|
|
2377
2402
|
this.mountRoot = parent
|
|
2378
|
-
this.
|
|
2403
|
+
this.settleMounted()
|
|
2379
2404
|
return true
|
|
2380
2405
|
}
|
|
2381
2406
|
|
|
@@ -2483,16 +2508,18 @@ export class Component79 {
|
|
|
2483
2508
|
return !event.defaultPrevented
|
|
2484
2509
|
}
|
|
2485
2510
|
|
|
2486
|
-
// `await $mounted()` suspends a setup script until
|
|
2487
|
-
//
|
|
2488
|
-
// is
|
|
2489
|
-
//
|
|
2490
|
-
//
|
|
2491
|
-
//
|
|
2511
|
+
// `await $mounted()` suspends a setup script until the component is
|
|
2512
|
+
// rendered *and* attached, so code below it can querySelector its own DOM.
|
|
2513
|
+
// If this instance is never mounted, the promise stays pending and the
|
|
2514
|
+
// script's tail never runs.
|
|
2515
|
+
//
|
|
2516
|
+
// Calling it is also how a script releases the first render - see the gate
|
|
2517
|
+
// below - so the two halves of the contract are one call: "put me on the
|
|
2518
|
+
// page, and don't wait for the rest of me"
|
|
2492
2519
|
let resolveMounted!: () => void
|
|
2493
2520
|
const mounted = new Promise<void>(resolve => { resolveMounted = resolve })
|
|
2494
2521
|
this.resolveMounted = resolveMounted
|
|
2495
|
-
|
|
2522
|
+
this.renderDone = false
|
|
2496
2523
|
|
|
2497
2524
|
// $self / $$self mirror $ / $$ but only search this instance's own
|
|
2498
2525
|
// output: the sibling nodes between its markers. They work detached too
|
|
@@ -2563,7 +2590,31 @@ export class Component79 {
|
|
|
2563
2590
|
// first line, so deferring doesn't shift the lines devtools reports for it
|
|
2564
2591
|
const defer = (code: string) => `await $mounted();${code}`
|
|
2565
2592
|
|
|
2593
|
+
// what the first render is still waiting for. A script holds the template
|
|
2594
|
+
// back until it returns or calls $mounted() - whichever comes first - so
|
|
2595
|
+
// `let rows = await fetch(...)` renders once, with rows, instead of
|
|
2596
|
+
// rendering empty and filling in. `:mounted` is not a special case here: it
|
|
2597
|
+
// *is* a script that yields on line 0, which is what `defer` above writes.
|
|
2598
|
+
//
|
|
2599
|
+
// One gate per script, not one per instance: a script yielding must not
|
|
2600
|
+
// release the render on behalf of a sibling script that is still fetching
|
|
2601
|
+
const gates: Promise<void>[] = []
|
|
2602
|
+
let allSync = true
|
|
2603
|
+
|
|
2566
2604
|
this.scripts.forEach((script, index) => {
|
|
2605
|
+
let resolveGate!: () => void
|
|
2606
|
+
gates.push(new Promise<void>(resolve => { resolveGate = resolve }))
|
|
2607
|
+
// whether this gate is already open on *this* stack, which is not the
|
|
2608
|
+
// same as the script having finished: a script that yields immediately
|
|
2609
|
+
// (`await $mounted()` on its first line, which is what `:mounted`
|
|
2610
|
+
// compiles to) never finishes synchronously but holds nothing up either.
|
|
2611
|
+
// Reading the promise instead would push every such render a microtask
|
|
2612
|
+
// later, for no one's benefit
|
|
2613
|
+
let open = false
|
|
2614
|
+
const release = () => { open = true; resolveGate() }
|
|
2615
|
+
// this script's own view of $mounted: the call releases its gate, the
|
|
2616
|
+
// promise it returns is the instance's (one mount, one resolution)
|
|
2617
|
+
const $mounted = () => { release(); return mounted }
|
|
2567
2618
|
// the file's other components are passed as parameters of the compiled
|
|
2568
2619
|
// script, not just left on the store's prototype: a factory script runs
|
|
2569
2620
|
// as plain lexical JS with no `with`, so a bare `Row` in one would
|
|
@@ -2572,20 +2623,40 @@ export class Component79 {
|
|
|
2572
2623
|
// parameter is what the name resolves to
|
|
2573
2624
|
const instanceHelpers = { $mounted, $self, $$self, ...injected, ...siblingScope }
|
|
2574
2625
|
const at: ScriptLocation = { filename: this.filename, index }
|
|
2626
|
+
const deferred = ":mounted" in script.attrs
|
|
2575
2627
|
const factoryCode = transformFactoryScript(script.content)
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2628
|
+
const run = ((): ScriptRun => {
|
|
2629
|
+
if (factoryCode !== null) {
|
|
2630
|
+
// a factory publishes its names by returning them, so one that yields
|
|
2631
|
+
// before it returns renders against a store where none of them exist.
|
|
2632
|
+
// In factory mode `:mounted` yields on line 0, which means *always* -
|
|
2633
|
+
// and unlike a setup script there is no way to put the useful half
|
|
2634
|
+
// above the yield. Awaiting $mounted() inside the factory does what
|
|
2635
|
+
// the author meant, and is what the message points at
|
|
2636
|
+
if (deferred) {
|
|
2637
|
+
console.warn(
|
|
2638
|
+
"jq79: :mounted on a factory script renders the template before the factory has returned, " +
|
|
2639
|
+
"so none of its bindings exist yet - await $mounted() inside the factory instead."
|
|
2640
|
+
)
|
|
2641
|
+
}
|
|
2642
|
+
declareProps(store, parseFactoryProps(script.content))
|
|
2643
|
+
const body = deferred ? defer(factoryCode) : factoryCode
|
|
2644
|
+
return runFactoryScript(body, store, fx.effect, instanceHelpers, $import, at)
|
|
2645
|
+
}
|
|
2646
|
+
const { vars, code } = transformSetupScript(script.content)
|
|
2647
|
+
declareProps(store, setupSignature(script))
|
|
2648
|
+
// pre-declare script vars on the store so `with` resolves assignments
|
|
2649
|
+
// to them (and reads of them) through the reactive proxy
|
|
2650
|
+
vars.forEach(name => { if (!(name in store)) (store as any)[name] = undefined })
|
|
2651
|
+
const body = deferred ? defer(code) : code
|
|
2652
|
+
return runSetupScript(body, store, fx.effect, instanceHelpers, $import, at)
|
|
2653
|
+
})()
|
|
2654
|
+
// a script that threw has nothing left to contribute, so its rejection
|
|
2655
|
+
// releases the gate exactly as completion does - the error is already
|
|
2656
|
+
// reported by the runner, and holding the template hostage to it would
|
|
2657
|
+
// turn one broken script into a blank component
|
|
2658
|
+
run.settled.then(release, release)
|
|
2659
|
+
if (!run.sync && !open) allSync = false
|
|
2589
2660
|
})
|
|
2590
2661
|
|
|
2591
2662
|
const content = document.createDocumentFragment()
|
|
@@ -2600,8 +2671,36 @@ export class Component79 {
|
|
|
2600
2671
|
? injected[key]
|
|
2601
2672
|
: Reflect.get(target, key, receiver),
|
|
2602
2673
|
})
|
|
2603
|
-
|
|
2674
|
+
// the markers go in either way, so render() returns something mountable
|
|
2675
|
+
// whether or not the template has been built yet: they are what detach()
|
|
2676
|
+
// collects between and what the deferred pass inserts before, exactly as
|
|
2677
|
+
// :if/:each anchors already work. That is what keeps render() and mount()
|
|
2678
|
+
// synchronous while the first render itself is allowed to wait
|
|
2679
|
+
content.append(this.startMarker, this.endMarker)
|
|
2604
2680
|
this.content = content
|
|
2681
|
+
if (allSync) {
|
|
2682
|
+
// nothing is pending, so the template is built on this stack - the
|
|
2683
|
+
// ordinary case, and byte-for-byte the timing render() has always had.
|
|
2684
|
+
//
|
|
2685
|
+
// Written out rather than routed through the closure below on purpose: a
|
|
2686
|
+
// component that nests itself recurses through here, so one extra frame
|
|
2687
|
+
// per level is one fewer level before the stack gives out - enough, when
|
|
2688
|
+
// this was a shared `paint()`, to overflow *underneath* the depth guard
|
|
2689
|
+
// at MAX_NESTING_DEPTH and turn a named error back into a RangeError
|
|
2690
|
+
this.endMarker.parentNode!.insertBefore(renderNodes(this.template, templateScope, fx, shadow), this.endMarker)
|
|
2691
|
+
this.renderDone = true
|
|
2692
|
+
this.settleMounted()
|
|
2693
|
+
} else {
|
|
2694
|
+
Promise.all(gates).then(() => {
|
|
2695
|
+
// destroy() nulls the markers and a re-render replaces them, so a gate
|
|
2696
|
+
// that opens after either one has nothing left to paint into
|
|
2697
|
+
if (marker !== this.startMarker) return
|
|
2698
|
+
this.endMarker!.parentNode!.insertBefore(renderNodes(this.template, templateScope, fx, shadow), this.endMarker!)
|
|
2699
|
+
this.renderDone = true
|
|
2700
|
+
this.settleMounted()
|
|
2701
|
+
})
|
|
2702
|
+
warnIfStuck(this, gates)
|
|
2703
|
+
}
|
|
2605
2704
|
|
|
2606
2705
|
if (shadow) {
|
|
2607
2706
|
this.styleEls = this.styles.map(style => {
|
|
@@ -2648,10 +2747,18 @@ export class Component79 {
|
|
|
2648
2747
|
if (this.useShadow) this.styleEls.forEach(el => root.appendChild(el))
|
|
2649
2748
|
root.appendChild(this.content!)
|
|
2650
2749
|
this.mountRoot = root
|
|
2651
|
-
this.
|
|
2750
|
+
this.settleMounted()
|
|
2652
2751
|
return this
|
|
2653
2752
|
}
|
|
2654
2753
|
|
|
2754
|
+
// `await $mounted()` means "rendered and on the page", so it waits for both -
|
|
2755
|
+
// whichever lands last calls this. In the ordinary synchronous flow the render
|
|
2756
|
+
// is already done and this is the attach; for a component whose first render
|
|
2757
|
+
// a script held back, it is the other way round
|
|
2758
|
+
private settleMounted() {
|
|
2759
|
+
if (this.renderDone && this.mountRoot) this.resolveMounted?.()
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2655
2762
|
// detaches from the DOM while keeping all state; a later mount() re-attaches
|
|
2656
2763
|
// with any updates that happened while detached already applied
|
|
2657
2764
|
detach(): this {
|
|
@@ -2687,6 +2794,7 @@ export class Component79 {
|
|
|
2687
2794
|
this.content = null
|
|
2688
2795
|
this.startMarker = null
|
|
2689
2796
|
this.endMarker = null
|
|
2797
|
+
this.renderDone = false
|
|
2690
2798
|
this.data = null
|
|
2691
2799
|
this.resolveMounted = null
|
|
2692
2800
|
return this
|