From 57e6a4d5cb4fc13748ab5f2563dec78a032555ed Mon Sep 17 00:00:00 2001 From: Chris Sauve Date: Mon, 23 Sep 2024 21:00:04 -0400 Subject: [PATCH] Update browser serializations to use a custom element instead of `` (#836) * Update browser serializations to use a custom element instead of `` * Use consistent attribute names * Fixes * Fix retrieval of initial serialization values * Fix environment variable e2e test --- .changeset/fresh-seahorses-cry.md | 8 ++++ .../react-query/source/ReactQueryContext.tsx | 4 +- packages/browser/source/browser.ts | 44 +++++++++++++++++-- packages/browser/source/index.ts | 2 +- packages/browser/source/server.ts | 14 +++--- .../source/server/components/Serialize.tsx | 10 ++--- .../source/server/hooks/serialized.ts | 4 +- .../quilt/source/server/request-router.tsx | 9 +++- tests/e2e/environment-variables.test.ts | 2 +- 9 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 .changeset/fresh-seahorses-cry.md diff --git a/.changeset/fresh-seahorses-cry.md b/.changeset/fresh-seahorses-cry.md new file mode 100644 index 000000000..d44bbaaca --- /dev/null +++ b/.changeset/fresh-seahorses-cry.md @@ -0,0 +1,8 @@ +--- +'@quilted/browser': patch +'@quilted/preact-browser': patch +'@quilted/react-query': patch +'@quilted/quilt': patch +--- + +Update browser serializations to use a custom element instead of `` diff --git a/integrations/react-query/source/ReactQueryContext.tsx b/integrations/react-query/source/ReactQueryContext.tsx index 0e979cf50..b4ea0b9d8 100644 --- a/integrations/react-query/source/ReactQueryContext.tsx +++ b/integrations/react-query/source/ReactQueryContext.tsx @@ -35,8 +35,8 @@ export function ReactQueryContext({ {typeof document === 'undefined' && ( + name={SERIALIZATION_ID} + content={() => dehydrate(client, { shouldDehydrateQuery: () => true, }) diff --git a/packages/browser/source/browser.ts b/packages/browser/source/browser.ts index 181a841a8..ee8940686 100644 --- a/packages/browser/source/browser.ts +++ b/packages/browser/source/browser.ts @@ -9,7 +9,7 @@ import { } from '@quilted/signals'; import type {BrowserDetails, CookieOptions, Cookies} from './types.ts'; -import {decode} from './encoding.ts'; +import {encode, decode} from './encoding.ts'; export class Browser implements BrowserDetails { readonly title = new BrowserTitle(); @@ -185,12 +185,48 @@ export class BrowserElementAttributes { } } +// Make this module execute in non-DOM environments +const HTMLElement: typeof globalThis.HTMLElement = + typeof globalThis.HTMLElement === 'function' + ? globalThis.HTMLElement + : (class HTMLElement {} as any); + +const DEFAULT_SERIALIZATION_ELEMENT_NAME = 'browser-serialization'; + +export class BrowserSerializationElement extends HTMLElement { + static define(name: string = DEFAULT_SERIALIZATION_ELEMENT_NAME) { + customElements.define(name, this); + } + + get name(): string | undefined { + return this.getAttribute('name') ?? undefined; + } + + set name(value: string | undefined) { + if (value) { + this.setAttribute('name', value); + } else { + this.removeAttribute('name'); + } + } + + get data() { + return getSerializedFromNode(this) as T; + } + + set data(value: T) { + this.setAttribute('content', JSON.stringify(encode(value))); + } +} + export class BrowserSerializations { private readonly serializations = new Map( Array.from( - document.querySelectorAll(`meta[name^="serialized:"]`), + document.querySelectorAll( + DEFAULT_SERIALIZATION_ELEMENT_NAME, + ), ).map((node) => [ - node.name.replace(/^serialized:/, ''), + node.getAttribute('name') ?? '_default', getSerializedFromNode(node), ]), ); @@ -213,7 +249,7 @@ export class BrowserSerializations { } function getSerializedFromNode(node: Element): T | undefined { - const value = (node as HTMLMetaElement).content; + const value = node.getAttribute('content'); try { return value ? (decode(JSON.parse(value)) as T) : undefined; diff --git a/packages/browser/source/index.ts b/packages/browser/source/index.ts index 08dde23c5..2c952ec8c 100644 --- a/packages/browser/source/index.ts +++ b/packages/browser/source/index.ts @@ -1,2 +1,2 @@ export * from './types.ts'; -export {Browser, BrowserCookies} from './browser.ts'; +export * from './browser.ts'; diff --git a/packages/browser/source/server.ts b/packages/browser/source/server.ts index 930610219..ae18e4688 100644 --- a/packages/browser/source/server.ts +++ b/packages/browser/source/server.ts @@ -219,9 +219,9 @@ export class BrowserResponseSerializations { readonly #serializations = new Map(); get value() { - return [...this.#serializations].map(([id, value]) => ({ - id, - value: encode(typeof value === 'function' ? value() : value), + return [...this.#serializations].map(([name, content]) => ({ + name, + content: encode(typeof content === 'function' ? content() : content), })); } @@ -245,11 +245,11 @@ export class BrowserResponseSerializations { return this.#serializations.get(id) as any; } - set(id: string, data: unknown) { - if (data === undefined) { - this.#serializations.delete(id); + set(name: string, content: unknown) { + if (content === undefined) { + this.#serializations.delete(name); } else { - this.#serializations.set(id, data); + this.#serializations.set(name, content); } } diff --git a/packages/preact-browser/source/server/components/Serialize.tsx b/packages/preact-browser/source/server/components/Serialize.tsx index 51abe25a9..5535e5062 100644 --- a/packages/preact-browser/source/server/components/Serialize.tsx +++ b/packages/preact-browser/source/server/components/Serialize.tsx @@ -1,13 +1,13 @@ import {useResponseSerialization} from '../hooks/serialized.ts'; export function Serialize({ - id, - value, + name, + content, }: { - id: string; - value: T | (() => T); + name: string; + content: T | (() => T); }) { if (typeof document === 'object') return null; - useResponseSerialization(id, value); + useResponseSerialization(name, content); return null; } diff --git a/packages/preact-browser/source/server/hooks/serialized.ts b/packages/preact-browser/source/server/hooks/serialized.ts index c9a25ab47..c05a4369f 100644 --- a/packages/preact-browser/source/server/hooks/serialized.ts +++ b/packages/preact-browser/source/server/hooks/serialized.ts @@ -4,10 +4,10 @@ import {useBrowserResponseAction} from './browser-response-action.ts'; * Sets a serialization for the HTML response. This value can then be read using * the `useSerialization` hook. */ -export function useResponseSerialization(key: string, value: unknown) { +export function useResponseSerialization(name: string, content: unknown) { if (typeof document === 'object') return; useBrowserResponseAction((response) => { - response.serializations.set(key, value); + response.serializations.set(name, content); }); } diff --git a/packages/quilt/source/server/request-router.tsx b/packages/quilt/source/server/request-router.tsx index 019fbd90c..71cd2fd73 100644 --- a/packages/quilt/source/server/request-router.tsx +++ b/packages/quilt/source/server/request-router.tsx @@ -224,8 +224,13 @@ export async function renderToResponse( {browserResponse.metas.value.map((meta) => ( )} /> ))} - {browserResponse.serializations.value.map(({id, value}) => ( - + {browserResponse.serializations.value.map(({name, content}) => ( + // @ts-expect-error a custom element that I don’t want to define, + // since it’s an optional part of the browser library. + ))} {synchronousAssets?.scripts.map((script) => ( { return ( <>
Hello, {builder}!
- + ); }