@webjsdev/core 0.7.5 → 0.7.6
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/dist/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +2 -2
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/component.js +205 -2
- package/src/render-server.js +83 -4
- package/src/task.js +13 -0
package/package.json
CHANGED
package/src/component.js
CHANGED
|
@@ -147,8 +147,143 @@ function defaultHasChanged(a, b) {
|
|
|
147
147
|
* ```
|
|
148
148
|
*/
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Inert `ElementInternals`-shaped object returned by the server shim's
|
|
152
|
+
* `attachInternals()`. Modeled on `@lit-labs/ssr-dom-shim`'s
|
|
153
|
+
* `ElementInternalsShim` (lit repo, `packages/labs/ssr-dom-shim/src/lib/
|
|
154
|
+
* element-internals.ts`): form-association, validity, and custom-state
|
|
155
|
+
* calls are no-ops at SSR (no form, no constraint validation, no `:state()`
|
|
156
|
+
* matching server-side), so a component that calls `this.attachInternals()`
|
|
157
|
+
* in its constructor renders instead of crashing. The browser runs the real
|
|
158
|
+
* `attachInternals()` on hydration.
|
|
159
|
+
*
|
|
160
|
+
* Deliberate deviation from lit: lit's `checkValidity` / `reportValidity`
|
|
161
|
+
* THROW on the server. webjs returns `true` instead, to keep SSR
|
|
162
|
+
* progressive-enhancement-safe (a stray validity call in a constructor must
|
|
163
|
+
* not 500 the page); the browser does the real validation.
|
|
164
|
+
* @returns {any}
|
|
165
|
+
*/
|
|
166
|
+
function makeServerInternals() {
|
|
167
|
+
return {
|
|
168
|
+
states: new Set(),
|
|
169
|
+
shadowRoot: null,
|
|
170
|
+
form: null,
|
|
171
|
+
labels: [],
|
|
172
|
+
role: '',
|
|
173
|
+
willValidate: true,
|
|
174
|
+
validity: /** @type {any} */ ({}),
|
|
175
|
+
validationMessage: '',
|
|
176
|
+
setFormValue() {},
|
|
177
|
+
setValidity() {},
|
|
178
|
+
checkValidity() { return true; },
|
|
179
|
+
reportValidity() { return true; },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Server-side stand-in for `HTMLElement`. The SSR pipeline constructs
|
|
185
|
+
* component instances in Node, where `HTMLElement` does not exist, so the
|
|
186
|
+
* base class is this shim. It is modeled on `@lit-labs/ssr-dom-shim`'s
|
|
187
|
+
* `ElementShim` (lit repo, `packages/labs/ssr-dom-shim/src/index.ts`): the
|
|
188
|
+
* attribute methods (`getAttribute` / `setAttribute` / `hasAttribute` /
|
|
189
|
+
* `removeAttribute` / `toggleAttribute`, the `attributes` getter) are backed
|
|
190
|
+
* by a Map, so lit muscle-memory patterns that read attributes in `render()`
|
|
191
|
+
* or set them while deriving state work server-side; the SSR walker seeds
|
|
192
|
+
* the Map from the element's source attributes and reads it back to surface
|
|
193
|
+
* reflected/added attributes in the output. Event methods are no-ops (no
|
|
194
|
+
* server event loop), and `attachInternals()` returns the inert object
|
|
195
|
+
* above. The genuinely browser-only surface (`querySelector`, layout reads,
|
|
196
|
+
* `attachShadow`, `focus`) is deliberately absent and still throws at SSR,
|
|
197
|
+
* which the `no-browser-globals-in-render` rule and the SSR crash hint flag.
|
|
198
|
+
*
|
|
199
|
+
* Deliberate deviation from lit: this shim lowercases attribute names so
|
|
200
|
+
* `getAttribute('Foo')` after `setAttribute('foo', x)` resolves, matching how
|
|
201
|
+
* a real browser treats HTML attribute names as case-insensitive. lit's shim
|
|
202
|
+
* keys the Map by the raw name (a known fidelity gap in lit-labs).
|
|
203
|
+
*/
|
|
204
|
+
class ServerElement {
|
|
205
|
+
constructor() {
|
|
206
|
+
/**
|
|
207
|
+
* Backing store for the attribute methods. Keys are lowercased
|
|
208
|
+
* attribute names (HTML attributes are case-insensitive). Seeded by the
|
|
209
|
+
* SSR walker from the element's source attributes.
|
|
210
|
+
* @type {Map<string, string>}
|
|
211
|
+
*/
|
|
212
|
+
this.__ssrAttrs = new Map();
|
|
213
|
+
/** @type {any} */
|
|
214
|
+
this.__internals = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Mirrors `Element.attributes`: an array of `{ name, value }`. */
|
|
218
|
+
get attributes() {
|
|
219
|
+
return [...this.__ssrAttrs].map(([name, value]) => ({ name, value }));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @param {string} name */
|
|
223
|
+
getAttribute(name) {
|
|
224
|
+
const v = this.__ssrAttrs.get(String(name).toLowerCase());
|
|
225
|
+
return v === undefined ? null : v;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @param {string} name @param {unknown} value */
|
|
229
|
+
setAttribute(name, value) {
|
|
230
|
+
// Emulate the browser casting all values to string (lit does the same).
|
|
231
|
+
this.__ssrAttrs.set(String(name).toLowerCase(), String(value));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @param {string} name */
|
|
235
|
+
removeAttribute(name) {
|
|
236
|
+
this.__ssrAttrs.delete(String(name).toLowerCase());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @param {string} name */
|
|
240
|
+
hasAttribute(name) {
|
|
241
|
+
return this.__ssrAttrs.has(String(name).toLowerCase());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** @param {string} name @param {boolean} [force] */
|
|
245
|
+
toggleAttribute(name, force) {
|
|
246
|
+
// Steps mirror https://dom.spec.whatwg.org/#dom-element-toggleattribute
|
|
247
|
+
const key = String(name).toLowerCase();
|
|
248
|
+
const present = this.__ssrAttrs.has(key);
|
|
249
|
+
const next = force === undefined ? !present : force;
|
|
250
|
+
if (next) {
|
|
251
|
+
this.__ssrAttrs.set(key, '');
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
this.__ssrAttrs.delete(key);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @returns {string[]} */
|
|
259
|
+
getAttributeNames() {
|
|
260
|
+
return [...this.__ssrAttrs.keys()];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// No server event loop: listeners never fire at SSR. The no-op keeps a
|
|
264
|
+
// constructor that wires delegated listeners (a common lit pattern) from
|
|
265
|
+
// crashing; the browser re-runs the constructor on hydration where the
|
|
266
|
+
// real HTMLElement methods apply.
|
|
267
|
+
addEventListener() {}
|
|
268
|
+
removeEventListener() {}
|
|
269
|
+
dispatchEvent() { return true; }
|
|
270
|
+
|
|
271
|
+
/** @returns {any} */
|
|
272
|
+
attachInternals() {
|
|
273
|
+
// Match the browser (and lit's shim): a second attach is an error.
|
|
274
|
+
if (this.__internals !== null) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"Failed to execute 'attachInternals' on 'HTMLElement': " +
|
|
277
|
+
'ElementInternals for the specified element was already attached.',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
this.__internals = makeServerInternals();
|
|
281
|
+
return this.__internals;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Base class choice: real HTMLElement on the browser, the shim on the server.
|
|
286
|
+
const Base = isBrowser ? HTMLElement : /** @type {any} */ (ServerElement);
|
|
152
287
|
|
|
153
288
|
export class WebComponent extends Base {
|
|
154
289
|
/** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
|
|
@@ -467,6 +602,13 @@ export class WebComponent extends Base {
|
|
|
467
602
|
|
|
468
603
|
_activate() {
|
|
469
604
|
this._connected = true;
|
|
605
|
+
// Reflect declared reflect:true properties from their current value now
|
|
606
|
+
// that the element is connected. Constructor / willUpdate defaults were
|
|
607
|
+
// set while disconnected (the setter skips reflection then), so this is
|
|
608
|
+
// what makes a freshly-created client element carry the same reflected
|
|
609
|
+
// attributes the SSR walker emitted. Same-value reflects are no-ops, so a
|
|
610
|
+
// hydrated element (whose attribute already arrived from SSR) is unchanged.
|
|
611
|
+
this._reflectDeclaredAttributes();
|
|
470
612
|
const Ctor = /** @type any */ (this.constructor);
|
|
471
613
|
if (Ctor.shadow === true) {
|
|
472
614
|
const hadSSRShadow = !!this.shadowRoot;
|
|
@@ -815,6 +957,67 @@ export class WebComponent extends Base {
|
|
|
815
957
|
}
|
|
816
958
|
}
|
|
817
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Run the pre-render half of the update cycle synchronously for SSR.
|
|
962
|
+
*
|
|
963
|
+
* The SSR walker constructs the instance, applies attributes/props, then
|
|
964
|
+
* calls this BEFORE `render()`. It mirrors how lit drives the update
|
|
965
|
+
* lifecycle server-side: `willUpdate` runs so derived state computed there
|
|
966
|
+
* is correct in the SSR'd HTML, then reactive controllers' `hostUpdate`
|
|
967
|
+
* runs, then reflect:true properties are written to attributes so they
|
|
968
|
+
* appear in the serialized output. It deliberately does NOT run
|
|
969
|
+
* `shouldUpdate` (SSR always produces the first paint, there is no prior
|
|
970
|
+
* render to skip), `update` (the walker commits by calling `render`
|
|
971
|
+
* itself), or the post-commit hooks `firstUpdated` / `updated` (browser-only
|
|
972
|
+
* DOM work).
|
|
973
|
+
*
|
|
974
|
+
* `_isUpdating` is set so a property assignment inside `willUpdate` folds
|
|
975
|
+
* into the current `changedProperties` snapshot instead of scheduling a
|
|
976
|
+
* fresh cycle (the SSR scheduler short-circuits anyway, since the element
|
|
977
|
+
* is not connected). Hook throws propagate to the walker's per-component
|
|
978
|
+
* try/catch, which logs an actionable hint and skips DSD for that element,
|
|
979
|
+
* the same as a throwing `render()`.
|
|
980
|
+
*/
|
|
981
|
+
performServerUpdate() {
|
|
982
|
+
const changedProperties = this._changedProperties;
|
|
983
|
+
this._isUpdating = true;
|
|
984
|
+
try {
|
|
985
|
+
this.willUpdate(changedProperties);
|
|
986
|
+
for (const c of this.__controllers) {
|
|
987
|
+
if (c.hostUpdate) c.hostUpdate();
|
|
988
|
+
}
|
|
989
|
+
this._reflectDeclaredAttributes();
|
|
990
|
+
} finally {
|
|
991
|
+
this._isUpdating = false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Reflect every reflect:true (non-state) property to its attribute from its
|
|
997
|
+
* CURRENT value, regardless of whether the value arrived via the setter.
|
|
998
|
+
*
|
|
999
|
+
* The property setter only reflects on a connected change, so a value set
|
|
1000
|
+
* in the constructor (a default) or in `willUpdate` is not reflected by the
|
|
1001
|
+
* setter alone. This syncs those: the SSR walker calls it in
|
|
1002
|
+
* `performServerUpdate` (writing into the server attribute shim, which the
|
|
1003
|
+
* walker reads back into the rendered HTML), and `_activate` calls it on
|
|
1004
|
+
* the client's first connected render. Running it on both sides is what
|
|
1005
|
+
* keeps a server-rendered element and a freshly-created client element
|
|
1006
|
+
* agree on their reflected attributes (matching lit, which reflects during
|
|
1007
|
+
* the first update). `_reflectAttribute`'s re-entrancy guard makes a
|
|
1008
|
+
* same-value reflect a no-op, so re-reflecting an attribute that already
|
|
1009
|
+
* arrived from SSR does not churn.
|
|
1010
|
+
* @private
|
|
1011
|
+
*/
|
|
1012
|
+
_reflectDeclaredAttributes() {
|
|
1013
|
+
const props = /** @type {any} */ (this.constructor).properties || {};
|
|
1014
|
+
for (const name of Object.keys(props)) {
|
|
1015
|
+
const decl = props[name];
|
|
1016
|
+
if (!decl || !decl.reflect || decl.state) continue;
|
|
1017
|
+
this._reflectAttribute(name, this[name], decl);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
818
1021
|
// ---------------------------------------------------------------------------
|
|
819
1022
|
// Lifecycle hooks: override in subclasses
|
|
820
1023
|
// ---------------------------------------------------------------------------
|
package/src/render-server.js
CHANGED
|
@@ -322,9 +322,13 @@ const SSR_BROWSER_GLOBALS = new Set([
|
|
|
322
322
|
'matchMedia', 'requestAnimationFrame', 'getComputedStyle',
|
|
323
323
|
'IntersectionObserver', 'MutationObserver', 'ResizeObserver',
|
|
324
324
|
]);
|
|
325
|
+
// Attribute methods (get/set/has/remove/toggleAttribute), the event methods
|
|
326
|
+
// (add/removeEventListener, dispatchEvent), and attachInternals are backed by
|
|
327
|
+
// the server-side element shim and work at SSR, so they are NOT listed here.
|
|
328
|
+
// What remains is the genuinely browser-only HTMLElement surface that still
|
|
329
|
+
// has no server stand-in and throws at SSR.
|
|
325
330
|
const SSR_HTMLELEMENT_METHODS = new Set([
|
|
326
|
-
'attachShadow', '
|
|
327
|
-
'hasAttribute', 'dispatchEvent', 'querySelector', 'querySelectorAll',
|
|
331
|
+
'attachShadow', 'querySelector', 'querySelectorAll',
|
|
328
332
|
'getBoundingClientRect', 'focus', 'blur', 'scrollIntoView',
|
|
329
333
|
]);
|
|
330
334
|
|
|
@@ -383,7 +387,7 @@ async function injectDSD(html, ctx) {
|
|
|
383
387
|
// Track which custom elements actually appeared: used by SSR to emit
|
|
384
388
|
// `<link rel="modulepreload">` hints for their module URLs.
|
|
385
389
|
if (ctx && ctx.usedComponents) ctx.usedComponents.add(tag);
|
|
386
|
-
|
|
390
|
+
let opening = selfClose ? `<${tag}${attrs}>` : match;
|
|
387
391
|
try {
|
|
388
392
|
const isShadow = /** @type any */ (Cls).shadow === true;
|
|
389
393
|
const instance = new /** @type any */ (Cls)();
|
|
@@ -394,8 +398,29 @@ async function injectDSD(html, ctx) {
|
|
|
394
398
|
// properties` type. Property bindings take priority on a name
|
|
395
399
|
// collision because they preserve the original JS reference.
|
|
396
400
|
const propValues = consumePropAttrs(attrMap);
|
|
401
|
+
// Names already present in the source opening tag (including the
|
|
402
|
+
// data-webjs-prop-* bindings, which were stripped from attrMap above
|
|
403
|
+
// but remain in the emitted `attrs` string). Reflected/added
|
|
404
|
+
// attributes are appended only when their name is NOT already here, so
|
|
405
|
+
// existing output stays byte-identical when nothing reflects.
|
|
406
|
+
const presentAttrNames = new Set(Object.keys(parseAttrs(attrs)).map((n) => n.toLowerCase()));
|
|
407
|
+
// Seed the server attribute shim so `this.getAttribute(...)` /
|
|
408
|
+
// `this.hasAttribute(...)` in willUpdate / render read the source
|
|
409
|
+
// attributes (a lit muscle-memory pattern) instead of reading empty.
|
|
410
|
+
seedServerAttrs(instance, attrMap);
|
|
397
411
|
applyAttrsToInstance(instance, attrMap, Cls);
|
|
398
412
|
for (const [k, v] of Object.entries(propValues)) instance[k] = v;
|
|
413
|
+
// Run the pre-render lifecycle (willUpdate, controllers' hostUpdate,
|
|
414
|
+
// then reflect reflect:true props) so derived state computed there is
|
|
415
|
+
// correct in the SSR'd HTML, matching how lit runs the update cycle at
|
|
416
|
+
// SSR. WebComponent instances expose performServerUpdate; bare
|
|
417
|
+
// Base-extending kit components (no lifecycle) do not, so it is guarded.
|
|
418
|
+
if (typeof instance.performServerUpdate === 'function') instance.performServerUpdate();
|
|
419
|
+
// Surface attributes the component set before render (reflected
|
|
420
|
+
// reflect:true props, or an explicit this.setAttribute in the
|
|
421
|
+
// constructor / willUpdate) that were not already in the source tag.
|
|
422
|
+
// Appending keeps the original tag byte-identical when nothing changed.
|
|
423
|
+
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
399
424
|
let tpl = instance.render ? instance.render() : '';
|
|
400
425
|
if (tpl && typeof tpl.then === 'function') tpl = await tpl;
|
|
401
426
|
// Render the template to HTML. injectDSD recurses on the result so
|
|
@@ -764,6 +789,55 @@ function parseAttrs(attrStr) {
|
|
|
764
789
|
return out;
|
|
765
790
|
}
|
|
766
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Seed the element's attributes from the source opening tag so reads like
|
|
794
|
+
* `this.getAttribute(name)` / `this.hasAttribute(name)` inside willUpdate /
|
|
795
|
+
* render return the real value during SSR. Goes through `setAttribute`, which
|
|
796
|
+
* both the server element shim (Node SSR) and a real `HTMLElement`
|
|
797
|
+
* (renderToString called in a browser, e.g. tests) implement, so the path
|
|
798
|
+
* does not depend on the shim's internal store. A bare Base-extending kit
|
|
799
|
+
* component without `setAttribute` is skipped.
|
|
800
|
+
*
|
|
801
|
+
* @param {any} instance
|
|
802
|
+
* @param {Record<string,string>} attrs parsed source attributes (data-webjs-prop-* already removed)
|
|
803
|
+
*/
|
|
804
|
+
function seedServerAttrs(instance, attrs) {
|
|
805
|
+
if (!instance || typeof instance.setAttribute !== 'function') return;
|
|
806
|
+
for (const [name, raw] of Object.entries(attrs)) {
|
|
807
|
+
instance.setAttribute(name, unescapeAttr(raw));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Append attributes the component set before render (reflected reflect:true
|
|
813
|
+
* properties, or an explicit `this.setAttribute` in the constructor /
|
|
814
|
+
* willUpdate) to the element's opening tag, skipping any name already present
|
|
815
|
+
* in the source tag. Reads via the standard `getAttributeNames` /
|
|
816
|
+
* `getAttribute` API so it works whether the instance is the server shim or a
|
|
817
|
+
* real `HTMLElement`. Returns the opening tag unchanged when there is nothing
|
|
818
|
+
* to add, so existing SSR output stays byte-identical when no component
|
|
819
|
+
* reflects, which preserves the elision on-vs-off differential invariant.
|
|
820
|
+
*
|
|
821
|
+
* @param {string} opening the element's opening tag, ending in `>`
|
|
822
|
+
* @param {any} instance
|
|
823
|
+
* @param {Set<string>} presentAttrNames lowercased names already in the source tag
|
|
824
|
+
* @returns {string}
|
|
825
|
+
*/
|
|
826
|
+
function appendReflectedAttrs(opening, instance, presentAttrNames) {
|
|
827
|
+
if (!instance || typeof instance.getAttributeNames !== 'function') return opening;
|
|
828
|
+
let extra = '';
|
|
829
|
+
for (const rawName of instance.getAttributeNames()) {
|
|
830
|
+
const name = String(rawName).toLowerCase();
|
|
831
|
+
if (presentAttrNames.has(name)) continue;
|
|
832
|
+
const value = instance.getAttribute(rawName);
|
|
833
|
+
extra += value === '' ? ` ${name}` : ` ${name}="${escapeAttr(String(value))}"`;
|
|
834
|
+
}
|
|
835
|
+
if (!extra) return opening;
|
|
836
|
+
// Insert before the closing `>` (the opening tag is normalised to end in
|
|
837
|
+
// `>`; a self-closing source tag was already rewritten without the slash).
|
|
838
|
+
return `${opening.slice(0, -1)}${extra}>`;
|
|
839
|
+
}
|
|
840
|
+
|
|
767
841
|
/**
|
|
768
842
|
* Coerce attribute strings to typed properties on a component instance
|
|
769
843
|
* based on its static `properties` declaration.
|
|
@@ -780,7 +854,12 @@ function applyAttrsToInstance(instance, attrs, Cls) {
|
|
|
780
854
|
if (def.type === Number) instance[propName] = Number(raw);
|
|
781
855
|
else if (def.type === Boolean) instance[propName] = raw !== 'false';
|
|
782
856
|
else if (def.type === Object || def.type === Array) {
|
|
783
|
-
|
|
857
|
+
// `raw` is the entity-encoded attribute text (parseAttrs returns the
|
|
858
|
+
// literal characters between the quotes), so decode the HTML entities
|
|
859
|
+
// before JSON.parse. A JSON attribute carries `"` for every `"`;
|
|
860
|
+
// parsing it raw throws and would silently fall back to the string,
|
|
861
|
+
// leaving an Object/Array prop holding a string at SSR.
|
|
862
|
+
try { instance[propName] = JSON.parse(unescapeAttr(raw)); } catch { instance[propName] = raw; }
|
|
784
863
|
} else instance[propName] = raw;
|
|
785
864
|
}
|
|
786
865
|
}
|
package/src/task.js
CHANGED
|
@@ -93,6 +93,16 @@
|
|
|
93
93
|
* @readonly
|
|
94
94
|
* @enum {number}
|
|
95
95
|
*/
|
|
96
|
+
// The SSR walker now runs controllers' hostUpdate during the pre-render pass
|
|
97
|
+
// (issue #217). A Task must NOT invoke its task function server-side (that
|
|
98
|
+
// would fire a fetch during SSR); the browser runs it on hydration. Read live
|
|
99
|
+
// (not cached) so the auto-run gate reflects the current environment: SSR
|
|
100
|
+
// ships the task's INITIAL state, unchanged from before hostUpdate fired at
|
|
101
|
+
// SSR, and the browser runs it on connect.
|
|
102
|
+
function inBrowser() {
|
|
103
|
+
return typeof window !== 'undefined';
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
export const TaskStatus = /** @type {const} */ ({
|
|
97
107
|
INITIAL: 0,
|
|
98
108
|
PENDING: 1,
|
|
@@ -326,6 +336,9 @@ export class Task {
|
|
|
326
336
|
*/
|
|
327
337
|
hostUpdate() {
|
|
328
338
|
if (!this._autoRun) return;
|
|
339
|
+
// Never auto-run server-side: SSR ships the INITIAL state and the task
|
|
340
|
+
// function (typically a fetch) runs only in the browser on hydration.
|
|
341
|
+
if (!inBrowser()) return;
|
|
329
342
|
|
|
330
343
|
const nextArgs = this._argsFn();
|
|
331
344
|
|