@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/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 {
|
|
@@ -140,9 +140,14 @@ export function retargetParts(plan, bound) {
|
|
|
140
140
|
if (!p.el) continue;
|
|
141
141
|
const live = plan.map.get(p.el);
|
|
142
142
|
if (!live) continue;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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);
|
|
146
151
|
}
|
|
147
152
|
p.el = live;
|
|
148
153
|
if (p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') {
|