@webjsdev/core 0.8.0 → 0.9.0
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/README.md +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +14 -9
- package/index.d.ts +22 -10
- package/index.js +17 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-reader.js +197 -23
- package/src/binding-prefixes.js +0 -12
- package/src/component/lifecycle.js +760 -209
- package/src/component/reactive.js +2 -10
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +151 -41
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +38 -1
- package/src/directives.js +158 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +4 -0
- package/src/html.js +35 -3
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +8 -3
- package/src/render-client/parts.js +745 -31
- package/src/render-client/reconciler.js +56 -11
- package/src/render-client/template-compiler.js +139 -44
- package/src/render-client.d.ts +5 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +34 -0
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +61 -8
- package/src/render-server/served-attrs.js +1 -2
- package/src/render-server/walk.js +135 -3
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +86 -5
- package/src/router-client/navigator.js +45 -9
- package/src/router-client/prefetch.js +3 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/swap.js +4 -1
- package/src/routes.d.ts +75 -18
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +1 -8
package/src/attribute-reader.js
CHANGED
|
@@ -73,9 +73,13 @@ export function isAttributeBacked(decl) {
|
|
|
73
73
|
* applied to the JSON and converter branches alone, which left a `String`-typed
|
|
74
74
|
* prop holding the raw source text at SSR while `getAttribute()` on the client
|
|
75
75
|
* returned the decoded one.
|
|
76
|
+
* @param {string} [propName] the property being read, named in the Boolean warning
|
|
76
77
|
* @returns {unknown}
|
|
77
78
|
*/
|
|
78
|
-
export function readAttributeValue(def, value) {
|
|
79
|
+
export function readAttributeValue(def, value, propName) {
|
|
80
|
+
// Lit's function form: a bare `converter: (value, type) => ...` IS the
|
|
81
|
+
// `fromAttribute` half, and reflection uses the default serialization.
|
|
82
|
+
if (typeof def.converter === 'function') return def.converter(value, def.type);
|
|
79
83
|
if (def.converter && def.converter.fromAttribute) {
|
|
80
84
|
// Deliberately UNGUARDED, and deliberately first (#1340). An author who
|
|
81
85
|
// supplies a converter owns the whole read, which is the same rule
|
|
@@ -109,7 +113,13 @@ export function readAttributeValue(def, value) {
|
|
|
109
113
|
return def.converter.fromAttribute(value, def.type);
|
|
110
114
|
}
|
|
111
115
|
if (def.type === Number) return value == null ? null : Number(value);
|
|
112
|
-
if (def.type === Boolean)
|
|
116
|
+
if (def.type === Boolean) {
|
|
117
|
+
// A boolean attribute is on whenever it is PRESENT, as in HTML and Lit,
|
|
118
|
+
// so `open="false"` is true. Say so once per property, since the literal
|
|
119
|
+
// reads as off to anyone coming from a JSX prop.
|
|
120
|
+
if (value === 'false') warnFalseBoolean(def, propName);
|
|
121
|
+
return value != null;
|
|
122
|
+
}
|
|
113
123
|
if (def.type === Object || def.type === Array) {
|
|
114
124
|
// An attribute that is not parseable JSON yields `null` rather than the raw
|
|
115
125
|
// string (#1253), because a STRING is never a valid value for a property
|
|
@@ -135,11 +145,49 @@ export function readAttributeValue(def, value) {
|
|
|
135
145
|
return value;
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/** Declarations already warned about a literal "false". @type {WeakSet<object>} */
|
|
149
|
+
const WARNED_FALSE = new WeakSet();
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {object} def
|
|
153
|
+
* @param {string | undefined} propName
|
|
154
|
+
*/
|
|
155
|
+
function warnFalseBoolean(def, propName) {
|
|
156
|
+
if (WARNED_FALSE.has(def)) return;
|
|
157
|
+
WARNED_FALSE.add(def);
|
|
158
|
+
const name = propName ? `\`${String(propName)}\`` : 'A Boolean property';
|
|
159
|
+
console.warn(
|
|
160
|
+
`[webjs] ${name} received the attribute value "false", which is TRUE: a Boolean property is on whenever its ` +
|
|
161
|
+
`attribute is present, as in HTML and Lit. To turn it off, leave the attribute out, or bind it with ` +
|
|
162
|
+
`?attr=\${false} in a template.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
138
166
|
/** Kebab-case a property name for its default HTML attribute. @param {string} s */
|
|
139
167
|
function hyphenate(s) {
|
|
140
168
|
return s.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
141
169
|
}
|
|
142
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The attribute a declared property reads and reflects to, Lit's
|
|
173
|
+
* `attribute` option: a string is used as written, `false` (or
|
|
174
|
+
* `state: true`) means none, and anything else (absent, or `true`) is Lit's
|
|
175
|
+
* default name, the LOWERCASED property name (`isOpen` -> `isopen`). The
|
|
176
|
+
* kebab spelling (`is-open`) stays readable as an alias (see
|
|
177
|
+
* `attributeBackedProps`) but is never what a property reflects to.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} propName
|
|
180
|
+
* @param {{ attribute?: string | boolean, state?: boolean }} decl normalised declaration
|
|
181
|
+
* @returns {string | undefined}
|
|
182
|
+
*/
|
|
183
|
+
export function attributeNameFor(propName, decl) {
|
|
184
|
+
if (!isAttributeBacked(decl)) return undefined;
|
|
185
|
+
if (typeof decl.attribute === 'string') return decl.attribute;
|
|
186
|
+
// A symbol-keyed property has no default attribute name, as in Lit.
|
|
187
|
+
if (typeof propName !== 'string') return undefined;
|
|
188
|
+
return propName.toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
/**
|
|
144
192
|
* Resolve an attribute NAME to the property it feeds, and to that property's
|
|
145
193
|
* normalised declaration.
|
|
@@ -182,14 +230,20 @@ function hyphenate(s) {
|
|
|
182
230
|
* which case the caller must IGNORE the attribute entirely (#1341).
|
|
183
231
|
*/
|
|
184
232
|
export function resolveAttributeProperty(Cls, attrName) {
|
|
185
|
-
// Primary names first,
|
|
186
|
-
//
|
|
233
|
+
// Primary names first, then aliases. Two props can share a primary name
|
|
234
|
+
// (`fooBar` and `foobar` both default to `foobar`); the prop literally
|
|
235
|
+
// named after the attribute keeps it, so a sibling never steals a real prop.
|
|
236
|
+
let primaryHit;
|
|
187
237
|
let aliasHit;
|
|
188
238
|
for (const { propName, def, primary, alias } of attributeBackedProps(Cls)) {
|
|
189
|
-
if (primary === attrName)
|
|
190
|
-
|
|
239
|
+
if (primary === attrName) {
|
|
240
|
+
if (propName === attrName) return { propName, def };
|
|
241
|
+
if (primaryHit === undefined) primaryHit = { propName, def };
|
|
242
|
+
} else if (aliasHit === undefined && alias === attrName) {
|
|
243
|
+
aliasHit = { propName, def };
|
|
244
|
+
}
|
|
191
245
|
}
|
|
192
|
-
return aliasHit;
|
|
246
|
+
return primaryHit ?? aliasHit;
|
|
193
247
|
}
|
|
194
248
|
|
|
195
249
|
/**
|
|
@@ -209,13 +263,128 @@ export function observedAttributeNames(Cls) {
|
|
|
209
263
|
return [...names];
|
|
210
264
|
}
|
|
211
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Every reactive property `Cls` declares, its own over its superclasses', the
|
|
268
|
+
* way Lit's `finalize` merges `elementProperties` down the chain. A class's own
|
|
269
|
+
* `static properties` (Lit's spelling, a field or a `static get`) and the map
|
|
270
|
+
* the `WebComponent({ ... })` factory puts on the base it returns are the same
|
|
271
|
+
* thing here, so a subclass adding props keeps its parent's, and a factory
|
|
272
|
+
* class may add a `static properties` block of its own.
|
|
273
|
+
*
|
|
274
|
+
* Memoized per class: the accessors (`installAccessors`), the attribute list
|
|
275
|
+
* (`attributeBackedProps`) and reflection all read this one map, so they
|
|
276
|
+
* cannot disagree about which props exist. The map is the same OBJECT for as
|
|
277
|
+
* long as the class's declarations, and its superclasses', are unchanged, so
|
|
278
|
+
* a memo derived from it is keyed on that identity.
|
|
279
|
+
*
|
|
280
|
+
* A memo holds its superclasses' declarations merged in, so `createProperty`
|
|
281
|
+
* on a parent has to reach every subclass already read, which a WeakMap
|
|
282
|
+
* cannot enumerate. So each memo records its parent's map and its own class's
|
|
283
|
+
* version, and stays valid while both are unchanged: a change rebuilds exactly
|
|
284
|
+
* the changed class and the classes below it. Checking that walks the chain,
|
|
285
|
+
* so it runs only after some class changed (`epoch` moved); until then a read
|
|
286
|
+
* is one lookup.
|
|
287
|
+
*
|
|
288
|
+
* @param {any} Cls the component class
|
|
289
|
+
* @returns {Record<string, any>}
|
|
290
|
+
*/
|
|
291
|
+
export function declaredProperties(Cls) {
|
|
292
|
+
if (!Cls || typeof Cls !== 'function') return (Cls && Cls.properties) || {};
|
|
293
|
+
const hit = DECLARED.get(Cls);
|
|
294
|
+
if (hit && hit.epoch === epoch) return hit.merged;
|
|
295
|
+
const parent = Object.getPrototypeOf(Cls);
|
|
296
|
+
const inherited = parent && parent !== Function.prototype ? declaredProperties(parent) : ROOT;
|
|
297
|
+
const version = VERSION.get(Cls) ?? 0;
|
|
298
|
+
if (hit && hit.inherited === inherited && hit.version === version) {
|
|
299
|
+
// Unaffected by whatever changed: keep the same map, valid at this epoch.
|
|
300
|
+
hit.epoch = epoch;
|
|
301
|
+
return hit.merged;
|
|
302
|
+
}
|
|
303
|
+
const own = Object.hasOwn(Cls, 'properties') ? Cls.properties : undefined;
|
|
304
|
+
const created = CREATED.get(Cls);
|
|
305
|
+
let merged = own && typeof own === 'object' ? { ...inherited, ...own } : inherited;
|
|
306
|
+
if (created) merged = { ...merged, ...created };
|
|
307
|
+
DECLARED.set(Cls, { epoch, inherited, version, merged });
|
|
308
|
+
return merged;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @type {WeakMap<Function, { epoch: number, inherited: Record<string, any>, version: number, merged: Record<string, any> }>} */
|
|
312
|
+
const DECLARED = new WeakMap();
|
|
313
|
+
|
|
314
|
+
/** The declarations above every class: none. Shared, so a root-level memo can be compared by identity. */
|
|
315
|
+
const ROOT = Object.freeze({});
|
|
316
|
+
|
|
317
|
+
/** Per class, how many times its own declarations changed (`createProperty`). @type {WeakMap<Function, number>} */
|
|
318
|
+
const VERSION = new WeakMap();
|
|
319
|
+
|
|
320
|
+
/** Moves whenever any class's declarations change, so a memo knows to re-check its chain. */
|
|
321
|
+
let epoch = 0;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The current declarations epoch, for a memo outside this module derived from
|
|
325
|
+
* `declaredProperties` (lifecycle.js's `elementProperties`): while it has not
|
|
326
|
+
* moved, no class's declarations changed and a memo needs no re-check.
|
|
327
|
+
* @returns {number}
|
|
328
|
+
*/
|
|
329
|
+
export function declarationsEpoch() {
|
|
330
|
+
return epoch;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Properties added to a class through Lit's `static createProperty(name,
|
|
335
|
+
* options)`, per class. Merged LAST in `declaredProperties`, so a created
|
|
336
|
+
* property wins over a same-named `static properties` entry, as it does in Lit
|
|
337
|
+
* (`createProperty` writes straight into `elementProperties`).
|
|
338
|
+
* @type {WeakMap<Function, Record<string, any>>}
|
|
339
|
+
*/
|
|
340
|
+
const CREATED = new WeakMap();
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Normalise one declaration: the bare-constructor shorthand (`count: Number`)
|
|
344
|
+
* becomes `{ type: Number }`, an options object is returned as is.
|
|
345
|
+
* @param {unknown} decl
|
|
346
|
+
* @returns {Record<string, any>}
|
|
347
|
+
*/
|
|
348
|
+
export function normalizeDecl(decl) {
|
|
349
|
+
return typeof decl === 'object' && decl !== null ? /** @type {any} */ (decl) : { type: decl };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Record a property created through `static createProperty()` and drop the
|
|
354
|
+
* class's memoized declarations so the next read includes it.
|
|
355
|
+
* @param {Function} Cls
|
|
356
|
+
* @param {string} name
|
|
357
|
+
* @param {Record<string, any>} decl
|
|
358
|
+
*/
|
|
359
|
+
export function addCreatedProperty(Cls, name, decl) {
|
|
360
|
+
let own = CREATED.get(Cls);
|
|
361
|
+
if (!own) CREATED.set(Cls, (own = {}));
|
|
362
|
+
own[name] = decl;
|
|
363
|
+
invalidateClass(Cls);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Drop every memo this module keeps for `Cls`, so its declarations are read
|
|
368
|
+
* again on next use.
|
|
369
|
+
* @param {Function} Cls
|
|
370
|
+
*/
|
|
371
|
+
export function invalidateClass(Cls) {
|
|
372
|
+
// This class's memo is rebuilt, which gives it a new map, and every
|
|
373
|
+
// subclass's memo then sees a parent map it was not built from. Classes on
|
|
374
|
+
// other chains re-check once and keep their maps.
|
|
375
|
+
VERSION.set(Cls, (VERSION.get(Cls) ?? 0) + 1);
|
|
376
|
+
epoch++;
|
|
377
|
+
}
|
|
378
|
+
|
|
212
379
|
/**
|
|
213
380
|
* The attribute-backed props of `Cls` with the names each answers to.
|
|
214
381
|
*
|
|
215
382
|
* `primary` is the custom `attribute` option, matched VERBATIM, or else the
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
383
|
+
* LOWERCASED property name, Lit's default attribute name and the one a
|
|
384
|
+
* reflected property writes. `alias` is the KEBAB-cased property name, kept
|
|
385
|
+
* as a read-only alias (WebJs's earlier default, and the spelling HTML
|
|
386
|
+
* authors reach for), present only when the prop has no custom `attribute`
|
|
387
|
+
* and the kebab name differs from the lowercase one. Without it,
|
|
219
388
|
* `<quiz-player quizId=${id}>` against `{ quizId: Number }` reached neither
|
|
220
389
|
* side: the parser lowercases the name to `quizid`, which matched nothing, so
|
|
221
390
|
* the prop silently stayed undefined on the server AND the client (#1540).
|
|
@@ -229,31 +398,36 @@ function attributeBackedProps(Cls) {
|
|
|
229
398
|
// Memoized per class (#1646): the SSR walk resolves every source attribute
|
|
230
399
|
// of every instance through this list, and rebuilding it (one hyphenate per
|
|
231
400
|
// declared prop) was a measurable share of a component's render. A class's
|
|
232
|
-
// declarations
|
|
233
|
-
//
|
|
234
|
-
// walk is the only one.
|
|
401
|
+
// declarations are fixed once it is defined (`declaredProperties` reads them
|
|
402
|
+
// once per class), so the first walk is the only one.
|
|
235
403
|
if (Cls && typeof Cls === 'function') {
|
|
404
|
+
// Valid while no declarations changed anywhere (one lookup); after a
|
|
405
|
+
// change, while the class's declarations map is the same object, which it
|
|
406
|
+
// stays unless this class or a superclass changed.
|
|
236
407
|
const hit = BACKED_PROPS.get(Cls);
|
|
237
|
-
if (hit) return hit;
|
|
408
|
+
if (hit && hit.epoch === epoch) return hit.built;
|
|
409
|
+
const merged = declaredProperties(Cls);
|
|
410
|
+
if (hit && hit.merged === merged) { hit.epoch = epoch; return hit.built; }
|
|
238
411
|
const built = buildAttributeBackedProps(Cls);
|
|
239
|
-
BACKED_PROPS.set(Cls, built);
|
|
412
|
+
BACKED_PROPS.set(Cls, { epoch, merged, built });
|
|
240
413
|
return built;
|
|
241
414
|
}
|
|
242
415
|
return buildAttributeBackedProps(Cls);
|
|
243
416
|
}
|
|
244
417
|
|
|
245
|
-
/** @type {WeakMap<Function, { propName: string, def: any, primary: string, alias: string | undefined }[]>} */
|
|
418
|
+
/** @type {WeakMap<Function, { epoch: number, merged: Record<string, any>, built: { propName: string, def: any, primary: string, alias: string | undefined }[] }>} */
|
|
246
419
|
const BACKED_PROPS = new WeakMap();
|
|
247
420
|
|
|
248
421
|
function buildAttributeBackedProps(Cls) {
|
|
249
|
-
const props = (Cls
|
|
422
|
+
const props = declaredProperties(Cls);
|
|
250
423
|
const out = [];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const primary =
|
|
255
|
-
|
|
256
|
-
const
|
|
424
|
+
// Symbol keys too: one reaches an attribute through its `attribute` option.
|
|
425
|
+
for (const k of Reflect.ownKeys(props)) {
|
|
426
|
+
const d = normalizeDecl(props[k]);
|
|
427
|
+
const primary = attributeNameFor(k, d);
|
|
428
|
+
if (primary === undefined) continue;
|
|
429
|
+
const kebab = typeof k === 'string' ? hyphenate(k) : primary;
|
|
430
|
+
const alias = typeof d.attribute !== 'string' && kebab !== primary ? kebab : undefined;
|
|
257
431
|
out.push({ propName: k, def: d, primary, alias });
|
|
258
432
|
}
|
|
259
433
|
return out;
|
package/src/binding-prefixes.js
CHANGED
|
@@ -10,18 +10,6 @@ import { nothing, noChange } from './sentinels.js';
|
|
|
10
10
|
* renderer (`render-server.js`, two sites) read it instead of hardcoding
|
|
11
11
|
* the prefix characters inline.
|
|
12
12
|
*
|
|
13
|
-
* It is also the anchor for the elision drift guard. The analyser
|
|
14
|
-
* (`packages/server/src/component-elision.js`) classifies every prefix as
|
|
15
|
-
* either a client-behaviour ship signal (it drops at SSR and implies the
|
|
16
|
-
* component does client work, so a component using it must ship) or an
|
|
17
|
-
* SSR-safe round-trip (it survives into the served HTML, so it is not a
|
|
18
|
-
* ship signal). The guard test
|
|
19
|
-
* (`packages/server/test/elision/sigil-coverage.test.js`) asserts that
|
|
20
|
-
* classification covers EXACTLY these keys, so a new prefix cannot be
|
|
21
|
-
* added here without the analyser being taught which kind it is. That
|
|
22
|
-
* closes the one gap the prototype-introspection guard
|
|
23
|
-
* (`lifecycle-coverage.test.js`) cannot reach, because a sigil is syntax,
|
|
24
|
-
* not a prototype method or a named export.
|
|
25
13
|
*
|
|
26
14
|
* @type {Readonly<Record<string, 'event' | 'prop' | 'bool'>>}
|
|
27
15
|
*/
|