@webjsdev/core 0.7.65 → 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 +22 -21
- 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 +18 -9
- package/index.d.ts +26 -10
- package/index.js +21 -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-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +213 -25
- package/src/binding-prefixes.js +10 -53
- package/src/component/lifecycle.js +921 -252
- package/src/component/reactive.js +7 -14
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +160 -44
- 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 +40 -1
- package/src/directives.js +160 -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 +8 -0
- package/src/html.js +55 -2
- 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 +250 -0
- package/src/render-client/parts.js +790 -42
- package/src/render-client/reconciler.js +84 -23
- package/src/render-client/template-compiler.js +152 -43
- package/src/render-client.d.ts +6 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +50 -2
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +100 -20
- package/src/render-server/served-attrs.js +78 -0
- package/src/render-server/walk.js +177 -5
- 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/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +107 -5
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +49 -9
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +6 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +53 -10
- package/src/routes.d.ts +75 -18
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- 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 +2 -9
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAttributeMap, attributeMapText } from '../attribute-directives.js';
|
|
2
|
+
import { nothing, noChange } from '../sentinels.js';
|
|
1
3
|
import { isTemplate } from '../html.js';
|
|
2
4
|
import { attrHoleValue } from '../binding-prefixes.js';
|
|
3
5
|
import { escapeText, escapeAttr } from '../escape.js';
|
|
@@ -10,6 +12,8 @@ import {
|
|
|
10
12
|
isSubmitterReflectedProp, FORM_ACTION_FIELD,
|
|
11
13
|
} from '../form-action.js';
|
|
12
14
|
import { isRepeat } from '../repeat.js';
|
|
15
|
+
import { isDirectiveResult, PartType } from '../directive.js';
|
|
16
|
+
import { isSignal } from '../signal.js';
|
|
13
17
|
import { isSuspense } from '../suspense.js';
|
|
14
18
|
import { isUnsafeHTML, isLive, isKeyed, isGuard, isTemplateContent, isRef, isCache, isUntil, isAsyncAppend, isAsyncReplace, isWatch } from '../directives.js';
|
|
15
19
|
import { stringify } from '../serialize.js';
|
|
@@ -81,6 +85,8 @@ export class Walker {
|
|
|
81
85
|
this.defer = 0;
|
|
82
86
|
/** @type {any} */
|
|
83
87
|
this.tagState = null;
|
|
88
|
+
/** How many component templates (render / renderError) are being walked. */
|
|
89
|
+
this.componentDepth = 0;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
// -------------------------------------------------------------------------
|
|
@@ -163,7 +169,11 @@ export class Walker {
|
|
|
163
169
|
* @param {unknown} value
|
|
164
170
|
*/
|
|
165
171
|
async value(value) {
|
|
166
|
-
if (value
|
|
172
|
+
if (isDirectiveResult(value)) value = resolveDirective(value, CHILD_PART);
|
|
173
|
+
// A bare `${signal}` renders its current value (the client binds the hole
|
|
174
|
+
// to the signal so it updates in place, as `watch()` does).
|
|
175
|
+
if (isSignal(value)) value = /** @type any */ (value).get();
|
|
176
|
+
if (value == null || value === nothing || value === noChange) return;
|
|
167
177
|
if (typeof (/** @type any */ (value).then) === 'function') {
|
|
168
178
|
return this.value(await value);
|
|
169
179
|
}
|
|
@@ -173,6 +183,14 @@ export class Walker {
|
|
|
173
183
|
for (const v of value) await this.value(v);
|
|
174
184
|
return;
|
|
175
185
|
}
|
|
186
|
+
// Any other iterable renders like an array, as in lit: a generator from
|
|
187
|
+
// `map()` / `join()` / `range()`, a Set, a Map's values. A string is
|
|
188
|
+
// iterable too and was handled above, and every directive marker is a
|
|
189
|
+
// plain object with no iterator, so none of them reaches this branch.
|
|
190
|
+
if (typeof value === 'object' && typeof (/** @type any */ (value))[Symbol.iterator] === 'function') {
|
|
191
|
+
for (const v of /** @type {Iterable<unknown>} */ (value)) await this.value(v);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
176
194
|
// An unsafeHTML value is raw markup. With expansion on, the components it
|
|
177
195
|
// carries render too (the output rescan used to find them), through the
|
|
178
196
|
// same tokenizer the template went through, so a tag written inside one
|
|
@@ -242,6 +260,12 @@ export class Walker {
|
|
|
242
260
|
|
|
243
261
|
/** @param {import('../html.js').TemplateResult} tr */
|
|
244
262
|
async template(tr) {
|
|
263
|
+
// Only a tagged template's strings carry `raw`, so a result that arrived as
|
|
264
|
+
// data (JSON with the `_$webjs` brand) is refused rather than rendered as
|
|
265
|
+
// markup, as the client renderer and lit's `trustFromTemplateString` do.
|
|
266
|
+
if (!Array.isArray(tr.strings) || !Object.prototype.hasOwnProperty.call(tr.strings, 'raw')) {
|
|
267
|
+
throw new Error("[webjs] refusing to render a template result whose strings are not a tagged template's (a TemplateResult built from data, not by html``).");
|
|
268
|
+
}
|
|
245
269
|
await this.ops(compile(tr.strings), tr.values);
|
|
246
270
|
this.flush();
|
|
247
271
|
}
|
|
@@ -300,8 +324,46 @@ export class Walker {
|
|
|
300
324
|
await this.attr(op, val);
|
|
301
325
|
break;
|
|
302
326
|
case VALUE:
|
|
327
|
+
if (op.quote) {
|
|
328
|
+
// Resolve the complete attribute before emitting any of it so a
|
|
329
|
+
// sentinel in any expression can omit the entire attribute.
|
|
330
|
+
this.buf = this.buf.slice(0, -op.prefix.length);
|
|
331
|
+
let attribute = op.prefix;
|
|
332
|
+
let omitted = false;
|
|
333
|
+
let piece = val;
|
|
334
|
+
for (let j = i; j < ops.length; j++) {
|
|
335
|
+
const next = ops[j];
|
|
336
|
+
if (next.t === HOLE) {
|
|
337
|
+
if (j !== i) {
|
|
338
|
+
piece = values[vi++];
|
|
339
|
+
if (piece && typeof piece.then === 'function') piece = await piece;
|
|
340
|
+
if (isLive(piece)) piece = piece.value;
|
|
341
|
+
}
|
|
342
|
+
piece = await attrValueOf(piece);
|
|
343
|
+
if (isDirectiveResult(piece)) piece = resolveDirective(piece, { type: PartType.ATTRIBUTE, name: op.name, tagName: op.tag?.toUpperCase() });
|
|
344
|
+
if (isSignal(piece) || isWatch(piece)) piece = signalValue(piece);
|
|
345
|
+
if (isAttributeMap(piece)) piece = attributeMapText(piece, op.name);
|
|
346
|
+
assertNotFunctionActionAttr(piece, op.name, op.tag);
|
|
347
|
+
if (piece === nothing) omitted = true;
|
|
348
|
+
attribute += piece === nothing || piece === noChange ? '' : escapeAttr(String(piece ?? ''));
|
|
349
|
+
} else if (next.t === TEXT) {
|
|
350
|
+
const close = next.s.indexOf(op.quote);
|
|
351
|
+
if (close >= 0) {
|
|
352
|
+
if (!omitted) this.buf += attribute + next.s.slice(0, close + 1);
|
|
353
|
+
this.buf += next.s.slice(close + 1);
|
|
354
|
+
i = j;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
attribute += next.s;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
303
362
|
// A hole inside a quoted/unquoted value, the `action="${fn}"` and
|
|
304
363
|
// mixed `action="/x/${fn}"` shapes (#1154).
|
|
364
|
+
val = await attrValueOf(val);
|
|
365
|
+
if (isDirectiveResult(val)) val = resolveDirective(val, { type: PartType.ATTRIBUTE, name: op.name, tagName: op.tag?.toUpperCase() });
|
|
366
|
+
if (isSignal(val) || isWatch(val)) val = signalValue(val);
|
|
305
367
|
assertNotFunctionActionAttr(val, op.name, op.tag);
|
|
306
368
|
this.buf += escapeAttr(String(val ?? ''));
|
|
307
369
|
break;
|
|
@@ -317,6 +379,8 @@ export class Walker {
|
|
|
317
379
|
// -------------------------------------------------------------------------
|
|
318
380
|
// Start tags
|
|
319
381
|
// -------------------------------------------------------------------------
|
|
382
|
+
// (attrValueOf, the attribute-position unwrap of the value-carrying
|
|
383
|
+
// built-in directives, is a module function below.)
|
|
320
384
|
|
|
321
385
|
/** @param {string} tag */
|
|
322
386
|
open(tag) {
|
|
@@ -366,11 +430,32 @@ export class Walker {
|
|
|
366
430
|
* @param {unknown} val
|
|
367
431
|
*/
|
|
368
432
|
async attr(op, val) {
|
|
433
|
+
if (op.kind !== 'event') val = await attrValueOf(val);
|
|
434
|
+
// A custom directive renders its `render()` result at SSR (lit's server
|
|
435
|
+
// rule: `update()` needs a DOM). An event part is client-only, so its
|
|
436
|
+
// directive never runs here.
|
|
437
|
+
if (isDirectiveResult(val) && op.kind !== 'event') {
|
|
438
|
+
const type = op.kind === 'prop' ? PartType.PROPERTY : op.kind === 'bool' ? PartType.BOOLEAN_ATTRIBUTE : PartType.ATTRIBUTE;
|
|
439
|
+
val = resolveDirective(val, { type, name: op.name, tagName: op.tag?.toUpperCase() });
|
|
440
|
+
}
|
|
441
|
+
// A bare signal in an attribute or boolean hole renders its current value;
|
|
442
|
+
// a `.prop` hole hands the signal object itself to the property, as lit's
|
|
443
|
+
// signals package does.
|
|
444
|
+
if ((isSignal(val) || isWatch(val)) && (op.kind === 'plain' || op.kind === 'bool')) val = signalValue(val);
|
|
445
|
+
if (isAttributeMap(val)) {
|
|
446
|
+
if (op.kind !== 'plain') throw new Error('classMap/styleMap require an attribute binding.');
|
|
447
|
+
val = attributeMapText(val, op.name);
|
|
448
|
+
}
|
|
449
|
+
if (val === noChange) return;
|
|
450
|
+
if (val === nothing && op.kind === 'prop') val = undefined;
|
|
369
451
|
const ts = this.tagState;
|
|
370
452
|
const name = /** @type string */ (op.name);
|
|
371
453
|
const tag = /** @type string */ (op.tag);
|
|
372
454
|
if (op.kind === 'event') {
|
|
373
|
-
// An event listener is client-only behaviour, dropped at SSR.
|
|
455
|
+
// An event listener is client-only behaviour, dropped at SSR. Outside
|
|
456
|
+
// any component template it can never run either: a page or layout
|
|
457
|
+
// never hydrates. Dev makes that a render error naming the fix.
|
|
458
|
+
if (this.dev && this.componentDepth === 0) throw new Error(pageBindingError(true, name, tag));
|
|
374
459
|
return;
|
|
375
460
|
}
|
|
376
461
|
if (op.kind === 'prop') {
|
|
@@ -384,6 +469,9 @@ export class Walker {
|
|
|
384
469
|
return;
|
|
385
470
|
}
|
|
386
471
|
if (!tag.includes('-')) {
|
|
472
|
+
if (this.dev && this.componentDepth === 0 && !isReflectedFormProp(name, tag)) {
|
|
473
|
+
throw new Error(pageBindingError(false, name, tag));
|
|
474
|
+
}
|
|
387
475
|
// A native element's `.prop` is dropped at SSR. It still refuses a
|
|
388
476
|
// function where the property is a REFLECTED IDL attribute (`.action`
|
|
389
477
|
// on a form, `.formAction` on a button or input), and records the
|
|
@@ -412,7 +500,7 @@ export class Walker {
|
|
|
412
500
|
// `?action=${fn}` is meaningless in every case; refusing it keeps the
|
|
413
501
|
// rule true for every sigil rather than only the quoted ones.
|
|
414
502
|
assertNotFunctionActionAttr(val, name, tag);
|
|
415
|
-
if (val) this.buf += `${name}=""`;
|
|
503
|
+
if (val && val !== nothing) this.buf += `${name}=""`;
|
|
416
504
|
return;
|
|
417
505
|
}
|
|
418
506
|
// A plain `name=${value}` hole.
|
|
@@ -424,7 +512,7 @@ export class Walker {
|
|
|
424
512
|
// page's own url; the identity is remembered for the `>`. Trailing
|
|
425
513
|
// whitespace goes with the attribute, since every injected attribute
|
|
426
514
|
// carries its own leading space.
|
|
427
|
-
if (ts) ts.actionId = assertIdentifiableAction(await resolveFormActionId(val), tag);
|
|
515
|
+
if (ts) ts.actionId = assertIdentifiableAction(await resolveFormActionId(val), tag, val);
|
|
428
516
|
this.buf = this.buf.replace(/\s+$/, '');
|
|
429
517
|
} else {
|
|
430
518
|
// #1207, the submitter binding. The identity replaces the
|
|
@@ -435,7 +523,7 @@ export class Walker {
|
|
|
435
523
|
if (attrs.has('name')) assertSubmitterHasNoName(attrs.get('name') || '', tag, false);
|
|
436
524
|
if (attrs.has('value')) assertSubmitterHasNoValue(tag);
|
|
437
525
|
if (attrs.has('form')) assertSubmitterHasNoFormAttribute(tag);
|
|
438
|
-
const subId = assertIdentifiableAction(await resolveFormActionId(val), tag);
|
|
526
|
+
const subId = assertIdentifiableAction(await resolveFormActionId(val), tag, val);
|
|
439
527
|
if (ts) ts.submitterTag = tag;
|
|
440
528
|
this.buf += `name="${FORM_ACTION_FIELD}" value="${escapeAttr(subId)}"`;
|
|
441
529
|
}
|
|
@@ -686,6 +774,37 @@ export class Walker {
|
|
|
686
774
|
|
|
687
775
|
const EMPTY = Object.freeze([]);
|
|
688
776
|
|
|
777
|
+
/**
|
|
778
|
+
* The current value of a bare signal or a `watch(signal)` marker in an
|
|
779
|
+
* attribute-shaped hole (SSR reads it once; the client binds the hole).
|
|
780
|
+
* @param {any} v
|
|
781
|
+
*/
|
|
782
|
+
function signalValue(v) {
|
|
783
|
+
return isWatch(v) ? v.signal.get() : v.get();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** The part info every child-position directive is constructed with. */
|
|
787
|
+
const CHILD_PART = Object.freeze({ type: PartType.CHILD });
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Resolve a custom directive result (`directive(C)(...values)`) to the value
|
|
791
|
+
* it renders, lit's server rule: construct the class with the part info and
|
|
792
|
+
* call `render(...values)` (never `update()`, which needs a DOM). A directive
|
|
793
|
+
* that returns another directive resolves in turn. A fresh instance per
|
|
794
|
+
* render is right here, since an SSR render is one shot.
|
|
795
|
+
* @param {any} value
|
|
796
|
+
* @param {{ type: number, name?: string, tagName?: string }} partInfo
|
|
797
|
+
* @returns {unknown}
|
|
798
|
+
*/
|
|
799
|
+
function resolveDirective(value, partInfo) {
|
|
800
|
+
for (let depth = 0; isDirectiveResult(value); depth++) {
|
|
801
|
+
if (depth > 100) throw new Error('[webjs] a custom directive kept returning directives (more than 100 levels deep).');
|
|
802
|
+
const instance = new value.ctor(partInfo);
|
|
803
|
+
value = instance.render(...value.values);
|
|
804
|
+
}
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
|
|
689
808
|
/**
|
|
690
809
|
* #1155: a `.method` / `.enctype` / `.encoding` prop on a form is dropped at
|
|
691
810
|
* SSR but applied for real in the browser, where all three are reflected IDL
|
|
@@ -756,3 +875,56 @@ export async function streamValue(value, ctx, controller) {
|
|
|
756
875
|
await w.value(value);
|
|
757
876
|
w.flush();
|
|
758
877
|
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* A form-shape property the dedicated form guards judge (and refuse with a
|
|
881
|
+
* more specific message), so the page-binding error leaves it to them.
|
|
882
|
+
* @param {string} name
|
|
883
|
+
* @param {string} tag
|
|
884
|
+
*/
|
|
885
|
+
function isReflectedFormProp(name, tag) {
|
|
886
|
+
return /^(?:action|method|enctype|encoding|formaction|formmethod|formenctype)$/i.test(name)
|
|
887
|
+
&& /^(?:form|button|input)$/.test(tag);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* The dev error for an `@event` or native `.prop` binding in a template no
|
|
892
|
+
* component renders (a page or layout, which never hydrates).
|
|
893
|
+
* @param {boolean} isEvent an `@event` listener (else a `.prop` binding)
|
|
894
|
+
* @param {string} name
|
|
895
|
+
* @param {string} tag
|
|
896
|
+
*/
|
|
897
|
+
function pageBindingError(isEvent, name, tag) {
|
|
898
|
+
const what = isEvent ? `an \`@${name}\` event listener` : `a \`.${name}\` property binding`;
|
|
899
|
+
return `webjs: ${what} on <${tag}> in a page or layout template. Pages and layouts render only on the server and never hydrate, so it is dropped and never runs. `
|
|
900
|
+
+ `Move the interactive markup into a WebComponent and render its tag from the page (its render() hydrates in the browser)`
|
|
901
|
+
+ (!isEvent ? ', or write a plain attribute (\`' + name.toLowerCase() + '=${value}\`) when the value only needs to reach the HTML' : '')
|
|
902
|
+
+ '. This check runs in dev only. See https://webjs.dev/docs/troubleshooting#binding-in-page-template';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* The value a value-carrying built-in directive stands for in an ATTRIBUTE,
|
|
907
|
+
* property or boolean position, as in Lit (#1686): `guard` renders its
|
|
908
|
+
* callback's result, `until` its first non-promise argument (else the first
|
|
909
|
+
* promise to settle, a rejection reading as no value), `keyed` and `cache`
|
|
910
|
+
* their value. A child position unwraps these in `value()`; an attribute hole
|
|
911
|
+
* used to stringify the directive object to "[object Object]".
|
|
912
|
+
* @param {unknown} v
|
|
913
|
+
* @returns {Promise<unknown>}
|
|
914
|
+
*/
|
|
915
|
+
async function attrValueOf(v) {
|
|
916
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
917
|
+
/** @type any */
|
|
918
|
+
const d = v;
|
|
919
|
+
if (isGuard(d)) v = d.fn();
|
|
920
|
+
else if (isKeyed(d) || isCache(d)) v = d.value;
|
|
921
|
+
else if (isLive(d)) v = d.value;
|
|
922
|
+
else if (isUntil(d)) {
|
|
923
|
+
const sync = d.args.find((a) => !a || typeof a.then !== 'function');
|
|
924
|
+
if (sync !== undefined || d.args.some((a) => !a || typeof a.then !== 'function')) v = sync;
|
|
925
|
+
else if (d.args.length) v = await Promise.race(d.args.map((p) => Promise.resolve(p).catch(() => undefined)));
|
|
926
|
+
else v = undefined;
|
|
927
|
+
} else return v;
|
|
928
|
+
}
|
|
929
|
+
return v;
|
|
930
|
+
}
|
package/src/render-server.d.ts
CHANGED
|
@@ -12,3 +12,6 @@ export function renderToStream(
|
|
|
12
12
|
value: unknown,
|
|
13
13
|
opts?: { ssr?: boolean; suspenseCtx?: SuspenseContext },
|
|
14
14
|
): ReadableStream;
|
|
15
|
+
/** A React-ism in an html template's static strings: `className=` / `htmlFor=` or a capitalised tag. */
|
|
16
|
+
export function findJsxIsm(strings: readonly string[]): { kind: 'attr' | 'tag'; name: string } | null;
|
|
17
|
+
export function jsxIsmMessage(ism: { kind: 'attr' | 'tag'; name: string }): string;
|
package/src/render-server.js
CHANGED
|
@@ -4,3 +4,5 @@
|
|
|
4
4
|
// backs the published `@webjsdev/core/server` subpath, so re-exporting it would
|
|
5
5
|
// widen the package's public API for a helper no consumer asked for.
|
|
6
6
|
export { renderToString, renderToStream } from './render-server/stream.js';
|
|
7
|
+
// The JSX-in-template scan, shared with `webjs check` (`no-jsx-attributes`).
|
|
8
|
+
export { findJsxIsm, jsxIsmMessage } from './jsx-guard.js';
|
package/src/repeat.d.ts
CHANGED
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
// exported, so it cannot be named, and no consumer constructs one by hand.
|
|
4
4
|
export interface RepeatDirective<T> {
|
|
5
5
|
items: T[];
|
|
6
|
-
keyFn: (item: T, i: number) =>
|
|
6
|
+
keyFn: (item: T, i: number) => unknown;
|
|
7
7
|
templateFn: (item: T, i: number) => unknown;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function repeat<T>(
|
|
11
11
|
items: Iterable<T>,
|
|
12
|
-
keyFn: (item: T, i: number) =>
|
|
12
|
+
keyFn: (item: T, i: number) => unknown,
|
|
13
|
+
templateFn: (item: T, i: number) => unknown,
|
|
14
|
+
): RepeatDirective<T>;
|
|
15
|
+
/** lit's two-argument form: no key function, so each item is keyed by its index. */
|
|
16
|
+
export function repeat<T>(
|
|
17
|
+
items: Iterable<T>,
|
|
13
18
|
templateFn: (item: T, i: number) => unknown,
|
|
14
19
|
): RepeatDirective<T>;
|
|
15
20
|
export function isRepeat(x: unknown): x is RepeatDirective<unknown>;
|
package/src/repeat.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* html`<ul>${repeat(items, (it) => it.id, (it) => html`<li>${it.title}</li>`)}</ul>`;
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
-
* Without `repeat()`, an array
|
|
11
|
-
*
|
|
10
|
+
* Without `repeat()`, an array updates positionally: the DOM for item N is
|
|
11
|
+
* reused for whatever is now at index N, so focus, selection and component
|
|
12
|
+
* state follow the position, not the item. With `repeat()`, the client
|
|
13
|
+
* renderer maintains a
|
|
12
14
|
* `Map<key, TemplateInstance>`: matched items update in place, new items
|
|
13
15
|
* mount fresh, removed items unmount, and the surviving DOM nodes are moved
|
|
14
16
|
* (not recreated) when the order changes.
|
|
@@ -16,19 +18,31 @@
|
|
|
16
18
|
* Keys must be stable and unique within the call. Bad keys (array indices)
|
|
17
19
|
* defeat the purpose; prefer a stable id from the data.
|
|
18
20
|
*
|
|
21
|
+
* The two-argument form, `repeat(items, (it) => html`...`)`, is lit's too: with
|
|
22
|
+
* no key function each item is keyed by its INDEX, which makes it a plain
|
|
23
|
+
* positional list (no DOM moves on reorder). Pass a key function whenever item
|
|
24
|
+
* identity has to follow the data.
|
|
25
|
+
*
|
|
19
26
|
* On the server this marker is just iterated: order matters, keys don't.
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
const REPEAT = Symbol.for('webjs.repeat');
|
|
23
30
|
|
|
31
|
+
/** The key of an item `repeat(items, template)` was given no key function for. */
|
|
32
|
+
const indexKey = (/** @type {unknown} */ _item, /** @type {number} */ i) => i;
|
|
33
|
+
|
|
24
34
|
/**
|
|
25
35
|
* @template T
|
|
26
36
|
* @param {Iterable<T>} items
|
|
27
|
-
* @param {(item: T, i: number) =>
|
|
28
|
-
*
|
|
37
|
+
* @param {((item: T, i: number) => unknown)} keyFnOrTemplate the key function,
|
|
38
|
+
* or, when `templateFn` is omitted, the template function (lit's two-argument form)
|
|
39
|
+
* @param {(item: T, i: number) => unknown} [templateFn]
|
|
29
40
|
*/
|
|
30
|
-
export function repeat(items,
|
|
31
|
-
|
|
41
|
+
export function repeat(items, keyFnOrTemplate, templateFn) {
|
|
42
|
+
if (templateFn === undefined) {
|
|
43
|
+
return { [REPEAT]: true, items: [...items], keyFn: indexKey, templateFn: keyFnOrTemplate };
|
|
44
|
+
}
|
|
45
|
+
return { [REPEAT]: true, items: [...items], keyFn: keyFnOrTemplate, templateFn };
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/** @param {unknown} x */
|
|
@@ -37,8 +37,10 @@ export function closestAnchor(target) {
|
|
|
37
37
|
const a = el.closest && el.closest('a');
|
|
38
38
|
if (a instanceof HTMLAnchorElement) return a;
|
|
39
39
|
}
|
|
40
|
+
// Only a real ShadowRoot has a host to cross to (a detached subtree's
|
|
41
|
+
// root can be an <a>, whose `host` is a URL string).
|
|
40
42
|
const root = node.getRootNode ? node.getRootNode() : null;
|
|
41
|
-
node =
|
|
43
|
+
node = typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot ? root.host : null;
|
|
42
44
|
}
|
|
43
45
|
return null;
|
|
44
46
|
}
|
|
@@ -159,6 +159,20 @@ export function collectBoundaries(root) {
|
|
|
159
159
|
* live: { routeKey: string, start: Comment, end: Comment },
|
|
160
160
|
* incoming: { routeKey: string, start: Comment, end: Comment } } | null}
|
|
161
161
|
*/
|
|
162
|
+
/**
|
|
163
|
+
* Does segment `inner` sit at or below segment `outer`. Boundary segments are
|
|
164
|
+
* nested path prefixes, so `/ui/docs` is within `/ui` and within `/`, and
|
|
165
|
+
* `/uix` is within neither `/ui` nor `/ui/docs`. The one statement of the
|
|
166
|
+
* nesting rule: the swap plan below and the stylesheet ownership in
|
|
167
|
+
* owned-styles.js both use it, so they cannot disagree about which range a
|
|
168
|
+
* segment belongs to.
|
|
169
|
+
* @param {string} inner
|
|
170
|
+
* @param {string} outer
|
|
171
|
+
*/
|
|
172
|
+
export function segmentWithin(inner, outer) {
|
|
173
|
+
return inner === outer || inner.startsWith(outer === '/' ? '/' : outer + '/');
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
export function planBoundarySwap(here, there) {
|
|
163
177
|
// Shared segments, shallowest first (a nested path prefix is shorter).
|
|
164
178
|
const shared = [...here.keys()].filter((s) => there.has(s)).sort((a, b) => a.length - b.length);
|
|
@@ -169,7 +183,7 @@ export function planBoundarySwap(here, there) {
|
|
|
169
183
|
let parent = null;
|
|
170
184
|
for (const p of shared) {
|
|
171
185
|
if (p === seg) break;
|
|
172
|
-
if (seg
|
|
186
|
+
if (seg !== p && segmentWithin(seg, p)) parent = p;
|
|
173
187
|
}
|
|
174
188
|
if (!parent) return null; // no anchored parent: degrade
|
|
175
189
|
return { mode: 'replace', segment: parent, live: here.get(parent), incoming: there.get(parent) };
|
|
@@ -204,3 +204,33 @@ export function _isNonHtmlPath(pathname) {
|
|
|
204
204
|
* alone rather than reverted.
|
|
205
205
|
*/
|
|
206
206
|
export const WRITE_BACK_ARMED_MS = 250;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Marks the router's temporary stylesheet copies (`stylesheets.js`). Here,
|
|
210
|
+
* in the leaf module, so `owned-styles.js` can skip those copies without
|
|
211
|
+
* importing `stylesheets.js`, which imports `head-merge.js`, which imports
|
|
212
|
+
* `owned-styles.js`.
|
|
213
|
+
*/
|
|
214
|
+
export const PRELOAD_ATTR = 'data-webjs-preload-style';
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Does `el`'s `rel` name a stylesheet that applies (`stylesheet`, not
|
|
218
|
+
* `alternate`). The one test both the stylesheet loader (`stylesheets.js`) and
|
|
219
|
+
* the owned-sheet merge (`owned-styles.js`) use, so the two cannot disagree.
|
|
220
|
+
* @param {Element} el
|
|
221
|
+
*/
|
|
222
|
+
export function relIsStylesheet(el) {
|
|
223
|
+
const rel = relTokens(el);
|
|
224
|
+
return rel.includes('stylesheet') && !rel.includes('alternate');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The lowercased tokens of `el`'s `rel`, the one parse every stylesheet test
|
|
229
|
+
* in the router builds on (`relIsStylesheet` here, and the head merge's
|
|
230
|
+
* never-remove rule, which counts an alternate stylesheet too).
|
|
231
|
+
* @param {Element} el
|
|
232
|
+
* @returns {string[]}
|
|
233
|
+
*/
|
|
234
|
+
export function relTokens(el) {
|
|
235
|
+
return (el.getAttribute('rel') || '').toLowerCase().split(/\s+/);
|
|
236
|
+
}
|
|
@@ -224,9 +224,25 @@ export function diffElementInPlace(dst, src) {
|
|
|
224
224
|
}
|
|
225
225
|
// Update attributes from src; remove ones not in src.
|
|
226
226
|
const srcAttrs = new Set();
|
|
227
|
+
// A server-passed `.prop` binding on a reused, upgraded component (#1686):
|
|
228
|
+
// its `data-webjs-prop-*` attribute was read once at upgrade and stripped,
|
|
229
|
+
// so writing the new value as an attribute changed nothing and the property
|
|
230
|
+
// stayed stale after a same-page update. A binding whose value changed is
|
|
231
|
+
// handed to the component to decode; an unchanged one is left alone, so a
|
|
232
|
+
// value the component changed locally survives, as in Lit.
|
|
233
|
+
const hydrated = isHydratedComponent(dst) && typeof (/** @type any */ (dst))._hydratePropAttrs === 'function';
|
|
234
|
+
let propsChanged = false;
|
|
227
235
|
for (const attr of src.attributes) {
|
|
228
236
|
srcAttrs.add(attr.name);
|
|
229
237
|
if (LIVE_ATTRS.has(attr.name)) continue;
|
|
238
|
+
if (hydrated && attr.name.startsWith('data-webjs-prop-')) {
|
|
239
|
+
const seen = /** @type any */ (dst).__webjsPropAttrValues;
|
|
240
|
+
if (!seen || seen.get(attr.name) !== attr.value) {
|
|
241
|
+
dst.setAttribute(attr.name, attr.value);
|
|
242
|
+
propsChanged = true;
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
230
246
|
// The serialized-restore stamp is a message to a NOT-YET-UPGRADED
|
|
231
247
|
// element's connectedCallback; copying it onto a live reused host would
|
|
232
248
|
// leave a consume-once marker lingering in the live DOM forever. Note
|
|
@@ -243,6 +259,7 @@ export function diffElementInPlace(dst, src) {
|
|
|
243
259
|
if (LIVE_ATTRS.has(attr.name)) continue;
|
|
244
260
|
if (!srcAttrs.has(attr.name)) dst.removeAttribute(attr.name);
|
|
245
261
|
}
|
|
262
|
+
if (propsChanged) /** @type any */ (dst)._hydratePropAttrs();
|
|
246
263
|
// For form-control-like elements, preserve live IDL state.
|
|
247
264
|
// (`value`, `checked`, `open`, etc.: see LIVE_ATTRS below for full list.)
|
|
248
265
|
// The attribute version is skipped above; we deliberately do nothing
|