jq79 0.7.3 → 0.7.4
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/README.md +2 -2
- package/dist/jq79-sw.js +1 -1
- package/dist/jq79.cjs +16 -16
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +12 -1
- 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/dist/precompile.cjs +1 -1
- package/dist/precompile.cjs.map +1 -1
- package/dist/precompile.js +1 -1
- package/dist/precompile.js.map +1 -1
- package/dist/reactive.d.ts +4 -0
- package/package.json +1 -1
- package/src/jq79.ts +181 -3
- package/src/precompile.ts +4 -3
- package/src/reactive.ts +29 -0
- package/src/source.ts +4 -3
- package/src/transform.ts +1 -1
package/dist/reactive.d.ts
CHANGED
|
@@ -53,4 +53,8 @@ export type EffectScope = {
|
|
|
53
53
|
dispose: () => void;
|
|
54
54
|
};
|
|
55
55
|
export declare const createEffectScope: (scope: Record<string, any>, deep?: boolean) => EffectScope;
|
|
56
|
+
export type Computed<T> = ReactiveDeepData<{
|
|
57
|
+
readonly value: T;
|
|
58
|
+
}>;
|
|
59
|
+
export declare const $computed: <T>(get: () => T) => Computed<T>;
|
|
56
60
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { $, $$, $create, sanitizeHTML, allowedHosts } from "./dom"
|
|
3
3
|
import type { AllowUrl, QueryAll, QueryOne } from "./dom"
|
|
4
|
-
import { $reactive, $toRaw, untracked, createEffectScope, ALSO_WAKEN_BY, OWNER } from "./reactive"
|
|
4
|
+
import { $reactive, $toRaw, $computed, untracked, createEffectScope, ALSO_WAKEN_BY, OWNER } from "./reactive"
|
|
5
5
|
import type { ReactiveDeepData, EffectScope } from "./reactive"
|
|
6
6
|
import { transformSetupScript, transformFactoryScript, parsePropsPattern, parseFactoryProps, type PropDecl } from "./transform"
|
|
7
7
|
import {
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
|
|
15
15
|
export { $, $$, $create } from "./dom"
|
|
16
16
|
export type { QueryOne, QueryAll } from "./dom"
|
|
17
|
-
export { $reactive, $toRaw } from "./reactive"
|
|
17
|
+
export { $reactive, $toRaw, $computed } from "./reactive"
|
|
18
|
+
export type { Computed } from "./reactive"
|
|
18
19
|
|
|
19
20
|
// the package version, substituted at build time (tsup/vitest `define`, read
|
|
20
21
|
// from package.json - releases bump it there and nowhere else). The typeof
|
|
@@ -4054,6 +4055,17 @@ export class Component79 {
|
|
|
4054
4055
|
// must not resolve on attach alone - a script awaiting it would wake to an
|
|
4055
4056
|
// empty component and find nothing to query
|
|
4056
4057
|
private renderDone = false
|
|
4058
|
+
// what this render generation's scripts asked to run when it is torn down
|
|
4059
|
+
// ($destroyed). Created on the first call, and taken by destroy() before
|
|
4060
|
+
// anything else goes, so a hook still sees the DOM and the store
|
|
4061
|
+
private destroyHooks: (() => void)[] | null = null
|
|
4062
|
+
// this generation's $attached / $detached hooks, and whether it has been
|
|
4063
|
+
// told it is on the page. Only an instance that registered one of them is
|
|
4064
|
+
// in pageWatchers, which is what keeps a page that uses neither from paying
|
|
4065
|
+
// for a walk on every mount (see settleAttached)
|
|
4066
|
+
private attachHooks: (() => void)[] | null = null
|
|
4067
|
+
private detachHooks: (() => void)[] | null = null
|
|
4068
|
+
private onPage = false
|
|
4057
4069
|
// instance-level listeners for $emit events, registered with on(). Kept
|
|
4058
4070
|
// outside the render generation so they survive re-render and destroy()
|
|
4059
4071
|
private emitListeners = new Map<string, Set<EmitListener>>()
|
|
@@ -4351,6 +4363,55 @@ export class Component79 {
|
|
|
4351
4363
|
this.resolveMounted = resolveMounted
|
|
4352
4364
|
this.renderDone = false
|
|
4353
4365
|
|
|
4366
|
+
// $destroyed(fn) runs fn when this generation is torn down: destroy(), a
|
|
4367
|
+
// re-render, a hot reload, or a parent's :if/:each removing it - never
|
|
4368
|
+
// detach(), which keeps state for a mount() to resume. Registered after
|
|
4369
|
+
// the generation is gone (a script that awaited past its own destroy), it
|
|
4370
|
+
// runs at once: holding it for a destroy that already happened would leak
|
|
4371
|
+
// exactly what it was written to stop. Returns the unregister
|
|
4372
|
+
const $destroyed = (fn: () => void): (() => void) => {
|
|
4373
|
+
if (typeof fn !== "function") throw new TypeError("jq79: $destroyed expects a function")
|
|
4374
|
+
if (marker !== this.startMarker) {
|
|
4375
|
+
runHook(fn, "$destroyed")
|
|
4376
|
+
return () => {}
|
|
4377
|
+
}
|
|
4378
|
+
return addHook((this.destroyHooks ??= []), fn)
|
|
4379
|
+
}
|
|
4380
|
+
// $attached(fn) runs fn every time this generation goes onto the page -
|
|
4381
|
+
// in the document, rendered - the first mount included; $detached(fn)
|
|
4382
|
+
// every time it leaves, and only after an $attached it balances. A nested
|
|
4383
|
+
// component hears its root's mount and detach (see settleAttached and
|
|
4384
|
+
// notifyDetached). `immediate`, on by default, also runs fn now when the
|
|
4385
|
+
// component is on the page already, so "while I'm on the page" holds
|
|
4386
|
+
// wherever the line sits - above `await $mounted()` or below it. A late
|
|
4387
|
+
// $detached runs at once, as a late $destroyed does; a late $attached has
|
|
4388
|
+
// nothing left to attach
|
|
4389
|
+
const $attached = (fn: () => void, { immediate = true }: { immediate?: boolean } = {}): (() => void) => {
|
|
4390
|
+
if (typeof fn !== "function") throw new TypeError("jq79: $attached expects a function")
|
|
4391
|
+
if (marker !== this.startMarker) return () => {}
|
|
4392
|
+
this.watchPage()
|
|
4393
|
+
const off = addHook((this.attachHooks ??= []), fn)
|
|
4394
|
+
if (immediate && this.onPage) runHook(fn, "$attached")
|
|
4395
|
+
return off
|
|
4396
|
+
}
|
|
4397
|
+
const $detached = (fn: () => void): (() => void) => {
|
|
4398
|
+
if (typeof fn !== "function") throw new TypeError("jq79: $detached expects a function")
|
|
4399
|
+
if (marker !== this.startMarker) {
|
|
4400
|
+
runHook(fn, "$detached")
|
|
4401
|
+
return () => {}
|
|
4402
|
+
}
|
|
4403
|
+
this.watchPage()
|
|
4404
|
+
return addHook((this.detachHooks ??= []), fn)
|
|
4405
|
+
}
|
|
4406
|
+
// the library's $computed, disposed with this generation: one made over a
|
|
4407
|
+
// shared store is otherwise held by that store, and kept recomputing, for
|
|
4408
|
+
// as long as the store lives
|
|
4409
|
+
const $instanceComputed = <T>(get: () => T) => {
|
|
4410
|
+
const computed = $computed(get)
|
|
4411
|
+
$destroyed(() => computed.$dispose())
|
|
4412
|
+
return computed
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4354
4415
|
// $self / $$self mirror $ / $$ but only search this instance's own
|
|
4355
4416
|
// output: the sibling nodes between its markers. They work detached too
|
|
4356
4417
|
// (the holding fragment keeps markers and rendered nodes as siblings),
|
|
@@ -4451,7 +4512,7 @@ export class Component79 {
|
|
|
4451
4512
|
// resolve to nothing at all. In setup mode this composes with `with` -
|
|
4452
4513
|
// scriptScope's `has` declines any name that is a helper, so the
|
|
4453
4514
|
// parameter is what the name resolves to
|
|
4454
|
-
const instanceHelpers = { $mounted, $self, $$self, ...injected, ...siblingScope }
|
|
4515
|
+
const instanceHelpers = { $mounted, $destroyed, $attached, $detached, $computed: $instanceComputed, $self, $$self, ...injected, ...siblingScope }
|
|
4455
4516
|
const at: ScriptLocation = { filename: this.filename, index }
|
|
4456
4517
|
const deferred = ":mounted" in script.attrs
|
|
4457
4518
|
const factoryCode = transformFactoryScript(script.content)
|
|
@@ -4541,6 +4602,8 @@ export class Component79 {
|
|
|
4541
4602
|
this.endMarker!.parentNode!.insertBefore(renderNodes(this.template, templateScope, fx, shadow), this.endMarker!)
|
|
4542
4603
|
this.renderDone = true
|
|
4543
4604
|
this.settleMounted()
|
|
4605
|
+
// a held render that paints on the page is this component's attach
|
|
4606
|
+
Component79.settleAttached()
|
|
4544
4607
|
})
|
|
4545
4608
|
warnIfStuck(this, gates)
|
|
4546
4609
|
}
|
|
@@ -4603,9 +4666,87 @@ export class Component79 {
|
|
|
4603
4666
|
root.appendChild(this.content!)
|
|
4604
4667
|
this.mountRoot = root
|
|
4605
4668
|
this.settleMounted()
|
|
4669
|
+
Component79.settleAttached()
|
|
4606
4670
|
return this
|
|
4607
4671
|
}
|
|
4608
4672
|
|
|
4673
|
+
// whether this generation's DOM is in the document, template built - the
|
|
4674
|
+
// one meaning "on the page" has for $attached. Not "attach() was called": a
|
|
4675
|
+
// nested component is mounted into a fragment its parent inserts later, and
|
|
4676
|
+
// a root can be mounted into an element that isn't in the document
|
|
4677
|
+
private isOnPage(): boolean {
|
|
4678
|
+
return this.renderDone && this.startMarker?.isConnected === true
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
// joins pageWatchers on the first $attached/$detached of a generation, with
|
|
4682
|
+
// the state as it is now: a hook registered while on the page must not fire
|
|
4683
|
+
// for an attach that already happened. Off the page, a settle is queued: a
|
|
4684
|
+
// component inserted by its parent's :if, :each or tag is built in a
|
|
4685
|
+
// fragment and put on the page later on this same stack, with no attach()
|
|
4686
|
+
// of its own to say so - one microtask late, never early
|
|
4687
|
+
private watchPage() {
|
|
4688
|
+
if (pageWatchers.has(this)) return
|
|
4689
|
+
pageWatchers.add(this)
|
|
4690
|
+
this.onPage = this.isOnPage()
|
|
4691
|
+
if (!this.onPage) Component79.queueSettle()
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
private static queueSettle() {
|
|
4695
|
+
if (settleQueued) return
|
|
4696
|
+
settleQueued = true
|
|
4697
|
+
queueMicrotask(() => {
|
|
4698
|
+
settleQueued = false
|
|
4699
|
+
Component79.settleAttached()
|
|
4700
|
+
})
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
// tells every watcher that is on the page and hasn't been told, in document
|
|
4704
|
+
// order - a parent before what it rendered, slot content included, because
|
|
4705
|
+
// the DOM is what says where a component is
|
|
4706
|
+
private static settleAttached() {
|
|
4707
|
+
if (pageWatchers.size === 0) return
|
|
4708
|
+
const due = [...pageWatchers].filter(component => !component.onPage && component.isOnPage())
|
|
4709
|
+
Component79.inDocumentOrder(due).forEach(component => {
|
|
4710
|
+
// a hook that ran before this one may have destroyed it
|
|
4711
|
+
if (component.onPage || !pageWatchers.has(component)) return
|
|
4712
|
+
component.onPage = true
|
|
4713
|
+
component.attachHooks?.slice().forEach(fn => runHook(fn, "$attached"))
|
|
4714
|
+
})
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
// tells this component and every watcher whose DOM is under its markers
|
|
4718
|
+
// that they are leaving the page - before anything moves, so the hooks see
|
|
4719
|
+
// the DOM where it was. One whose DOM already left without it (an :if
|
|
4720
|
+
// removes its branch, then destroys what was in it) has only itself to tell
|
|
4721
|
+
private notifyDetached() {
|
|
4722
|
+
if (pageWatchers.size === 0) return
|
|
4723
|
+
let due: Component79[] = []
|
|
4724
|
+
if (this.startMarker?.isConnected) {
|
|
4725
|
+
const top = new Set<Node>()
|
|
4726
|
+
for (let node: Node | null = this.startMarker; node; node = node.nextSibling) {
|
|
4727
|
+
top.add(node)
|
|
4728
|
+
if (node === this.endMarker) break
|
|
4729
|
+
}
|
|
4730
|
+
const under = (node: Node | null): boolean => {
|
|
4731
|
+
for (; node; node = node.parentNode) if (top.has(node)) return true
|
|
4732
|
+
return false
|
|
4733
|
+
}
|
|
4734
|
+
due = Component79.inDocumentOrder([...pageWatchers].filter(component => component.onPage && under(component.startMarker)))
|
|
4735
|
+
} else if (this.onPage) {
|
|
4736
|
+
due = [this]
|
|
4737
|
+
}
|
|
4738
|
+
due.forEach(component => {
|
|
4739
|
+
if (!component.onPage) return
|
|
4740
|
+
component.onPage = false
|
|
4741
|
+
component.detachHooks?.slice().forEach(fn => runHook(fn, "$detached"))
|
|
4742
|
+
})
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
private static inDocumentOrder(components: Component79[]): Component79[] {
|
|
4746
|
+
return components.sort((a, b) =>
|
|
4747
|
+
a.startMarker!.compareDocumentPosition(b.startMarker!) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4609
4750
|
// where a shadow-rendered component's styles go: <style> elements ahead of
|
|
4610
4751
|
// its content, as always - or, under safe mode, sheets adopted by the shadow
|
|
4611
4752
|
// root. Built on first placement, since only then is there a root; one that
|
|
@@ -4640,6 +4781,7 @@ export class Component79 {
|
|
|
4640
4781
|
// with any updates that happened while detached already applied
|
|
4641
4782
|
detach(): this {
|
|
4642
4783
|
if (!this.mountRoot || !this.content || !this.startMarker || !this.endMarker) return this
|
|
4784
|
+
this.notifyDetached()
|
|
4643
4785
|
|
|
4644
4786
|
// move everything between the markers (inclusive) back into the holding
|
|
4645
4787
|
// fragment - including nodes :if/:each inserted after mounting
|
|
@@ -4656,6 +4798,18 @@ export class Component79 {
|
|
|
4656
4798
|
}
|
|
4657
4799
|
|
|
4658
4800
|
destroy(): this {
|
|
4801
|
+
// first, while everything they might read is still there: leaving the
|
|
4802
|
+
// page ($detached, for this component and what it rendered), then going
|
|
4803
|
+
// ($destroyed). Taken before running, so a hook that destroys again (or
|
|
4804
|
+
// re-renders) finds none
|
|
4805
|
+
this.notifyDetached()
|
|
4806
|
+
pageWatchers.delete(this)
|
|
4807
|
+
this.attachHooks = null
|
|
4808
|
+
this.detachHooks = null
|
|
4809
|
+
this.onPage = false
|
|
4810
|
+
const hooks = this.destroyHooks
|
|
4811
|
+
this.destroyHooks = null
|
|
4812
|
+
hooks?.forEach(fn => runHook(fn, "$destroyed"))
|
|
4659
4813
|
this.detach()
|
|
4660
4814
|
this.fx?.dispose()
|
|
4661
4815
|
this.fx = null
|
|
@@ -4789,6 +4943,30 @@ export type Component<P = any, E extends string = never, S extends string = neve
|
|
|
4789
4943
|
|
|
4790
4944
|
export const parseComponent = (component: string): Component79 => new Component79(component)
|
|
4791
4945
|
|
|
4946
|
+
// one broken hook must not leave the others' timers running, or the
|
|
4947
|
+
// teardown half done: reported, and the rest still run
|
|
4948
|
+
const runHook = (fn: () => void, name: string) => {
|
|
4949
|
+
try {
|
|
4950
|
+
fn()
|
|
4951
|
+
} catch (error) {
|
|
4952
|
+
console.error(`jq79: error in a ${name} hook`, error)
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
// registers fn on a generation's hook list; the returned function takes it off
|
|
4957
|
+
const addHook = (hooks: (() => void)[], fn: () => void): (() => void) => {
|
|
4958
|
+
hooks.push(fn)
|
|
4959
|
+
return () => {
|
|
4960
|
+
const at = hooks.indexOf(fn)
|
|
4961
|
+
if (at !== -1) hooks.splice(at, 1)
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
// the instances with an $attached or $detached hook, in this generation -
|
|
4966
|
+
// the only ones a mount or a detach has to look at (see settleAttached)
|
|
4967
|
+
const pageWatchers = new Set<Component79>()
|
|
4968
|
+
let settleQueued = false
|
|
4969
|
+
|
|
4792
4970
|
// library helpers injected into setup scripts. They behave like extra
|
|
4793
4971
|
// globals: a same-named scope property (render data or a top-level
|
|
4794
4972
|
// declaration) shadows them. Their names, in this order, are also
|
package/src/precompile.ts
CHANGED
|
@@ -175,9 +175,10 @@ const precompileComponent = (
|
|
|
175
175
|
|
|
176
176
|
// the helper names a script is compiled with: renderWith's
|
|
177
177
|
// { ...SETUP_HELPERS, ...instanceHelpers }, where instanceHelpers is
|
|
178
|
-
// { $mounted, $
|
|
179
|
-
//
|
|
180
|
-
//
|
|
178
|
+
// { $mounted, $destroyed, $attached, $detached, $computed, $self, $$self,
|
|
179
|
+
// ...injected, ...siblingScope } - key order included, because the
|
|
180
|
+
// parameters are positional (built as objects, so a sibling named like a
|
|
181
|
+
// helper keeps the helper's place, as it does there)
|
|
181
182
|
//
|
|
182
183
|
// Reading the signatures is also the first thing renderWith does, and where
|
|
183
184
|
// it refuses a component - a factory destructuring a ctx name out of props
|
package/src/reactive.ts
CHANGED
|
@@ -1200,3 +1200,32 @@ class Scope implements EffectScope {
|
|
|
1200
1200
|
}
|
|
1201
1201
|
|
|
1202
1202
|
export const createEffectScope = (scope: Record<string, any>, deep = false): EffectScope => new Scope(scope, deep)
|
|
1203
|
+
|
|
1204
|
+
// a derived value: `get`'s result on `.value`, recomputed whenever anything it
|
|
1205
|
+
// read changes - in any store, the way any effect is woken. What comes back is
|
|
1206
|
+
// a store, so it does everything one does (passed as a prop, held in another
|
|
1207
|
+
// store and bridged there, $on("value")), seen through a read-only view: a
|
|
1208
|
+
// write would be overwritten by the next recompute, so it is refused aloud
|
|
1209
|
+
// instead (see RECORD/2026-10-01.computed.md)
|
|
1210
|
+
export type Computed<T> = ReactiveDeepData<{ readonly value: T }>
|
|
1211
|
+
|
|
1212
|
+
export const $computed = <T>(get: () => T): Computed<T> => {
|
|
1213
|
+
const box = $reactive({ value: undefined as T })
|
|
1214
|
+
box.$effect(() => {
|
|
1215
|
+
let value: T
|
|
1216
|
+
try {
|
|
1217
|
+
value = get()
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
// left to propagate, it would throw out of whatever write woke it -
|
|
1220
|
+
// `cart.items.push(x)` failing for code that has nothing to do with it
|
|
1221
|
+
console.error("jq79: error in $computed, keeping its last value", error)
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
box.value = value
|
|
1225
|
+
})
|
|
1226
|
+
const refuse = (_target: object, key: string | symbol): boolean => {
|
|
1227
|
+
console.warn(`jq79: a $computed is read-only - the write to ${String(key)} was ignored`)
|
|
1228
|
+
return true
|
|
1229
|
+
}
|
|
1230
|
+
return new Proxy(box, { set: refuse, deleteProperty: refuse, defineProperty: refuse }) as Computed<T>
|
|
1231
|
+
}
|
package/src/source.ts
CHANGED
|
@@ -364,12 +364,13 @@ export const declaredPropNames = (scripts: TagBlock[], signature: (script: TagBl
|
|
|
364
364
|
// the names every setup and factory script is compiled with, in the order
|
|
365
365
|
// renderWith passes them: SETUP_HELPERS (whose values live in jq79.ts, beside
|
|
366
366
|
// the functions they are) and then the per-instance ones it builds -
|
|
367
|
-
// $mounted, $
|
|
368
|
-
//
|
|
367
|
+
// $mounted, $destroyed, $attached, $detached, $computed, $self and $$self,
|
|
368
|
+
// then the injected
|
|
369
|
+
// $emit, $updateModel and $slots. They are positional parameters, so the order is part of what
|
|
369
370
|
// precompile has to reproduce; change one side and precompiled scripts stop
|
|
370
371
|
// matching, which tests/precompile.test.ts and `npm run check:precompile` catch
|
|
371
372
|
export const SETUP_HELPER_NAMES = ["$", "$$", "$create", "$reactive", "$toRaw", "Component79"]
|
|
372
|
-
export const INSTANCE_HELPER_NAMES = ["$mounted", "$self", "$$self", "$emit", "$updateModel", "$slots"]
|
|
373
|
+
export const INSTANCE_HELPER_NAMES = ["$mounted", "$destroyed", "$attached", "$detached", "$computed", "$self", "$$self", "$emit", "$updateModel", "$slots"]
|
|
373
374
|
|
|
374
375
|
// the global a precompiled script leaves its functions on, for the runtime to
|
|
375
376
|
// drain: (self.__jq79precompiled = self.__jq79precompiled || []).push([params,
|
package/src/transform.ts
CHANGED
|
@@ -401,7 +401,7 @@ export const transformSetupScript = (src: string): SetupTransform => {
|
|
|
401
401
|
// plain lexical module instead of a `with`-scoped setup script: no implicit
|
|
402
402
|
// reactivity, no `$:` labels - standard JS that editors and type-checkers
|
|
403
403
|
// understand. The default export is called with the instance context
|
|
404
|
-
// ({ $data, $effect, $emit, $mounted, $self, $$self }) and a returned object
|
|
404
|
+
// ({ $data, $effect, $emit, $mounted, $destroyed, $attached, $detached, $computed, $self, $$self }) and a returned object
|
|
405
405
|
// is merged into the reactive store for the template to use.
|
|
406
406
|
// Detection is backwards-safe: `export default` is a SyntaxError inside a
|
|
407
407
|
// setup script, so no previously-working component can change behavior.
|