@webjsdev/core 0.7.63 → 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/dist/webjs-core-browser.js +4 -4
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -22
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/attribute-reader.js +20 -0
- package/src/binding-prefixes.js +20 -1
- package/src/component/lifecycle.js +90 -48
- package/src/escape.js +12 -2
- package/src/form-action.js +7 -7
- package/src/optimistic.js +3 -3
- package/src/registry.d.ts +5 -0
- package/src/registry.js +35 -4
- package/src/render-client/parts.js +2 -2
- package/src/render-server/attrs.js +5 -4
- package/src/render-server/compile.js +419 -0
- package/src/render-server/dsd.js +11 -435
- package/src/render-server/errors.js +84 -0
- package/src/render-server/expand.js +319 -0
- package/src/render-server/html-scan.js +19 -358
- package/src/render-server/stream.js +11 -13
- package/src/render-server/template-renderer.js +13 -936
- package/src/render-server/walk.js +758 -0
- package/src/router-client/dom-differ.js +25 -2
- package/src/router-client/form-state.js +190 -0
- package/src/router-client/navigator.js +12 -0
- package/src/router-client/snapshot-cache.js +2 -12
- package/src/routes.d.ts +2 -0
- package/src/slot.js +1 -1
- package/src/render-server/slots.js +0 -213
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 {
|
|
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 {
|
|
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
|
-
//
|
|
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 {
|
|
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.
|
|
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",
|
package/src/attribute-reader.js
CHANGED
|
@@ -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)) {
|
package/src/binding-prefixes.js
CHANGED
|
@@ -41,11 +41,26 @@ export function isBindingPrefix(ch) {
|
|
|
41
41
|
return Object.prototype.hasOwnProperty.call(BINDING_PREFIXES, ch);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The HTML boolean attributes: present means on, absent means off, and the
|
|
46
|
+
* value is meant to be empty. A boolean in a PLAIN hole on one of these renders
|
|
47
|
+
* exactly like `?attr` (#1579), so `checked=${isDefault}` cannot serve the
|
|
48
|
+
* `checked="false"` that HTML reads as checked. Lowercase, from the HTML spec's
|
|
49
|
+
* attribute index.
|
|
50
|
+
*/
|
|
51
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
52
|
+
'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',
|
|
53
|
+
'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert',
|
|
54
|
+
'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate',
|
|
55
|
+
'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected',
|
|
56
|
+
'shadowrootclonable', 'shadowrootdelegatesfocus', 'shadowrootserializable',
|
|
57
|
+
]);
|
|
58
|
+
|
|
44
59
|
/**
|
|
45
60
|
* What a PLAIN attribute hole (`name=${value}`, unquoted, the whole value)
|
|
46
61
|
* writes, or `null` when the attribute is omitted. The one rule both renderers
|
|
47
62
|
* follow, so the server never serves an attribute the client then removes
|
|
48
|
-
* (#1573):
|
|
63
|
+
* (#1573, #1579):
|
|
49
64
|
*
|
|
50
65
|
* - `null` / `undefined` omit the attribute. This is the "omit" value, so a
|
|
51
66
|
* conditional attribute is `aria-current=${active ? 'page' : null}`.
|
|
@@ -53,6 +68,9 @@ export function isBindingPrefix(ch) {
|
|
|
53
68
|
* `"false"`. For a tri-state ARIA attribute (`aria-expanded`,
|
|
54
69
|
* `aria-pressed`, `aria-checked`) `"false"` and absent mean different
|
|
55
70
|
* things, so the value an author passes is the value the user agent gets.
|
|
71
|
+
* - `true` on an HTML boolean attribute (`checked`, `selected`, `disabled`,
|
|
72
|
+
* and the rest) writes the empty value, exactly as `?attr=${true}` does. Anywhere
|
|
73
|
+
* else it is `"true"`.
|
|
56
74
|
* - anything else is `String(value)`.
|
|
57
75
|
*
|
|
58
76
|
* A quoted or mixed value (`title="${x}"`, `class="a ${x}"`) is not a plain
|
|
@@ -66,5 +84,6 @@ export function isBindingPrefix(ch) {
|
|
|
66
84
|
export function attrHoleValue(name, value) {
|
|
67
85
|
if (value == null) return null;
|
|
68
86
|
if (value === false) return /^aria-/i.test(name) ? 'false' : null;
|
|
87
|
+
if (value === true && BOOLEAN_ATTRIBUTES.has(String(name).toLowerCase())) return '';
|
|
69
88
|
return String(value);
|
|
70
89
|
}
|
|
@@ -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
|
-
*
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
-
*
|
|
296
|
-
*
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
// (
|
|
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
|
-
|
|
9
|
+
const str = String(s);
|
|
10
|
+
if (!TEXT_SPECIAL.test(str)) return str;
|
|
11
|
+
return str
|
|
4
12
|
.replace(/&/g, '&')
|
|
5
13
|
.replace(/</g, '<')
|
|
6
14
|
.replace(/>/g, '>');
|
|
@@ -8,7 +16,9 @@ export function escapeText(s) {
|
|
|
8
16
|
|
|
9
17
|
/** @param {string} s */
|
|
10
18
|
export function escapeAttr(s) {
|
|
11
|
-
|
|
19
|
+
const str = String(s);
|
|
20
|
+
if (!ATTR_SPECIAL.test(str)) return str;
|
|
21
|
+
return str
|
|
12
22
|
.replace(/&/g, '&')
|
|
13
23
|
.replace(/"/g, '"')
|
|
14
24
|
.replace(/</g, '<');
|
package/src/form-action.js
CHANGED
|
@@ -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
|
-
*
|
|
91
|
-
* `renderToStream(v, { ssr: false })`,
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
971
|
-
*
|
|
972
|
-
* and not
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 `
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
|
457
|
-
//
|
|
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
|
|
46
|
-
instance.setAttribute(name, decodeAttrEntities(
|
|
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
|
|
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`.
|