@jxsuite/runtime 4.0.0 → 4.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "upgrade": "bun update --latest && bun install"
34
34
  },
35
35
  "dependencies": {
36
- "@jxsuite/schema": "^2.2.0",
37
- "@vue/reactivity": "3.5.42"
36
+ "@jxsuite/schema": "^2.3.0",
37
+ "@vue/reactivity": "3.5.43"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@happy-dom/global-registrator": "^20.14.5"
package/src/runtime.ts CHANGED
@@ -1915,27 +1915,30 @@ const SCOPE_TOKEN = "jx-scope";
1915
1915
  * wholesale. Sweeping on `isConnected` instead would be wrong rather than merely redundant: an
1916
1916
  * element is routinely styled before it is inserted.
1917
1917
  *
1918
+ * Both call sites reach this only once an element is already IN `scratchOwners`, and the only place
1919
+ * that happens (`scratchOwners.set`, below) always opens `scratch` first — so it is never still
1920
+ * null here.
1921
+ *
1918
1922
  * @param {SheetState} state
1919
1923
  */
1920
1924
  function rewriteScratch(state: SheetState) {
1921
- if (!state.scratch) {
1922
- return;
1923
- }
1924
- state.scratch.replaceAll([...state.scratchOwners.values()].flat());
1925
+ state.scratch!.replaceAll([...state.scratchOwners.values()].flat());
1925
1926
  }
1926
1927
 
1927
1928
  /**
1928
1929
  * Drop one reference to an interned rule set, removing its rules when the last one goes.
1929
1930
  *
1931
+ * `key` always names an entry `retainInterned` already created: {@link releaseElementStyles}'s two
1932
+ * call sites pass back exactly the `entry.interned`/`entry.hoisted` keys it was handed at
1933
+ * `retainInterned` time, and a stale `state` (one `resetDocumentStyles` already replaced) is turned
1934
+ * away before either call site is reached.
1935
+ *
1930
1936
  * @param {SheetState} state
1931
1937
  * @param {Map<string, InternEntry>} table
1932
1938
  * @param {string} key
1933
1939
  */
1934
1940
  function releaseInterned(state: SheetState, table: Map<string, InternEntry>, key: string) {
1935
- const entry = table.get(key);
1936
- if (!entry) {
1937
- return;
1938
- }
1941
+ const entry = table.get(key)!;
1939
1942
  entry.refs -= 1;
1940
1943
  if (entry.refs <= 0) {
1941
1944
  table.delete(key);
@@ -2467,6 +2470,11 @@ function renderMappedArrayInto(
2467
2470
  parentEl.append(anchor);
2468
2471
  const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef, key: keyRef } = arrayDef;
2469
2472
  const keyPointer = isRefObj(keyRef) ? keyRef.$ref : null;
2473
+ /* The instantiation chain the list belongs to (spec.md §16.9), for the same reason
2474
+ {@link renderSwitch} keeps one: a row that arrives with new data is inserted from an effect
2475
+ re-run, and the instance it holds must connect under the chain of the definition that drew
2476
+ the list. */
2477
+ const instancePath = _instancePath;
2470
2478
 
2471
2479
  let warned = false;
2472
2480
  const warnOnce = (message: string): void => {
@@ -2572,7 +2580,7 @@ function renderMappedArrayInto(
2572
2580
 
2573
2581
  pauseTracking();
2574
2582
  try {
2575
- reconcile(list, keys);
2583
+ underInstancePath(reexpand(instancePath), () => reconcile(list, keys));
2576
2584
  } finally {
2577
2585
  resetTracking();
2578
2586
  }
@@ -2687,6 +2695,11 @@ export function resolveTagName(tagName: unknown, scope: JxScope): string {
2687
2695
  function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions) {
2688
2696
  const path = options?._path ?? [];
2689
2697
  const container = document.createElement(resolveTagName(def.tagName, state));
2698
+ /* The instantiation chain this switch belongs to (spec.md §16.9), read now because the effect
2699
+ below re-runs with nothing on the stack. A case that flips after the first paint puts it back
2700
+ around the insertion, so an instance the new case creates connects under the chain of the
2701
+ definition that drew the switch rather than under none. See {@link _instancePath}. */
2702
+ const instancePath = _instancePath;
2690
2703
 
2691
2704
  if (options?.onNodeCreated) {
2692
2705
  options.onNodeCreated(container, path, def, state);
@@ -2753,7 +2766,9 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
2753
2766
  return;
2754
2767
  }
2755
2768
  container.replaceChildren();
2756
- scope.run(() => container.append(renderNode(doc, childScope, childOpts)));
2769
+ underInstancePath(reexpand(instancePath), () =>
2770
+ scope.run(() => container.append(renderNode(doc, childScope, childOpts))),
2771
+ );
2757
2772
  })
2758
2773
  .catch((error: unknown) =>
2759
2774
  console.error("Jx $switch: failed to load external case", caseDef.$ref, error),
@@ -2766,7 +2781,9 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
2766
2781
  // Change to anything it read. The bindings inside it track for themselves.
2767
2782
  pauseTracking();
2768
2783
  try {
2769
- scope.run(() => container.append(renderNode(caseDef, state, childOpts)));
2784
+ underInstancePath(reexpand(instancePath), () =>
2785
+ scope.run(() => container.append(renderNode(caseDef, state, childOpts))),
2786
+ );
2770
2787
  } finally {
2771
2788
  resetTracking();
2772
2789
  }
@@ -3117,6 +3134,9 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
3117
3134
  * Resolve a .class.json schema-defined class. Fetches the schema, follows $implementation if
3118
3135
  * hybrid, or constructs dynamically if self-contained.
3119
3136
  *
3137
+ * Its one caller, {@link resolveExternalPrototype}, already rejects a missing `$src` before this
3138
+ * runs, so `def.$src` is never empty here.
3139
+ *
3120
3140
  * @param {JxPrototypeDef} def
3121
3141
  * @param {JxScope} state
3122
3142
  * @param {string} key
@@ -3124,10 +3144,7 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
3124
3144
  * @returns {Promise<unknown>}
3125
3145
  */
3126
3146
  async function resolveClassJson(def: JxPrototypeDef, state: JxScope, key: string, base?: string) {
3127
- const src = def.$src;
3128
- if (!src) {
3129
- throw new Error(`Jx: class entry '${key}' has no $src`);
3130
- }
3147
+ const src = def.$src!;
3131
3148
  let classDef: JxClassDef;
3132
3149
 
3133
3150
  // Bare specifiers (package references like @scope/pkg/file) can't be fetched directly —
@@ -3943,6 +3960,219 @@ function injectHead(entries: JxHeadEntry[], _base: string) {
3943
3960
  }
3944
3961
  }
3945
3962
 
3963
+ // ─── Instantiation chain (spec.md §16.9) ─────────────────────────────────────
3964
+
3965
+ /**
3966
+ * How deep component nesting may go before an instance refuses to render.
3967
+ *
3968
+ * The same number the compiler holds in `MAX_COMPONENT_NESTING` (`@jxsuite/compiler`, `shared.ts`),
3969
+ * and for the same reason: the cycle check in {@link extendInstancePath} catches a definition that
3970
+ * renders ITSELF — same tag, same props — which is the only shape that can never terminate, while a
3971
+ * self-reference whose props change at every level is data-driven recursion that a `$switch` may or
3972
+ * may not bottom out. The seen-set cannot tell those apart, so this cap is what turns a forgotten
3973
+ * stop into a diagnostic instead of a page that never settles. It is written twice because the
3974
+ * compiler depends on the runtime and not the other way round, and the runtime's browser bundle
3975
+ * cannot reach into the build tool; spec.md §16.9 is the one statement both are held to, and the
3976
+ * compiler's `nesting-cap-agreement.test.ts` asserts its copy against this export, so the two
3977
+ * numbers cannot part without a red test in the workspace that owns the copy.
3978
+ */
3979
+ export const MAX_COMPONENT_NESTING = 32;
3980
+
3981
+ /** One instance on the chain: its tag, and the props it was given, as an identity. */
3982
+ interface InstanceFrame {
3983
+ tag: string;
3984
+ /** Every value the instance supplied that its definition's state read (§16.9). */
3985
+ props: Record<string, unknown>;
3986
+ }
3987
+
3988
+ /**
3989
+ * The chain an instance is created under, and where on it the current expansion began.
3990
+ *
3991
+ * `frames` is every instance from the page-level one down, outermost first. `expansion` is the
3992
+ * index of the first frame that belongs to the SAME synchronous pass as the instance being created:
3993
+ * the cycle check (§16.9 rule 1) looks back no further than that, while the cap (rule 2) counts
3994
+ * every frame. The distinction is what an effect re-run does to certainty. Along one pass a
3995
+ * repeated frame is a render that repeats itself, deterministically, forever. Across a re-run it is
3996
+ * not: the frames above the boundary rendered to completion once, and whether the page asks for one
3997
+ * of them again is its data — an `onMount` that flips a `$switch` into the same tag every time, or
3998
+ * a click that opens one more level of a node that starts closed — which the runtime cannot tell
3999
+ * apart. So the cycle check is not asked across the boundary, and the loop that lives in re-runs is
4000
+ * ended by the cap instead: thirty-three empty hosts and a diagnostic, not a page that never
4001
+ * settles.
4002
+ */
4003
+ interface InstancePath {
4004
+ frames: readonly InstanceFrame[];
4005
+ expansion: number;
4006
+ }
4007
+
4008
+ /** The chain a page-level render runs under: nothing above it. */
4009
+ const ROOT_INSTANCE_PATH: InstancePath = { expansion: 0, frames: [] };
4010
+
4011
+ /**
4012
+ * The chain of instances whose definitions are being rendered RIGHT NOW.
4013
+ *
4014
+ * A module-level stack rather than a WeakMap from element to chain, and it is worth saying why that
4015
+ * survives `connectedCallback` being async. The callback awaits `buildScope` before it touches the
4016
+ * DOM, and by the time it resumes, whatever the enclosing instance pushed here has long been popped
4017
+ * — so the stack alone would be empty for every instance and catch nothing. What saves it is WHEN
4018
+ * an instance connects: `this.append(child)` in the enclosing render runs the child's
4019
+ * `connectedCallback` synchronously, and every way a definition brings an instance into being —
4020
+ * `renderCustomElementWithProps`, the bare-tag branch of `renderNode`, a nested element inside
4021
+ * either, and a prerendered host drawing its children again as it upgrades — appends it inside that
4022
+ * same synchronous stretch. So the callback reads the stack ONCE, before its first `await`, and
4023
+ * carries the copy across the gap. A WeakMap would need a write at each of those creation sites to
4024
+ * say the same thing.
4025
+ *
4026
+ * A subtree an effect renders LATER — a `$switch` case that flips after the first paint, a `$map`
4027
+ * row that arrives with new data — connects with nothing on the stack, because no enclosing render
4028
+ * is running when the effect re-runs; a write from `onMount` or a click is what triggered it. So
4029
+ * the two renderers that create nodes from an effect each capture the stack when they are first
4030
+ * rendered (which is inside the enclosing pass) and put it back, re-expanded, around every later
4031
+ * insertion ({@link underInstancePath}). That is the one thing this stack needs beyond the
4032
+ * synchronous stretch, and it is what makes the `onMount`-flip shape a diagnostic rather than a
4033
+ * hang. Reading `@vue/reactivity`'s `getCurrentScope()` there instead would find nothing: an effect
4034
+ * re-run restores no scope, and a case or a row renders in a DETACHED scope with no parent.
4035
+ */
4036
+ let _instancePath: InstancePath = ROOT_INSTANCE_PATH;
4037
+
4038
+ /**
4039
+ * Run `fn` with `path` as the chain, so every instance that CONNECTS during it reads that chain.
4040
+ *
4041
+ * Restored rather than cleared afterwards: the enclosing render's chain is what was there, and a
4042
+ * restore needs no argument about whether anything was.
4043
+ *
4044
+ * @param {InstancePath} path - The chain the instances `fn` connects are created under
4045
+ * @param {() => T} fn - The render, or the insertion that connects what a render made
4046
+ * @returns {T} What `fn` returned
4047
+ */
4048
+ function underInstancePath<T>(path: InstancePath, fn: () => T): T {
4049
+ const was = _instancePath;
4050
+ _instancePath = path;
4051
+ try {
4052
+ return fn();
4053
+ } finally {
4054
+ _instancePath = was;
4055
+ }
4056
+ }
4057
+
4058
+ /**
4059
+ * The chain a later insertion runs under: the same frames, with the expansion beginning now.
4060
+ *
4061
+ * Called on EVERY run of a `$switch` case or a `$map` reconcile, the first one included — harmless
4062
+ * there, because a first run builds into a detached container and nothing connects until the
4063
+ * enclosing pass appends it, under the enclosing pass's own chain.
4064
+ *
4065
+ * @param {InstancePath} path - The chain captured when the renderer was first rendered
4066
+ * @returns {InstancePath}
4067
+ */
4068
+ function reexpand(path: InstancePath): InstancePath {
4069
+ return { expansion: path.frames.length, frames: path.frames };
4070
+ }
4071
+
4072
+ /**
4073
+ * Whether two prop values are the same input to a definition.
4074
+ *
4075
+ * Reference equality first — a `$ref` into the parent's state forwards the same primitive, the same
4076
+ * reactive proxy or the same function to the next level — and structural equality for plain data
4077
+ * rebuilt at each level (`data-jx-props` parsed twice is two objects with one meaning). Plain data
4078
+ * is what JSON can describe: an array, or an object with no prototype but `Object`'s, reactive or
4079
+ * raw. Anything else — a `Map`, a `Set`, a class instance, a circular object — is different from
4080
+ * everything but itself, because its JSON form would not say what it holds and two different values
4081
+ * that serialise alike would close a cycle that was never there. The cost of that policy is a loop
4082
+ * the depth cap catches instead; the alternative is a false diagnostic on a page that would have
4083
+ * ended. Inside plain data the comparison IS the JSON form, so a member JSON cannot see (a
4084
+ * function, a `Map` two levels down) is not seen here either; §16.9 says so.
4085
+ *
4086
+ * @param {unknown} a
4087
+ * @param {unknown} b
4088
+ * @returns {boolean}
4089
+ */
4090
+ function samePropValue(a: unknown, b: unknown): boolean {
4091
+ if (Object.is(a, b)) {
4092
+ return true;
4093
+ }
4094
+ if (!isPlainData(a) || !isPlainData(b)) {
4095
+ return false;
4096
+ }
4097
+ try {
4098
+ return JSON.stringify(a) === JSON.stringify(b);
4099
+ } catch {
4100
+ return false;
4101
+ }
4102
+ }
4103
+
4104
+ /**
4105
+ * Whether a value is an array or a plain object — the shapes whose JSON form is the whole value.
4106
+ *
4107
+ * @param {unknown} value
4108
+ * @returns {value is object}
4109
+ */
4110
+ function isPlainData(value: unknown): value is object {
4111
+ if (typeof value !== "object" || value === null) {
4112
+ return false;
4113
+ }
4114
+ const raw: unknown = toRaw(value);
4115
+ if (Array.isArray(raw)) {
4116
+ return true;
4117
+ }
4118
+ const proto: unknown = Object.getPrototypeOf(raw);
4119
+ return proto === Object.prototype || proto === null;
4120
+ }
4121
+
4122
+ /**
4123
+ * Whether two frames are the same instance: one tag, one set of prop names, equal values.
4124
+ *
4125
+ * @param {InstanceFrame} a
4126
+ * @param {InstanceFrame} b
4127
+ * @returns {boolean}
4128
+ */
4129
+ function sameFrame(a: InstanceFrame, b: InstanceFrame): boolean {
4130
+ if (a.tag !== b.tag) {
4131
+ return false;
4132
+ }
4133
+ const keys = Object.keys(a.props);
4134
+ if (keys.length !== Object.keys(b.props).length) {
4135
+ return false;
4136
+ }
4137
+ return keys.every(
4138
+ (key) => Object.hasOwn(b.props, key) && samePropValue(a.props[key], b.props[key]),
4139
+ );
4140
+ }
4141
+
4142
+ /**
4143
+ * The chain an instance's own render runs under: the enclosing chain plus itself — or the
4144
+ * diagnostic that says why it must not render at all.
4145
+ *
4146
+ * The two messages are the compiler's, word for word (`renderComponentInstance` in
4147
+ * `@jxsuite/compiler`), so a page that fails to build and a canvas that fails to paint say the same
4148
+ * thing about the same definition. Meeting a frame already on the chain, within the current
4149
+ * expansion, is the instance rendering itself with the same props, which never ends; a chain at the
4150
+ * cap is a self-reference whose props change — or whose repeats live in re-runs, where the cycle
4151
+ * check is not asked ({@link InstancePath}) — and whose stop case never came.
4152
+ *
4153
+ * @param {InstancePath} path - The chain the instance was created under
4154
+ * @param {InstanceFrame} frame - The instance about to render
4155
+ * @returns {InstancePath} The chain its children render under
4156
+ * @throws {Error} The named diagnostic — never a stack overflow, never a hang
4157
+ * @docs framework/concepts/components
4158
+ */
4159
+ function extendInstancePath(path: InstancePath, frame: InstanceFrame): InstancePath {
4160
+ const chain = [...path.frames.map((f) => f.tag), frame.tag].join(" → ");
4161
+ if (path.frames.slice(path.expansion).some((f) => sameFrame(f, frame))) {
4162
+ throw new Error(
4163
+ `Component <${frame.tag}> renders itself: ${chain}. A component cannot appear inside its own ` +
4164
+ `definition with the same props — the expansion would never end.`,
4165
+ );
4166
+ }
4167
+ if (path.frames.length >= MAX_COMPONENT_NESTING) {
4168
+ throw new Error(
4169
+ `Component nesting exceeds ${MAX_COMPONENT_NESTING} levels: ${chain}. A component that ` +
4170
+ `renders itself with changing props needs a case that stops.`,
4171
+ );
4172
+ }
4173
+ return { expansion: path.expansion, frames: [...path.frames, frame] };
4174
+ }
4175
+
3946
4176
  /**
3947
4177
  * Register a custom element from a Jx document.
3948
4178
  *
@@ -4009,6 +4239,17 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4009
4239
  }
4010
4240
  this._jxInitialized = true;
4011
4241
 
4242
+ /* The chain this instance was created under, read BEFORE the first `await`: the enclosing
4243
+ render is still on the stack now and will not be when this callback resumes. See
4244
+ {@link _instancePath} for why that ordering is the whole mechanism. */
4245
+ const enclosingPath = _instancePath;
4246
+ /* What this instance was given, recorded beside each write into state below. It is the
4247
+ instance's identity on the chain (§16.9): two nested instances of one tag that were given
4248
+ the same values render the same thing, forever. Only a value the definition's state READS
4249
+ is recorded — a `$props` key no state entry declares changes nothing about the render, so
4250
+ a definition that "varies" one at each level is still rendering itself. */
4251
+ const supplied: Record<string, unknown> = {};
4252
+
4012
4253
  // The definition as it stands NOW — a redefinition since this class was made is honoured.
4013
4254
  const live = _elementDefs.get(tagName) ?? { base: initialBase, doc: initialDef };
4014
4255
  const def = live.doc;
@@ -4026,6 +4267,9 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4026
4267
  for (const attr of observedAttrs) {
4027
4268
  if (this.hasAttribute(attr)) {
4028
4269
  absorbAttribute(state, attr, this.getAttribute(attr));
4270
+ // The raw string, not the coerced value: coercion is a function of it, so two equal
4271
+ // Strings are one input.
4272
+ supplied[attr] = this.getAttribute(attr);
4029
4273
  }
4030
4274
  }
4031
4275
 
@@ -4039,6 +4283,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4039
4283
  }
4040
4284
  if (key in (def.state ?? {})) {
4041
4285
  state[key] = val;
4286
+ supplied[key] = val;
4042
4287
  }
4043
4288
  }
4044
4289
  } catch {}
@@ -4060,6 +4305,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4060
4305
  }
4061
4306
  if (key in (def.state ?? {})) {
4062
4307
  state[key] = this.getAttribute(name);
4308
+ supplied[key] = this.getAttribute(name);
4063
4309
  this.removeAttribute(name);
4064
4310
  }
4065
4311
  }
@@ -4075,8 +4321,49 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4075
4321
  instanceSupplies(this, key)
4076
4322
  ) {
4077
4323
  state[key] = (this as Record<string, unknown>)[key];
4324
+ supplied[key] = (this as Record<string, unknown>)[key];
4078
4325
  }
4079
4326
  }
4327
+
4328
+ /* Every input is known now, so this is where an instance that would never finish is
4329
+ refused (§16.9): before it has a state, a style or a child, so a refused host is an empty
4330
+ element the page renders around — emptied of what the definition wrote into it for its
4331
+ slots too, since nothing will distribute that. The diagnostic is REPORTED rather than
4332
+ thrown onward: a throw from an async `connectedCallback` is an unhandled rejection that
4333
+ names no element and stops nothing. It goes two ways. The console is the runtime's
4334
+ surface for a render that fails after the page has settled (`$switch` external cases and
4335
+ the dev proxies report there too). The `jx-error` event is the surface a HOST can hold:
4336
+ it bubbles from the refused element with the error on it, so a page, or the studio canvas
4337
+ wrapping one, can collect it where a console line inside an iframe reaches nobody. The
4338
+ rest of the page keeps rendering, which is the point.
4339
+
4340
+ Neither surface is used for a host that is no longer in the document. A refused instance
4341
+ was connected when this callback began, and one that is not by the time the verdict is in
4342
+ belongs to a subtree the page discarded during the `await` above: a prerendered host of the
4343
+ same tag that was parsed INSIDE another and re-rendered from the definition by the outer
4344
+ one's upgrade, which captures it as slot content and empties itself. Its replacement is
4345
+ the instance that reports, on the chain the outer upgrade draws; reporting from the
4346
+ discarded copy too said one defect twice. The refusal itself still stands, so a host
4347
+ re-attached later is an empty element and not a hang. */
4348
+ let ownPath: InstancePath;
4349
+ try {
4350
+ ownPath = extendInstancePath(enclosingPath, { props: supplied, tag: tagName });
4351
+ } catch (error) {
4352
+ this.replaceChildren();
4353
+ if (!this.isConnected) {
4354
+ return;
4355
+ }
4356
+ console.error(`Jx: <${tagName}> was not rendered:`, error);
4357
+ this.dispatchEvent(
4358
+ new ErrorEvent("jx-error", {
4359
+ bubbles: true,
4360
+ composed: true,
4361
+ error,
4362
+ message: error instanceof Error ? error.message : String(error),
4363
+ }),
4364
+ );
4365
+ return;
4366
+ }
4080
4367
  /*
4081
4368
  * Set up property getters/setters that forward into reactive state. Private entries get NO
4082
4369
  * accessor: the property-first interface IS the props mechanism, so defining one would leave
@@ -4134,10 +4421,14 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
4134
4421
  themselves. Restored rather than cleared, because one definition may render another. */
4135
4422
  const wasInside = _canvasInsideStampedHost;
4136
4423
  _canvasInsideStampedHost ||= this.dataset.jxPath !== undefined;
4424
+ /* And the chain, for the same span: an instance this definition draws connects inside
4425
+ `this.append` below and reads it there ({@link underInstancePath}). */
4137
4426
  try {
4138
- for (const childDef of children) {
4139
- this.append(renderNode(childDef, state));
4140
- }
4427
+ underInstancePath(ownPath, () => {
4428
+ for (const childDef of children) {
4429
+ this.append(renderNode(childDef, state));
4430
+ }
4431
+ });
4141
4432
  } finally {
4142
4433
  _canvasInsideStampedHost = wasInside;
4143
4434
  }