@webjsdev/core 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +2 -2
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/index-browser.js +10 -2
- package/index.d.ts +52 -1
- package/index.js +9 -2
- package/package.json +1 -1
- package/src/component.js +114 -0
- package/src/directives.js +7 -1
- package/src/metadata.d.ts +279 -0
- package/src/render-server.js +22 -8
- package/src/router-client.js +17 -0
- package/src/routes.d.ts +136 -0
- package/src/webjs-config.d.ts +179 -0
package/index-browser.js
CHANGED
|
@@ -48,8 +48,16 @@ export { WebjsFrame } from './src/webjs-frame.js';
|
|
|
48
48
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
49
49
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
50
50
|
|
|
51
|
-
// Directives, also available via '@webjsdev/core/directives'
|
|
52
|
-
|
|
51
|
+
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
52
|
+
// lit-html-parity set is re-exported here so the dist browser bundle (which
|
|
53
|
+
// the `@webjsdev/core/directives` subpath collapses onto in dist mode) carries
|
|
54
|
+
// every directive, matching what src/directives.js exports in dev.
|
|
55
|
+
export {
|
|
56
|
+
unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
|
|
57
|
+
templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
|
|
58
|
+
until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
|
|
59
|
+
watch, isWatch,
|
|
60
|
+
} from './src/directives.js';
|
|
53
61
|
|
|
54
62
|
// Context Protocol, also available via '@webjsdev/core/context'
|
|
55
63
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
package/index.d.ts
CHANGED
|
@@ -9,6 +9,50 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export * from './src/component.d.ts';
|
|
12
|
+
export type {
|
|
13
|
+
Metadata,
|
|
14
|
+
MetadataContext,
|
|
15
|
+
TitleMetadata,
|
|
16
|
+
ViewportMetadata,
|
|
17
|
+
RobotsMetadata,
|
|
18
|
+
AlternatesMetadata,
|
|
19
|
+
VerificationMetadata,
|
|
20
|
+
OpenGraphMetadata,
|
|
21
|
+
TwitterMetadata,
|
|
22
|
+
AppleWebAppMetadata,
|
|
23
|
+
IconsMetadata,
|
|
24
|
+
IconDescriptor,
|
|
25
|
+
AuthorMetadata,
|
|
26
|
+
PreloadDescriptor,
|
|
27
|
+
JsonLd,
|
|
28
|
+
} from './src/metadata.d.ts';
|
|
29
|
+
|
|
30
|
+
// Typed route props + the opt-in generated route union (#258). The
|
|
31
|
+
// `WebjsRoutes` / `RouteParamMap` interfaces are exported as VALUES-of-types
|
|
32
|
+
// (interfaces) so app code and the generated `.webjs/routes.d.ts` can augment
|
|
33
|
+
// them via `declare module '@webjsdev/core'`.
|
|
34
|
+
export type {
|
|
35
|
+
Route,
|
|
36
|
+
RouteParams,
|
|
37
|
+
PageProps,
|
|
38
|
+
LayoutProps,
|
|
39
|
+
RouteHandlerContext,
|
|
40
|
+
WebjsRoutes,
|
|
41
|
+
RouteParamMap,
|
|
42
|
+
} from './src/routes.d.ts';
|
|
43
|
+
|
|
44
|
+
// The package.json `webjs` config block (#259). Typed reference for the
|
|
45
|
+
// elide / headers / redirects / trailingSlash / csp / ingress-limit knobs;
|
|
46
|
+
// the companion JSON Schema (packages/server/webjs-config.schema.json) gives
|
|
47
|
+
// editors native validation of package.json itself.
|
|
48
|
+
export type {
|
|
49
|
+
WebjsConfig,
|
|
50
|
+
WebjsHeaderRule,
|
|
51
|
+
WebjsHeaderDirective,
|
|
52
|
+
WebjsRedirectRule,
|
|
53
|
+
WebjsTrailingSlash,
|
|
54
|
+
WebjsCspConfig,
|
|
55
|
+
} from './src/webjs-config.d.ts';
|
|
12
56
|
|
|
13
57
|
export { html, isTemplate, MARKER } from './src/html.js';
|
|
14
58
|
export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
|
|
@@ -22,7 +66,14 @@ export { repeat, isRepeat } from './src/repeat.js';
|
|
|
22
66
|
export { Suspense, isSuspense } from './src/suspense.js';
|
|
23
67
|
export { connectWS } from './src/websocket-client.js';
|
|
24
68
|
export { richFetch } from './src/rich-fetch.js';
|
|
25
|
-
export { enableClientRouter, disableClientRouter
|
|
69
|
+
export { enableClientRouter, disableClientRouter } from './src/router-client.js';
|
|
70
|
+
|
|
71
|
+
// `navigate` is typed against the generated `Route` union (#258) rather than
|
|
72
|
+
// the JSDoc `string`. Until an app runs `webjs types`, `Route` resolves to
|
|
73
|
+
// `string`, so this is non-breaking; once generated, a bogus in-app path is a
|
|
74
|
+
// tsserver error. The runtime is the same async function in router-client.js.
|
|
75
|
+
import type { Route } from './src/routes.d.ts';
|
|
76
|
+
export function navigate(url: Route, opts?: { replace?: boolean }): Promise<void>;
|
|
26
77
|
export { unsafeHTML, isUnsafeHTML, live, isLive } from './src/directives.js';
|
|
27
78
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
|
28
79
|
export { Task, TaskStatus } from './src/task.js';
|
package/index.js
CHANGED
|
@@ -30,8 +30,15 @@ export { WebjsFrame } from './src/webjs-frame.js';
|
|
|
30
30
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
31
31
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
32
32
|
|
|
33
|
-
// Directives, also available via '@webjsdev/core/directives'
|
|
34
|
-
|
|
33
|
+
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
34
|
+
// lit-html-parity set is re-exported so the bare specifier exposes the same
|
|
35
|
+
// directive surface in Node as the browser bundle does.
|
|
36
|
+
export {
|
|
37
|
+
unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
|
|
38
|
+
templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
|
|
39
|
+
until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
|
|
40
|
+
watch, isWatch,
|
|
41
|
+
} from './src/directives.js';
|
|
35
42
|
|
|
36
43
|
// Context Protocol, also available via '@webjsdev/core/context'
|
|
37
44
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
package/package.json
CHANGED
package/src/component.js
CHANGED
|
@@ -260,6 +260,96 @@ class ServerElement {
|
|
|
260
260
|
return [...this.__ssrAttrs.keys()];
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Minimal `Element.closest()` for SSR. The SSR walker threads the chain
|
|
265
|
+
* of enclosing custom-element instances into each instance
|
|
266
|
+
* (`__ssrAncestors`); this walks self-then-ancestors and returns the
|
|
267
|
+
* nearest whose tag matches. Only bare tag-name selectors are supported
|
|
268
|
+
* server-side (`closest('ui-tabs')`), which is what compound components
|
|
269
|
+
* need to read parent state for a correct first paint; anything more
|
|
270
|
+
* specific (class, attribute, or descendant selectors) returns null,
|
|
271
|
+
* matching the pre-shim behaviour. The browser runs the real `closest()`
|
|
272
|
+
* on hydration.
|
|
273
|
+
* @param {string} selector
|
|
274
|
+
* @returns {any}
|
|
275
|
+
*/
|
|
276
|
+
closest(selector) {
|
|
277
|
+
const sel = String(selector).trim().toLowerCase();
|
|
278
|
+
// Tag-name selectors only at SSR; bail (null) on anything else.
|
|
279
|
+
if (!/^[a-z][a-z0-9-]*$/.test(sel)) return null;
|
|
280
|
+
if (this.__ssrTag === sel) return this;
|
|
281
|
+
const chain = this.__ssrAncestors;
|
|
282
|
+
if (!Array.isArray(chain)) return null;
|
|
283
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
284
|
+
if (chain[i] && chain[i].__ssrTag === sel) return chain[i];
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `HTMLElement.dataset`: a live view over the element's `data-*`
|
|
291
|
+
* attributes, backed by the SSR attribute Map. Reading / writing
|
|
292
|
+
* `el.dataset.fooBar` maps to the `data-foo-bar` attribute (camelCase
|
|
293
|
+
* to kebab-case), so a `render()` that sets `this.dataset.state = 'on'`
|
|
294
|
+
* surfaces `data-state="on"` in the SSR'd host tag instead of crashing
|
|
295
|
+
* on an undefined `dataset`.
|
|
296
|
+
* @returns {Record<string, string>}
|
|
297
|
+
*/
|
|
298
|
+
get dataset() {
|
|
299
|
+
if (this.__dataset) return this.__dataset;
|
|
300
|
+
const el = this;
|
|
301
|
+
const toAttr = (p) => 'data-' + String(p).replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
302
|
+
this.__dataset = new Proxy(/** @type {Record<string,string>} */ ({}), {
|
|
303
|
+
get(_t, prop) {
|
|
304
|
+
if (typeof prop !== 'string') return undefined;
|
|
305
|
+
const v = el.getAttribute(toAttr(prop));
|
|
306
|
+
return v === null ? undefined : v;
|
|
307
|
+
},
|
|
308
|
+
set(_t, prop, value) {
|
|
309
|
+
if (typeof prop === 'string') el.setAttribute(toAttr(prop), value);
|
|
310
|
+
return true;
|
|
311
|
+
},
|
|
312
|
+
has(_t, prop) {
|
|
313
|
+
return typeof prop === 'string' && el.hasAttribute(toAttr(prop));
|
|
314
|
+
},
|
|
315
|
+
deleteProperty(_t, prop) {
|
|
316
|
+
if (typeof prop === 'string') el.removeAttribute(toAttr(prop));
|
|
317
|
+
return true;
|
|
318
|
+
},
|
|
319
|
+
ownKeys() {
|
|
320
|
+
return el.getAttributeNames()
|
|
321
|
+
.filter((n) => n.startsWith('data-'))
|
|
322
|
+
.map((n) => n.slice(5).replace(/-([a-z])/g, (_m, c) => c.toUpperCase()));
|
|
323
|
+
},
|
|
324
|
+
getOwnPropertyDescriptor(_t, prop) {
|
|
325
|
+
if (typeof prop === 'string' && el.hasAttribute(toAttr(prop))) {
|
|
326
|
+
return { enumerable: true, configurable: true, value: el.getAttribute(toAttr(prop)) };
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return this.__dataset;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// IDL properties that reflect to a content attribute. A render() that
|
|
335
|
+
// mutates these on the host (a light-DOM compound-component pattern, e.g.
|
|
336
|
+
// `this.className = ...`, `this.hidden = !active`) then surfaces the
|
|
337
|
+
// matching attribute in the SSR'd host tag, matching the browser.
|
|
338
|
+
get className() { return this.getAttribute('class') ?? ''; }
|
|
339
|
+
set className(v) { this.setAttribute('class', v); }
|
|
340
|
+
get hidden() { return this.hasAttribute('hidden'); }
|
|
341
|
+
set hidden(v) { this.toggleAttribute('hidden', !!v); }
|
|
342
|
+
get id() { return this.getAttribute('id') ?? ''; }
|
|
343
|
+
set id(v) { this.setAttribute('id', v); }
|
|
344
|
+
get title() { return this.getAttribute('title') ?? ''; }
|
|
345
|
+
set title(v) { this.setAttribute('title', v); }
|
|
346
|
+
get slot() { return this.getAttribute('slot') ?? ''; }
|
|
347
|
+
set slot(v) { this.setAttribute('slot', v); }
|
|
348
|
+
get role() { return this.getAttribute('role'); }
|
|
349
|
+
set role(v) { v == null ? this.removeAttribute('role') : this.setAttribute('role', v); }
|
|
350
|
+
get tabIndex() { const v = this.getAttribute('tabindex'); return v === null ? -1 : (Number.parseInt(v, 10) || 0); }
|
|
351
|
+
set tabIndex(v) { this.setAttribute('tabindex', String(v)); }
|
|
352
|
+
|
|
263
353
|
// No server event loop: listeners never fire at SSR. The no-op keeps a
|
|
264
354
|
// constructor that wires delegated listeners (a common lit pattern) from
|
|
265
355
|
// crashing; the browser re-runs the constructor on hydration where the
|
|
@@ -282,6 +372,30 @@ class ServerElement {
|
|
|
282
372
|
}
|
|
283
373
|
}
|
|
284
374
|
|
|
375
|
+
// ARIAMixin IDL reflections (`el.ariaPressed = 'true'` writes aria-pressed).
|
|
376
|
+
// A render() that sets ARIA state via the IDL properties then surfaces the
|
|
377
|
+
// matching aria-* attribute in the SSR'd host tag, matching the browser. The
|
|
378
|
+
// IDL name maps to the content attribute by lowercasing the part after
|
|
379
|
+
// `aria` and prefixing `aria-` (ariaPressed -> aria-pressed).
|
|
380
|
+
const ARIA_IDL_PROPS = [
|
|
381
|
+
'ariaAtomic', 'ariaAutoComplete', 'ariaBusy', 'ariaChecked', 'ariaColCount',
|
|
382
|
+
'ariaColIndex', 'ariaColSpan', 'ariaCurrent', 'ariaDescription', 'ariaDisabled',
|
|
383
|
+
'ariaExpanded', 'ariaHasPopup', 'ariaHidden', 'ariaInvalid', 'ariaKeyShortcuts',
|
|
384
|
+
'ariaLabel', 'ariaLevel', 'ariaLive', 'ariaModal', 'ariaMultiLine',
|
|
385
|
+
'ariaMultiSelectable', 'ariaOrientation', 'ariaPlaceholder', 'ariaPosInSet',
|
|
386
|
+
'ariaPressed', 'ariaReadOnly', 'ariaRequired', 'ariaRoleDescription',
|
|
387
|
+
'ariaRowCount', 'ariaRowIndex', 'ariaRowSpan', 'ariaSelected', 'ariaSetSize',
|
|
388
|
+
'ariaSort', 'ariaValueMax', 'ariaValueMin', 'ariaValueNow', 'ariaValueText',
|
|
389
|
+
];
|
|
390
|
+
for (const idl of ARIA_IDL_PROPS) {
|
|
391
|
+
const attr = 'aria-' + idl.slice(4).toLowerCase();
|
|
392
|
+
Object.defineProperty(ServerElement.prototype, idl, {
|
|
393
|
+
configurable: true,
|
|
394
|
+
get() { return this.getAttribute(attr); },
|
|
395
|
+
set(v) { v == null ? this.removeAttribute(attr) : this.setAttribute(attr, String(v)); },
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
285
399
|
// Base class choice: real HTMLElement on the browser, the shim on the server.
|
|
286
400
|
const Base = isBrowser ? HTMLElement : /** @type {any} */ (ServerElement);
|
|
287
401
|
|
package/src/directives.js
CHANGED
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
* } from '@webjsdev/core/directives';
|
|
13
13
|
* ```
|
|
14
14
|
*
|
|
15
|
-
* `repeat()`
|
|
15
|
+
* `repeat()` is implemented in `./repeat.js`; it is re-exported here so the
|
|
16
|
+
* `@webjsdev/core/directives` subpath exposes the whole documented directive
|
|
17
|
+
* table in src/dev mode too (the dist browser bundle already carries it).
|
|
16
18
|
*
|
|
17
19
|
* @module directives
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
// Re-export so the `/directives` subpath surface matches the documented table
|
|
23
|
+
// in both src and dist mode. The implementation stays in repeat.js.
|
|
24
|
+
export { repeat, isRepeat } from './repeat.js';
|
|
25
|
+
|
|
20
26
|
/* ================================================================
|
|
21
27
|
* unsafeHTML
|
|
22
28
|
* ================================================================ */
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript overlay for the `metadata` / `generateMetadata` return shape.
|
|
3
|
+
*
|
|
4
|
+
* A page or layout exports `metadata` (static) or `generateMetadata(ctx)`
|
|
5
|
+
* (request-scoped). Both return a plain object that the SSR pipeline
|
|
6
|
+
* (packages/server/src/ssr.js) reads field by field and emits into the
|
|
7
|
+
* document `<head>`. This file types that object so authors get
|
|
8
|
+
* autocomplete and typo checking:
|
|
9
|
+
*
|
|
10
|
+
* import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
* export const metadata: Metadata = { title: 'Home', description: '…' };
|
|
12
|
+
* export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> { … }
|
|
13
|
+
*
|
|
14
|
+
* The shape mirrors what ssr.js actually consumes (and agent-docs/metadata.md
|
|
15
|
+
* documents), NOT Next.js's full superset. Every field is optional. Zero
|
|
16
|
+
* runtime cost: nothing in this file ships to the browser.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A URL or relative path. Relative values resolve against `metadataBase`. */
|
|
20
|
+
type MetadataUrl = string;
|
|
21
|
+
|
|
22
|
+
/** `title` accepts a plain string or the template object Next.js uses. */
|
|
23
|
+
export type TitleMetadata =
|
|
24
|
+
| string
|
|
25
|
+
| {
|
|
26
|
+
/** Wraps a deeper plain-string title via the `%s` placeholder. */
|
|
27
|
+
template?: string;
|
|
28
|
+
/** Title used when a deeper layer supplies none. */
|
|
29
|
+
default?: string;
|
|
30
|
+
/** Bypasses the inherited template entirely for this layer. */
|
|
31
|
+
absolute?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** An author entry: a bare name, or `{ name, url? }`. */
|
|
35
|
+
export type AuthorMetadata = string | { name?: string; url?: MetadataUrl };
|
|
36
|
+
|
|
37
|
+
/** `viewport` object form (Next.js 14+ split-export shape). */
|
|
38
|
+
export interface ViewportMetadata {
|
|
39
|
+
width?: string | number;
|
|
40
|
+
height?: string | number;
|
|
41
|
+
initialScale?: number;
|
|
42
|
+
minimumScale?: number;
|
|
43
|
+
maximumScale?: number;
|
|
44
|
+
/** `false` emits `user-scalable=no`, `true` emits `yes`. */
|
|
45
|
+
userScalable?: boolean;
|
|
46
|
+
viewportFit?: 'auto' | 'contain' | 'cover';
|
|
47
|
+
interactiveWidget?: 'resizes-visual' | 'resizes-content' | 'overlays-content';
|
|
48
|
+
/** May also carry `themeColor` / `colorScheme` on the split `viewport` export. */
|
|
49
|
+
themeColor?: string;
|
|
50
|
+
colorScheme?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `robots` object form (a bare string is also accepted). */
|
|
54
|
+
export interface RobotsMetadata {
|
|
55
|
+
index?: boolean;
|
|
56
|
+
follow?: boolean;
|
|
57
|
+
noarchive?: boolean;
|
|
58
|
+
nosnippet?: boolean;
|
|
59
|
+
noimageindex?: boolean;
|
|
60
|
+
/** Emitted verbatim as `<meta name="googlebot">`. */
|
|
61
|
+
googleBot?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `alternates`: canonical + i18n / media / type alternates. */
|
|
65
|
+
export interface AlternatesMetadata {
|
|
66
|
+
canonical?: MetadataUrl;
|
|
67
|
+
/** hreflang -> URL, e.g. `{ 'es-ES': '/es' }`. */
|
|
68
|
+
languages?: Record<string, MetadataUrl>;
|
|
69
|
+
/** media query -> URL, e.g. `{ '(max-width: 600px)': '/mobile' }`. */
|
|
70
|
+
media?: Record<string, MetadataUrl>;
|
|
71
|
+
/** MIME type -> URL, e.g. `{ 'application/rss+xml': '/rss.xml' }`. */
|
|
72
|
+
types?: Record<string, MetadataUrl>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Site-verification tokens. Each value is a token or list of tokens. */
|
|
76
|
+
export interface VerificationMetadata {
|
|
77
|
+
google?: string | string[];
|
|
78
|
+
yandex?: string | string[];
|
|
79
|
+
yahoo?: string | string[];
|
|
80
|
+
/** IndieAuth / personal `<meta name="me">`. */
|
|
81
|
+
me?: string | string[];
|
|
82
|
+
/** Arbitrary `<meta name="…">` verification entries. */
|
|
83
|
+
other?: Record<string, string | string[]>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A single icon descriptor. */
|
|
87
|
+
export type IconDescriptor =
|
|
88
|
+
| MetadataUrl
|
|
89
|
+
| { url: MetadataUrl; sizes?: string; type?: string };
|
|
90
|
+
|
|
91
|
+
/** `icons`: a bare URL, a list, or the bucketed object form. */
|
|
92
|
+
export type IconsMetadata =
|
|
93
|
+
| MetadataUrl
|
|
94
|
+
| IconDescriptor[]
|
|
95
|
+
| {
|
|
96
|
+
icon?: IconDescriptor | IconDescriptor[];
|
|
97
|
+
apple?: IconDescriptor | IconDescriptor[];
|
|
98
|
+
shortcut?: IconDescriptor | IconDescriptor[];
|
|
99
|
+
/** Catch-all: each entry carries its own `rel`. */
|
|
100
|
+
other?:
|
|
101
|
+
| { rel: string; url: MetadataUrl; sizes?: string; type?: string }
|
|
102
|
+
| Array<{ rel: string; url: MetadataUrl; sizes?: string; type?: string }>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `openGraph`. Each key is emitted as `<meta property="og:<key>">`, so the
|
|
107
|
+
* indexer carries the documented keys plus an open-ended fallback for the
|
|
108
|
+
* `og:image:width` / `image:height` / `image:alt` style entries.
|
|
109
|
+
*/
|
|
110
|
+
export interface OpenGraphMetadata {
|
|
111
|
+
type?: string;
|
|
112
|
+
title?: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
url?: MetadataUrl;
|
|
115
|
+
image?: MetadataUrl;
|
|
116
|
+
site_name?: string;
|
|
117
|
+
[key: string]: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `twitter` card. Each key is emitted as `<meta name="twitter:<key>">`. */
|
|
121
|
+
export interface TwitterMetadata {
|
|
122
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
123
|
+
title?: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
image?: MetadataUrl;
|
|
126
|
+
site?: string;
|
|
127
|
+
creator?: string;
|
|
128
|
+
[key: string]: string | undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A startup-image entry for `appleWebApp.startupImage`. */
|
|
132
|
+
export type AppleStartupImage =
|
|
133
|
+
| MetadataUrl
|
|
134
|
+
| { url: MetadataUrl; media?: string };
|
|
135
|
+
|
|
136
|
+
/** `appleWebApp`: `true` (capable) or the descriptor object. */
|
|
137
|
+
export type AppleWebAppMetadata =
|
|
138
|
+
| boolean
|
|
139
|
+
| {
|
|
140
|
+
capable?: boolean;
|
|
141
|
+
title?: string;
|
|
142
|
+
statusBarStyle?: 'default' | 'black' | 'black-translucent';
|
|
143
|
+
startupImage?: AppleStartupImage | AppleStartupImage[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A single JSON-LD structured-data object (schema.org). The author owns the
|
|
148
|
+
* shape (Article, Product, BreadcrumbList, Organization, FAQPage, etc.), so
|
|
149
|
+
* this is intentionally permissive. The framework serializes it with
|
|
150
|
+
* `JSON.stringify` and HTML-safe-escapes the output into a
|
|
151
|
+
* `<script type="application/ld+json">` block.
|
|
152
|
+
*/
|
|
153
|
+
export type JsonLd = Record<string, unknown>;
|
|
154
|
+
|
|
155
|
+
/** A `metadata.preload` link descriptor (emitted as `<link rel="preload">`). */
|
|
156
|
+
export interface PreloadDescriptor {
|
|
157
|
+
href: MetadataUrl;
|
|
158
|
+
as?: string;
|
|
159
|
+
type?: string;
|
|
160
|
+
crossorigin?: string;
|
|
161
|
+
media?: string;
|
|
162
|
+
[attr: string]: string | undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The return shape of `metadata` / `generateMetadata`.
|
|
167
|
+
*
|
|
168
|
+
* Mirrors exactly what packages/server/src/ssr.js consumes. Every field is
|
|
169
|
+
* optional. Where the framework accepts a string OR an object (`title`,
|
|
170
|
+
* `viewport`, `robots`, `appleWebApp`, `icons`), the type is a union.
|
|
171
|
+
*
|
|
172
|
+
* @see agent-docs/metadata.md for the field-by-field emission reference.
|
|
173
|
+
*/
|
|
174
|
+
export interface Metadata {
|
|
175
|
+
/** -> `<title>`. Plain string or `{ template, default, absolute }`. */
|
|
176
|
+
title?: TitleMetadata;
|
|
177
|
+
/** -> `<meta name="description">`. */
|
|
178
|
+
description?: string;
|
|
179
|
+
/** -> `<meta name="keywords">`. A list is comma-joined. */
|
|
180
|
+
keywords?: string | string[];
|
|
181
|
+
/** -> `<meta name="author">` (+ optional `<link rel="author">`). */
|
|
182
|
+
authors?: AuthorMetadata | AuthorMetadata[];
|
|
183
|
+
/** -> `<meta name="creator">`. */
|
|
184
|
+
creator?: string;
|
|
185
|
+
/** -> `<meta name="publisher">`. */
|
|
186
|
+
publisher?: string;
|
|
187
|
+
/** -> `<meta name="application-name">`. */
|
|
188
|
+
applicationName?: string;
|
|
189
|
+
/** -> `<meta name="generator">`. */
|
|
190
|
+
generator?: string;
|
|
191
|
+
/** -> `<meta name="referrer">`. */
|
|
192
|
+
referrer?: string;
|
|
193
|
+
|
|
194
|
+
/** Base for resolving every relative URL in this object. */
|
|
195
|
+
metadataBase?: string;
|
|
196
|
+
|
|
197
|
+
/** -> `<meta name="viewport">`. String form OR object form. */
|
|
198
|
+
viewport?: string | ViewportMetadata;
|
|
199
|
+
/** -> `<meta name="theme-color">`. */
|
|
200
|
+
themeColor?: string;
|
|
201
|
+
/** -> `<meta name="color-scheme">`. */
|
|
202
|
+
colorScheme?: string;
|
|
203
|
+
|
|
204
|
+
/** -> `<meta name="robots">`. A string OR the object form. */
|
|
205
|
+
robots?: string | RobotsMetadata;
|
|
206
|
+
/** Canonical + i18n / media / type alternates. */
|
|
207
|
+
alternates?: AlternatesMetadata;
|
|
208
|
+
/** Site-verification tokens. */
|
|
209
|
+
verification?: VerificationMetadata;
|
|
210
|
+
|
|
211
|
+
/** Icon links. Bare URL, list, or bucketed object. */
|
|
212
|
+
icons?: IconsMetadata;
|
|
213
|
+
/** -> `<link rel="manifest">`. */
|
|
214
|
+
manifest?: string;
|
|
215
|
+
|
|
216
|
+
/** Open Graph tags (`<meta property="og:*">`). */
|
|
217
|
+
openGraph?: OpenGraphMetadata;
|
|
218
|
+
/** Twitter card tags (`<meta name="twitter:*">`). */
|
|
219
|
+
twitter?: TwitterMetadata;
|
|
220
|
+
|
|
221
|
+
/** iOS web-app meta. `true` (capable) or the descriptor object. */
|
|
222
|
+
appleWebApp?: AppleWebAppMetadata;
|
|
223
|
+
/** -> `<meta name="format-detection">`. Each value is a boolean. */
|
|
224
|
+
formatDetection?: Record<string, boolean>;
|
|
225
|
+
/** -> `<meta name="apple-itunes-app">`. */
|
|
226
|
+
itunes?: { appId: string; appArgument?: string };
|
|
227
|
+
|
|
228
|
+
/** -> `<meta name="category">`. */
|
|
229
|
+
category?: string;
|
|
230
|
+
/** -> `<meta name="classification">`. */
|
|
231
|
+
classification?: string;
|
|
232
|
+
/** -> `<meta name="abstract">`. */
|
|
233
|
+
abstract?: string;
|
|
234
|
+
/** -> `<link rel="archives">`. */
|
|
235
|
+
archives?: string | string[];
|
|
236
|
+
/** -> `<link rel="assets">`. */
|
|
237
|
+
assets?: string | string[];
|
|
238
|
+
/** -> `<link rel="bookmark">`. */
|
|
239
|
+
bookmarks?: string | string[];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Response `Cache-Control` header (NOT a `<meta>` tag). Pages default to
|
|
243
|
+
* `no-store`; a public value (e.g. `public, max-age=60`) also enables
|
|
244
|
+
* conditional GET on the page.
|
|
245
|
+
*/
|
|
246
|
+
cacheControl?: string;
|
|
247
|
+
/** `<link rel="preload">` hints (fonts, images, etc.). */
|
|
248
|
+
preload?: PreloadDescriptor[];
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* JSON-LD structured data (schema.org), emitted as one or more
|
|
252
|
+
* `<script type="application/ld+json">` blocks in `<head>`. A single
|
|
253
|
+
* object emits ONE script; an array emits one script PER element. The
|
|
254
|
+
* author owns the schema.org shape; the framework serializes with
|
|
255
|
+
* `JSON.stringify` and HTML-safe-escapes the output automatically (a
|
|
256
|
+
* value containing `</script>` can never break out of the tag). Works in
|
|
257
|
+
* `generateMetadata(ctx)` for per-request data (a per-post Article, etc.).
|
|
258
|
+
*/
|
|
259
|
+
jsonLd?: JsonLd | JsonLd[];
|
|
260
|
+
|
|
261
|
+
/** Catch-all `<meta name="…">` entries. Value may be a list. */
|
|
262
|
+
other?: Record<string, string | number | Array<string | number>>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The argument `generateMetadata(ctx)` receives. Mirrors the page context
|
|
267
|
+
* (the same `{ params, searchParams, url, actionData }` a page function gets).
|
|
268
|
+
*/
|
|
269
|
+
export interface MetadataContext {
|
|
270
|
+
params: Record<string, string>;
|
|
271
|
+
searchParams?: Record<string, string | string[]>;
|
|
272
|
+
url: string;
|
|
273
|
+
/**
|
|
274
|
+
* Present only on the re-render after a failed page `action` submission.
|
|
275
|
+
* `undefined` on a normal GET render. Read it to vary the title on a
|
|
276
|
+
* validation-error re-render.
|
|
277
|
+
*/
|
|
278
|
+
actionData?: unknown;
|
|
279
|
+
}
|
package/src/render-server.js
CHANGED
|
@@ -362,7 +362,7 @@ function browserMemberHint(e) {
|
|
|
362
362
|
* @param {SuspenseCtx} [ctx]
|
|
363
363
|
* @returns {Promise<string>}
|
|
364
364
|
*/
|
|
365
|
-
async function injectDSD(html, ctx) {
|
|
365
|
+
async function injectDSD(html, ctx, ancestors = []) {
|
|
366
366
|
const tags = allTags();
|
|
367
367
|
if (!tags.length) return html;
|
|
368
368
|
// Sort longest tag name first so the regex alternation tries the most
|
|
@@ -391,6 +391,12 @@ async function injectDSD(html, ctx) {
|
|
|
391
391
|
try {
|
|
392
392
|
const isShadow = /** @type any */ (Cls).shadow === true;
|
|
393
393
|
const instance = new /** @type any */ (Cls)();
|
|
394
|
+
// Thread the ancestor chain (the enclosing custom-element instances)
|
|
395
|
+
// so the server element shim's closest() can resolve a parent at SSR.
|
|
396
|
+
// Set before performServerUpdate so a willUpdate() that reads a parent
|
|
397
|
+
// via closest() sees the chain. Each child recursion below extends it.
|
|
398
|
+
instance.__ssrTag = tag;
|
|
399
|
+
instance.__ssrAncestors = ancestors;
|
|
394
400
|
const attrMap = parseAttrs(attrs);
|
|
395
401
|
// Decode `data-webjs-prop-*` attributes first (rich-typed values
|
|
396
402
|
// emitted for `.prop=${val}` bindings in the parent template),
|
|
@@ -416,13 +422,16 @@ async function injectDSD(html, ctx) {
|
|
|
416
422
|
// SSR. WebComponent instances expose performServerUpdate; bare
|
|
417
423
|
// Base-extending kit components (no lifecycle) do not, so it is guarded.
|
|
418
424
|
if (typeof instance.performServerUpdate === 'function') instance.performServerUpdate();
|
|
419
|
-
// Surface attributes the component set before render (reflected
|
|
420
|
-
// reflect:true props, or an explicit this.setAttribute in the
|
|
421
|
-
// constructor / willUpdate) that were not already in the source tag.
|
|
422
|
-
// Appending keeps the original tag byte-identical when nothing changed.
|
|
423
|
-
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
424
425
|
let tpl = instance.render ? instance.render() : '';
|
|
425
426
|
if (tpl && typeof tpl.then === 'function') tpl = await tpl;
|
|
427
|
+
// Surface attributes the component set up to and including render()
|
|
428
|
+
// that were not already in the source tag: reflected reflect:true
|
|
429
|
+
// props, an explicit this.setAttribute in the constructor / willUpdate,
|
|
430
|
+
// or a host-attribute mutation inside render() itself (a light-DOM
|
|
431
|
+
// compound-component pattern, e.g. this.dataset.state / this.className /
|
|
432
|
+
// this.hidden on the host). Reading after render() captures all three.
|
|
433
|
+
// Appending keeps the original tag byte-identical when nothing changed.
|
|
434
|
+
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
426
435
|
// Render the template to HTML. injectDSD recurses on the result so
|
|
427
436
|
// nested custom elements (e.g. <theme-toggle> inside <blog-shell>)
|
|
428
437
|
// get their own DSD pass.
|
|
@@ -432,7 +441,7 @@ async function injectDSD(html, ctx) {
|
|
|
432
441
|
// Shadow DOM: native <slot> stays as-is in the DSD template. The
|
|
433
442
|
// browser handles projection from the host's light-DOM children
|
|
434
443
|
// into the shadow tree natively. No framework substitution here.
|
|
435
|
-
const innerProcessed = await injectDSD(rawInner, ctx);
|
|
444
|
+
const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance]);
|
|
436
445
|
const rawStyles = /** @type any */ (Cls).styles;
|
|
437
446
|
const styleList = Array.isArray(rawStyles) ? rawStyles : rawStyles && isCSS(rawStyles) ? [rawStyles] : [];
|
|
438
447
|
const styleStr = stylesToString(styleList);
|
|
@@ -495,7 +504,7 @@ async function injectDSD(html, ctx) {
|
|
|
495
504
|
}
|
|
496
505
|
const partitioned = partitionAuthoredBySlot(authoredInner);
|
|
497
506
|
const innerWithSlots = substituteSlotsInRender(rawInner, partitioned);
|
|
498
|
-
const innerProcessed = await injectDSD(innerWithSlots, ctx);
|
|
507
|
+
const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance]);
|
|
499
508
|
edits.push({
|
|
500
509
|
start: m.index,
|
|
501
510
|
end: closeEnd,
|
|
@@ -523,6 +532,11 @@ async function injectDSD(html, ctx) {
|
|
|
523
532
|
// independently against the original html, but those inner elements
|
|
524
533
|
// are processed by the recursive injectDSD call on innerWithSlots.
|
|
525
534
|
// Keeping both edits would double-process them and corrupt the output.
|
|
535
|
+
// A consequence: a nested instance's render() runs once per chain depth
|
|
536
|
+
// (the discarded top-level pass sees an empty ancestor chain, so its
|
|
537
|
+
// closest() reads null; the kept recursive pass has the real chain). The
|
|
538
|
+
// kept pass is the only output, and closest() is a read, so render() must
|
|
539
|
+
// stay pure at SSR (the standard SSR contract), not branch on side effects.
|
|
526
540
|
edits.sort((a, b) => a.start - b.start);
|
|
527
541
|
/** @type {{start:number, end:number, text:string}[]} */
|
|
528
542
|
const filtered = [];
|
package/src/router-client.js
CHANGED
|
@@ -1473,6 +1473,23 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild) {
|
|
|
1473
1473
|
blurOutgoingFocus();
|
|
1474
1474
|
return;
|
|
1475
1475
|
}
|
|
1476
|
+
// The response did not carry the requested frame (source null), or the
|
|
1477
|
+
// target frame is gone from the live DOM (target null). Falling through
|
|
1478
|
+
// would wholesale-replace the document, a silent full-page swap that
|
|
1479
|
+
// destroys the page (e.g. an auth redirect returning a login page without
|
|
1480
|
+
// the frame). Surface the contract violation with a cancelable event
|
|
1481
|
+
// instead. Default: warn and leave the frame unchanged. A listener that
|
|
1482
|
+
// calls preventDefault owns the outcome.
|
|
1483
|
+
const evt = new CustomEvent('webjs:frame-missing', {
|
|
1484
|
+
bubbles: true,
|
|
1485
|
+
cancelable: true,
|
|
1486
|
+
detail: { frameId, url: href || (typeof location !== 'undefined' ? location.href : null), document: doc },
|
|
1487
|
+
});
|
|
1488
|
+
(target || document).dispatchEvent(evt);
|
|
1489
|
+
if (!evt.defaultPrevented) {
|
|
1490
|
+
console.warn(`[webjs] frame "${frameId}" was not in the navigation response, leaving it unchanged. Handle "webjs:frame-missing" (preventDefault) to override.`);
|
|
1491
|
+
}
|
|
1492
|
+
return;
|
|
1476
1493
|
}
|
|
1477
1494
|
|
|
1478
1495
|
// 2. Auto-derived layout-marker swap.
|