@webjsdev/core 0.7.57 → 0.7.59

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": "@webjsdev/core",
3
- "version": "0.7.57",
3
+ "version": "0.7.59",
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",
@@ -109,9 +109,8 @@ export function readAttributeValue(def, value) {
109
109
  //
110
110
  // The two readers now see the SAME attribute set, so this fallback is not
111
111
  // the only thing they agree on (#1341). Both resolve a name through
112
- // `resolveAttributeProperty` below, which matches the same
113
- // `d.attribute || hyphenate(k)` expression `observedAttributes` maps over
114
- // and nothing else, so it skips a `state: true` prop and ignores a name that
112
+ // `resolveAttributeProperty` below, which matches the same names
113
+ // `observedAttributes` lists (via `observedAttributeNames`) and nothing else, so it skips a `state: true` prop and ignores a name that
115
114
  // maps to no attribute-backed property exactly as the observed list does.
116
115
  // The SSR caller lowercases its source name first, because the browser
117
116
  // lowercases while parsing. And both are handed a value with every
@@ -148,8 +147,9 @@ function hyphenate(s) {
148
147
  * SSR read MORE than a browser can deliver, which is the bug this function
149
148
  * exists to remove, so do not add it.
150
149
  *
151
- * The match is EXACTLY `d.attribute || hyphenate(k)`, the same expression
152
- * `observedAttributes` maps over, and nothing else. A `props[attrName] ||
150
+ * The match is EXACTLY the names `observedAttributeNames` lists (the
151
+ * `d.attribute || hyphenate(k)` primary plus, for a camelCase prop with no
152
+ * custom attribute, its lowercased alias, #1540), and nothing else. A `props[attrName] ||
153
153
  * props[camelCase(attrName)]` fallback used to sit after the loop, so a prop
154
154
  * declaring a custom attribute also answered to its PROPERTY name. Both readers
155
155
  * carried that code, which read like an agreement and was not one: the browser
@@ -168,11 +168,59 @@ function hyphenate(s) {
168
168
  * which case the caller must IGNORE the attribute entirely (#1341).
169
169
  */
170
170
  export function resolveAttributeProperty(Cls, attrName) {
171
+ // Primary names first, so a prop literally named `foobar` keeps `foobar`
172
+ // even when a sibling `fooBar` would answer to it as an alias.
173
+ let aliasHit;
174
+ for (const { propName, def, primary, alias } of attributeBackedProps(Cls)) {
175
+ if (primary === attrName) return { propName, def };
176
+ if (aliasHit === undefined && alias === attrName) aliasHit = { propName, def };
177
+ }
178
+ return aliasHit;
179
+ }
180
+
181
+ /**
182
+ * Every attribute name the browser must observe for `Cls`, the list
183
+ * `observedAttributes` returns. Built from the same `attributeBackedProps` walk
184
+ * `resolveAttributeProperty` matches against, so a name is observed exactly
185
+ * when it resolves, on both sides (#1341, #1540).
186
+ *
187
+ * @param {any} Cls the component class
188
+ * @returns {string[]}
189
+ */
190
+ export function observedAttributeNames(Cls) {
191
+ const names = new Set();
192
+ const entries = attributeBackedProps(Cls);
193
+ for (const { primary } of entries) names.add(primary);
194
+ for (const { alias } of entries) if (alias) names.add(alias);
195
+ return [...names];
196
+ }
197
+
198
+ /**
199
+ * The attribute-backed props of `Cls` with the names each answers to.
200
+ *
201
+ * `primary` is the custom `attribute` option, matched VERBATIM, or else the
202
+ * kebab-cased property name. `alias` is the LOWERCASED property name, lit's
203
+ * default attribute name, present only when the prop has no custom `attribute`
204
+ * and the lowercased name differs from the kebab one. Without it,
205
+ * `<quiz-player quizId=${id}>` against `{ quizId: Number }` reached neither
206
+ * side: the parser lowercases the name to `quizid`, which matched nothing, so
207
+ * the prop silently stayed undefined on the server AND the client (#1540).
208
+ * The alias is only ever lowercase, which is the one form the platform can
209
+ * deliver, so this does not reopen the SSR-reads-more gap #1341 closed.
210
+ *
211
+ * @param {any} Cls
212
+ * @returns {{ propName: string, def: PropertyDeclaration, primary: string, alias: string | undefined }[]}
213
+ */
214
+ function attributeBackedProps(Cls) {
171
215
  const props = (Cls && Cls.properties) || {};
216
+ const out = [];
172
217
  for (const [k, decl] of Object.entries(props)) {
173
218
  const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
174
219
  if (d.state) continue;
175
- if ((d.attribute || hyphenate(k)) === attrName) return { propName: k, def: d };
220
+ const primary = d.attribute || hyphenate(k);
221
+ const lower = k.toLowerCase();
222
+ const alias = !d.attribute && lower !== primary ? lower : undefined;
223
+ out.push({ propName: k, def: d, primary, alias });
176
224
  }
177
- return undefined;
225
+ return out;
178
226
  }
@@ -1,6 +1,6 @@
1
1
  import { render as clientRender } from '../render-client.js';
2
- import { readAttributeValue, resolveAttributeProperty } from '../attribute-reader.js';
3
- import { setActiveActionSignal } from '../action-abort-client.js';
2
+ import { readAttributeValue, resolveAttributeProperty, observedAttributeNames } from '../attribute-reader.js';
3
+ import { setActiveActionSignal, activeActionSignal } from '../action-abort-client.js';
4
4
  import { carriesFunction } from '../form-action.js';
5
5
  import { isCSS, adoptStyles } from '../css.js';
6
6
  import { register, tagOf } from '../registry.js';
@@ -184,10 +184,12 @@ class WebComponentBase extends Base {
184
184
  * @returns {string[]}
185
185
  */
186
186
  static get observedAttributes() {
187
- const props = this.properties || {};
188
- return Object.keys(props)
189
- .filter((k) => !(typeof props[k] === 'object' && props[k].state))
190
- .map((k) => (typeof props[k] === 'object' && props[k].attribute) || hyphenate(k));
187
+ // Derived from the same walk `resolveAttributeProperty` matches against, so
188
+ // every observed name resolves and nothing else does. A camelCase prop with
189
+ // no custom `attribute` is observed under its kebab name AND its lowercased
190
+ // name (lit's default), since the parser turns `quizId=` into `quizid`
191
+ // (#1540).
192
+ return observedAttributeNames(this);
191
193
  }
192
194
 
193
195
  constructor() {
@@ -872,23 +874,18 @@ class WebComponentBase extends Base {
872
874
  // by-sync case (#469); a shouldUpdate=false cycle never reaches here,
873
875
  // so it does not invalidate an in-flight async render.
874
876
  this.__renderToken = (this.__renderToken || 0) + 1;
875
- // Abort the superseded render's in-flight action fetches (#492), start a
876
- // fresh controller for this render, and bind it as the active signal so
877
- // the RPC stub ties its fetches to this render. A superseded fetch's
878
- // AbortError is dropped by the render-token guard in _commitAsync.
877
+ // Abort the superseded render's in-flight action fetches (#492) and
878
+ // start a fresh controller for this render. update() binds its signal
879
+ // around render() alone (see there), never the DOM commit. A
880
+ // superseded fetch's AbortError is dropped by the render-token guard
881
+ // in _commitAsync.
879
882
  if (this.__renderAbort) this.__renderAbort.abort();
880
883
  this.__renderAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
881
- setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
882
884
  try {
883
885
  const r = this.update(changedProperties);
884
886
  if (r && typeof r.then === 'function') pendingCommit = r;
885
887
  } catch (error) {
886
888
  this._handleRenderError(/** @type {Error} */ (error));
887
- } finally {
888
- // The synchronous portion of render() has run; actions invoked there
889
- // already captured the signal. Clear it so a later event handler is
890
- // not bound to a stale render's controller.
891
- setActiveActionSignal(null);
892
889
  }
893
890
 
894
891
  if (!pendingCommit) {
@@ -1126,7 +1123,21 @@ class WebComponentBase extends Base {
1126
1123
  });
1127
1124
  }
1128
1125
  let tpl;
1129
- this.__signalWatcher.observe(() => { tpl = this.render(); });
1126
+ // Bind this render's abort signal (#492) around the synchronous part of
1127
+ // render() ONLY, so the RPC stub ties an action called there to this
1128
+ // render and the next render cancels it. The commit below runs unbound:
1129
+ // it connects the child elements this template creates, and an action a
1130
+ // child starts in its connectedCallback belongs to the child, not to this
1131
+ // render, so the parent's next render must not cancel it (#1514). The
1132
+ // previous binding is restored rather than cleared, so a render nested
1133
+ // inside another's synchronous work leaves the outer one intact.
1134
+ const outer = activeActionSignal();
1135
+ setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
1136
+ try {
1137
+ this.__signalWatcher.observe(() => { tpl = this.render(); });
1138
+ } finally {
1139
+ setActiveActionSignal(outer || null);
1140
+ }
1130
1141
  // Bare-await async render (#469): render() returned a promise. Use
1131
1142
  // stale-while-revalidate: keep the current DOM (the SSR first paint on
1132
1143
  // hydration, or the prior content on a re-fetch) visible until the new