@webjsdev/core 0.7.58 → 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.58",
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,5 +1,5 @@
1
1
  import { render as clientRender } from '../render-client.js';
2
- import { readAttributeValue, resolveAttributeProperty } from '../attribute-reader.js';
2
+ import { readAttributeValue, resolveAttributeProperty, observedAttributeNames } from '../attribute-reader.js';
3
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';
@@ -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() {