jq79 0.7.5 → 0.7.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 +16 -16
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +5 -0
- package/dist/jq79.global.js +16 -16
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +16 -16
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +112 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Mini reactive component library: single-file .html components, Svelte-style setup scripts, fine-grained proxy reactivity. No compiler, no bundler, no dependencies — works from any static host.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reactive",
|
package/src/jq79.ts
CHANGED
|
@@ -1341,6 +1341,12 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
1341
1341
|
} finally {
|
|
1342
1342
|
nestingDepth--
|
|
1343
1343
|
}
|
|
1344
|
+
// a child whose script holds its render paints later than this template:
|
|
1345
|
+
// its owner's $mounted() waits for it. Absent under renderComponent(),
|
|
1346
|
+
// where no instance owns the template
|
|
1347
|
+
const box: PendingScripts | undefined = (scope as any)[PENDING_SCRIPTS]
|
|
1348
|
+
if (box) instance.holdOwner(box.owner)
|
|
1349
|
+
|
|
1344
1350
|
// the box holds this instance and nothing else, so appending is the whole
|
|
1345
1351
|
// of it; without one, the end anchor is the only fixed point there is - and
|
|
1346
1352
|
// its parentNode is the fragment before this site is inserted and the real
|
|
@@ -3717,9 +3723,13 @@ const UNFILLED_PROPS = Symbol("jq79.unfilledProps")
|
|
|
3717
3723
|
// awaited $mounted() is exactly the case the count keeps quiet. Read live, so
|
|
3718
3724
|
// an :if that opens after everything settled is judged against the scripts as
|
|
3719
3725
|
// they are then, not as they were at the first paint
|
|
3726
|
+
//
|
|
3727
|
+
// It also names the instance whose render generation it belongs to, so a
|
|
3728
|
+
// nested component rendered anywhere in that template - an :each row, slot
|
|
3729
|
+
// content - can tell its owner it hasn't painted yet (see holdOwner)
|
|
3720
3730
|
const PENDING_SCRIPTS = Symbol("jq79.pendingScripts")
|
|
3721
3731
|
|
|
3722
|
-
type PendingScripts = { count: number }
|
|
3732
|
+
type PendingScripts = { count: number; owner: Component79 }
|
|
3723
3733
|
|
|
3724
3734
|
// default-import interop for factory scripts: real modules expose .default,
|
|
3725
3735
|
// while importing an .html component resolves to the Component79 itself
|
|
@@ -3839,6 +3849,11 @@ export const hotUpdate = (filename: string, src: string): number => {
|
|
|
3839
3849
|
// parsed once and shared by every instance - which is already what a
|
|
3840
3850
|
// definition and the clones :component makes from it do
|
|
3841
3851
|
const parts = parseComponentString(src)
|
|
3852
|
+
// the next fetch of this file - a component mounted after the edit - gets the
|
|
3853
|
+
// edit, not the source the cache held from before it
|
|
3854
|
+
componentCache?.forEach((_, url) => {
|
|
3855
|
+
if (hotKey(url) === key) componentCache!.set(url, Promise.resolve(src))
|
|
3856
|
+
})
|
|
3842
3857
|
// the file is the hot-reload unit, so one reparse serves every component it
|
|
3843
3858
|
// declares: an instance is handed the parts of the component it *is*, by
|
|
3844
3859
|
// name. A name that is no longer in the file (a <template> renamed or
|
|
@@ -3928,17 +3943,53 @@ const resolveBare = (url: string): string => {
|
|
|
3928
3943
|
}
|
|
3929
3944
|
}
|
|
3930
3945
|
|
|
3931
|
-
|
|
3932
|
-
|
|
3946
|
+
// a component's source by the absolute URL it came from, so a file that a
|
|
3947
|
+
// page fetches, fetchAll()s and import()s from a dozen setup scripts is
|
|
3948
|
+
// downloaded once (Component79.cache). The text, not the component: each
|
|
3949
|
+
// fetch still hands back an instance of its own, as it always has - mounting
|
|
3950
|
+
// one never touches another. A promise, so two requests in flight share one
|
|
3951
|
+
// download; a failed one is dropped, and the next request tries again.
|
|
3952
|
+
// Under safeEval()'s worker the entry settles once the precompiled functions
|
|
3953
|
+
// are in too, so those load once per file as well
|
|
3954
|
+
let componentCache: Map<string, Promise<string>> | null = new Map()
|
|
3955
|
+
|
|
3956
|
+
const cacheKey = (url: string): string => {
|
|
3957
|
+
try {
|
|
3958
|
+
return new URL(url, document.baseURI).href.replace(/#.*$/, "")
|
|
3959
|
+
} catch {
|
|
3960
|
+
return url
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
const downloadComponent = (url: string): Promise<string> =>
|
|
3933
3965
|
// under safeEval()'s worker, the component's functions arrive beside it,
|
|
3934
3966
|
// and both have to be in before anyone can render it
|
|
3935
|
-
|
|
3967
|
+
Promise.all([
|
|
3936
3968
|
fetch(url).then(response => {
|
|
3937
3969
|
if (!response.ok) throw new Error(`failed to fetch component from ${url}: ${response.status}`)
|
|
3938
3970
|
return response.text()
|
|
3939
3971
|
}),
|
|
3940
3972
|
safeEvalWorker?.then(() => loadPrecompiled(url)),
|
|
3941
|
-
])
|
|
3973
|
+
]).then(([text]) => text)
|
|
3974
|
+
|
|
3975
|
+
const componentSource = (url: string): Promise<string> => {
|
|
3976
|
+
if (!componentCache) return downloadComponent(url)
|
|
3977
|
+
const key = cacheKey(url)
|
|
3978
|
+
let text = componentCache.get(key)
|
|
3979
|
+
if (!text) {
|
|
3980
|
+
text = downloadComponent(url)
|
|
3981
|
+
componentCache.set(key, text)
|
|
3982
|
+
const pending = text
|
|
3983
|
+
pending.catch(() => {
|
|
3984
|
+
if (componentCache?.get(key) === pending) componentCache.delete(key)
|
|
3985
|
+
})
|
|
3986
|
+
}
|
|
3987
|
+
return text
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
const fetchComponent = async (rawUrl: string): Promise<Component79> => {
|
|
3991
|
+
const url = resolveBare(rawUrl)
|
|
3992
|
+
const text = await componentSource(url)
|
|
3942
3993
|
// the URL names the component's scripts in devtools, and is where the
|
|
3943
3994
|
// browser will look for the source when a breakpoint lands in one
|
|
3944
3995
|
return new Component79(text, { filename: url })
|
|
@@ -4082,6 +4133,14 @@ export class Component79 {
|
|
|
4082
4133
|
// must not resolve on attach alone - a script awaiting it would wake to an
|
|
4083
4134
|
// empty component and find nothing to query
|
|
4084
4135
|
private renderDone = false
|
|
4136
|
+
// how many components this generation's first render created that haven't
|
|
4137
|
+
// painted yet - a child whose script awaits an import paints after this
|
|
4138
|
+
// template does. $mounted() means the DOM below is there to query, so it
|
|
4139
|
+
// waits for them too (RECORD/2026-10-22.mounted-waits-for-children.md)
|
|
4140
|
+
private held = 0
|
|
4141
|
+
// set by the usage site of a nested instance that hadn't painted when it was
|
|
4142
|
+
// rendered: tells the owner it has, or that it never will (destroyed first)
|
|
4143
|
+
private onPaint: (() => void) | null = null
|
|
4085
4144
|
// what this render generation's scripts asked to run when it is torn down
|
|
4086
4145
|
// ($destroyed). Created on the first call, and taken by destroy() before
|
|
4087
4146
|
// anything else goes, so a hook still sees the DOM and the store
|
|
@@ -4271,6 +4330,24 @@ export class Component79 {
|
|
|
4271
4330
|
return Promise.all(steps).then(() => undefined)
|
|
4272
4331
|
}
|
|
4273
4332
|
|
|
4333
|
+
// whether fetch(), fetchAll() and a setup script's import() of an .html
|
|
4334
|
+
// download a file once and reuse its source after that. On by default:
|
|
4335
|
+
//
|
|
4336
|
+
// Component79.cache() // true - is it on?
|
|
4337
|
+
// Component79.cache(false) // off, and what it held is dropped
|
|
4338
|
+
// Component79.clearCache() // stays on, starts empty
|
|
4339
|
+
//
|
|
4340
|
+
// Returns the setting as it stands after the call
|
|
4341
|
+
static cache(enabled?: boolean): boolean {
|
|
4342
|
+
if (enabled === false) componentCache = null
|
|
4343
|
+
else if (enabled === true) componentCache ??= new Map()
|
|
4344
|
+
return !!componentCache
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
static clearCache(): void {
|
|
4348
|
+
componentCache?.clear()
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4274
4351
|
static fetch(url: string): PendingComponent79 {
|
|
4275
4352
|
if (Array.isArray(url)) throw new TypeError("Component79.fetch takes one URL; use fetchAll for an array")
|
|
4276
4353
|
return new PendingComponent79(fetchComponent(url))
|
|
@@ -4334,7 +4411,7 @@ export class Component79 {
|
|
|
4334
4411
|
// renders, so a component nested inside itself pays for it once per level -
|
|
4335
4412
|
// and the depth guard at MAX_NESTING_DEPTH only beats a RangeError while
|
|
4336
4413
|
// this function stays small (see the note in docs/development.md)
|
|
4337
|
-
Object.defineProperty(raw, PENDING_SCRIPTS, { value: { count: 0 } as PendingScripts })
|
|
4414
|
+
Object.defineProperty(raw, PENDING_SCRIPTS, { value: { count: 0, owner: this } as PendingScripts })
|
|
4338
4415
|
|
|
4339
4416
|
const store = $reactive(raw)
|
|
4340
4417
|
const fx = createEffectScope(store)
|
|
@@ -4389,6 +4466,7 @@ export class Component79 {
|
|
|
4389
4466
|
const mounted = new Promise<void>(resolve => { resolveMounted = resolve })
|
|
4390
4467
|
this.resolveMounted = resolveMounted
|
|
4391
4468
|
this.renderDone = false
|
|
4469
|
+
this.held = 0
|
|
4392
4470
|
|
|
4393
4471
|
// $destroyed(fn) runs fn when this generation is torn down: destroy(), a
|
|
4394
4472
|
// re-render, a hot reload, or a parent's :if/:each removing it - never
|
|
@@ -4800,8 +4878,27 @@ export class Component79 {
|
|
|
4800
4878
|
// whichever lands last calls this. In the ordinary synchronous flow the render
|
|
4801
4879
|
// is already done and this is the attach; for a component whose first render
|
|
4802
4880
|
// a script held back, it is the other way round
|
|
4881
|
+
//
|
|
4882
|
+
// "Rendered" includes the components the template created: one whose own
|
|
4883
|
+
// render a script held back counts in `held` until it paints
|
|
4803
4884
|
private settleMounted() {
|
|
4804
|
-
if (this.renderDone
|
|
4885
|
+
if (!this.renderDone || this.held > 0) return
|
|
4886
|
+
const onPaint = this.onPaint
|
|
4887
|
+
this.onPaint = null
|
|
4888
|
+
onPaint?.()
|
|
4889
|
+
if (this.mountRoot) this.resolveMounted?.()
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
// Internal: called by the usage site right after rendering this instance
|
|
4893
|
+
// into `owner`'s template. One that has painted already - every synchronous
|
|
4894
|
+
// component - costs nothing more
|
|
4895
|
+
holdOwner(owner: Component79) {
|
|
4896
|
+
if (this.renderDone && this.held === 0) return
|
|
4897
|
+
owner.held++
|
|
4898
|
+
this.onPaint = () => {
|
|
4899
|
+
owner.held--
|
|
4900
|
+
owner.settleMounted()
|
|
4901
|
+
}
|
|
4805
4902
|
}
|
|
4806
4903
|
|
|
4807
4904
|
// detaches from the DOM while keeping all state; a later mount() re-attaches
|
|
@@ -4837,6 +4934,14 @@ export class Component79 {
|
|
|
4837
4934
|
const hooks = this.destroyHooks
|
|
4838
4935
|
this.destroyHooks = null
|
|
4839
4936
|
hooks?.forEach(fn => runHook(fn, "$destroyed"))
|
|
4937
|
+
// before the children go: each one destroyed unpainted releases this
|
|
4938
|
+
// instance, and that must not resolve a generation that is going away
|
|
4939
|
+
this.resolveMounted = null
|
|
4940
|
+
// and one destroyed before it painted releases its owner, which would
|
|
4941
|
+
// otherwise wait for a paint that is never coming
|
|
4942
|
+
const onPaint = this.onPaint
|
|
4943
|
+
this.onPaint = null
|
|
4944
|
+
onPaint?.()
|
|
4840
4945
|
this.detach()
|
|
4841
4946
|
this.fx?.dispose()
|
|
4842
4947
|
this.fx = null
|