foldkit 0.147.0 → 0.148.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/dist/buildToken.d.ts +3 -0
- package/dist/buildToken.d.ts.map +1 -0
- package/dist/buildToken.js +21 -0
- package/dist/controlledDomState.d.ts +25 -0
- package/dist/controlledDomState.d.ts.map +1 -0
- package/dist/controlledDomState.js +240 -0
- package/dist/cssStyleProperties.d.ts +6 -0
- package/dist/cssStyleProperties.d.ts.map +1 -0
- package/dist/cssStyleProperties.js +91 -0
- package/dist/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/server/host.d.ts +102 -8
- package/dist/experimental/server/host.d.ts.map +1 -1
- package/dist/experimental/server/host.js +203 -13
- package/dist/experimental/server/public.d.ts +2 -2
- package/dist/experimental/server/public.d.ts.map +1 -1
- package/dist/experimental/server/public.js +1 -1
- package/dist/experimental/server/serialize.d.ts +15 -5
- package/dist/experimental/server/serialize.d.ts.map +1 -1
- package/dist/experimental/server/serialize.js +367 -144
- package/dist/experimental/server/server.d.ts +55 -14
- package/dist/experimental/server/server.d.ts.map +1 -1
- package/dist/experimental/server/server.js +544 -21
- package/dist/experimental/server/template.d.ts +8 -0
- package/dist/experimental/server/template.d.ts.map +1 -1
- package/dist/experimental/server/template.js +437 -2
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +436 -29
- package/dist/hydrate.d.ts +1 -1
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +449 -121
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +70 -0
- package/dist/nativeInnerHtml.d.ts +13 -0
- package/dist/nativeInnerHtml.d.ts.map +1 -0
- package/dist/nativeInnerHtml.js +30 -0
- package/dist/propertyProvenance.d.ts +33 -0
- package/dist/propertyProvenance.d.ts.map +1 -0
- package/dist/propertyProvenance.js +78 -0
- package/dist/propsModule.d.ts.map +1 -1
- package/dist/propsModule.js +149 -15
- package/dist/runtime/public.d.ts +1 -1
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/runtime.d.ts +30 -6
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +230 -27
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- package/dist/test/apps/attributes.d.ts +1 -0
- package/dist/test/apps/attributes.d.ts.map +1 -1
- package/dist/test/apps/attributes.js +8 -1
- package/dist/test/apps/login.js +1 -1
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +2 -1
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +2 -1
- package/package.json +2 -2
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Array, Cause, Context, Duration, Effect, Exit, Fiber, Function, Layer, Match, Option, Predicate, PubSub, Record, Ref, Runtime, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
2
|
+
import { HYDRATION_BUILD_ATTRIBUTE } from '../buildToken.js';
|
|
2
3
|
import { __CurrentRegistry as __CurrentInterruptRegistry, __makeRegistry as __makeInterruptRegistry, } from '../command/interruptible/index.js';
|
|
3
4
|
import { createDevToolsStore, } from '../devTools/store.js';
|
|
4
5
|
import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
|
|
@@ -195,8 +196,8 @@ export class Dispatch extends Context.Service()('@foldkit/Dispatch') {
|
|
|
195
196
|
}
|
|
196
197
|
const hydrationForRoot = (root, isFlagsRequired) => {
|
|
197
198
|
const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
|
|
198
|
-
const
|
|
199
|
-
return { root, runtimeId,
|
|
199
|
+
const flagsScripts = pipe(Array.fromIterable(document.querySelectorAll(`script[${FOLDKIT_FLAGS_ATTRIBUTE}]`)), Array.filter(script => script.getAttribute(FOLDKIT_FLAGS_ATTRIBUTE) === runtimeId));
|
|
200
|
+
return { root, runtimeId, flagsScripts, isFlagsRequired };
|
|
200
201
|
};
|
|
201
202
|
// NOTE: hydration is scoped to the app's own stamped root so a server-rendered
|
|
202
203
|
// app never adopts another app's DOM. A container that carries the stamp is that
|
|
@@ -212,8 +213,159 @@ const hydrationForRoot = (root, isFlagsRequired) => {
|
|
|
212
213
|
// root took the placeholder's place and `getElementById` no longer finds it, so
|
|
213
214
|
// the stamp is the only handle; more than one stamped root is then ambiguous and
|
|
214
215
|
// a hard error rather than a silent wrong-DOM adoption.
|
|
216
|
+
// A runtime id names one application for the whole page: it pairs a root with
|
|
217
|
+
// its Flags payload, and it keys the Model and scroll position hot reloading
|
|
218
|
+
// preserves. Two roots sharing one are not two applications but one claimed
|
|
219
|
+
// twice, so whichever boots second would read the other's handoff and restore
|
|
220
|
+
// the other's Model. `injectIntoTemplate` refuses to build such a page; this is
|
|
221
|
+
// the check for a page assembled some other way.
|
|
222
|
+
//
|
|
223
|
+
// Distinct ids do not make two hydrated applications independent, which is not
|
|
224
|
+
// a supported arrangement: each page-owning application rewrites the document's
|
|
225
|
+
// metadata and installs document-wide navigation listeners.
|
|
226
|
+
const assertRuntimeIdsAreUnique = (stampedRoots) => {
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
for (const root of stampedRoots) {
|
|
229
|
+
const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
|
|
230
|
+
if (seen.has(runtimeId)) {
|
|
231
|
+
containRefusedPage(root.ownerDocument);
|
|
232
|
+
throw new Error(`[foldkit] Found more than one server-rendered root stamped ` +
|
|
233
|
+
`"${runtimeId}". A runtime id names one application for the whole ` +
|
|
234
|
+
'page: it pairs a root with its Flags payload and keys the Model and ' +
|
|
235
|
+
'scroll position hot reloading preserves, so two roots sharing one ' +
|
|
236
|
+
"would take each other's state. Render each application with its " +
|
|
237
|
+
'own `runtimeId`.');
|
|
238
|
+
}
|
|
239
|
+
seen.add(runtimeId);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
// The reason this page cannot be adopted by this client, or `undefined` when
|
|
243
|
+
// the two name the same deployment.
|
|
244
|
+
//
|
|
245
|
+
// The client's id is required and must be non-empty. An absent one would
|
|
246
|
+
// otherwise equal the absent marker on a page served before build ids existed,
|
|
247
|
+
// which reads a page from an unknown deployment as one of this build's own: the
|
|
248
|
+
// exact case the id exists to refuse.
|
|
249
|
+
const buildSkew = (root, buildId, runtimeId) => {
|
|
250
|
+
if (buildId === undefined || buildId === '') {
|
|
251
|
+
return new Error('[foldkit] Runtime.hydrate was given no build id. Hydration compares ' +
|
|
252
|
+
'the id the server stamped on the root with this client’s own before ' +
|
|
253
|
+
'it adopts any DOM, and without one a page from any deployment would ' +
|
|
254
|
+
'be adopted as this one. Pass ' +
|
|
255
|
+
'`buildId: import.meta.env.FOLDKIT_BUILD_ID`, the same value the ' +
|
|
256
|
+
'server entry passes to `renderToString`.');
|
|
257
|
+
}
|
|
258
|
+
const servedBuild = root.getAttribute(HYDRATION_BUILD_ATTRIBUTE);
|
|
259
|
+
if (servedBuild === buildId) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return new Error(`[foldkit] Runtime.hydrate found application "${runtimeId}" served by ` +
|
|
263
|
+
`${servedBuild === null ? 'no known deployment' : `deployment "${servedBuild}"`}` +
|
|
264
|
+
`, but this client belongs to deployment "${buildId}". Startup stops ` +
|
|
265
|
+
'here rather than reading a handoff written by other code: the Flags in ' +
|
|
266
|
+
'the page are that deployment’s, and this build could accept them while ' +
|
|
267
|
+
'every value in them means something else. Serve the page from the ' +
|
|
268
|
+
'running deployment, and keep stale HTML out of shared caches.');
|
|
269
|
+
};
|
|
270
|
+
const REFUSED_ATTRIBUTE = 'data-foldkit-refused';
|
|
271
|
+
const REFUSAL_SHIELD_ATTRIBUTE = 'data-foldkit-refusal-shield';
|
|
272
|
+
const refusalShields = new WeakMap();
|
|
273
|
+
const REFUSAL_SHIELD_INPUT_EVENTS = [
|
|
274
|
+
'auxclick',
|
|
275
|
+
'click',
|
|
276
|
+
'contextmenu',
|
|
277
|
+
'dblclick',
|
|
278
|
+
'keydown',
|
|
279
|
+
'keypress',
|
|
280
|
+
'keyup',
|
|
281
|
+
'mousedown',
|
|
282
|
+
'mouseup',
|
|
283
|
+
'pointerdown',
|
|
284
|
+
'pointerup',
|
|
285
|
+
'touchend',
|
|
286
|
+
'touchstart',
|
|
287
|
+
];
|
|
288
|
+
const preventRefusalShieldInteraction = (event) => {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
event.stopImmediatePropagation();
|
|
291
|
+
};
|
|
292
|
+
const openRefusalShield = (shield) => {
|
|
293
|
+
shield.inert = true;
|
|
294
|
+
shield.setAttribute('inert', '');
|
|
295
|
+
if (shield.open && typeof shield.close === 'function') {
|
|
296
|
+
shield.close();
|
|
297
|
+
}
|
|
298
|
+
if (typeof shield.showModal === 'function') {
|
|
299
|
+
shield.showModal();
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
shield.setAttribute('open', '');
|
|
303
|
+
}
|
|
304
|
+
shield.inert = false;
|
|
305
|
+
shield.removeAttribute('inert');
|
|
306
|
+
shield.focus({ preventScroll: true });
|
|
307
|
+
};
|
|
308
|
+
const installRefusalShield = (ownerDocument) => {
|
|
309
|
+
const existing = refusalShields.get(ownerDocument);
|
|
310
|
+
if (existing !== undefined && existing.isConnected) {
|
|
311
|
+
openRefusalShield(existing);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const shield = ownerDocument.createElement('dialog');
|
|
315
|
+
shield.setAttribute(REFUSAL_SHIELD_ATTRIBUTE, '');
|
|
316
|
+
shield.setAttribute('aria-label', 'Page unavailable');
|
|
317
|
+
shield.setAttribute('aria-modal', 'true');
|
|
318
|
+
shield.setAttribute('closedby', 'none');
|
|
319
|
+
shield.tabIndex = -1;
|
|
320
|
+
shield.textContent =
|
|
321
|
+
'This page could not start safely. Reload to get the current version.';
|
|
322
|
+
shield.style.alignItems = 'center';
|
|
323
|
+
shield.style.background = 'rgba(15, 23, 42, 0.96)';
|
|
324
|
+
shield.style.border = '0';
|
|
325
|
+
shield.style.boxSizing = 'border-box';
|
|
326
|
+
shield.style.color = 'white';
|
|
327
|
+
shield.style.font = '600 1rem/1.5 system-ui, sans-serif';
|
|
328
|
+
shield.style.display = 'grid';
|
|
329
|
+
shield.style.height = '100vh';
|
|
330
|
+
shield.style.inset = '0';
|
|
331
|
+
shield.style.margin = '0';
|
|
332
|
+
shield.style.maxHeight = 'none';
|
|
333
|
+
shield.style.maxWidth = 'none';
|
|
334
|
+
shield.style.overflow = 'hidden';
|
|
335
|
+
shield.style.padding = '2rem';
|
|
336
|
+
shield.style.position = 'fixed';
|
|
337
|
+
shield.style.touchAction = 'none';
|
|
338
|
+
shield.style.userSelect = 'none';
|
|
339
|
+
shield.style.width = '100vw';
|
|
340
|
+
shield.addEventListener('cancel', preventRefusalShieldInteraction);
|
|
341
|
+
for (const eventName of REFUSAL_SHIELD_INPUT_EVENTS) {
|
|
342
|
+
shield.addEventListener(eventName, preventRefusalShieldInteraction, {
|
|
343
|
+
capture: true,
|
|
344
|
+
passive: false,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
ownerDocument.documentElement.appendChild(shield);
|
|
348
|
+
refusalShields.set(ownerDocument, shield);
|
|
349
|
+
openRefusalShield(shield);
|
|
350
|
+
};
|
|
351
|
+
const containRefusedPage = (ownerDocument) => {
|
|
352
|
+
const boundary = ownerDocument.body ?? ownerDocument.documentElement;
|
|
353
|
+
boundary.inert = true;
|
|
354
|
+
boundary.setAttribute('inert', '');
|
|
355
|
+
boundary.setAttribute('aria-hidden', 'true');
|
|
356
|
+
boundary.setAttribute(REFUSED_ATTRIBUTE, '');
|
|
357
|
+
installRefusalShield(ownerDocument);
|
|
358
|
+
};
|
|
359
|
+
// Whether this page carries anything a Foldkit server render leaves behind. A
|
|
360
|
+
// resolution failure on such a page is a refused handoff, and the markup is
|
|
361
|
+
// contained; the same failure on a page with none of these markers is a client
|
|
362
|
+
// application whose container never existed, where there is no server render to
|
|
363
|
+
// refuse and nothing to take out of reach.
|
|
364
|
+
const hasServerRenderedMarkup = (ownerDocument) => ownerDocument.querySelector(`[${FOLDKIT_APP_ATTRIBUTE}], [${HYDRATION_BUILD_ATTRIBUTE}], ` +
|
|
365
|
+
`[${FOLDKIT_FLAGS_ATTRIBUTE}]`) !== null;
|
|
215
366
|
const findDocumentHydration = (container, isFlagsRequired) => {
|
|
216
367
|
const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
|
|
368
|
+
assertRuntimeIdsAreUnique(stampedRoots);
|
|
217
369
|
if (container !== null) {
|
|
218
370
|
if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
|
|
219
371
|
return hydrationForRoot(container, isFlagsRequired);
|
|
@@ -230,6 +382,7 @@ const findDocumentHydration = (container, isFlagsRequired) => {
|
|
|
230
382
|
});
|
|
231
383
|
}
|
|
232
384
|
if (stampedRoots.length > 1) {
|
|
385
|
+
containRefusedPage(document);
|
|
233
386
|
throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
|
|
234
387
|
`\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
|
|
235
388
|
'Give each app its own container element so the runtime can tell ' +
|
|
@@ -404,7 +557,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
404
557
|
const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
|
|
405
558
|
? hydration.runtimeId
|
|
406
559
|
: (container?.id ?? '');
|
|
407
|
-
const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags) => {
|
|
560
|
+
const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags, buildId) => {
|
|
408
561
|
// NOTE: one notifier per runtime, provided across the whole runtime
|
|
409
562
|
// Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
|
|
410
563
|
// the same signal. A commit in one embedded application must never wake a
|
|
@@ -532,25 +685,54 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
532
685
|
Effect.succeed(undefined),
|
|
533
686
|
onSome: provideResources,
|
|
534
687
|
});
|
|
535
|
-
|
|
688
|
+
// Every hydration refusal that knows which root it was going to adopt
|
|
689
|
+
// contains that root first. The build id is one reason to refuse; a
|
|
690
|
+
// missing, duplicated, malformed, or Schema-incompatible Flags payload
|
|
691
|
+
// is another, and the page left behind is just as live in each case.
|
|
692
|
+
const refuseHydration = (root, message, cause) => {
|
|
693
|
+
containRefusedPage(root.ownerDocument);
|
|
694
|
+
return Effect.die(cause === undefined
|
|
695
|
+
? new Error(message)
|
|
696
|
+
: new Error(message, { cause }));
|
|
697
|
+
};
|
|
698
|
+
const decodeFlagsPayload = (payload, runtimeId, root) => Effect.try({
|
|
536
699
|
try: () => {
|
|
537
700
|
const parsedPayload = JSON.parse(payload);
|
|
538
701
|
return pipe(
|
|
539
702
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
540
703
|
Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
|
|
541
704
|
},
|
|
542
|
-
catch: cause =>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
705
|
+
catch: cause => cause,
|
|
706
|
+
}).pipe(Effect.catch(cause => refuseHydration(root, '[foldkit] Runtime.hydrate could not decode the server ' +
|
|
707
|
+
`Flags payload for application "${runtimeId}". The HTML ` +
|
|
708
|
+
'and client bundle must use the same Flags Schema.', cause)));
|
|
546
709
|
const maybeRequestedHydration = bootMode === 'Hydrate'
|
|
547
710
|
? Option.fromNullishOr(hydration)
|
|
548
711
|
: Option.none();
|
|
549
712
|
if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
|
|
713
|
+
// A hydrating client that finds no stamped root will not adopt
|
|
714
|
+
// whatever the page holds, so the page is contained whether or not the
|
|
715
|
+
// caller named a container.
|
|
716
|
+
containRefusedPage(container === null ? document : container.ownerDocument);
|
|
550
717
|
return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
|
|
551
718
|
`root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
|
|
552
719
|
'Runtime.run for a fresh client boot.'));
|
|
553
720
|
}
|
|
721
|
+
// The build the served page came from is settled here, before the Flags
|
|
722
|
+
// payload text is accessed, parsed, or decoded, before `init` runs, and
|
|
723
|
+
// therefore before any Command, Subscription, ManagedResource, or port
|
|
724
|
+
// this boot would start. A page from another deployment carries that
|
|
725
|
+
// deployment's Flags, which the current Schema may well accept while
|
|
726
|
+
// every value in them means something else, so deferring the comparison
|
|
727
|
+
// to the DOM patch lets stale data reach new code that already acted on
|
|
728
|
+
// it.
|
|
729
|
+
if (Option.isSome(maybeRequestedHydration)) {
|
|
730
|
+
const skew = buildSkew(maybeRequestedHydration.value.root, buildId, maybeRequestedHydration.value.runtimeId);
|
|
731
|
+
if (skew !== undefined) {
|
|
732
|
+
containRefusedPage(maybeRequestedHydration.value.root.ownerDocument);
|
|
733
|
+
return yield* Effect.die(skew);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
554
736
|
// NOTE: an HMR-restored Model wins over DOM adoption because the
|
|
555
737
|
// server DOM reflects older code. The hydration handoff is still
|
|
556
738
|
// required, but the restored Model gets a fresh patch against its
|
|
@@ -561,15 +743,15 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
561
743
|
const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
|
|
562
744
|
onNone: () => Effect.succeed(Option.none()),
|
|
563
745
|
onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
|
|
564
|
-
? Array.match(requestedHydration.
|
|
565
|
-
onEmpty: () =>
|
|
746
|
+
? Array.match(requestedHydration.flagsScripts, {
|
|
747
|
+
onEmpty: () => refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found application ' +
|
|
566
748
|
`"${requestedHydration.runtimeId}" but its ` +
|
|
567
|
-
'server Flags payload is missing.')
|
|
568
|
-
onNonEmpty: ([
|
|
569
|
-
?
|
|
749
|
+
'server Flags payload is missing.'),
|
|
750
|
+
onNonEmpty: ([payloadScript, ...remainingScripts]) => Array.isArrayNonEmpty(remainingScripts)
|
|
751
|
+
? refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found multiple ' +
|
|
570
752
|
'server Flags payloads for application ' +
|
|
571
|
-
`"${requestedHydration.runtimeId}".`)
|
|
572
|
-
: decodeFlagsPayload(
|
|
753
|
+
`"${requestedHydration.runtimeId}".`)
|
|
754
|
+
: decodeFlagsPayload(payloadScript.textContent ?? '', requestedHydration.runtimeId, requestedHydration.root),
|
|
573
755
|
})
|
|
574
756
|
: /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
575
757
|
Effect.succeed(undefined), Option.some),
|
|
@@ -1061,7 +1243,11 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
1061
1243
|
// container (a dispose-then-embed remount) from re-detecting
|
|
1062
1244
|
// this now-consumed root as hydratable.
|
|
1063
1245
|
hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
|
|
1064
|
-
|
|
1246
|
+
// An empty id reaches the adoption step's own check as a
|
|
1247
|
+
// value that matches nothing. Boot already refused a
|
|
1248
|
+
// hydration without an id, so this stands in only for a
|
|
1249
|
+
// caller that reached here another way.
|
|
1250
|
+
return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
|
|
1065
1251
|
}
|
|
1066
1252
|
return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
|
|
1067
1253
|
}));
|
|
@@ -1502,7 +1688,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
1502
1688
|
ports,
|
|
1503
1689
|
};
|
|
1504
1690
|
runtimeInternals.set(program, {
|
|
1505
|
-
startWith: (maybeConnector, hmrModel, bootMode, flags) => startWith(maybeConnector, hmrModel, bootMode, flags),
|
|
1691
|
+
startWith: (maybeConnector, hmrModel, bootMode, flags, buildId) => startWith(maybeConnector, hmrModel, bootMode, flags, buildId),
|
|
1506
1692
|
kind,
|
|
1507
1693
|
isEmbedActive: false,
|
|
1508
1694
|
maybeActiveFiber: Option.none(),
|
|
@@ -1631,6 +1817,14 @@ export function makeApplication(config) {
|
|
|
1631
1817
|
const hydration = findDocumentHydration(container, hasFlags);
|
|
1632
1818
|
const resolvedContainer = hydration?.root ?? container;
|
|
1633
1819
|
if (resolvedContainer === null) {
|
|
1820
|
+
// A server-rendered page whose root lost its stamp reaches exactly here:
|
|
1821
|
+
// template injection put the render where the placeholder was, so
|
|
1822
|
+
// `getElementById` finds nothing and the stamp that would have named the
|
|
1823
|
+
// root is gone. There is no handoff to refuse further along, and the markup
|
|
1824
|
+
// is as live as any other refused page, so it is contained here.
|
|
1825
|
+
if (hasServerRenderedMarkup(document)) {
|
|
1826
|
+
containRefusedPage(document);
|
|
1827
|
+
}
|
|
1634
1828
|
throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
|
|
1635
1829
|
'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
|
|
1636
1830
|
'rendered, and your script runs after it). On a server-rendered page ' +
|
|
@@ -1853,7 +2047,7 @@ const resolveHmrModel = (runtimeId) => {
|
|
|
1853
2047
|
};
|
|
1854
2048
|
/** Starts a program Effect with explicit boot inputs for runtime tests.
|
|
1855
2049
|
* @internal */
|
|
1856
|
-
export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
2050
|
+
export const __startProgram = (program, hmrModel, bootMode, flags, buildId) => {
|
|
1857
2051
|
const internals = runtimeInternals.get(program);
|
|
1858
2052
|
if (Predicate.isUndefined(internals)) {
|
|
1859
2053
|
return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
|
|
@@ -1863,7 +2057,7 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
|
1863
2057
|
return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
|
|
1864
2058
|
'makeApplication.'));
|
|
1865
2059
|
}
|
|
1866
|
-
return internals.startWith(Option.none(), hmrModel, bootMode, flags);
|
|
2060
|
+
return internals.startWith(Option.none(), hmrModel, bootMode, flags, buildId);
|
|
1867
2061
|
};
|
|
1868
2062
|
// NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
|
|
1869
2063
|
// runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
|
|
@@ -1876,8 +2070,8 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
|
1876
2070
|
// interrupt at all and lets the document take the runtime with it. Error
|
|
1877
2071
|
// reporting and the keep-alive interval come from `makeRunMain` either way.
|
|
1878
2072
|
const runMainWithoutUnloadInterrupt = Runtime.makeRunMain(Function.constVoid);
|
|
1879
|
-
const startProgram = (program, bootMode, flags) => {
|
|
1880
|
-
runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags))));
|
|
2073
|
+
const startProgram = (program, bootMode, flags, buildId) => {
|
|
2074
|
+
runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags, buildId))));
|
|
1881
2075
|
};
|
|
1882
2076
|
export function run(program, options) {
|
|
1883
2077
|
startProgram(program, 'Fresh', options?.flags);
|
|
@@ -1886,15 +2080,24 @@ export function run(program, options) {
|
|
|
1886
2080
|
* of building it fresh. Use this as the client entry for a page served by
|
|
1887
2081
|
* `renderToString`: the first render attaches to the stamped root, keeps the
|
|
1888
2082
|
* existing nodes, and reconstructs the Model from the Flags the server
|
|
1889
|
-
* embedded. The handoff is strict: a missing server root,
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
2083
|
+
* embedded. The handoff is strict: a missing server root, a root stamped more
|
|
2084
|
+
* than once, more than one root with no container to choose between them, a
|
|
2085
|
+
* missing Flags payload, an undecodable payload, or a page from another
|
|
2086
|
+
* deployment terminates startup. Every one of those contains the page first:
|
|
2087
|
+
* the document's body is marked `inert` and a nondismissable modal shield is
|
|
2088
|
+
* opened above existing top-layer content, so native links, forms, controls,
|
|
2089
|
+
* and focus targets stop responding. Containment leaves author-owned dialogs
|
|
2090
|
+
* open without calling `close` or dispatching `cancel`. Nothing is moved, so
|
|
2091
|
+
* no custom element reconnects and no frame reloads. This is not a script or
|
|
2092
|
+
* event sandbox: existing capture-phase handlers, browser-generated top-layer
|
|
2093
|
+
* events, timers, and other stale scripts can still run and can open newer
|
|
2094
|
+
* top-layer UI. Use `run` in a separate client-only entry when the page should
|
|
2095
|
+
* boot without server output.
|
|
1893
2096
|
*
|
|
1894
2097
|
* @experimental Server rendering and hydration are experimental while their
|
|
1895
2098
|
* contracts settle. */
|
|
1896
|
-
export const hydrate = (program) => {
|
|
1897
|
-
startProgram(program, 'Hydrate');
|
|
2099
|
+
export const hydrate = (program, options) => {
|
|
2100
|
+
startProgram(program, 'Hydrate', undefined, options.buildId);
|
|
1898
2101
|
};
|
|
1899
2102
|
const buildPortHandles = (ports, connector) => {
|
|
1900
2103
|
const handles = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../src/snabbdom/attributes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../src/snabbdom/attributes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;AAkG7D,eAAO,MAAM,gBAAgB,EAAE,MAI9B,CAAA"}
|
|
@@ -1,12 +1,65 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
2
|
+
import { parsedAttributeName } from '../domReflection.js';
|
|
1
3
|
import { VNodeDataMask } from './vnode.js';
|
|
2
4
|
const xlinkNS = 'http://www.w3.org/1999/xlink';
|
|
3
5
|
const xmlnsNS = 'http://www.w3.org/2000/xmlns/';
|
|
4
6
|
const xmlNS = 'http://www.w3.org/XML/1998/namespace';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
7
|
+
const htmlNS = 'http://www.w3.org/1999/xhtml';
|
|
8
|
+
const FOREIGN_ATTRIBUTE_NAMESPACES = {
|
|
9
|
+
'xlink:actuate': xlinkNS,
|
|
10
|
+
'xlink:arcrole': xlinkNS,
|
|
11
|
+
'xlink:href': xlinkNS,
|
|
12
|
+
'xlink:role': xlinkNS,
|
|
13
|
+
'xlink:show': xlinkNS,
|
|
14
|
+
'xlink:title': xlinkNS,
|
|
15
|
+
'xlink:type': xlinkNS,
|
|
16
|
+
'xml:base': xmlNS,
|
|
17
|
+
'xml:lang': xmlNS,
|
|
18
|
+
'xml:space': xmlNS,
|
|
19
|
+
xmlns: xmlnsNS,
|
|
20
|
+
'xmlns:xlink': xmlnsNS,
|
|
21
|
+
};
|
|
22
|
+
const namespaceFor = (elementNamespace, name) => {
|
|
23
|
+
if (elementNamespace === null || elementNamespace === htmlNS) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return FOREIGN_ATTRIBUTE_NAMESPACES[name];
|
|
27
|
+
};
|
|
28
|
+
const normalizedAttributes = (attrs, namespace) => {
|
|
29
|
+
const normalized = new Map();
|
|
30
|
+
for (const name of Object.keys(attrs)) {
|
|
31
|
+
normalized.set(parsedAttributeName(namespace, name), attrs[name]);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
};
|
|
35
|
+
const setAttribute = (element, name, value) => {
|
|
36
|
+
const namespace = namespaceFor(element.namespaceURI, name);
|
|
37
|
+
if (value === true) {
|
|
38
|
+
if (namespace === undefined) {
|
|
39
|
+
element.setAttribute(name, '');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
element.setAttributeNS(namespace, name, '');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (value === false) {
|
|
46
|
+
if (namespace === undefined) {
|
|
47
|
+
element.removeAttribute(name);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const separator = name.indexOf(':');
|
|
51
|
+
const localName = separator === -1 ? name : name.slice(separator + 1);
|
|
52
|
+
element.removeAttributeNS(namespace, localName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (namespace === undefined) {
|
|
56
|
+
element.setAttribute(name, value);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
element.setAttributeNS(namespace, name, value);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
8
62
|
function updateAttrs(oldVnode, vnode) {
|
|
9
|
-
let key;
|
|
10
63
|
const elm = vnode.elm;
|
|
11
64
|
let oldAttrs = oldVnode.data.attrs;
|
|
12
65
|
let attrs = vnode.data.attrs;
|
|
@@ -16,43 +69,18 @@ function updateAttrs(oldVnode, vnode) {
|
|
|
16
69
|
return;
|
|
17
70
|
oldAttrs = oldAttrs || {};
|
|
18
71
|
attrs = attrs || {};
|
|
72
|
+
const normalizedOld = normalizedAttributes(oldAttrs, elm.namespaceURI);
|
|
73
|
+
const normalizedNext = normalizedAttributes(attrs, elm.namespaceURI);
|
|
19
74
|
// update modified attributes, add new attributes
|
|
20
|
-
for (
|
|
21
|
-
const
|
|
22
|
-
const old = oldAttrs[key];
|
|
75
|
+
for (const [name, cur] of normalizedNext) {
|
|
76
|
+
const old = normalizedOld.get(name);
|
|
23
77
|
if (old !== cur) {
|
|
24
|
-
|
|
25
|
-
elm.setAttribute(key, '');
|
|
26
|
-
}
|
|
27
|
-
else if (cur === false) {
|
|
28
|
-
elm.removeAttribute(key);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
if (key.charCodeAt(0) !== xChar) {
|
|
32
|
-
elm.setAttribute(key, cur);
|
|
33
|
-
}
|
|
34
|
-
else if (key.charCodeAt(3) === colonChar) {
|
|
35
|
-
// Assume xml namespace
|
|
36
|
-
elm.setAttributeNS(xmlNS, key, cur);
|
|
37
|
-
}
|
|
38
|
-
else if (key.charCodeAt(5) === colonChar) {
|
|
39
|
-
// Assume 'xmlns' or 'xlink' namespace
|
|
40
|
-
key.charCodeAt(1) === mChar
|
|
41
|
-
? elm.setAttributeNS(xmlnsNS, key, cur)
|
|
42
|
-
: elm.setAttributeNS(xlinkNS, key, cur);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
elm.setAttribute(key, cur);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
78
|
+
setAttribute(elm, name, cur);
|
|
48
79
|
}
|
|
49
80
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for (key in oldAttrs) {
|
|
54
|
-
if (!(key in attrs)) {
|
|
55
|
-
elm.removeAttribute(key);
|
|
81
|
+
for (const name of normalizedOld.keys()) {
|
|
82
|
+
if (!normalizedNext.has(name)) {
|
|
83
|
+
setAttribute(elm, name, false);
|
|
56
84
|
}
|
|
57
85
|
}
|
|
58
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/snabbdom/style.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/snabbdom/style.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;AAEvD,MAAM,MAAM,UAAU,GAAG,YAAY,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACvB,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/C,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9C,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChD,CAAA;AAyJH,eAAO,MAAM,WAAW,EAAE,MAOzB,CAAA"}
|
package/dist/snabbdom/style.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
2
|
+
import { serializedStylePropertyName } from '../domReflection.js';
|
|
1
3
|
import { VNodeDataMask } from './vnode.js';
|
|
2
4
|
// Binding `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
|
|
3
5
|
const raf = typeof window !== 'undefined' &&
|
|
@@ -10,11 +12,35 @@ const nextFrame = function (fn) {
|
|
|
10
12
|
});
|
|
11
13
|
};
|
|
12
14
|
let reflowForced = false;
|
|
13
|
-
function
|
|
15
|
+
function setStyle(style, propertyName, value) {
|
|
16
|
+
style.setProperty(serializedStylePropertyName(propertyName), String(value));
|
|
17
|
+
}
|
|
18
|
+
function removeStyle(style, propertyName) {
|
|
19
|
+
style.removeProperty(serializedStylePropertyName(propertyName));
|
|
20
|
+
}
|
|
21
|
+
function setNextFrame(style, propertyName, value) {
|
|
14
22
|
nextFrame(function () {
|
|
15
|
-
|
|
23
|
+
setStyle(style, propertyName, value);
|
|
16
24
|
});
|
|
17
25
|
}
|
|
26
|
+
function createStyle(_oldVnode, vnode) {
|
|
27
|
+
const element = vnode.elm;
|
|
28
|
+
const style = vnode.data.style;
|
|
29
|
+
if (!style) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(style)) {
|
|
33
|
+
if (name !== 'delayed' && name !== 'destroy' && name !== 'remove') {
|
|
34
|
+
setStyle(element.style, name, style[name]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const delayed = Object.hasOwn(style, 'delayed') ? style.delayed : undefined;
|
|
38
|
+
if (typeof delayed === 'object' && delayed !== null) {
|
|
39
|
+
for (const name of Object.keys(delayed)) {
|
|
40
|
+
setNextFrame(element.style, name, delayed[name]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
18
44
|
function updateStyle(oldVnode, vnode) {
|
|
19
45
|
let cur;
|
|
20
46
|
let name;
|
|
@@ -27,38 +53,31 @@ function updateStyle(oldVnode, vnode) {
|
|
|
27
53
|
return;
|
|
28
54
|
oldStyle = oldStyle || {};
|
|
29
55
|
style = style || {};
|
|
30
|
-
const oldHasDel = 'delayed'
|
|
31
|
-
for (name
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else {
|
|
38
|
-
;
|
|
39
|
-
elm.style[name] = '';
|
|
40
|
-
}
|
|
56
|
+
const oldHasDel = Object.hasOwn(oldStyle, 'delayed');
|
|
57
|
+
for (name of Object.keys(oldStyle)) {
|
|
58
|
+
if (name !== 'delayed' &&
|
|
59
|
+
name !== 'destroy' &&
|
|
60
|
+
name !== 'remove' &&
|
|
61
|
+
!Object.hasOwn(style, name)) {
|
|
62
|
+
removeStyle(elm.style, name);
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
|
-
for (name
|
|
65
|
+
for (name of Object.keys(style)) {
|
|
44
66
|
cur = style[name];
|
|
45
|
-
if (name === 'delayed' &&
|
|
46
|
-
|
|
67
|
+
if (name === 'delayed' &&
|
|
68
|
+
typeof style.delayed === 'object' &&
|
|
69
|
+
style.delayed !== null) {
|
|
70
|
+
for (const name2 of Object.keys(style.delayed)) {
|
|
47
71
|
cur = style.delayed[name2];
|
|
48
72
|
if (!oldHasDel || cur !== oldStyle.delayed[name2]) {
|
|
49
73
|
setNextFrame(elm.style, name2, cur);
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
|
-
else if (name !== '
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
;
|
|
60
|
-
elm.style[name] = cur;
|
|
61
|
-
}
|
|
77
|
+
else if (name !== 'destroy' &&
|
|
78
|
+
name !== 'remove' &&
|
|
79
|
+
cur !== oldStyle[name]) {
|
|
80
|
+
setStyle(elm.style, name, cur);
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
@@ -67,16 +86,15 @@ function applyDestroyStyle(vnode) {
|
|
|
67
86
|
let name;
|
|
68
87
|
const elm = vnode.elm;
|
|
69
88
|
const s = vnode.data.style;
|
|
70
|
-
if (!s || !(style = s['destroy']))
|
|
89
|
+
if (!s || !Object.hasOwn(s, 'destroy') || !(style = s['destroy']))
|
|
71
90
|
return;
|
|
72
|
-
for (name
|
|
73
|
-
;
|
|
74
|
-
elm.style[name] = style[name];
|
|
91
|
+
for (name of Object.keys(style)) {
|
|
92
|
+
setStyle(elm.style, name, style[name]);
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
function applyRemoveStyle(vnode, rm) {
|
|
78
96
|
const s = vnode.data.style;
|
|
79
|
-
if (!s || !s.remove) {
|
|
97
|
+
if (!s || !Object.hasOwn(s, 'remove') || !s.remove) {
|
|
80
98
|
rm();
|
|
81
99
|
return;
|
|
82
100
|
}
|
|
@@ -92,9 +110,10 @@ function applyRemoveStyle(vnode, rm) {
|
|
|
92
110
|
const style = s.remove;
|
|
93
111
|
let amount = 0;
|
|
94
112
|
const applied = [];
|
|
95
|
-
for (name
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
for (name of Object.keys(style)) {
|
|
114
|
+
const propertyName = serializedStylePropertyName(name);
|
|
115
|
+
applied.push(propertyName);
|
|
116
|
+
setStyle(elm.style, name, style[name]);
|
|
98
117
|
}
|
|
99
118
|
const compStyle = getComputedStyle(elm);
|
|
100
119
|
const props = compStyle['transition-property'].split(', ');
|
|
@@ -116,7 +135,7 @@ function forceReflow() {
|
|
|
116
135
|
export const styleModule = {
|
|
117
136
|
dataMask: VNodeDataMask.Style,
|
|
118
137
|
pre: forceReflow,
|
|
119
|
-
create:
|
|
138
|
+
create: createStyle,
|
|
120
139
|
update: updateStyle,
|
|
121
140
|
destroy: applyDestroyStyle,
|
|
122
141
|
remove: applyRemoveStyle,
|
|
@@ -5,6 +5,7 @@ export declare const Message: S.Union<readonly [import("../../schema/index.js").
|
|
|
5
5
|
export type Message = typeof Message.Type;
|
|
6
6
|
export type Model = Readonly<{
|
|
7
7
|
attribute: Attribute<Message>;
|
|
8
|
+
tagName?: string;
|
|
8
9
|
}>;
|
|
9
10
|
export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<never>];
|
|
10
11
|
export declare const testId = "attribute-host";
|