@webjsdev/core 0.7.64 → 0.7.65

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/index-browser.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export { html, isTemplate, MARKER } from './src/html.js';
27
27
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
28
28
  export { WebComponent, prop } from './src/component.js';
29
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
29
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
30
30
  export { render } from './src/render-client.js';
31
31
  export { escapeText, escapeAttr } from './src/escape.js';
32
32
  export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
package/index.d.ts CHANGED
@@ -80,7 +80,7 @@ export { html, isTemplate, MARKER } from './src/html.js';
80
80
  // docs/website can annotate a value as `TemplateResult` (#772).
81
81
  export type { TemplateResult } from './src/html.js';
82
82
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
83
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
83
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
84
84
  export { renderToString, renderToStream } from './src/render-server.js';
85
85
  export { render } from './src/render-client.js';
86
86
  export { escapeText, escapeAttr } from './src/escape.js';
@@ -152,7 +152,7 @@ export function optimistic<State>(
152
152
  ): OptimisticState<State, State>;
153
153
 
154
154
 
155
- // Legacy Imperative Signature (Signal-based rollback)
155
+ // Imperative signature (signal-based rollback)
156
156
  export function optimistic<T, R>(
157
157
  signal: { get(): T; set(v: T): void },
158
158
  value: T,
package/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  export { html, isTemplate, MARKER } from './src/html.js';
10
10
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
11
11
  export { WebComponent, prop } from './src/component.js';
12
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
12
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
13
13
  export { renderToString, renderToStream } from './src/render-server.js';
14
14
  export { render } from './src/render-client.js';
15
15
  export { escapeText, escapeAttr } from './src/escape.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.64",
3
+ "version": "0.7.65",
4
4
  "type": "module",
5
5
  "description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
6
6
  "types": "./index.d.ts",
@@ -212,6 +212,26 @@ export function observedAttributeNames(Cls) {
212
212
  * @returns {{ propName: string, def: PropertyDeclaration, primary: string, alias: string | undefined }[]}
213
213
  */
214
214
  function attributeBackedProps(Cls) {
215
+ // Memoized per class (#1646): the SSR walk resolves every source attribute
216
+ // of every instance through this list, and rebuilding it (one hyphenate per
217
+ // declared prop) was a measurable share of a component's render. A class's
218
+ // declarations never change after it is defined (a `static properties`
219
+ // block throws at runtime, and the factory fixes the set), so the first
220
+ // walk is the only one.
221
+ if (Cls && typeof Cls === 'function') {
222
+ const hit = BACKED_PROPS.get(Cls);
223
+ if (hit) return hit;
224
+ const built = buildAttributeBackedProps(Cls);
225
+ BACKED_PROPS.set(Cls, built);
226
+ return built;
227
+ }
228
+ return buildAttributeBackedProps(Cls);
229
+ }
230
+
231
+ /** @type {WeakMap<Function, { propName: string, def: any, primary: string, alias: string | undefined }[]>} */
232
+ const BACKED_PROPS = new WeakMap();
233
+
234
+ function buildAttributeBackedProps(Cls) {
215
235
  const props = (Cls && Cls.properties) || {};
216
236
  const out = [];
217
237
  for (const [k, decl] of Object.entries(props)) {
@@ -115,6 +115,56 @@ const FACTORY_PROPS = Symbol('webjs.factoryProps');
115
115
  // Per-class memo so the constructor-time enforcement walk runs once per class.
116
116
  const _propsChecked = new WeakSet();
117
117
 
118
+ /** A settled `updateComplete` shared by every instance until its first update. */
119
+ const RESOLVED_TRUE = Promise.resolve(true);
120
+ /** Iterated in place of a controller set no instance has created. */
121
+ const NO_CONTROLLERS = new Set();
122
+
123
+ /**
124
+ * Per class, the declared reactive properties with their accessors installed
125
+ * on the prototype.
126
+ * @type {WeakMap<Function, { propName: string, decl: any }[]>}
127
+ */
128
+ const _accessorPlans = new WeakMap();
129
+
130
+ /**
131
+ * Define the getter/setter pair for every declared property on the class's
132
+ * prototype, once, and record the plan the constructor applies per instance
133
+ * (own-value capture and `default`). The setter coerces nothing itself (the
134
+ * attribute readers do), runs `hasChanged`, reflects, and schedules an update.
135
+ * @param {any} Ctor
136
+ * @param {Record<string, unknown>} props
137
+ */
138
+ function installAccessors(Ctor, props) {
139
+ /** @type {{ propName: string, decl: any }[]} */
140
+ const plan = [];
141
+ for (const [propName, decl] of Object.entries(props)) {
142
+ const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
143
+ plan.push({ propName, decl: d });
144
+ const changedCheck = d.hasChanged || defaultHasChanged;
145
+ Object.defineProperty(Ctor.prototype, propName, {
146
+ configurable: true,
147
+ enumerable: true,
148
+ get() { return this.__propValues[propName]; },
149
+ set(newVal) {
150
+ const oldVal = this.__propValues[propName];
151
+ if (!changedCheck(newVal, oldVal)) return;
152
+ this.__propValues[propName] = newVal;
153
+ // Reflect to attribute if requested (and not internal state).
154
+ if (d.reflect && !d.state && this._connected) {
155
+ this._reflectAttribute(propName, newVal, d);
156
+ }
157
+ // requestUpdate records the (name, oldValue) entry AND schedules a
158
+ // render. During the update phase the scheduler short-circuits and
159
+ // the entry folds into the current cycle's changedProperties.
160
+ this.requestUpdate(propName, oldVal);
161
+ },
162
+ });
163
+ }
164
+ _accessorPlans.set(Ctor, plan);
165
+ return plan;
166
+ }
167
+
118
168
  class WebComponentBase extends Base {
119
169
  /** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
120
170
  static shadow = false;
@@ -199,10 +249,12 @@ class WebComponentBase extends Base {
199
249
  this._connected = false;
200
250
 
201
251
  /**
202
- * Set of attached reactive controllers.
203
- * @type {Set<ReactiveController>}
252
+ * Set of attached reactive controllers, created by the first
253
+ * `addController` (#1646: most instances never attach one, and the SSR
254
+ * walk constructs an instance per rendered element).
255
+ * @type {Set<ReactiveController> | null}
204
256
  */
205
- this.__controllers = new Set();
257
+ this.__controllers = null;
206
258
 
207
259
  /**
208
260
  * Whether the component has completed its first render.
@@ -235,7 +287,7 @@ class WebComponentBase extends Base {
235
287
  * @type {Promise<boolean>}
236
288
  * @private
237
289
  */
238
- this._updatePromise = Promise.resolve(true);
290
+ this._updatePromise = RESOLVED_TRUE;
239
291
 
240
292
  /**
241
293
  * Set while the component is inside the update phase (between
@@ -288,13 +340,14 @@ class WebComponentBase extends Base {
288
340
  }
289
341
 
290
342
  /**
291
- * For every key in `static properties`, create a getter/setter pair on
292
- * the instance that coerces values, runs `hasChanged`, schedules updates,
293
- * and optionally reflects to the HTML attribute.
343
+ * Give the instance its reactive-property backing store and make sure the
344
+ * class's getter/setter pairs (which run `hasChanged`, schedule updates and
345
+ * optionally reflect to the HTML attribute) are installed on its PROTOTYPE,
346
+ * once per class. Per instance this only lifts a value written before the
347
+ * element upgraded and applies the declared `default`s.
294
348
  *
295
- * This is called once from the constructor. The backing store is a plain
296
- * object (`this.__propValues`) so accessors don't collide with the
297
- * prototype.
349
+ * Called once from the constructor. The backing store is a plain object
350
+ * (`this.__propValues`), so the accessors never collide with it.
298
351
  * @private
299
352
  */
300
353
  _initializeProperties() {
@@ -305,35 +358,24 @@ class WebComponentBase extends Base {
305
358
  /** @type {Record<string, unknown>} */
306
359
  this.__propValues = {};
307
360
 
308
- for (const [propName, decl] of Object.entries(props)) {
309
- const d = typeof decl === 'object' ? decl : { type: decl };
310
- // Capture any value set before the accessor was installed (e.g. via
311
- // attribute or property assignment before `super()` returns).
312
- const initial = /** @type {any} */ (this)[propName];
313
-
314
- Object.defineProperty(this, propName, {
315
- configurable: true,
316
- enumerable: true,
317
- get: () => this.__propValues[propName],
318
- set: (newVal) => {
319
- const oldVal = this.__propValues[propName];
320
- const changed = (d.hasChanged || defaultHasChanged)(newVal, oldVal);
321
- if (!changed) return;
322
- this.__propValues[propName] = newVal;
323
-
324
- // Reflect to attribute if requested (and not internal state).
325
- if (d.reflect && !d.state && this._connected) {
326
- this._reflectAttribute(propName, newVal, d);
327
- }
328
-
329
- // requestUpdate records the (name, oldValue) entry AND schedules
330
- // a render. When called during the update phase (willUpdate /
331
- // hostUpdate / update / hostUpdated), the scheduler short-circuits
332
- // and the entry folds into the current cycle's changedProperties.
333
- this.requestUpdate(propName, oldVal);
334
- },
335
- });
336
-
361
+ // The accessors live on the PROTOTYPE, installed once per class (#1646).
362
+ // They used to be defined on every instance, two closures and a
363
+ // defineProperty per declared property, which was the largest single
364
+ // cost of constructing a component on the SSR path (one instance per
365
+ // rendered element) and a measurable one on upgrade in the browser.
366
+ let plan = _accessorPlans.get(Ctor);
367
+ if (plan === undefined) plan = installAccessors(Ctor, props);
368
+
369
+ for (let i = 0; i < plan.length; i++) {
370
+ const { propName, decl: d } = plan[i];
371
+ // Capture any value set before the accessor could apply (an own data
372
+ // property written on the element before it upgraded). It shadows the
373
+ // prototype accessor, so it is lifted into the backing store.
374
+ let initial;
375
+ if (Object.hasOwn(this, propName)) {
376
+ initial = /** @type {any} */ (this)[propName];
377
+ delete (/** @type {any} */ (this))[propName];
378
+ }
337
379
  if (initial !== undefined) {
338
380
  this.__propValues[propName] = initial;
339
381
  } else if (d.default !== undefined) {
@@ -596,7 +638,7 @@ class WebComponentBase extends Base {
596
638
  // b. SSR hydration (first mount, <!--webjs-hydrate--> marker
597
639
  // present). Children are already inside their
598
640
  // <slot data-webjs-light data-projection="actual"> elements
599
- // (injectDSD placed them). Adopt those assignments BEFORE
641
+ // (the SSR walk placed them). Adopt those assignments BEFORE
600
642
  // _performRender so we retain references to the SSR'd nodes;
601
643
  // the renderer's createInstance().replaceChildren() will
602
644
  // detach them, but the slot-apply step re-attaches the same
@@ -648,7 +690,7 @@ class WebComponentBase extends Base {
648
690
  }
649
691
 
650
692
  // Notify all controllers that the host is connected.
651
- for (const c of this.__controllers) {
693
+ for (const c of this.__controllers || NO_CONTROLLERS) {
652
694
  if (c.hostConnected) c.hostConnected();
653
695
  }
654
696
 
@@ -735,7 +777,7 @@ class WebComponentBase extends Base {
735
777
  this.__signalWatcher.dispose();
736
778
  this.__signalWatcher = undefined;
737
779
  }
738
- for (const c of this.__controllers) {
780
+ for (const c of this.__controllers || NO_CONTROLLERS) {
739
781
  if (c.hostDisconnected) c.hostDisconnected();
740
782
  }
741
783
  }
@@ -862,7 +904,7 @@ class WebComponentBase extends Base {
862
904
  this.willUpdate(changedProperties);
863
905
 
864
906
  // --- 4. controllers' hostUpdate ---
865
- for (const c of this.__controllers) {
907
+ for (const c of this.__controllers || NO_CONTROLLERS) {
866
908
  if (c.hostUpdate) c.hostUpdate();
867
909
  }
868
910
 
@@ -890,7 +932,7 @@ class WebComponentBase extends Base {
890
932
 
891
933
  if (!pendingCommit) {
892
934
  // --- 6. controllers' hostUpdated (sync commit) ---
893
- for (const c of this.__controllers) {
935
+ for (const c of this.__controllers || NO_CONTROLLERS) {
894
936
  if (c.hostUpdated) c.hostUpdated();
895
937
  }
896
938
  didCommit = true;
@@ -926,7 +968,7 @@ class WebComponentBase extends Base {
926
968
  // A newer render superseded this one; let the newer cycle finish.
927
969
  if (token !== this.__renderToken) return;
928
970
  // --- 6. controllers' hostUpdated (after the async commit) ---
929
- for (const c of this.__controllers) {
971
+ for (const c of this.__controllers || NO_CONTROLLERS) {
930
972
  if (c.hostUpdated) c.hostUpdated();
931
973
  }
932
974
  // --- 7-8 + updateComplete ---
@@ -1018,7 +1060,7 @@ class WebComponentBase extends Base {
1018
1060
  this._isUpdating = true;
1019
1061
  try {
1020
1062
  this.willUpdate(changedProperties);
1021
- for (const c of this.__controllers) {
1063
+ for (const c of this.__controllers || NO_CONTROLLERS) {
1022
1064
  if (c.hostUpdate) c.hostUpdate();
1023
1065
  }
1024
1066
  this._reflectDeclaredAttributes();
@@ -1374,7 +1416,7 @@ class WebComponentBase extends Base {
1374
1416
  * @param {ReactiveController} controller
1375
1417
  */
1376
1418
  addController(controller) {
1377
- this.__controllers.add(controller);
1419
+ (this.__controllers ??= new Set()).add(controller);
1378
1420
  if (this._connected && controller.hostConnected) {
1379
1421
  controller.hostConnected();
1380
1422
  }
@@ -1393,7 +1435,7 @@ class WebComponentBase extends Base {
1393
1435
  * @param {ReactiveController} controller
1394
1436
  */
1395
1437
  removeController(controller) {
1396
- this.__controllers.delete(controller);
1438
+ if (this.__controllers) this.__controllers.delete(controller);
1397
1439
  }
1398
1440
 
1399
1441
  /**
package/src/escape.js CHANGED
@@ -1,6 +1,14 @@
1
+ // Both escapers skip the three replaces when the string carries none of the
2
+ // characters they rewrite, which is the common case for a template value and
3
+ // was measurable on the SSR hot path (#1646).
4
+ const TEXT_SPECIAL = /[&<>]/;
5
+ const ATTR_SPECIAL = /[&"<]/;
6
+
1
7
  /** @param {string} s */
2
8
  export function escapeText(s) {
3
- return String(s)
9
+ const str = String(s);
10
+ if (!TEXT_SPECIAL.test(str)) return str;
11
+ return str
4
12
  .replace(/&/g, '&amp;')
5
13
  .replace(/</g, '&lt;')
6
14
  .replace(/>/g, '&gt;');
@@ -8,7 +16,9 @@ export function escapeText(s) {
8
16
 
9
17
  /** @param {string} s */
10
18
  export function escapeAttr(s) {
11
- return String(s)
19
+ const str = String(s);
20
+ if (!ATTR_SPECIAL.test(str)) return str;
21
+ return str
12
22
  .replace(/&/g, '&amp;')
13
23
  .replace(/"/g, '&quot;')
14
24
  .replace(/</g, '&lt;');
@@ -87,10 +87,10 @@ import { escapeAttr } from './escape.js';
87
87
  * it looks like from any one file and are easy to change one at a time. That
88
88
  * is why the predicates live here rather than at each site.
89
89
  *
90
- * The second SSR machine (`streamTemplate`) is reached only through
91
- * `renderToStream(v, { ssr: false })`, which no page render uses: the server
92
- * renders every page, Suspense included, via `renderToString`. It is kept in
93
- * lockstep for the public API surface rather than for a live page.
90
+ * Since #1646 there is ONE SSR interpreter (`render-server/walk.js`), shared
91
+ * by the buffered page render and `renderToStream(v, { ssr: false })`, so the
92
+ * two can no longer drift; the predicates still live here because the CLIENT
93
+ * renderer applies the same rules.
94
94
  */
95
95
 
96
96
  /**
@@ -967,9 +967,9 @@ function ensureIdentityField(form, id) {
967
967
  /**
968
968
  * SSR: judge one submitter's emitted START TAG, at its `>`.
969
969
  *
970
- * Called from BOTH state machines so the rules cannot drift between them, which
971
- * is the failure #1154 already had once (a guard shipped in `renderTemplate`
972
- * and not in `streamTemplate`). Judged at the `>` rather than at the hole
970
+ * Called from the one SSR interpreter (`render-server/walk.js`, #1646); there
971
+ * used to be two hand-synchronised state machines, which is the failure #1154
972
+ * had once (a guard shipped in one and not the other). Judged at the `>` rather than at the hole
973
973
  * because every input is an attribute the author may have written AFTER the
974
974
  * binding.
975
975
  *
package/src/optimistic.js CHANGED
@@ -44,7 +44,7 @@ class OptimisticState {
44
44
  }
45
45
  }
46
46
 
47
- async function runLegacyOptimistic(signal, value, action) {
47
+ async function runImperativeOptimistic(signal, value, action) {
48
48
  const prev = signal.get();
49
49
  signal.set(value);
50
50
  let result;
@@ -105,7 +105,7 @@ async function runLegacyOptimistic(signal, value, action) {
105
105
  * 4. The returned `release()` fn removes the update by ID and re-renders.
106
106
  * 5. Concurrent updates stack; each release removes only its own entry.
107
107
  *
108
- * Backward-compatible imperative API (signal-based rollback):
108
+ * Imperative form (signal-based rollback), for a one-shot toggle:
109
109
  * await optimistic(signal, value, () => likePost(postId));
110
110
  *
111
111
  * Client-only: it calls `host.requestUpdate()` (client work), so a component
@@ -119,7 +119,7 @@ async function runLegacyOptimistic(signal, value, action) {
119
119
  */
120
120
  export function optimistic(first, second, third) {
121
121
  if (first && typeof first.get === 'function' && typeof first.set === 'function') {
122
- return runLegacyOptimistic(first, second, third);
122
+ return runImperativeOptimistic(first, second, third);
123
123
  }
124
124
  return new OptimisticState(first, second);
125
125
  }
package/src/registry.d.ts CHANGED
@@ -15,3 +15,8 @@ export function lookupModuleUrl(tag: string): string | undefined;
15
15
  export function isLazy(tag: string): boolean;
16
16
  export function tagOf(cls: WebComponentInstanceConstructor): string | undefined;
17
17
  export function allTags(): string[];
18
+ /**
19
+ * The registry's tag-set version: bumped on a first-time registration, so a
20
+ * memo keyed on it (the SSR scanner's tag regex) is valid while it is unchanged.
21
+ */
22
+ export function registryVersion(): number;
package/src/registry.js CHANGED
@@ -31,7 +31,7 @@
31
31
  * user-app: ThemeToggle.register('theme-toggle') → instance-A registry
32
32
  * server: lookup('theme-toggle') → instance-B registry (empty)
33
33
  *
34
- * SSR's `injectDSD` would then skip the tag and emit bare
34
+ * The SSR walk's `lookup(tag)` would then find no class and emit bare
35
35
  * `<theme-toggle></theme-toggle>` even though the user registered it
36
36
  * correctly. Symptom: components render with no children server-side
37
37
  * and only "appear" after the browser hydrates.
@@ -50,6 +50,21 @@ const registry = _g[REGISTRY_KEY] || (_g[REGISTRY_KEY] = new Map());
50
50
  /** @type {WeakMap<Function, string>} */
51
51
  const classToTag = _g[CLASS_TO_TAG_KEY] || (_g[CLASS_TO_TAG_KEY] = new WeakMap());
52
52
 
53
+ /**
54
+ * The registry's tag-set version (#1644): bumped whenever a NEW key enters
55
+ * the registry, by a class registration OR by `primeModuleUrl` (the prod
56
+ * component scan creates the entry before the class arrives), so the SSR
57
+ * scanner can memoize the regex it builds from every registered tag and
58
+ * rebuild it only when the set changed. Shared on
59
+ * `globalThis` like the registry itself, because under `bun --hot` the app
60
+ * imports a fresh copy of this module while the first run's server keeps
61
+ * rendering through the old one; a copy-local counter would leave that server
62
+ * blind to a tag the new copy registered.
63
+ * @type {{ v: number }}
64
+ */
65
+ const REGISTRY_VERSION_KEY = Symbol.for('webjs:registryVersion');
66
+ const version = _g[REGISTRY_VERSION_KEY] || (_g[REGISTRY_VERSION_KEY] = { v: 0 });
67
+
53
68
  const isBrowser =
54
69
  typeof window !== 'undefined' && typeof customElements !== 'undefined';
55
70
 
@@ -76,6 +91,8 @@ function registerInternal(tag, cls) {
76
91
  entry.lazy = lazy;
77
92
  } else {
78
93
  registry.set(tag, { cls, moduleUrl: null, lazy, tag });
94
+ // A new tag changes the set the SSR scanner matches on (#1644).
95
+ version.v += 1;
79
96
  }
80
97
  classToTag.set(cls, tag);
81
98
  }
@@ -126,9 +143,10 @@ if (!isBrowser) {
126
143
  * ------------------------------------------------------------------ */
127
144
 
128
145
  /**
129
- * @deprecated Low-level internal: prefer `customElements.define(tag, cls)`.
130
- * Kept as a minimal wrapper for back-compat with any framework code that
131
- * calls it directly.
146
+ * Register a component class under a tag. The primitive behind
147
+ * `Class.register('tag')` (the app-facing idiom): `customElements.define` in
148
+ * the browser, the SSR registry on the server. Reachable as
149
+ * `@webjsdev/core/registry` for framework tooling; apps use the static method.
132
150
  *
133
151
  * @param {string} tag
134
152
  * @param {typeof import('./component.js').WebComponent} cls
@@ -163,6 +181,10 @@ export function primeModuleUrl(tag, moduleUrl) {
163
181
  lazy: false,
164
182
  tag,
165
183
  });
184
+ // A primed tag is a new KEY, and `allTags()` lists keys: the SSR scanner's
185
+ // pattern must include it before its class arrives, or a pattern memoized
186
+ // while the registry was empty would stay empty for the process (#1644).
187
+ version.v += 1;
166
188
  }
167
189
 
168
190
  /** @param {string} tag */
@@ -188,3 +210,12 @@ export function tagOf(cls) {
188
210
  export function allTags() {
189
211
  return [...registry.keys()];
190
212
  }
213
+
214
+ /**
215
+ * The current tag-set version: moves only when a tag is registered for the
216
+ * first time. A memo keyed on it is valid for as long as it is unchanged.
217
+ * @returns {number}
218
+ */
219
+ export function registryVersion() {
220
+ return version.v;
221
+ }
@@ -453,8 +453,8 @@ export function applyElement(part, value) {
453
453
  }
454
454
  }
455
455
  partAny.__refElement = part.el;
456
- // Keep the legacy `lastTarget` field in sync for clearInstance /
457
- // disposeInstance which read it for template-disposal cleanup.
456
+ // Keep `lastTarget` in sync for clearInstance / disposeInstance, which
457
+ // read it for template-disposal cleanup.
458
458
  part.lastTarget = nextTarget;
459
459
  }
460
460
  }
@@ -42,8 +42,8 @@ export function parseAttrs(attrStr) {
42
42
  */
43
43
  export function seedServerAttrs(instance, attrs) {
44
44
  if (!instance || typeof instance.setAttribute !== 'function') return;
45
- for (const [name, raw] of Object.entries(attrs)) {
46
- instance.setAttribute(name, decodeAttrEntities(raw));
45
+ for (const name in attrs) {
46
+ instance.setAttribute(name, decodeAttrEntities(attrs[name]));
47
47
  }
48
48
  }
49
49
 
@@ -55,7 +55,7 @@ export function seedServerAttrs(instance, attrs) {
55
55
  * @returns {string}
56
56
  */
57
57
  export function withHostMarker(opening) {
58
- if (/\sdata-wj-host(?=[\s>=])/i.test(opening)) return opening;
58
+ if (opening.includes('data-wj-host') && /\sdata-wj-host(?=[\s>=])/i.test(opening)) return opening;
59
59
  return `${opening.slice(0, -1)} data-wj-host>`;
60
60
  }
61
61
 
@@ -94,7 +94,8 @@ export function appendReflectedAttrs(opening, instance, presentAttrNames) {
94
94
  * based on its static `properties` declaration.
95
95
  */
96
96
  export function applyAttrsToInstance(instance, attrs, Cls) {
97
- for (const [sourceName, sourceValue] of Object.entries(attrs)) {
97
+ for (const sourceName in attrs) {
98
+ const sourceValue = attrs[sourceName];
98
99
  // The browser LOWERCASES every attribute name while parsing an HTML
99
100
  // document, so `cfgData="x"` reaches the client reader as `cfgdata` and a
100
101
  // camelCase name can never match anything in `observedAttributes`.