foldkit 0.147.0 → 0.148.1
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 +373 -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 +546 -22
- 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 +72 -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 +33 -6
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +240 -27
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/class.d.ts.map +1 -1
- package/dist/snabbdom/class.js +61 -5
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +57 -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,166 @@ 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 (!Predicate.isString(buildId) || 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 REFUSAL_DOCUMENT_INPUT_EVENTS = ['keydown', 'keypress', 'keyup'];
|
|
289
|
+
const preventRefusalShieldInteraction = (event) => {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
event.stopImmediatePropagation();
|
|
292
|
+
};
|
|
293
|
+
const openRefusalShield = (shield) => {
|
|
294
|
+
shield.inert = true;
|
|
295
|
+
shield.setAttribute('inert', '');
|
|
296
|
+
if (shield.open && typeof shield.close === 'function') {
|
|
297
|
+
shield.close();
|
|
298
|
+
}
|
|
299
|
+
if (typeof shield.showModal === 'function') {
|
|
300
|
+
shield.showModal();
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
shield.setAttribute('open', '');
|
|
304
|
+
}
|
|
305
|
+
shield.inert = false;
|
|
306
|
+
shield.removeAttribute('inert');
|
|
307
|
+
shield.focus({ preventScroll: true });
|
|
308
|
+
};
|
|
309
|
+
const installRefusalShield = (ownerDocument) => {
|
|
310
|
+
const existing = refusalShields.get(ownerDocument);
|
|
311
|
+
if (existing !== undefined && existing.isConnected) {
|
|
312
|
+
openRefusalShield(existing);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const shield = ownerDocument.createElement('dialog');
|
|
316
|
+
shield.setAttribute(REFUSAL_SHIELD_ATTRIBUTE, '');
|
|
317
|
+
shield.setAttribute('aria-label', 'Page unavailable');
|
|
318
|
+
shield.setAttribute('aria-modal', 'true');
|
|
319
|
+
shield.setAttribute('closedby', 'none');
|
|
320
|
+
shield.tabIndex = -1;
|
|
321
|
+
shield.textContent =
|
|
322
|
+
'This page could not start safely. Reload to get the current version.';
|
|
323
|
+
shield.style.alignItems = 'center';
|
|
324
|
+
shield.style.background = 'rgba(15, 23, 42, 0.96)';
|
|
325
|
+
shield.style.border = '0';
|
|
326
|
+
shield.style.boxSizing = 'border-box';
|
|
327
|
+
shield.style.color = 'white';
|
|
328
|
+
shield.style.font = '600 1rem/1.5 system-ui, sans-serif';
|
|
329
|
+
shield.style.display = 'grid';
|
|
330
|
+
shield.style.height = '100vh';
|
|
331
|
+
shield.style.inset = '0';
|
|
332
|
+
shield.style.margin = '0';
|
|
333
|
+
shield.style.maxHeight = 'none';
|
|
334
|
+
shield.style.maxWidth = 'none';
|
|
335
|
+
shield.style.overflow = 'hidden';
|
|
336
|
+
shield.style.padding = '2rem';
|
|
337
|
+
shield.style.position = 'fixed';
|
|
338
|
+
shield.style.touchAction = 'none';
|
|
339
|
+
shield.style.userSelect = 'none';
|
|
340
|
+
shield.style.width = '100vw';
|
|
341
|
+
shield.addEventListener('cancel', preventRefusalShieldInteraction);
|
|
342
|
+
for (const eventName of REFUSAL_SHIELD_INPUT_EVENTS) {
|
|
343
|
+
shield.addEventListener(eventName, preventRefusalShieldInteraction, {
|
|
344
|
+
capture: true,
|
|
345
|
+
passive: false,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
for (const eventName of REFUSAL_DOCUMENT_INPUT_EVENTS) {
|
|
349
|
+
ownerDocument.addEventListener(eventName, preventRefusalShieldInteraction, {
|
|
350
|
+
capture: true,
|
|
351
|
+
passive: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
ownerDocument.documentElement.appendChild(shield);
|
|
355
|
+
refusalShields.set(ownerDocument, shield);
|
|
356
|
+
openRefusalShield(shield);
|
|
357
|
+
};
|
|
358
|
+
const containRefusedPage = (ownerDocument) => {
|
|
359
|
+
const boundary = ownerDocument.body ?? ownerDocument.documentElement;
|
|
360
|
+
boundary.inert = true;
|
|
361
|
+
boundary.setAttribute('inert', '');
|
|
362
|
+
boundary.setAttribute('aria-hidden', 'true');
|
|
363
|
+
boundary.setAttribute(REFUSED_ATTRIBUTE, '');
|
|
364
|
+
installRefusalShield(ownerDocument);
|
|
365
|
+
};
|
|
366
|
+
// Whether this page carries anything a Foldkit server render leaves behind. A
|
|
367
|
+
// resolution failure on such a page is a refused handoff, and the markup is
|
|
368
|
+
// contained; the same failure on a page with none of these markers is a client
|
|
369
|
+
// application whose container never existed, where there is no server render to
|
|
370
|
+
// refuse and nothing to take out of reach.
|
|
371
|
+
const hasServerRenderedMarkup = (ownerDocument) => ownerDocument.querySelector(`[${FOLDKIT_APP_ATTRIBUTE}], [${HYDRATION_BUILD_ATTRIBUTE}], ` +
|
|
372
|
+
`[${FOLDKIT_FLAGS_ATTRIBUTE}]`) !== null;
|
|
215
373
|
const findDocumentHydration = (container, isFlagsRequired) => {
|
|
216
374
|
const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
|
|
375
|
+
assertRuntimeIdsAreUnique(stampedRoots);
|
|
217
376
|
if (container !== null) {
|
|
218
377
|
if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
|
|
219
378
|
return hydrationForRoot(container, isFlagsRequired);
|
|
@@ -230,6 +389,7 @@ const findDocumentHydration = (container, isFlagsRequired) => {
|
|
|
230
389
|
});
|
|
231
390
|
}
|
|
232
391
|
if (stampedRoots.length > 1) {
|
|
392
|
+
containRefusedPage(document);
|
|
233
393
|
throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
|
|
234
394
|
`\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
|
|
235
395
|
'Give each app its own container element so the runtime can tell ' +
|
|
@@ -404,7 +564,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
404
564
|
const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
|
|
405
565
|
? hydration.runtimeId
|
|
406
566
|
: (container?.id ?? '');
|
|
407
|
-
const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags) => {
|
|
567
|
+
const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags, buildId) => {
|
|
408
568
|
// NOTE: one notifier per runtime, provided across the whole runtime
|
|
409
569
|
// Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
|
|
410
570
|
// the same signal. A commit in one embedded application must never wake a
|
|
@@ -532,25 +692,54 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
532
692
|
Effect.succeed(undefined),
|
|
533
693
|
onSome: provideResources,
|
|
534
694
|
});
|
|
535
|
-
|
|
695
|
+
// Every hydration refusal that knows which root it was going to adopt
|
|
696
|
+
// contains that root first. The build id is one reason to refuse; a
|
|
697
|
+
// missing, duplicated, malformed, or Schema-incompatible Flags payload
|
|
698
|
+
// is another, and the page left behind is just as live in each case.
|
|
699
|
+
const refuseHydration = (root, message, cause) => {
|
|
700
|
+
containRefusedPage(root.ownerDocument);
|
|
701
|
+
return Effect.die(cause === undefined
|
|
702
|
+
? new Error(message)
|
|
703
|
+
: new Error(message, { cause }));
|
|
704
|
+
};
|
|
705
|
+
const decodeFlagsPayload = (payload, runtimeId, root) => Effect.try({
|
|
536
706
|
try: () => {
|
|
537
707
|
const parsedPayload = JSON.parse(payload);
|
|
538
708
|
return pipe(
|
|
539
709
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
540
710
|
Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
|
|
541
711
|
},
|
|
542
|
-
catch: cause =>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
712
|
+
catch: cause => cause,
|
|
713
|
+
}).pipe(Effect.catch(cause => refuseHydration(root, '[foldkit] Runtime.hydrate could not decode the server ' +
|
|
714
|
+
`Flags payload for application "${runtimeId}". The HTML ` +
|
|
715
|
+
'and client bundle must use the same Flags Schema.', cause)));
|
|
546
716
|
const maybeRequestedHydration = bootMode === 'Hydrate'
|
|
547
717
|
? Option.fromNullishOr(hydration)
|
|
548
718
|
: Option.none();
|
|
549
719
|
if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
|
|
720
|
+
// A hydrating client that finds no stamped root will not adopt
|
|
721
|
+
// whatever the page holds, so the page is contained whether or not the
|
|
722
|
+
// caller named a container.
|
|
723
|
+
containRefusedPage(container === null ? document : container.ownerDocument);
|
|
550
724
|
return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
|
|
551
725
|
`root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
|
|
552
726
|
'Runtime.run for a fresh client boot.'));
|
|
553
727
|
}
|
|
728
|
+
// The build the served page came from is settled here, before the Flags
|
|
729
|
+
// payload text is accessed, parsed, or decoded, before `init` runs, and
|
|
730
|
+
// therefore before any Command, Subscription, ManagedResource, or port
|
|
731
|
+
// this boot would start. A page from another deployment carries that
|
|
732
|
+
// deployment's Flags, which the current Schema may well accept while
|
|
733
|
+
// every value in them means something else, so deferring the comparison
|
|
734
|
+
// to the DOM patch lets stale data reach new code that already acted on
|
|
735
|
+
// it.
|
|
736
|
+
if (Option.isSome(maybeRequestedHydration)) {
|
|
737
|
+
const skew = buildSkew(maybeRequestedHydration.value.root, buildId, maybeRequestedHydration.value.runtimeId);
|
|
738
|
+
if (skew !== undefined) {
|
|
739
|
+
containRefusedPage(maybeRequestedHydration.value.root.ownerDocument);
|
|
740
|
+
return yield* Effect.die(skew);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
554
743
|
// NOTE: an HMR-restored Model wins over DOM adoption because the
|
|
555
744
|
// server DOM reflects older code. The hydration handoff is still
|
|
556
745
|
// required, but the restored Model gets a fresh patch against its
|
|
@@ -561,15 +750,15 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
561
750
|
const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
|
|
562
751
|
onNone: () => Effect.succeed(Option.none()),
|
|
563
752
|
onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
|
|
564
|
-
? Array.match(requestedHydration.
|
|
565
|
-
onEmpty: () =>
|
|
753
|
+
? Array.match(requestedHydration.flagsScripts, {
|
|
754
|
+
onEmpty: () => refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found application ' +
|
|
566
755
|
`"${requestedHydration.runtimeId}" but its ` +
|
|
567
|
-
'server Flags payload is missing.')
|
|
568
|
-
onNonEmpty: ([
|
|
569
|
-
?
|
|
756
|
+
'server Flags payload is missing.'),
|
|
757
|
+
onNonEmpty: ([payloadScript, ...remainingScripts]) => Array.isArrayNonEmpty(remainingScripts)
|
|
758
|
+
? refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found multiple ' +
|
|
570
759
|
'server Flags payloads for application ' +
|
|
571
|
-
`"${requestedHydration.runtimeId}".`)
|
|
572
|
-
: decodeFlagsPayload(
|
|
760
|
+
`"${requestedHydration.runtimeId}".`)
|
|
761
|
+
: decodeFlagsPayload(payloadScript.textContent ?? '', requestedHydration.runtimeId, requestedHydration.root),
|
|
573
762
|
})
|
|
574
763
|
: /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
575
764
|
Effect.succeed(undefined), Option.some),
|
|
@@ -1061,7 +1250,11 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
1061
1250
|
// container (a dispose-then-embed remount) from re-detecting
|
|
1062
1251
|
// this now-consumed root as hydratable.
|
|
1063
1252
|
hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
|
|
1064
|
-
|
|
1253
|
+
// An empty id reaches the adoption step's own check as a
|
|
1254
|
+
// value that matches nothing. Boot already refused a
|
|
1255
|
+
// hydration without an id, so this stands in only for a
|
|
1256
|
+
// caller that reached here another way.
|
|
1257
|
+
return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
|
|
1065
1258
|
}
|
|
1066
1259
|
return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
|
|
1067
1260
|
}));
|
|
@@ -1502,7 +1695,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
|
|
|
1502
1695
|
ports,
|
|
1503
1696
|
};
|
|
1504
1697
|
runtimeInternals.set(program, {
|
|
1505
|
-
startWith: (maybeConnector, hmrModel, bootMode, flags) => startWith(maybeConnector, hmrModel, bootMode, flags),
|
|
1698
|
+
startWith: (maybeConnector, hmrModel, bootMode, flags, buildId) => startWith(maybeConnector, hmrModel, bootMode, flags, buildId),
|
|
1506
1699
|
kind,
|
|
1507
1700
|
isEmbedActive: false,
|
|
1508
1701
|
maybeActiveFiber: Option.none(),
|
|
@@ -1631,6 +1824,14 @@ export function makeApplication(config) {
|
|
|
1631
1824
|
const hydration = findDocumentHydration(container, hasFlags);
|
|
1632
1825
|
const resolvedContainer = hydration?.root ?? container;
|
|
1633
1826
|
if (resolvedContainer === null) {
|
|
1827
|
+
// A server-rendered page whose root lost its stamp reaches exactly here:
|
|
1828
|
+
// template injection put the render where the placeholder was, so
|
|
1829
|
+
// `getElementById` finds nothing and the stamp that would have named the
|
|
1830
|
+
// root is gone. There is no handoff to refuse further along, and the markup
|
|
1831
|
+
// is as live as any other refused page, so it is contained here.
|
|
1832
|
+
if (hasServerRenderedMarkup(document)) {
|
|
1833
|
+
containRefusedPage(document);
|
|
1834
|
+
}
|
|
1634
1835
|
throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
|
|
1635
1836
|
'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
|
|
1636
1837
|
'rendered, and your script runs after it). On a server-rendered page ' +
|
|
@@ -1853,7 +2054,7 @@ const resolveHmrModel = (runtimeId) => {
|
|
|
1853
2054
|
};
|
|
1854
2055
|
/** Starts a program Effect with explicit boot inputs for runtime tests.
|
|
1855
2056
|
* @internal */
|
|
1856
|
-
export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
2057
|
+
export const __startProgram = (program, hmrModel, bootMode, flags, buildId) => {
|
|
1857
2058
|
const internals = runtimeInternals.get(program);
|
|
1858
2059
|
if (Predicate.isUndefined(internals)) {
|
|
1859
2060
|
return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
|
|
@@ -1863,7 +2064,7 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
|
1863
2064
|
return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
|
|
1864
2065
|
'makeApplication.'));
|
|
1865
2066
|
}
|
|
1866
|
-
return internals.startWith(Option.none(), hmrModel, bootMode, flags);
|
|
2067
|
+
return internals.startWith(Option.none(), hmrModel, bootMode, flags, buildId);
|
|
1867
2068
|
};
|
|
1868
2069
|
// NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
|
|
1869
2070
|
// runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
|
|
@@ -1876,8 +2077,8 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
|
|
|
1876
2077
|
// interrupt at all and lets the document take the runtime with it. Error
|
|
1877
2078
|
// reporting and the keep-alive interval come from `makeRunMain` either way.
|
|
1878
2079
|
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))));
|
|
2080
|
+
const startProgram = (program, bootMode, flags, buildId) => {
|
|
2081
|
+
runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags, buildId))));
|
|
1881
2082
|
};
|
|
1882
2083
|
export function run(program, options) {
|
|
1883
2084
|
startProgram(program, 'Fresh', options?.flags);
|
|
@@ -1886,15 +2087,27 @@ export function run(program, options) {
|
|
|
1886
2087
|
* of building it fresh. Use this as the client entry for a page served by
|
|
1887
2088
|
* `renderToString`: the first render attaches to the stamped root, keeps the
|
|
1888
2089
|
* existing nodes, and reconstructs the Model from the Flags the server
|
|
1889
|
-
* embedded. The handoff is strict: a missing server root,
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
2090
|
+
* embedded. The handoff is strict: a missing server root, a root stamped more
|
|
2091
|
+
* than once, more than one root with no container to choose between them, a
|
|
2092
|
+
* missing Flags payload, an undecodable payload, or a page from another
|
|
2093
|
+
* deployment terminates startup. Every one of those contains the page first:
|
|
2094
|
+
* the document's body is marked `inert` and a nondismissable modal shield is
|
|
2095
|
+
* opened above existing top-layer content, so pointer and physical keyboard
|
|
2096
|
+
* input do not activate same-document native links, forms, or controls.
|
|
2097
|
+
* Containment leaves author-owned dialogs open without calling `close` or
|
|
2098
|
+
* dispatching `cancel`.
|
|
2099
|
+
* Nothing is moved, so no custom element reconnects and no frame reloads.
|
|
2100
|
+
* This is not a script, event, or embedded-document sandbox: existing
|
|
2101
|
+
* capture-phase handlers, browser-generated top-layer events, timers, and
|
|
2102
|
+
* stale scripts can still run. Controls in embedded documents can still
|
|
2103
|
+
* receive input if stale code focuses them. Stale code can also open newer
|
|
2104
|
+
* top-layer UI. Use `run` in a separate client-only entry when the page should
|
|
2105
|
+
* boot without server output.
|
|
1893
2106
|
*
|
|
1894
2107
|
* @experimental Server rendering and hydration are experimental while their
|
|
1895
2108
|
* contracts settle. */
|
|
1896
|
-
export const hydrate = (program) => {
|
|
1897
|
-
startProgram(program, 'Hydrate');
|
|
2109
|
+
export const hydrate = (program, options) => {
|
|
2110
|
+
startProgram(program, 'Hydrate', undefined, options?.buildId);
|
|
1898
2111
|
};
|
|
1899
2112
|
const buildPortHandles = (ports, connector) => {
|
|
1900
2113
|
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":"class.d.ts","sourceRoot":"","sources":["../../src/snabbdom/class.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../../src/snabbdom/class.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAwG7C,eAAO,MAAM,WAAW,EAAE,MAIzB,CAAA"}
|
package/dist/snabbdom/class.js
CHANGED
|
@@ -1,26 +1,82 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
2
|
+
import { parsedAttributeName } from '../domReflection.js';
|
|
1
3
|
import { VNodeDataMask } from './vnode.js';
|
|
4
|
+
const absentRawClassAttribute = { _tag: 'Absent' };
|
|
5
|
+
const ASCII_WHITESPACE = /[\t\n\f\r ]+/;
|
|
6
|
+
const rawClassAttributeFor = (vnode, namespace) => {
|
|
7
|
+
const attrs = vnode.data.attrs;
|
|
8
|
+
if (attrs === undefined) {
|
|
9
|
+
return absentRawClassAttribute;
|
|
10
|
+
}
|
|
11
|
+
let attribute = absentRawClassAttribute;
|
|
12
|
+
for (const name of Object.keys(attrs)) {
|
|
13
|
+
if (parsedAttributeName(namespace, name) === 'class') {
|
|
14
|
+
attribute = { _tag: 'Present', value: attrs[name] };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return attribute;
|
|
18
|
+
};
|
|
19
|
+
const rawClassAttributesAgree = (previous, next) => {
|
|
20
|
+
if (previous._tag === 'Absent') {
|
|
21
|
+
return next._tag === 'Absent';
|
|
22
|
+
}
|
|
23
|
+
return next._tag === 'Present' && previous.value === next.value;
|
|
24
|
+
};
|
|
25
|
+
const rawClassNamesFor = (attribute) => {
|
|
26
|
+
const names = new Set();
|
|
27
|
+
if (attribute._tag === 'Absent' || typeof attribute.value === 'boolean') {
|
|
28
|
+
return names;
|
|
29
|
+
}
|
|
30
|
+
for (const name of String(attribute.value).split(ASCII_WHITESPACE)) {
|
|
31
|
+
if (name !== '') {
|
|
32
|
+
names.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
};
|
|
2
37
|
function updateClass(oldVnode, vnode) {
|
|
3
38
|
let cur;
|
|
4
39
|
let name;
|
|
5
40
|
const elm = vnode.elm;
|
|
6
41
|
let oldClass = oldVnode.data.class;
|
|
7
42
|
let klass = vnode.data.class;
|
|
43
|
+
const oldAttrs = oldVnode.data.attrs;
|
|
44
|
+
const attrs = vnode.data.attrs;
|
|
8
45
|
if (!oldClass && !klass)
|
|
9
46
|
return;
|
|
10
|
-
if (oldClass === klass)
|
|
47
|
+
if (oldClass === klass && oldAttrs === attrs)
|
|
48
|
+
return;
|
|
49
|
+
const previousRawClass = rawClassAttributeFor(oldVnode, elm.namespaceURI);
|
|
50
|
+
const nextRawClass = rawClassAttributeFor(vnode, elm.namespaceURI);
|
|
51
|
+
if (oldClass === klass &&
|
|
52
|
+
rawClassAttributesAgree(previousRawClass, nextRawClass)) {
|
|
11
53
|
return;
|
|
54
|
+
}
|
|
12
55
|
oldClass = oldClass || {};
|
|
13
56
|
klass = klass || {};
|
|
57
|
+
if (oldClass === klass) {
|
|
58
|
+
for (name in klass) {
|
|
59
|
+
if (klass[name] && !elm.classList.contains(name)) {
|
|
60
|
+
elm.classList.add(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const nextRawClassNames = rawClassNamesFor(nextRawClass);
|
|
14
66
|
for (name in oldClass) {
|
|
15
|
-
if (oldClass[name] &&
|
|
16
|
-
|
|
67
|
+
if (oldClass[name] &&
|
|
68
|
+
klass[name] !== true &&
|
|
69
|
+
!nextRawClassNames.has(name)) {
|
|
17
70
|
elm.classList.remove(name);
|
|
18
71
|
}
|
|
19
72
|
}
|
|
20
73
|
for (name in klass) {
|
|
21
74
|
cur = klass[name];
|
|
22
|
-
if (cur
|
|
23
|
-
elm.classList
|
|
75
|
+
if (cur && !elm.classList.contains(name)) {
|
|
76
|
+
elm.classList.add(name);
|
|
77
|
+
}
|
|
78
|
+
else if (cur !== oldClass[name] && !nextRawClassNames.has(name)) {
|
|
79
|
+
elm.classList.remove(name);
|
|
24
80
|
}
|
|
25
81
|
}
|
|
26
82
|
}
|
|
@@ -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":"AAKA,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;AA+JH,eAAO,MAAM,WAAW,EAAE,MAOzB,CAAA"}
|