@webjsdev/core 0.7.58 → 0.7.60

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.60",
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() {
@@ -22,6 +22,17 @@
22
22
  * // ...
23
23
  * }
24
24
  * MyWidget.register('my-lazy-widget');
25
+ *
26
+ * A component that another shipping module imports for registration
27
+ * (`import './my-lazy-widget.ts'`) stays lazy too (#1524): the server keeps
28
+ * that import for SSR, and the browser copy of the importer is served with
29
+ * the line rewritten to an `observeLazy({...})` call, so the module loads
30
+ * when an element with the tag is first visible: a hidden tab panel when it
31
+ * shows, a dialog's content when it opens, an element added later by a
32
+ * client render (the MutationObserver below catches it).
33
+ *
34
+ * The scan reads the light DOM (`document.querySelectorAll`), so a lazy tag
35
+ * rendered inside another component's shadow root is not found.
25
36
  */
26
37
 
27
38
  /** @type {Map<string, string>} tag → module URL */
@@ -30,6 +41,14 @@ const pending = new Map();
30
41
  /** @type {IntersectionObserver | null} */
31
42
  let observer = null;
32
43
 
44
+ /**
45
+ * ONE MutationObserver for the whole page, however often `observeLazy` runs
46
+ * (#1524): the boot script calls it, and so does every served module whose
47
+ * side-effect import of a `static lazy` component was deferred to it.
48
+ * @type {MutationObserver | null}
49
+ */
50
+ let mutations = null;
51
+
33
52
  /** @type {Map<string, Promise<unknown>>} in-flight module loads, keyed by URL */
34
53
  const inflight = new Map();
35
54
 
@@ -78,15 +97,18 @@ export function observeLazy(entries) {
78
97
  scan();
79
98
 
80
99
  // Also watch for dynamically added elements via MutationObserver.
81
- if (typeof MutationObserver !== 'undefined') {
82
- const mo = new MutationObserver(() => scan());
83
- mo.observe(document.body, { childList: true, subtree: true });
100
+ if (!mutations && typeof MutationObserver !== 'undefined') {
101
+ mutations = new MutationObserver(() => scan());
102
+ mutations.observe(document.body, { childList: true, subtree: true });
84
103
  }
85
104
  }
86
105
 
87
106
  function scan() {
88
107
  if (!observer || !pending.size) return;
89
108
  for (const tag of pending.keys()) {
109
+ // Already defined some other way (an eager import elsewhere): nothing
110
+ // left to load for this tag.
111
+ if (customElements.get(tag)) { pending.delete(tag); continue; }
90
112
  for (const el of document.querySelectorAll(tag)) {
91
113
  observer.observe(el);
92
114
  }