@webjsdev/core 0.7.7 → 0.7.9
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 +1 -1
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core.js +1 -1
- package/dist/webjs-core.js.map +2 -2
- package/index.d.ts +53 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/expose.js +94 -4
- package/src/metadata.d.ts +279 -0
- package/src/router-client.js +17 -0
- package/src/routes.d.ts +136 -0
- package/src/webjs-config.d.ts +179 -0
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';
|
|
@@ -17,12 +61,19 @@ export { renderToString, renderToStream } from './src/render-server.js';
|
|
|
17
61
|
export { render } from './src/render-client.js';
|
|
18
62
|
export { escapeText, escapeAttr } from './src/escape.js';
|
|
19
63
|
export { notFound, redirect, isNotFound, isRedirect } from './src/nav.js';
|
|
20
|
-
export { expose, getExposed } from './src/expose.js';
|
|
64
|
+
export { expose, getExposed, validateInput } from './src/expose.js';
|
|
21
65
|
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
|
@@ -15,7 +15,7 @@ export { render } from './src/render-client.js';
|
|
|
15
15
|
export { escapeText, escapeAttr } from './src/escape.js';
|
|
16
16
|
export { notFound, redirect, isNotFound, isRedirect } from './src/nav.js';
|
|
17
17
|
export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
|
|
18
|
-
export { expose, getExposed } from './src/expose.js';
|
|
18
|
+
export { expose, getExposed, validateInput } from './src/expose.js';
|
|
19
19
|
export { repeat, isRepeat } from './src/repeat.js';
|
|
20
20
|
export { Suspense, isSuspense } from './src/suspense.js';
|
|
21
21
|
export { connectWS } from './src/websocket-client.js';
|
package/package.json
CHANGED
package/src/expose.js
CHANGED
|
@@ -22,10 +22,22 @@
|
|
|
22
22
|
* - For methods with a body (POST/PUT/PATCH/DELETE), the parsed JSON body
|
|
23
23
|
* is merged on top of params/query. The function receives ONE argument:
|
|
24
24
|
* the merged object.
|
|
25
|
-
* - If `opts.validate` is provided, it runs BEFORE the function
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* - If `opts.validate` is provided, it runs BEFORE the function on EVERY
|
|
26
|
+
* call path (the REST path here AND the internal RPC path), so a single
|
|
27
|
+
* validator declared once guards both. The framework calls `validate(input)`
|
|
28
|
+
* and interprets the return (see the validator contract below):
|
|
29
|
+
* - a `{ success: true, data? }` envelope passes (the action receives
|
|
30
|
+
* `data` when present, else the original input);
|
|
31
|
+
* - a `{ success: false, fieldErrors, message? }` envelope FAILS with a
|
|
32
|
+
* structured field-error result (422), without calling the action;
|
|
33
|
+
* - a THROW fails (→ 400 on REST, sanitized error on RPC), preserving the
|
|
34
|
+
* classic `Schema.parse`-style contract;
|
|
35
|
+
* - any OTHER returned plain value is treated as the transformed input
|
|
36
|
+
* (back-compat with the `validate: Schema.parse` transform style).
|
|
37
|
+
* The zod adapter is three lines and keeps the framework zod-free:
|
|
38
|
+
* `validate: (i) => { const r = Schema.safeParse(i);
|
|
39
|
+
* return r.success ? { success: true, data: r.data }
|
|
40
|
+
* : { success: false, fieldErrors: r.error.flatten().fieldErrors }; }`
|
|
29
41
|
* - Return value becomes a JSON `Response`; throw or return a `Response`
|
|
30
42
|
* directly for full control.
|
|
31
43
|
*
|
|
@@ -84,6 +96,84 @@ function normaliseCors(c) {
|
|
|
84
96
|
};
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Attach an input validator to a server action WITHOUT exposing it as a REST
|
|
101
|
+
* endpoint. The validator runs SERVER-SIDE before the action body on BOTH call
|
|
102
|
+
* paths: the internal RPC path (a client component importing the action) AND
|
|
103
|
+
* the `expose()` REST path (if the action is also exposed). This is the shared,
|
|
104
|
+
* declare-once validation surface (#245).
|
|
105
|
+
*
|
|
106
|
+
* It reuses the EXACT same `__webjsHttp` storage `expose()` writes, so the
|
|
107
|
+
* action index's `getExposed(fn)` already surfaces the validator with no new
|
|
108
|
+
* plumbing. The action stays a plain callable function (a direct unit-test
|
|
109
|
+
* import still works, just without the framework running validate).
|
|
110
|
+
*
|
|
111
|
+
* If the action is already `expose()`d, prefer passing `{ validate }` to
|
|
112
|
+
* `expose()` directly; calling both keeps the LAST validator attached.
|
|
113
|
+
*
|
|
114
|
+
* ```js
|
|
115
|
+
* // actions/posts.server.js
|
|
116
|
+
* 'use server';
|
|
117
|
+
* import { validateInput } from '@webjsdev/core';
|
|
118
|
+
*
|
|
119
|
+
* export const createPost = validateInput(
|
|
120
|
+
* async ({ title }) => { ... },
|
|
121
|
+
* (input) => {
|
|
122
|
+
* const errs = {};
|
|
123
|
+
* if (!input?.title?.trim()) errs.title = 'Title is required';
|
|
124
|
+
* return Object.keys(errs).length
|
|
125
|
+
* ? { success: false, fieldErrors: errs }
|
|
126
|
+
* : { success: true };
|
|
127
|
+
* },
|
|
128
|
+
* );
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* The validator contract (the framework calls `validate(input)`):
|
|
132
|
+
* - returns `{ success: true, data? }` → valid; the action is called with
|
|
133
|
+
* `data` if present, else the original input;
|
|
134
|
+
* - returns `{ success: false, fieldErrors, message? }` (an object with a
|
|
135
|
+
* boolean `success` of `false`, or with a `fieldErrors`) → FAILED; the
|
|
136
|
+
* framework returns a structured `ActionResult`
|
|
137
|
+
* `{ success: false, fieldErrors, error: message?, status: 422 }` without
|
|
138
|
+
* calling the action body;
|
|
139
|
+
* - THROWS → mapped to a sanitized failure result (the classic
|
|
140
|
+
* `Schema.parse` contract);
|
|
141
|
+
* - returns ANY OTHER plain value → treated as the validated/coerced input
|
|
142
|
+
* and passed to the action (the `validate: Schema.parse` transform style).
|
|
143
|
+
*
|
|
144
|
+
* Disambiguation: a return is a result-envelope ONLY when it is an object with
|
|
145
|
+
* a boolean `success` property OR a `fieldErrors` property; otherwise it is a
|
|
146
|
+
* transformed input value. Caveat: if your validator returns the input
|
|
147
|
+
* VERBATIM (the bare-transform style) and that input can itself carry a
|
|
148
|
+
* `success` boolean or a `fieldErrors` key, it would be misread as an envelope.
|
|
149
|
+
* Return the explicit `{ success: true, data }` envelope in that case (the zod
|
|
150
|
+
* adapter and the recommended hand-rolled form already do, so they never
|
|
151
|
+
* collide).
|
|
152
|
+
*
|
|
153
|
+
* Server-side only; import inside `.server.{js,ts}` files. The bare
|
|
154
|
+
* `@webjsdev/core` specifier resolves to the browser entry, which excludes
|
|
155
|
+
* `validateInput` (like `expose`), so an import from client-bound code reads
|
|
156
|
+
* `undefined`. The validator itself lives in the `.server` file and never
|
|
157
|
+
* reaches the client.
|
|
158
|
+
*
|
|
159
|
+
* @template {Function} F
|
|
160
|
+
* @param {F} fn the async action implementation
|
|
161
|
+
* @param {(input: any) => any} validate the validator (a plain function or a
|
|
162
|
+
* zod-`safeParse` adapter)
|
|
163
|
+
* @returns {F} the same function, tagged with the validator
|
|
164
|
+
*/
|
|
165
|
+
export function validateInput(fn, validate) {
|
|
166
|
+
if (typeof fn !== 'function') {
|
|
167
|
+
throw new Error('validateInput(): first argument must be the action function');
|
|
168
|
+
}
|
|
169
|
+
if (typeof validate !== 'function') {
|
|
170
|
+
throw new Error('validateInput(): second argument must be a validator function');
|
|
171
|
+
}
|
|
172
|
+
const existing = /** @type any */ (fn).__webjsHttp || {};
|
|
173
|
+
/** @type any */ (fn).__webjsHttp = { ...existing, validate };
|
|
174
|
+
return fn;
|
|
175
|
+
}
|
|
176
|
+
|
|
87
177
|
/** @param {unknown} fn */
|
|
88
178
|
export function getExposed(fn) {
|
|
89
179
|
return fn && typeof fn === 'function' ? /** @type any */ (fn).__webjsHttp || null : null;
|
|
@@ -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/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.
|
package/src/routes.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript overlay for typed page / layout / route-handler props plus an
|
|
3
|
+
* opt-in, generated route union that types `navigate()` and catches bad
|
|
4
|
+
* hrefs at tsserver time (#258).
|
|
5
|
+
*
|
|
6
|
+
* webjs identity holds: NO build step, NO runtime cost, types-only, opt-in
|
|
7
|
+
* for JSDoc apps. The mechanism mirrors Next 15's `typedRoutes` but via
|
|
8
|
+
* interface declaration-merging instead of a bundler.
|
|
9
|
+
*
|
|
10
|
+
* import type { PageProps, LayoutProps } from '@webjsdev/core';
|
|
11
|
+
* export default function Post({ params }: PageProps<'/blog/[slug]'>) { … }
|
|
12
|
+
*
|
|
13
|
+
* Two augmentation targets, `WebjsRoutes` and `RouteParamMap`, are EMPTY by
|
|
14
|
+
* default, so an un-generated app sees `Route = string` and `params =
|
|
15
|
+
* Record<string, string>` (non-breaking). Running `webjs types` writes
|
|
16
|
+
* `.webjs/routes.d.ts`, which augments both with one key per route literal,
|
|
17
|
+
* narrowing `Route` to the concrete app routes and giving every page typed
|
|
18
|
+
* `params`.
|
|
19
|
+
*
|
|
20
|
+
* The shapes mirror what packages/server/src/ssr.js actually constructs for
|
|
21
|
+
* the page / layout context and what packages/server/src/api.js passes a
|
|
22
|
+
* route handler, NOT a guess. Zero runtime cost: nothing here ships to the
|
|
23
|
+
* browser.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { TemplateResult } from './html.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The augmentation target the generated `.webjs/routes.d.ts` fills with one
|
|
30
|
+
* key per route literal (e.g. `'/blog/[slug]': true`). Empty by default so an
|
|
31
|
+
* un-generated app keeps `Route = string`. App code (or the generator)
|
|
32
|
+
* augments it via `declare module '@webjsdev/core'`.
|
|
33
|
+
*/
|
|
34
|
+
export interface WebjsRoutes {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The augmentation target mapping a dynamic route key to its params object
|
|
38
|
+
* (`'/blog/[slug]': { slug: string }`). Emitted by the generator with the
|
|
39
|
+
* exact `string` / `string[]` / optional shapes known at generation time,
|
|
40
|
+
* which is far more robust than deep template-literal inference. Empty by
|
|
41
|
+
* default; a static route is never given an entry here, so its params fall
|
|
42
|
+
* through to `Record<string, string>` (the runtime default), not `{}`.
|
|
43
|
+
*/
|
|
44
|
+
export interface RouteParamMap {}
|
|
45
|
+
|
|
46
|
+
/** True once either augmentation target carries at least one key. */
|
|
47
|
+
type _HasRoutes = keyof WebjsRoutes extends never ? false : true;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Turn a route key into the set of concrete path patterns it matches by
|
|
51
|
+
* replacing each dynamic segment with `${string}`. The optional catch-all
|
|
52
|
+
* `[[...x]]` is handled by the GENERATOR, which emits BOTH the with-segment
|
|
53
|
+
* and without-segment keys as separate `WebjsRoutes` entries (so `/docs` and
|
|
54
|
+
* `/docs/a/b` both satisfy a `/docs/[[...slug]]` route). Keeping that collapse
|
|
55
|
+
* in the generator (not in pure types) avoids the fragile `//`-collapse, so
|
|
56
|
+
* `RoutePattern` only ever needs the single-segment `[x]` and catch-all
|
|
57
|
+
* `[...x]` cases.
|
|
58
|
+
*/
|
|
59
|
+
type RoutePattern<K extends string> =
|
|
60
|
+
K extends `${infer A}[...${string}]${infer B}`
|
|
61
|
+
? `${A}${string}${RoutePattern<B>}`
|
|
62
|
+
: K extends `${infer A}[${string}]${infer B}`
|
|
63
|
+
? `${A}${string}${RoutePattern<B>}`
|
|
64
|
+
: K;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The valid-href type. `string` when no routes are generated (so `navigate()`
|
|
68
|
+
* and JSDoc apps are never broken); once `WebjsRoutes` is augmented it is the
|
|
69
|
+
* union of every static route literal PLUS a template-literal pattern per
|
|
70
|
+
* dynamic route.
|
|
71
|
+
*/
|
|
72
|
+
export type Route = _HasRoutes extends true
|
|
73
|
+
? { [K in keyof WebjsRoutes]: RoutePattern<K & string> }[keyof WebjsRoutes]
|
|
74
|
+
: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The params object for a route key. A generated dynamic route resolves to
|
|
78
|
+
* its `RouteParamMap` entry (`{ slug: string }`, `{ rest: string[] }`,
|
|
79
|
+
* `{ slug?: string[] }`); anything else (a static route, or any route in an
|
|
80
|
+
* un-generated app) resolves to `Record<string, string>`, the runtime
|
|
81
|
+
* default the SSR pipeline produces.
|
|
82
|
+
*/
|
|
83
|
+
export type RouteParams<R extends string> = R extends keyof RouteParamMap
|
|
84
|
+
? RouteParamMap[R]
|
|
85
|
+
: Record<string, string>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The argument a page default-export receives. Mirrors the `ctx` object
|
|
89
|
+
* packages/server/src/ssr.js builds: `{ params, searchParams, url,
|
|
90
|
+
* actionData }`.
|
|
91
|
+
*
|
|
92
|
+
* `searchParams` is typed `Record<string, string | string[]>` to match the
|
|
93
|
+
* established page-context convention, even though the runtime builds it with
|
|
94
|
+
* `Object.fromEntries(url.searchParams.entries())` (so a repeated key is
|
|
95
|
+
* last-wins `string`, never an array). The wider type keeps a future
|
|
96
|
+
* multi-value reader source-compatible.
|
|
97
|
+
*/
|
|
98
|
+
export interface PageProps<R extends string = string> {
|
|
99
|
+
/**
|
|
100
|
+
* Path params. A generated dynamic route narrows this to its exact shape
|
|
101
|
+
* (`{ slug: string }`); a static or un-generated route is
|
|
102
|
+
* `Record<string, string>`.
|
|
103
|
+
*/
|
|
104
|
+
params: R extends keyof RouteParamMap ? RouteParamMap[R] : Record<string, string>;
|
|
105
|
+
/** Query string, as an object. Repeated keys are last-wins at runtime. */
|
|
106
|
+
searchParams: Record<string, string | string[]>;
|
|
107
|
+
/** The full request URL string. */
|
|
108
|
+
url: string;
|
|
109
|
+
/**
|
|
110
|
+
* Present ONLY on the re-render after a failed page `action` submission
|
|
111
|
+
* (#244). `undefined` on a normal GET render. Read `actionData.fieldErrors`
|
|
112
|
+
* / `actionData.values` to surface validation errors and repopulate inputs.
|
|
113
|
+
*/
|
|
114
|
+
actionData?: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The argument a layout default-export receives: every `PageProps` field plus
|
|
119
|
+
* the rendered `children`. Mirrors the `{ children, params, searchParams, url
|
|
120
|
+
* }` object packages/server/src/ssr.js passes a layout.
|
|
121
|
+
*/
|
|
122
|
+
export interface LayoutProps<R extends string = string> extends PageProps<R> {
|
|
123
|
+
/** The nested page / inner-layout content this layout must embed. */
|
|
124
|
+
children: TemplateResult;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The 2nd argument a `route.{js,ts}` `GET` / `POST` / … handler receives.
|
|
129
|
+
* Mirrors what packages/server/src/api.js passes: `{ params }`. The params
|
|
130
|
+
* are a plain `Record<string, string>` at the handler layer (the route table
|
|
131
|
+
* decodes each captured segment to a string), with an optional `R` generic to
|
|
132
|
+
* narrow them against a generated route the same way `PageProps` does.
|
|
133
|
+
*/
|
|
134
|
+
export interface RouteHandlerContext<R extends string = string> {
|
|
135
|
+
params: R extends keyof RouteParamMap ? RouteParamMap[R] : Record<string, string>;
|
|
136
|
+
}
|