@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
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { scanSeeds } from '../action-seed-client.js';
|
|
10
|
+
import { syncRootAttributes } from './root-attrs.js';
|
|
10
11
|
import { collectBoundaries, planBoundarySwap } from './boundaries.js';
|
|
11
12
|
import { reportFallback } from './diagnostics.js';
|
|
12
13
|
import { blurOutgoingFocus, diffChildren, replaceBoundaryRange, resyncEnclosingHostSlots, swapMarkerRange } from './dom-differ.js';
|
|
13
14
|
import { trackedReloadSignature } from './frames.js';
|
|
14
|
-
import { addNewHeadElements, mergeHead } from './head-merge.js';
|
|
15
|
+
import { addNewHeadElements, mergeHead, mergeHeadStylesheets } from './head-merge.js';
|
|
15
16
|
import { morphLayoutChrome, planLayoutChrome } from './layout-chrome.js';
|
|
17
|
+
import { primeLiveStylesheetKeys, releaseIncomingSheets, switchOffDepartedStylesheets } from './owned-styles.js';
|
|
16
18
|
import { clearPrefetchRefused, prefetchClearAll } from './prefetch.js';
|
|
17
19
|
import { snapshotCache } from './snapshot-cache.js';
|
|
18
20
|
import { hardNavigate } from './state.js';
|
|
@@ -84,6 +86,13 @@ export let _swapCommit = Promise.resolve();
|
|
|
84
86
|
* history (a revalidation, a refresh, the popstate restore).
|
|
85
87
|
*/
|
|
86
88
|
export function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc, refresh, recordHistoryNow) {
|
|
89
|
+
// The URL the incoming page came from, which its relative stylesheet hrefs
|
|
90
|
+
// resolve against, and the live sheets' keys taken NOW, while the document
|
|
91
|
+
// still has the outgoing page's URL: every commit point below records the
|
|
92
|
+
// new history entry before it merges the head (#1669).
|
|
93
|
+
let incomingBase = typeof location !== 'undefined' ? location.href : '';
|
|
94
|
+
if (href) { try { incomingBase = new URL(href, incomingBase).href; } catch { /* keep the live url */ } }
|
|
95
|
+
primeLiveStylesheetKeys();
|
|
87
96
|
// SSR action seeding (#472): ingest the incoming page's seed payload BEFORE
|
|
88
97
|
// its components are grafted into the live DOM and upgrade, so a
|
|
89
98
|
// soft-navigated async component resolves from the seed instead of
|
|
@@ -105,6 +114,8 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
105
114
|
if (seedsScanned) return;
|
|
106
115
|
seedsScanned = true;
|
|
107
116
|
try { scanSeeds(doc, { frame: !!frameId }); } catch { /* seeding is best-effort */ }
|
|
117
|
+
// The root layout's <html> / <body> attributes follow the server (#1686).
|
|
118
|
+
if (!frameId) { try { syncRootAttributes(doc); } catch { /* best-effort */ } }
|
|
108
119
|
};
|
|
109
120
|
|
|
110
121
|
// Every host in this parsed doc is FRAMEWORK-SERIALIZED markup (an SSR
|
|
@@ -115,7 +126,7 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
115
126
|
// conditionally closed slot at snapshot time) where the structural
|
|
116
127
|
// slot-marker detector has nothing to see. The chooser consumes and removes
|
|
117
128
|
// the attribute on upgrade.
|
|
118
|
-
// (
|
|
129
|
+
// (A server-only host never upgrades, so its stamp is retained as
|
|
119
130
|
// an inert attribute; diffElementInPlace never copies it onto a live host,
|
|
120
131
|
// and the upgrade path consumes it, so it cannot mis-route anything.)
|
|
121
132
|
try {
|
|
@@ -279,7 +290,7 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
279
290
|
// ADD-ONLY head merge: preserve runtime-generated head content
|
|
280
291
|
// (Tailwind CSS injection, etc.) that the outer layout's scripts
|
|
281
292
|
// already produced.
|
|
282
|
-
addNewHeadElements(doc.head);
|
|
293
|
+
addNewHeadElements(doc.head, { base: incomingBase, frame: true });
|
|
283
294
|
// `diffChildren` -> `reconcileChildren` regrafts permanent elements
|
|
284
295
|
// by node identity (it imports the incoming children first, then
|
|
285
296
|
// swaps the live permanent node into the imported tree), so the live
|
|
@@ -288,6 +299,8 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
288
299
|
// ALSO progressively streams a Suspense boundary gates its resolves on the
|
|
289
300
|
// committed frame swap, not a stale prior _swapCommit (#1048).
|
|
290
301
|
_swapCommit = runWithTransition(() => {
|
|
302
|
+
// The frame's incoming sheets loaded held; they apply with the swap.
|
|
303
|
+
releaseIncomingSheets();
|
|
291
304
|
diffChildren(target, source);
|
|
292
305
|
reactivateScripts(target);
|
|
293
306
|
upgradeCustomElements(target);
|
|
@@ -334,7 +347,7 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
334
347
|
// commit point without a reader having to prove reachability first, which
|
|
335
348
|
// is what stops the next person adding a commit point from omitting it.
|
|
336
349
|
if (recordHistoryNow) recordHistoryNow();
|
|
337
|
-
swapFullBody(doc);
|
|
350
|
+
swapFullBody(doc, incomingBase, true);
|
|
338
351
|
return;
|
|
339
352
|
}
|
|
340
353
|
|
|
@@ -361,7 +374,7 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
361
374
|
if (plan && refresh === 'layouts' && !chrome) {
|
|
362
375
|
ingestSeeds();
|
|
363
376
|
if (recordHistoryNow) recordHistoryNow();
|
|
364
|
-
swapFullBody(doc);
|
|
377
|
+
swapFullBody(doc, incomingBase, true);
|
|
365
378
|
return;
|
|
366
379
|
}
|
|
367
380
|
|
|
@@ -375,11 +388,32 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
375
388
|
if (recordHistoryNow) recordHistoryNow();
|
|
376
389
|
const { mode, live, incoming } = plan;
|
|
377
390
|
// ADD-ONLY head merge: the outer layout stays mounted, so its head-bound
|
|
378
|
-
// runtime state (Tailwind injection, etc.) must not be invalidated.
|
|
379
|
-
|
|
391
|
+
// runtime state (Tailwind injection, etc.) must not be invalidated. It runs
|
|
392
|
+
// ahead of the commit, as the incoming sheets' load-before-swap copies
|
|
393
|
+
// already do (#1666, Turbo's order), so the new page's sheets apply from
|
|
394
|
+
// the swap's first frame.
|
|
395
|
+
// Its stylesheets wait for the commit (#1669, below).
|
|
396
|
+
addNewHeadElements(doc.head, { base: incomingBase, styles: false });
|
|
397
|
+
// A live element inside the range this swap replaces is on its way out,
|
|
398
|
+
// so a stylesheet link there does not count as one the page keeps.
|
|
399
|
+
/** @param {Element} el */
|
|
400
|
+
const leaving = (el) => !!(live.start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
401
|
+
&& !!(live.end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
380
402
|
_swapCommit = runWithTransition(() => {
|
|
403
|
+
// Everything stylesheet-related happens inside the commit, so the
|
|
404
|
+
// outgoing page, and a view transition's capture of it, keep exactly the
|
|
405
|
+
// sheets that page used, and the crossing restyles once (#1669): the
|
|
406
|
+
// incoming sheets (loaded held, stylesheets.js) are adopted, switched
|
|
407
|
+
// back on or inserted, then released, before the new markup lands.
|
|
408
|
+
mergeHeadStylesheets(doc.head, { base: incomingBase, segment: plan.segment, leaving });
|
|
409
|
+
releaseIncomingSheets();
|
|
381
410
|
if (mode === 'replace') replaceBoundaryRange(live, incoming);
|
|
382
411
|
else swapMarkerRange(live, incoming, doc);
|
|
412
|
+
// The swapped range's own stylesheets go with it unless the incoming
|
|
413
|
+
// page links them, in its head or its body, on a replace and on a morph
|
|
414
|
+
// alike: the server re-rendered the whole range either way, so a sheet a
|
|
415
|
+
// searchParams change dropped is retired too.
|
|
416
|
+
switchOffDepartedStylesheets(plan.segment, doc, incomingBase);
|
|
383
417
|
// No key sync is needed on the anchor's own comments: the plan's anchor
|
|
384
418
|
// carries EQUAL live/incoming route-keys in every tier (a changed-key
|
|
385
419
|
// REPLACE anchors at a parent already compared equal; the other tiers
|
|
@@ -438,7 +472,7 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
438
472
|
// because the user is already viewing the page).
|
|
439
473
|
ingestSeeds(); // committed: past both discard branches above
|
|
440
474
|
if (recordHistoryNow) recordHistoryNow();
|
|
441
|
-
swapFullBody(doc);
|
|
475
|
+
swapFullBody(doc, incomingBase, false);
|
|
442
476
|
}
|
|
443
477
|
|
|
444
478
|
/**
|
|
@@ -460,9 +494,12 @@ export function applySwap(doc, frameId, revalidating, href, incomingBuild, incom
|
|
|
460
494
|
* it only past their own discard branches.
|
|
461
495
|
*
|
|
462
496
|
* @param {Document} doc
|
|
497
|
+
* @param {string} incomingBase the URL the incoming page came from
|
|
498
|
+
* @param {boolean} complete the response rendered every layout (no X-Webjs-Have), so it can retire a departed section's sheets
|
|
463
499
|
*/
|
|
464
|
-
function swapFullBody(doc) {
|
|
465
|
-
|
|
500
|
+
function swapFullBody(doc, incomingBase, complete) {
|
|
501
|
+
// Its stylesheets wait for the commit, as on a boundary swap (#1669).
|
|
502
|
+
mergeHead(doc.head, { base: incomingBase, styles: false });
|
|
466
503
|
// Persist permanent elements by node identity across the full-body
|
|
467
504
|
// swap: move each live [data-webjs-permanent][id] node into the matching
|
|
468
505
|
// position in the incoming body BEFORE replaceChildren reads it, so the
|
|
@@ -470,7 +507,13 @@ function swapFullBody(doc) {
|
|
|
470
507
|
regraftPermanentElements(document.body, doc.body);
|
|
471
508
|
const newChildren = [...doc.body.childNodes];
|
|
472
509
|
const doSwap = () => {
|
|
510
|
+
// The whole body is replaced, so every live body link is leaving.
|
|
511
|
+
mergeHeadStylesheets(doc.head, { base: incomingBase, segment: '/', leaving: () => true });
|
|
512
|
+
releaseIncomingSheets();
|
|
473
513
|
document.body.replaceChildren(...newChildren);
|
|
514
|
+
// Only a response known to be complete can say a section's sheet is gone:
|
|
515
|
+
// the background restore path's head can be a reduced one (#936).
|
|
516
|
+
if (complete) switchOffDepartedStylesheets('/', doc, incomingBase);
|
|
474
517
|
reactivateScripts(document.body);
|
|
475
518
|
upgradeCustomElements(document.body);
|
|
476
519
|
blurOutgoingFocus();
|
package/src/routes.d.ts
CHANGED
|
@@ -73,16 +73,44 @@ export type Route = _HasRoutes extends true
|
|
|
73
73
|
? { [K in keyof WebjsRoutes]: RoutePattern<K & string> }[keyof WebjsRoutes]
|
|
74
74
|
: string;
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The params a route key's own text declares, read without a generated
|
|
78
|
+
* `RouteParamMap`: `[x]` is a `string`, `[...x]` a `string[]` (as in Next.js)
|
|
79
|
+
* and `[[...x]]` an optional `string[]`. Segments are read left to right from
|
|
80
|
+
* the first `[`, so every one is kept; the text up to the first `]` of
|
|
81
|
+
* `[[...x]]` is `[...x`, which is how the optional form is told apart.
|
|
82
|
+
*/
|
|
83
|
+
type InferredParams<K extends string> =
|
|
84
|
+
K extends `${string}[${infer Seg}]${infer Rest}`
|
|
85
|
+
? Seg extends `[...${infer N}`
|
|
86
|
+
? { [P in N]?: string[] } & InferredParams<Rest extends `]${infer After}` ? After : Rest>
|
|
87
|
+
: Seg extends `...${infer N}`
|
|
88
|
+
? { [P in N]: string[] } & InferredParams<Rest>
|
|
89
|
+
: { [P in Seg]: string } & InferredParams<Rest>
|
|
90
|
+
: {};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The params for a route key with no generated entry: what its text declares,
|
|
94
|
+
* or, for a static route and for the wide `string` key, the runtime default
|
|
95
|
+
* `Record<string, string>`.
|
|
96
|
+
*/
|
|
97
|
+
type FallbackParams<R extends string> = string extends R
|
|
98
|
+
? Record<string, string>
|
|
99
|
+
: keyof InferredParams<R> extends never
|
|
100
|
+
? Record<string, string>
|
|
101
|
+
: InferredParams<R>;
|
|
102
|
+
|
|
76
103
|
/**
|
|
77
104
|
* The params object for a route key. A generated dynamic route resolves to
|
|
78
105
|
* its `RouteParamMap` entry (`{ slug: string }`, `{ rest: string[] }`,
|
|
79
|
-
* `{ slug?: string[] }`)
|
|
80
|
-
*
|
|
81
|
-
*
|
|
106
|
+
* `{ slug?: string[] }`). Without one (an app that never ran `webjs types`),
|
|
107
|
+
* a dynamic route key still yields the same shape, read from the key itself,
|
|
108
|
+
* and a static route resolves to `Record<string, string>`, the runtime default
|
|
109
|
+
* the SSR pipeline produces.
|
|
82
110
|
*/
|
|
83
111
|
export type RouteParams<R extends string> = R extends keyof RouteParamMap
|
|
84
112
|
? RouteParamMap[R]
|
|
85
|
-
:
|
|
113
|
+
: FallbackParams<R>;
|
|
86
114
|
|
|
87
115
|
/**
|
|
88
116
|
* A value that is BOTH synchronously usable as `T` and `await`-able to `T`
|
|
@@ -97,11 +125,9 @@ export type Awaitable<T> = T & PromiseLike<T>;
|
|
|
97
125
|
* packages/server/src/ssr.js builds: `{ params, searchParams, url,
|
|
98
126
|
* actionData }`.
|
|
99
127
|
*
|
|
100
|
-
* `searchParams` is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* last-wins `string`, never an array). The wider type keeps a future
|
|
104
|
-
* multi-value reader source-compatible.
|
|
128
|
+
* `searchParams` is `Record<string, string | string[]>`, as in Next.js: a key
|
|
129
|
+
* that repeats in the query (`?tag=a&tag=b`) is an array of every value, in
|
|
130
|
+
* order, and any other key is its string.
|
|
105
131
|
*
|
|
106
132
|
* `params` / `searchParams` are `Awaitable<T>` (#848): synchronously readable
|
|
107
133
|
* (`params.id`) AND `await`-able (`const { id } = await params`) for Next
|
|
@@ -109,13 +135,42 @@ export type Awaitable<T> = T & PromiseLike<T>;
|
|
|
109
135
|
* `then` (see packages/server/src/thenable-params.js), so a spread / JSON /
|
|
110
136
|
* `Object.keys` sees only the data keys.
|
|
111
137
|
*/
|
|
112
|
-
|
|
138
|
+
/**
|
|
139
|
+
* What a form action hands the page, by default: a failure result re-rendered
|
|
140
|
+
* (`{ success: false, error, fieldErrors }` is the usual shape) with the
|
|
141
|
+
* submitted text fields on `values`, or a success result carried across the
|
|
142
|
+
* redirect (`{ success: true, data }`). The common keys are typed; anything
|
|
143
|
+
* else the action returned reads as `any`, so `actionData?.data.manageKey`
|
|
144
|
+
* compiles without a cast, the way an untyped `useActionState` state does in
|
|
145
|
+
* Next. Name the exact shape with `PageProps`'s second type parameter,
|
|
146
|
+
* `PageProps<'/route', Awaited<ReturnType<typeof myAction>>>`, for full typing.
|
|
147
|
+
*/
|
|
148
|
+
export interface FormActionData {
|
|
149
|
+
success?: boolean;
|
|
150
|
+
error?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Per-field messages. Typed loosely, as `data` is: the page cannot see the
|
|
153
|
+
* action's return type, and actions return a `Record<string, string>` as
|
|
154
|
+
* often as a validator's `Record<string, string[]>`, so either assigns
|
|
155
|
+
* without a cast.
|
|
156
|
+
*/
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
fieldErrors?: Record<string, any>;
|
|
159
|
+
values?: Record<string, string>;
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
data?: any;
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
[key: string]: any;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface PageProps<R extends string = string, A = FormActionData> {
|
|
113
167
|
/**
|
|
114
|
-
* Path params
|
|
115
|
-
*
|
|
116
|
-
* `Record<string, string>`.
|
|
168
|
+
* Path params, narrowed to the route's shape (`{ slug: string }`,
|
|
169
|
+
* `{ rest: string[] }`) from the generated map or, without one, from the
|
|
170
|
+
* route key itself; a static route is `Record<string, string>`.
|
|
171
|
+
* Sync-readable and `await`-able (#848).
|
|
117
172
|
*/
|
|
118
|
-
params: Awaitable<R
|
|
173
|
+
params: Awaitable<RouteParams<R>>;
|
|
119
174
|
/** Query string, as an object. Repeated keys are last-wins at runtime. Sync-readable and `await`-able (#848). */
|
|
120
175
|
searchParams: Awaitable<Record<string, string | string[]>>;
|
|
121
176
|
/** The full request URL string. */
|
|
@@ -125,9 +180,11 @@ export interface PageProps<R extends string = string> {
|
|
|
125
180
|
* (#244). `undefined` on a normal GET render. Read `actionData.fieldErrors`
|
|
126
181
|
* / `actionData.values` to surface validation errors and repopulate inputs.
|
|
127
182
|
* On a failure `values` carries every submitted text field, with the action's
|
|
128
|
-
* own `values` on top (#1581).
|
|
183
|
+
* own `values` on top (#1581). Typed {@link FormActionData} by default; pass
|
|
184
|
+
* the action's result type as the second parameter,
|
|
185
|
+
* `PageProps<'/route', MyResult>`, to type it exactly.
|
|
129
186
|
*/
|
|
130
|
-
actionData?:
|
|
187
|
+
actionData?: A;
|
|
131
188
|
}
|
|
132
189
|
|
|
133
190
|
/**
|
|
@@ -135,7 +192,7 @@ export interface PageProps<R extends string = string> {
|
|
|
135
192
|
* the rendered `children`. Mirrors the `{ children, params, searchParams, url
|
|
136
193
|
* }` object packages/server/src/ssr.js passes a layout.
|
|
137
194
|
*/
|
|
138
|
-
export interface LayoutProps<R extends string = string> extends PageProps<R> {
|
|
195
|
+
export interface LayoutProps<R extends string = string, A = FormActionData> extends PageProps<R, A> {
|
|
139
196
|
/** The nested page / inner-layout content this layout must embed. */
|
|
140
197
|
children: TemplateResult;
|
|
141
198
|
}
|
|
@@ -149,5 +206,5 @@ export interface LayoutProps<R extends string = string> extends PageProps<R> {
|
|
|
149
206
|
*/
|
|
150
207
|
export interface RouteHandlerContext<R extends string = string> {
|
|
151
208
|
/** Sync-readable and `await`-able (#848), same as `PageProps['params']`. */
|
|
152
|
-
params: Awaitable<R
|
|
209
|
+
params: Awaitable<RouteParams<R>>;
|
|
153
210
|
}
|
package/src/sentinels.js
ADDED
package/src/serialize.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Error / typed arrays / Blob / File / FormData / Symbols / cycles).
|
|
4
4
|
*/
|
|
5
5
|
export function stringify(value: unknown): Promise<string>;
|
|
6
|
+
/** `stringify` done synchronously, for a value holding no File or Blob (it throws on one). */
|
|
7
|
+
export function stringifySync(value: unknown): string;
|
|
6
8
|
export function parse(text: string): unknown;
|
|
7
9
|
export function serialize(value: unknown): Promise<unknown>;
|
|
8
10
|
export function deserialize(value: unknown): unknown;
|
package/src/serialize.js
CHANGED
|
@@ -96,6 +96,23 @@ export async function stringify(value) {
|
|
|
96
96
|
return JSON.stringify(await serialize(value));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `stringify` for a value holding no Blob, File or FormData file, done
|
|
101
|
+
* synchronously. A bound form action's arguments need it: `.bind()` returns at
|
|
102
|
+
* once and the browser renderer reads the identity synchronously. Throws on a
|
|
103
|
+
* file, whose bytes can only be read asynchronously.
|
|
104
|
+
*
|
|
105
|
+
* @param {unknown} value
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export function stringifySync(value) {
|
|
109
|
+
const ctx = newEncodeCtx();
|
|
110
|
+
if (walkRefs(value, ctx).length) {
|
|
111
|
+
throw new TypeError('[webjs] a File or Blob cannot be serialized synchronously (it cannot be a bound argument of a form action).');
|
|
112
|
+
}
|
|
113
|
+
return JSON.stringify(encodeOne(value, ctx));
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
/**
|
|
100
117
|
* Inverse of `stringify`. Synchronous: no async types need awaiting
|
|
101
118
|
* during decode (binary is already inlined as base64).
|
|
@@ -153,10 +170,24 @@ function newEncodeCtx() {
|
|
|
153
170
|
|
|
154
171
|
/**
|
|
155
172
|
* Pass 1: walk the value, count references, pre-read Blob/File bytes.
|
|
156
|
-
* Async because `Blob.arrayBuffer()` is async.
|
|
157
|
-
*
|
|
173
|
+
* Async because `Blob.arrayBuffer()` is async. The walk itself is the
|
|
174
|
+
* synchronous `walkRefs`; only the byte reads it collects are awaited.
|
|
158
175
|
*/
|
|
159
176
|
async function countRefs(value, ctx) {
|
|
177
|
+
for (const blob of walkRefs(value, ctx)) {
|
|
178
|
+
ctx.blobBytes.set(blob, new Uint8Array(await blob.arrayBuffer()));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Count references and assign ids to shared objects, returning the Blob/File
|
|
184
|
+
* values found (whose bytes the async path then reads). Iterative (an explicit
|
|
185
|
+
* stack) to avoid recursion limits on deeply nested values.
|
|
186
|
+
* @returns {Blob[]}
|
|
187
|
+
*/
|
|
188
|
+
function walkRefs(value, ctx) {
|
|
189
|
+
/** @type {Blob[]} */
|
|
190
|
+
const blobs = [];
|
|
160
191
|
const stack = [value];
|
|
161
192
|
while (stack.length) {
|
|
162
193
|
const v = stack.pop();
|
|
@@ -165,8 +196,7 @@ async function countRefs(value, ctx) {
|
|
|
165
196
|
ctx.refCount.set(v, cur + 1);
|
|
166
197
|
if (cur > 0) continue; // already traversed children
|
|
167
198
|
if (typeof Blob !== 'undefined' && v instanceof Blob) {
|
|
168
|
-
|
|
169
|
-
ctx.blobBytes.set(v, buf);
|
|
199
|
+
blobs.push(v);
|
|
170
200
|
continue;
|
|
171
201
|
}
|
|
172
202
|
if (typeof FormData !== 'undefined' && v instanceof FormData) {
|
|
@@ -195,6 +225,7 @@ async function countRefs(value, ctx) {
|
|
|
195
225
|
for (const [obj, count] of ctx.refCount) {
|
|
196
226
|
if (count > 1) ctx.idOf.set(obj, ctx.nextId++);
|
|
197
227
|
}
|
|
228
|
+
return blobs;
|
|
198
229
|
}
|
|
199
230
|
|
|
200
231
|
function encodeOne(v, ctx) {
|
package/src/signal.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export function computed<T>(fn: () => T, options?: Record<string, unknown>): Sig
|
|
|
15
15
|
export function isSignal(v: unknown): v is SignalState | SignalComputed;
|
|
16
16
|
export function effect(fn: () => void): () => void;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Lit's `SignalWatcher` mixin, an identity here: a `WebComponent` already
|
|
20
|
+
* re-renders when a signal it read changes.
|
|
21
|
+
*/
|
|
22
|
+
export function SignalWatcher<T extends abstract new (...args: any[]) => any>(Base: T): T;
|
|
23
|
+
export { watch } from './directives.js';
|
|
24
|
+
|
|
18
25
|
export const Signal: {
|
|
19
26
|
State: new <T>(initial: T, options?: Record<string, unknown>) => SignalState<T>;
|
|
20
27
|
Computed: new <T>(fn: () => T, options?: Record<string, unknown>) => SignalComputed<T>;
|
package/src/signal.js
CHANGED
|
@@ -426,7 +426,7 @@ class SignalComputed {
|
|
|
426
426
|
* Watcher
|
|
427
427
|
* ============================================================ */
|
|
428
428
|
|
|
429
|
-
class
|
|
429
|
+
class WatcherImpl {
|
|
430
430
|
/** @param {() => void} notifyCb */
|
|
431
431
|
constructor(notifyCb) {
|
|
432
432
|
this.__notify = notifyCb;
|
|
@@ -553,7 +553,7 @@ function untrack(fn) {
|
|
|
553
553
|
try { return fn(); } finally { activeConsumer = prev; }
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
/** @param {SignalComputed |
|
|
556
|
+
/** @param {SignalComputed | WatcherImpl} sink */
|
|
557
557
|
function introspectSources(sink) {
|
|
558
558
|
return sink.producers.slice();
|
|
559
559
|
}
|
|
@@ -568,7 +568,7 @@ function hasSinks(producer) {
|
|
|
568
568
|
return !!producer.liveConsumers && producer.liveConsumers.size > 0;
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
/** @param {SignalComputed |
|
|
571
|
+
/** @param {SignalComputed | WatcherImpl} sink */
|
|
572
572
|
function hasSources(sink) {
|
|
573
573
|
return sink.producers.length > 0;
|
|
574
574
|
}
|
|
@@ -618,7 +618,7 @@ export const Signal = {
|
|
|
618
618
|
State: SignalState,
|
|
619
619
|
Computed: SignalComputed,
|
|
620
620
|
subtle: {
|
|
621
|
-
Watcher:
|
|
621
|
+
Watcher: WatcherImpl,
|
|
622
622
|
untrack,
|
|
623
623
|
introspectSources,
|
|
624
624
|
introspectSinks,
|
|
@@ -662,7 +662,7 @@ export function isSignal(v) {
|
|
|
662
662
|
*
|
|
663
663
|
* Effects are useful for browser-only side effects such as DOM
|
|
664
664
|
* mutations, timers, or fetch calls. Avoid in render paths; the
|
|
665
|
-
* component's built-in
|
|
665
|
+
* component's built-in signal watching already covers re-render.
|
|
666
666
|
*
|
|
667
667
|
* The spec forbids reads/writes inside a Watcher notify, so effects
|
|
668
668
|
* defer the re-run to a microtask.
|
|
@@ -674,7 +674,7 @@ export function effect(fn) {
|
|
|
674
674
|
let scheduled = false;
|
|
675
675
|
let disposed = false;
|
|
676
676
|
const computed_ = new SignalComputed(fn);
|
|
677
|
-
const w = new
|
|
677
|
+
const w = new WatcherImpl(() => {
|
|
678
678
|
if (scheduled || disposed) return;
|
|
679
679
|
scheduled = true;
|
|
680
680
|
queueMicrotask(() => {
|
|
@@ -692,3 +692,23 @@ export function effect(fn) {
|
|
|
692
692
|
w.dispose();
|
|
693
693
|
};
|
|
694
694
|
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Lit's `SignalWatcher` mixin (`@lit-labs/signals`), here an IDENTITY: a
|
|
698
|
+
* `WebComponent` already re-renders when a signal it read changes, so
|
|
699
|
+
* `class X extends SignalWatcher(WebComponent)` is the same class as
|
|
700
|
+
* `class X extends WebComponent`. Kept so code written from Lit's signals
|
|
701
|
+
* package works unchanged.
|
|
702
|
+
*
|
|
703
|
+
* @template {abstract new (...args: any[]) => any} T
|
|
704
|
+
* @param {T} Base
|
|
705
|
+
* @returns {T}
|
|
706
|
+
*/
|
|
707
|
+
export function SignalWatcher(Base) {
|
|
708
|
+
return Base;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// `watch(signal)` from `@lit-labs/signals` is the fine-grained binding
|
|
712
|
+
// directive WebJs already ships, re-exported so `@webjsdev/core/signals`
|
|
713
|
+
// carries the whole Lit signals surface.
|
|
714
|
+
export { watch } from './directives.js';
|
package/src/slot/polyfills.js
CHANGED
|
@@ -186,11 +186,11 @@ export function manualSlotFor(state, node) {
|
|
|
186
186
|
/**
|
|
187
187
|
* Walk up from a light slot to its owning host: the nearest SLOT_STATE
|
|
188
188
|
* ancestor that actually OWNS the slot. A slot separated from that ancestor
|
|
189
|
-
* by another custom element (a forwarded slot inside a foreign /
|
|
189
|
+
* by another custom element (a forwarded slot inside a foreign / server-only
|
|
190
190
|
* component) is nobody's here: attributing it to the outer host would
|
|
191
191
|
* redirect the outer host's own same-named slot, so `assign()` on such a
|
|
192
|
-
* slot is inert instead (the same carve-out as every other
|
|
193
|
-
*
|
|
192
|
+
* slot is inert instead (the same carve-out as every other write to a
|
|
193
|
+
* server-only component).
|
|
194
194
|
*/
|
|
195
195
|
function hostOfSlot(slot) {
|
|
196
196
|
for (let p = slot.parentElement; p; p = p.parentElement) {
|
package/src/slot.js
CHANGED
|
@@ -59,14 +59,10 @@
|
|
|
59
59
|
* it); read assigned content from `slotchange` or after a microtask.
|
|
60
60
|
*
|
|
61
61
|
* Live writes need the component's JS on the page. Interception + sensors
|
|
62
|
-
* install in connectedCallback, so a component
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* references its tag); for an imperative consumer reaching it through a
|
|
67
|
-
* string selector the analyser cannot see, force the ship with
|
|
68
|
-
* `static interactive = true`. Shadow components always ship (the DSD
|
|
69
|
-
* carve-out), so this is the one place elision, not slots, sets the boundary.
|
|
62
|
+
* install in connectedCallback, so a server-only component (one in a
|
|
63
|
+
* `*.server.ts` file) ships no JS and its post-mount native writes are inert,
|
|
64
|
+
* like anything else on a server-only component. Every component in an
|
|
65
|
+
* ordinary file ships (#1686), so it always has them.
|
|
70
66
|
*
|
|
71
67
|
* Polyfill safety. Every prototype patch checks for the `data-webjs-light`
|
|
72
68
|
* attribute and falls through to native otherwise, so real shadow-DOM slots
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TemplateResult } from './html.js';
|
|
2
|
+
|
|
3
|
+
/** A value spliced into a template's strings (lit's `StaticValue`). */
|
|
4
|
+
export interface StaticValue {
|
|
5
|
+
readonly _$webjsStatic: string;
|
|
6
|
+
readonly r: symbol;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** lit's `unsafeStatic`: trusted text inserted as static markup. NEVER user input. */
|
|
10
|
+
export function unsafeStatic(value: string): StaticValue;
|
|
11
|
+
/** lit's `literal`: a static value whose interpolations must themselves be static. */
|
|
12
|
+
export function literal(strings: TemplateStringsArray, ...values: unknown[]): StaticValue;
|
|
13
|
+
/** lit's `withStatic`: wrap a template tag so it accepts static values. */
|
|
14
|
+
export function withStatic<R>(
|
|
15
|
+
coreTag: (strings: TemplateStringsArray | string[], ...values: unknown[]) => R,
|
|
16
|
+
): (strings: TemplateStringsArray | string[], ...values: unknown[]) => R;
|
|
17
|
+
/** `html` that accepts `literal` / `unsafeStatic` values in any position. */
|
|
18
|
+
export function html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
|
|
19
|
+
/** `svg` that accepts static values. */
|
|
20
|
+
export function svg(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
|
|
21
|
+
/** `mathml` that accepts static values. */
|
|
22
|
+
export function mathml(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
|