@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
package/src/metadata.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* export const metadata: Metadata = { title: 'Home', description: '…' };
|
|
12
12
|
* export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> { … }
|
|
13
13
|
*
|
|
14
|
-
* The shape mirrors what ssr.js actually consumes (and
|
|
15
|
-
*
|
|
14
|
+
* The shape mirrors what ssr.js actually consumes (and
|
|
15
|
+
* https://webjs.dev/docs/metadata-routes documents), NOT Next.js's full superset.
|
|
16
16
|
* Every field is optional. Zero
|
|
17
17
|
* runtime cost: nothing in this file ships to the browser.
|
|
18
18
|
*/
|
|
@@ -209,7 +209,7 @@ export interface PreloadDescriptor {
|
|
|
209
209
|
* optional. Where the framework accepts a string OR an object (`title`,
|
|
210
210
|
* `viewport`, `robots`, `appleWebApp`, `icons`), the type is a union.
|
|
211
211
|
*
|
|
212
|
-
* @see
|
|
212
|
+
* @see https://webjs.dev/docs/metadata-routes for the field-by-field emission reference.
|
|
213
213
|
*/
|
|
214
214
|
export interface Metadata {
|
|
215
215
|
/** -> `<title>`. Plain string or `{ template, default, absolute }`. */
|
|
@@ -337,3 +337,22 @@ export interface MetadataContext {
|
|
|
337
337
|
*/
|
|
338
338
|
actionData?: unknown;
|
|
339
339
|
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A page's or layout's `generateMetadata` export, Next.js's: called per
|
|
343
|
+
* request with the page context and a promise of the metadata the outer
|
|
344
|
+
* layouts resolved, it returns this segment's {@link Metadata}. Export a static
|
|
345
|
+
* `metadata` object instead when nothing depends on the request.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* // app/posts/[slug]/page.ts
|
|
350
|
+
* import type { Metadata, MetadataContext } from '@webjsdev/core';
|
|
351
|
+
* export async function generateMetadata({ params }: MetadataContext, parent: Promise<Metadata>): Promise<Metadata> {
|
|
352
|
+
* const post = await getPost(params.slug);
|
|
353
|
+
* const images = (await parent).openGraph?.images ?? [];
|
|
354
|
+
* return { title: post?.title ?? 'Not found', openGraph: { images } };
|
|
355
|
+
* }
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
export type GenerateMetadata = (ctx: MetadataContext, parent: Promise<Metadata>) => Metadata | Promise<Metadata>;
|
package/src/nav.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function notFound(): never;
|
|
2
|
-
|
|
2
|
+
/** How the client records a redirect that reaches the browser (Next's RedirectType). */
|
|
3
|
+
export type RedirectType = 'push' | 'replace';
|
|
4
|
+
export function redirect(url: string, status?: number | RedirectType | { status?: number; type?: RedirectType }): never;
|
|
5
|
+
export function permanentRedirect(url: string, type?: RedirectType): never;
|
|
3
6
|
export function forbidden(): never;
|
|
4
7
|
export function unauthorized(): never;
|
|
5
8
|
export function isNotFound(e: unknown): boolean;
|
package/src/nav.js
CHANGED
|
@@ -59,31 +59,64 @@ export function unauthorized() {
|
|
|
59
59
|
* each kind of redirect gets the conventional code:
|
|
60
60
|
* - thrown during a GET page/layout render (a gating redirect like an auth
|
|
61
61
|
* bounce to `/login`): 302 Found, the conventional GET-to-GET code.
|
|
62
|
-
* - thrown from a
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* - thrown from a `route.ts` handler: 307 Temporary Redirect.
|
|
63
|
+
* - thrown from a server action bound to a form: always 303 See Other, so
|
|
64
|
+
* the browser follows with a GET and the action runs once (Next parity).
|
|
65
|
+
* - thrown from a server action called over RPC: the client router
|
|
66
|
+
* navigates to the target.
|
|
65
67
|
* Pass an explicit status to override: `redirect('/x', 308)` (permanent) or the
|
|
66
|
-
* options form `redirect('/x', { status: 301 })`. An explicit status wins
|
|
67
|
-
*
|
|
68
|
+
* options form `redirect('/x', { status: 301 })`. An explicit status wins
|
|
69
|
+
* everywhere except a form-bound action, which always answers 303.
|
|
70
|
+
*
|
|
71
|
+
* The second argument may instead be `'push'` or `'replace'` (Next's
|
|
72
|
+
* `RedirectType`): how the client records the navigation when the redirect
|
|
73
|
+
* reaches the browser from an action called over RPC. `'push'` is the
|
|
74
|
+
* default there, as in a Next.js server action.
|
|
68
75
|
*
|
|
69
76
|
* @param {string} url
|
|
70
|
-
* @param {number | { status?: number }} [status]
|
|
71
|
-
*
|
|
72
|
-
*
|
|
77
|
+
* @param {number | RedirectType | { status?: number, type?: RedirectType }} [status]
|
|
78
|
+
* explicit status (number), a `RedirectType`, or `{ status, type }`; omit to
|
|
79
|
+
* let the catching site pick.
|
|
73
80
|
* @returns {never}
|
|
74
81
|
*/
|
|
75
82
|
export function redirect(url, status) {
|
|
76
|
-
|
|
83
|
+
/** @type {number | undefined} */
|
|
84
|
+
let code;
|
|
85
|
+
/** @type {RedirectType | undefined} */
|
|
86
|
+
let type;
|
|
87
|
+
if (status === 'push' || status === 'replace') type = status;
|
|
88
|
+
else if (typeof status === 'object' && status !== null) {
|
|
89
|
+
code = status.status;
|
|
90
|
+
if (status.type === 'push' || status.type === 'replace') type = status.type;
|
|
91
|
+
} else if (typeof status === 'number') code = status;
|
|
77
92
|
const err = new Error(`webjs: redirect(${url})`);
|
|
78
93
|
/** @type any */ (err).__webjs = REDIRECT;
|
|
79
94
|
/** @type any */ (err).url = url;
|
|
80
95
|
// Left undefined when the caller did not specify one, so the catching site
|
|
81
|
-
// can apply its convention (302 GET gate
|
|
82
|
-
// code is stored verbatim and overrides that convention
|
|
96
|
+
// can apply its convention (302 GET gate, 307 route handler, 303 form action).
|
|
97
|
+
// A caller-supplied code is stored verbatim and overrides that convention,
|
|
98
|
+
// except at a form-bound action, which always answers 303.
|
|
83
99
|
/** @type any */ (err).status = typeof code === 'number' ? code : undefined;
|
|
100
|
+
/** @type any */ (err).redirectType = type;
|
|
84
101
|
throw err;
|
|
85
102
|
}
|
|
86
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Throw a PERMANENT redirect sentinel, Next.js's `permanentRedirect()`: a 308
|
|
106
|
+
* Permanent Redirect everywhere except a form-bound action, which answers 303. Use it
|
|
107
|
+
* when a URL has moved for good (a renamed slug); browsers and search engines
|
|
108
|
+
* remember a 308.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} url
|
|
111
|
+
* @param {RedirectType} [type] how an RPC caller's client records the navigation.
|
|
112
|
+
* @returns {never}
|
|
113
|
+
*/
|
|
114
|
+
export function permanentRedirect(url, type) {
|
|
115
|
+
return redirect(url, { status: 308, type });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @typedef {'push' | 'replace'} RedirectType */
|
|
119
|
+
|
|
87
120
|
/** @param {unknown} e */
|
|
88
121
|
export function isNotFound(e) {
|
|
89
122
|
return !!e && /** @type any */ (e).__webjs === NOT_FOUND;
|
package/src/optimistic.js
CHANGED
|
@@ -108,8 +108,8 @@ async function runImperativeOptimistic(signal, value, action) {
|
|
|
108
108
|
* Imperative form (signal-based rollback), for a one-shot toggle:
|
|
109
109
|
* await optimistic(signal, value, () => likePost(postId));
|
|
110
110
|
*
|
|
111
|
-
* Client-only: it calls `host.requestUpdate()` (client work), so
|
|
112
|
-
*
|
|
111
|
+
* Client-only: it calls `host.requestUpdate()` (client work), so it belongs in
|
|
112
|
+
* a component, never in a server-only one.
|
|
113
113
|
*
|
|
114
114
|
* @template State
|
|
115
115
|
* @template Action
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `x-webjs-refresh` header a server action's response carries when it
|
|
3
|
+
* called `revalidatePath()`. A leaf module (no imports), so the router's
|
|
4
|
+
* fetch path and the action stub's effects can both read it without an
|
|
5
|
+
* import cycle.
|
|
6
|
+
*
|
|
7
|
+
* @module refresh-header
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse an `x-webjs-refresh` header: comma-separated, percent-encoded paths,
|
|
12
|
+
* each optionally suffixed `;layout` (Next's `revalidatePath(path, 'layout')`).
|
|
13
|
+
* @param {string | null | undefined} value
|
|
14
|
+
* @returns {{ path: string, layout: boolean }[]}
|
|
15
|
+
*/
|
|
16
|
+
export function parseRefreshHeader(value) {
|
|
17
|
+
if (!value) return [];
|
|
18
|
+
/** @type {{ path: string, layout: boolean }[]} */
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const part of String(value).split(',')) {
|
|
21
|
+
const raw = part.trim();
|
|
22
|
+
if (!raw) continue;
|
|
23
|
+
const layout = raw.endsWith(';layout');
|
|
24
|
+
let path = layout ? raw.slice(0, -';layout'.length) : raw;
|
|
25
|
+
try { path = decodeURIComponent(path); } catch { /* keep as sent */ }
|
|
26
|
+
if (path.startsWith('/')) out.push({ path, layout });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether `pathname` is covered by one refreshed entry. A `[param]` segment
|
|
33
|
+
* matches any one segment, a `[...param]` segment one or more and a
|
|
34
|
+
* `[[...param]]` segment zero or more of the rest
|
|
35
|
+
* (Next lets `revalidatePath` name a route pattern); a `layout` entry also
|
|
36
|
+
* covers every path below it.
|
|
37
|
+
* @param {{ path: string, layout: boolean }} entry
|
|
38
|
+
* @param {string} pathname
|
|
39
|
+
*/
|
|
40
|
+
export function refreshCovers(entry, pathname) {
|
|
41
|
+
const want = entry.path.split('/').filter(Boolean);
|
|
42
|
+
const have = pathname.split('/').filter(Boolean);
|
|
43
|
+
for (let i = 0; i < want.length; i++) {
|
|
44
|
+
const w = want[i];
|
|
45
|
+
if (/^\[\[\.\.\./.test(w)) return true;
|
|
46
|
+
if (/^\[\.\.\./.test(w)) return have.length > i;
|
|
47
|
+
if (i >= have.length) return false;
|
|
48
|
+
if (/^\[[^\]]+\]$/.test(w)) continue;
|
|
49
|
+
if (w !== have[i]) return false;
|
|
50
|
+
}
|
|
51
|
+
return entry.layout || have.length === want.length;
|
|
52
|
+
}
|
package/src/registry.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* @typedef {{ cls: typeof import('./component.js').WebComponent, moduleUrl: string | null, lazy: boolean, tag: string }} RegistryEntry
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { hotReplacer } from './hot.js';
|
|
21
|
+
|
|
20
22
|
/* ------------------------------------------------------------------
|
|
21
23
|
* Shared registry on globalThis.
|
|
22
24
|
*
|
|
@@ -106,7 +108,11 @@ function registerInternal(tag, cls) {
|
|
|
106
108
|
if (isBrowser) {
|
|
107
109
|
const native = customElements.define.bind(customElements);
|
|
108
110
|
/** @type {any} */ (customElements).define = function (tag, cls, options) {
|
|
109
|
-
|
|
111
|
+
const prev = customElements.get(tag);
|
|
112
|
+
if (!prev) native(tag, cls, options);
|
|
113
|
+
// A re-define of a known tag under the dev hot client (#1689) patches the
|
|
114
|
+
// registered class in place, so the registry keeps the class the browser uses.
|
|
115
|
+
else if (prev !== cls) { const replace = hotReplacer(); if (replace) cls = replace(tag, prev, cls); }
|
|
110
116
|
registerInternal(tag, cls);
|
|
111
117
|
};
|
|
112
118
|
}
|
|
@@ -152,6 +158,9 @@ if (!isBrowser) {
|
|
|
152
158
|
* @param {typeof import('./component.js').WebComponent} cls
|
|
153
159
|
*/
|
|
154
160
|
export function register(tag, cls) {
|
|
161
|
+
// Lit's `finalize()`, so the class's metadata is ready before the first
|
|
162
|
+
// instance (a class not built on WebComponent has none).
|
|
163
|
+
if (typeof (/** @type {any} */ (cls)).finalize === 'function') /** @type {any} */ (cls).finalize();
|
|
155
164
|
if (isBrowser) {
|
|
156
165
|
/** @type any */ (customElements).define(tag, cls);
|
|
157
166
|
} else {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydration slot adoption: keep a light-DOM host's SSR-projected children in
|
|
3
|
+
* the document while its first client render replaces the SSR markup (#1666).
|
|
4
|
+
*
|
|
5
|
+
* Hydration renders the template from a fresh clone and swaps it in for the
|
|
6
|
+
* SSR markup. The projected children sit inside the SSR `<slot>`, so swapping
|
|
7
|
+
* the SSR tree out took them out of the document, and the first slot apply put
|
|
8
|
+
* them back a microtask later. A removal is not free: the HTML spec navigates
|
|
9
|
+
* a re-inserted iframe again (and `loading="lazy"` defers only the FIRST
|
|
10
|
+
* insertion, so a hidden lazy frame loads), and every nested custom element
|
|
11
|
+
* disconnects and reconnects.
|
|
12
|
+
*
|
|
13
|
+
* So for each of the template's own slots whose SSR counterpart holds exactly
|
|
14
|
+
* the nodes the host's record adopted, the SSR slot and its ancestor chain up
|
|
15
|
+
* to the host are KEPT: the parts that target a fresh element on that chain
|
|
16
|
+
* are pointed at its live twin, and the fresh siblings around the chain are
|
|
17
|
+
* inserted into the live elements instead of the live tree being replaced. No
|
|
18
|
+
* kept node moves, so the projected children never leave the document. The
|
|
19
|
+
* fresh slot's cloned fallback goes to the slot's holding fragment, where the
|
|
20
|
+
* slot part would have put it.
|
|
21
|
+
*
|
|
22
|
+
* Conservative by construction: a slot is adopted only when every link of
|
|
23
|
+
* both chains matches by tag, no link is a custom element (whose SSR markup is
|
|
24
|
+
* its own rendered template) or a `<form>` (form-action reconciliation inserts
|
|
25
|
+
* children into it), the slot's name is static, and the kept nodes sit in the
|
|
26
|
+
* same relative order on both sides. Anything else, a slot inside a
|
|
27
|
+
* conditional template among them, falls back to the plain swap.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_OWNER_ATTR,
|
|
33
|
+
ensureSlotState, hasSlotState, keyOfName,
|
|
34
|
+
} from '../slot.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live SSR slots a hydration render adopted. The slot part's finalize reads
|
|
38
|
+
* it to leave the slot's children (the projected nodes) in place instead of
|
|
39
|
+
* harvesting them as fallback, since the holding fragment is already set.
|
|
40
|
+
* @type {WeakSet<Element>}
|
|
41
|
+
*/
|
|
42
|
+
export const adoptedSlots = new WeakSet();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{ map: Map<Node, Element>, slots: [Element, Element][], chains: [Element, Element][] }} AdoptionPlan
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plan the adoption for a hydration render, after the parts are bound and
|
|
50
|
+
* before any is applied. Returns null when no slot qualifies.
|
|
51
|
+
*
|
|
52
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
53
|
+
* @param {DocumentFragment} frag the fresh clone
|
|
54
|
+
* @param {any[]} bound the template's bound parts
|
|
55
|
+
* @returns {AdoptionPlan | null}
|
|
56
|
+
*/
|
|
57
|
+
export function planSlotAdoption(container, frag, bound) {
|
|
58
|
+
if (/** @type {any} */ (container).nodeType !== 1 || !hasSlotState(/** @type {Element} */ (container))) return null;
|
|
59
|
+
const state = ensureSlotState(/** @type {Element} */ (container));
|
|
60
|
+
/** @type {Set<Element>} */
|
|
61
|
+
const dynamicName = new Set();
|
|
62
|
+
for (const p of bound) {
|
|
63
|
+
if ((p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') && p.name === 'name') dynamicName.add(p.el);
|
|
64
|
+
}
|
|
65
|
+
/** @type {Map<Node, Element>} fresh element to its live twin */
|
|
66
|
+
const map = new Map();
|
|
67
|
+
/** @type {Set<Element>} */
|
|
68
|
+
const usedLive = new Set();
|
|
69
|
+
/** @type {[Element, Element][]} */
|
|
70
|
+
const slots = [];
|
|
71
|
+
/** @type {[Element, Element][]} */
|
|
72
|
+
const chains = [];
|
|
73
|
+
for (const p of bound) {
|
|
74
|
+
if (p.kind !== 'slot') continue;
|
|
75
|
+
const slotF = /** @type {Element} */ (p.slotEl);
|
|
76
|
+
if (dynamicName.has(slotF)) continue;
|
|
77
|
+
const assigned = state.assignedByName.get(keyOfName(slotF.getAttribute('name')));
|
|
78
|
+
if (!assigned || assigned.length === 0) continue;
|
|
79
|
+
const slotL = /** @type {Element | null} */ (assigned[0].parentNode);
|
|
80
|
+
if (!slotL || slotL.nodeType !== 1 || slotL.localName !== 'slot' || usedLive.has(slotL)) continue;
|
|
81
|
+
if (slotL.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) continue;
|
|
82
|
+
if (!sameNodes(slotL.childNodes, assigned)) continue;
|
|
83
|
+
const fChain = ancestorsUpTo(slotF, frag);
|
|
84
|
+
const lChain = ancestorsUpTo(slotL, container);
|
|
85
|
+
if (!fChain || !lChain || fChain.length !== lChain.length) continue;
|
|
86
|
+
let ok = true;
|
|
87
|
+
for (let i = 0; i < fChain.length && ok; i++) {
|
|
88
|
+
const f = fChain[i];
|
|
89
|
+
const l = lChain[i];
|
|
90
|
+
const name = f.localName;
|
|
91
|
+
if (name !== l.localName || name.includes('-') || name === 'form') ok = false;
|
|
92
|
+
else {
|
|
93
|
+
const prior = map.get(f);
|
|
94
|
+
if (prior ? prior !== l : usedLive.has(l)) ok = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!ok) continue;
|
|
98
|
+
map.set(slotF, slotL);
|
|
99
|
+
usedLive.add(slotL);
|
|
100
|
+
slots.push([slotF, slotL]);
|
|
101
|
+
for (let i = 0; i < fChain.length; i++) {
|
|
102
|
+
if (map.has(fChain[i])) continue;
|
|
103
|
+
map.set(fChain[i], lChain[i]);
|
|
104
|
+
usedLive.add(lChain[i]);
|
|
105
|
+
chains.push([fChain[i], lChain[i]]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (slots.length === 0) return null;
|
|
109
|
+
// No kept node may have to move: under each kept parent the kept children
|
|
110
|
+
// must already sit in the order the template puts them in.
|
|
111
|
+
for (const fParent of [frag, ...chains.map(([f]) => f)]) {
|
|
112
|
+
let last = null;
|
|
113
|
+
for (const child of fParent.childNodes) {
|
|
114
|
+
const live = map.get(child);
|
|
115
|
+
if (!live) continue;
|
|
116
|
+
if (last && !(last.compareDocumentPosition(live) & Node.DOCUMENT_POSITION_FOLLOWING)) return null;
|
|
117
|
+
last = live;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { map, slots, chains };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Point every bound part that targets a kept fresh element at its live twin,
|
|
125
|
+
* and give each kept element the fresh one's static attributes. Runs before
|
|
126
|
+
* the parts apply, so every dynamic value lands on the live element directly.
|
|
127
|
+
*
|
|
128
|
+
* @param {AdoptionPlan} plan
|
|
129
|
+
* @param {any[]} bound
|
|
130
|
+
*/
|
|
131
|
+
export function retargetParts(plan, bound) {
|
|
132
|
+
/** @type {Map<Element, Set<string>>} attributes a part owns, per live element */
|
|
133
|
+
const owned = new Map();
|
|
134
|
+
for (const p of bound) {
|
|
135
|
+
if (p.kind === 'slot') {
|
|
136
|
+
const live = plan.map.get(p.slotEl);
|
|
137
|
+
if (live) p.slotEl = live;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!p.el) continue;
|
|
141
|
+
const live = plan.map.get(p.el);
|
|
142
|
+
if (!live) continue;
|
|
143
|
+
// An event part registers its dispatcher in applyPart, with the handler as
|
|
144
|
+
// the options bag (capture / once / passive). This runs before the first
|
|
145
|
+
// apply, so usually nothing is registered yet; move a registration only
|
|
146
|
+
// when one exists, with the SAME options, or a capture listener would be
|
|
147
|
+
// registered twice and a `once` listener would lose its `once`.
|
|
148
|
+
if (p.kind === 'event' && p.handler) {
|
|
149
|
+
p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
150
|
+
live.addEventListener(p.name, p.dispatcher, p.handler);
|
|
151
|
+
}
|
|
152
|
+
p.el = live;
|
|
153
|
+
if (p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') {
|
|
154
|
+
let names = owned.get(live);
|
|
155
|
+
if (!names) owned.set(live, names = new Set());
|
|
156
|
+
names.add(p.name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const [fresh, live] of [...plan.chains, ...plan.slots]) {
|
|
160
|
+
const isSlot = live.localName === 'slot';
|
|
161
|
+
const names = owned.get(live);
|
|
162
|
+
for (const a of Array.from(live.attributes)) {
|
|
163
|
+
if (fresh.hasAttribute(a.name) || names?.has(a.name)) continue;
|
|
164
|
+
// The SSR projection stamps stay: the slot IS projected, and the owner
|
|
165
|
+
// carrier is what isOwnSlot reads until the owner symbol is stamped.
|
|
166
|
+
if (isSlot && (a.name === PROJECTION_ATTR || a.name === SLOT_OWNER_ATTR)) continue;
|
|
167
|
+
live.removeAttribute(a.name);
|
|
168
|
+
}
|
|
169
|
+
for (const a of Array.from(fresh.attributes)) {
|
|
170
|
+
if (live.getAttribute(a.name) !== a.value) live.setAttribute(a.name, a.value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Commit the fresh template into the container around the kept live nodes:
|
|
177
|
+
* every live child that is not kept is removed, every fresh node is inserted
|
|
178
|
+
* in template order, and no kept node moves. The fresh slots' cloned fallback
|
|
179
|
+
* goes to each kept slot's holding fragment.
|
|
180
|
+
*
|
|
181
|
+
* @param {AdoptionPlan} plan
|
|
182
|
+
* @param {Element} container
|
|
183
|
+
* @param {Node[]} top the container's new children, bookends included
|
|
184
|
+
*/
|
|
185
|
+
export function commitAdoption(plan, container, top) {
|
|
186
|
+
const mapped = (/** @type {Node} */ n) => plan.map.get(n) || n;
|
|
187
|
+
const state = ensureSlotState(container);
|
|
188
|
+
for (const [fresh, live] of plan.chains) mergeChildren(live, Array.from(fresh.childNodes, mapped));
|
|
189
|
+
mergeChildren(container, top.map(mapped));
|
|
190
|
+
for (const [fresh, live] of plan.slots) {
|
|
191
|
+
const fallback = document.createDocumentFragment();
|
|
192
|
+
while (fresh.firstChild) fallback.appendChild(fresh.firstChild);
|
|
193
|
+
/** @type {any} */ (live)[SLOT_FALLBACK_FRAG] = fallback;
|
|
194
|
+
adoptedSlots.add(live);
|
|
195
|
+
// Forget the adopted snapshot so the first apply reports the initial
|
|
196
|
+
// projection (one slotchange), exactly as the plain swap did.
|
|
197
|
+
state.lastSnapshot.delete(live);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Make `parent`'s children exactly `want`, removing what is not wanted and
|
|
203
|
+
* inserting the rest, without moving a node already in place in order.
|
|
204
|
+
*
|
|
205
|
+
* @param {Node} parent
|
|
206
|
+
* @param {Node[]} want
|
|
207
|
+
*/
|
|
208
|
+
function mergeChildren(parent, want) {
|
|
209
|
+
const keep = new Set(want);
|
|
210
|
+
for (const c of Array.from(parent.childNodes)) {
|
|
211
|
+
if (!keep.has(c)) parent.removeChild(c);
|
|
212
|
+
}
|
|
213
|
+
let cursor = parent.firstChild;
|
|
214
|
+
for (const node of want) {
|
|
215
|
+
if (node === cursor) {
|
|
216
|
+
cursor = cursor.nextSibling;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
parent.insertBefore(node, cursor);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The element ancestors of `node` below `root`, nearest first, or null when
|
|
225
|
+
* `root` is not an ancestor.
|
|
226
|
+
*
|
|
227
|
+
* @param {Node} node
|
|
228
|
+
* @param {Node} root
|
|
229
|
+
* @returns {Element[] | null}
|
|
230
|
+
*/
|
|
231
|
+
function ancestorsUpTo(node, root) {
|
|
232
|
+
const out = [];
|
|
233
|
+
let p = node.parentNode;
|
|
234
|
+
while (p && p !== root) {
|
|
235
|
+
if (p.nodeType !== 1) return null;
|
|
236
|
+
out.push(/** @type {Element} */ (p));
|
|
237
|
+
p = p.parentNode;
|
|
238
|
+
}
|
|
239
|
+
return p === root ? out : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {NodeListOf<ChildNode>} list
|
|
244
|
+
* @param {Node[]} nodes
|
|
245
|
+
*/
|
|
246
|
+
function sameNodes(list, nodes) {
|
|
247
|
+
if (list.length !== nodes.length) return false;
|
|
248
|
+
for (let i = 0; i < nodes.length; i++) if (list[i] !== nodes[i]) return false;
|
|
249
|
+
return true;
|
|
250
|
+
}
|