Skip to content

Commit

Permalink
Update browser serializations to use a custom element instead of `<me…
Browse files Browse the repository at this point in the history
…ta>` (#836)

* Update browser serializations to use a custom element instead of `<meta>`

* Use consistent attribute names

* Fixes

* Fix retrieval of initial serialization values

* Fix environment variable e2e test
  • Loading branch information
lemonmade authored Sep 24, 2024
1 parent 7b51d45 commit 57e6a4d
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 24 deletions.
8 changes: 8 additions & 0 deletions .changeset/fresh-seahorses-cry.md
Original file line number Diff line number Diff line change
@@ -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 `<meta>`
4 changes: 2 additions & 2 deletions integrations/react-query/source/ReactQueryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function ReactQueryContext({
</HydrationBoundary>
{typeof document === 'undefined' && (
<Serialize
id={SERIALIZATION_ID}
value={() =>
name={SERIALIZATION_ID}
content={() =>
dehydrate(client, {
shouldDehydrateQuery: () => true,
})
Expand Down
44 changes: 40 additions & 4 deletions packages/browser/source/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -185,12 +185,48 @@ export class BrowserElementAttributes<Element extends HTMLElement> {
}
}

// 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<T = unknown> 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<T>(this) as T;
}

set data(value: T) {
this.setAttribute('content', JSON.stringify(encode(value)));
}
}

export class BrowserSerializations {
private readonly serializations = new Map<string, unknown>(
Array.from(
document.querySelectorAll<HTMLMetaElement>(`meta[name^="serialized:"]`),
document.querySelectorAll<HTMLElement>(
DEFAULT_SERIALIZATION_ELEMENT_NAME,
),
).map((node) => [
node.name.replace(/^serialized:/, ''),
node.getAttribute('name') ?? '_default',
getSerializedFromNode(node),
]),
);
Expand All @@ -213,7 +249,7 @@ export class BrowserSerializations {
}

function getSerializedFromNode<T = unknown>(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;
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/source/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './types.ts';
export {Browser, BrowserCookies} from './browser.ts';
export * from './browser.ts';
14 changes: 7 additions & 7 deletions packages/browser/source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@ export class BrowserResponseSerializations {
readonly #serializations = new Map<string, unknown>();

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),
}));
}

Expand All @@ -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);
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/preact-browser/source/server/components/Serialize.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {useResponseSerialization} from '../hooks/serialized.ts';

export function Serialize<T = unknown>({
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;
}
4 changes: 2 additions & 2 deletions packages/preact-browser/source/server/hooks/serialized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
9 changes: 7 additions & 2 deletions packages/quilt/source/server/request-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,13 @@ export async function renderToResponse(
{browserResponse.metas.value.map((meta) => (
<meta {...(meta as JSX.HTMLAttributes<HTMLMetaElement>)} />
))}
{browserResponse.serializations.value.map(({id, value}) => (
<meta name={`serialized:${id}`} content={JSON.stringify(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.
<browser-serialization
name={name}
content={JSON.stringify(content)}
/>
))}
{synchronousAssets?.scripts.map((script) => (
<ScriptAsset
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/environment-variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('app builds', () => {
return (
<>
<div>Hello, {builder}!</div>
<Serialize id="Builder" value={Env.BUILDER} />
<Serialize name="Builder" content={Env.BUILDER} />
</>
);
}
Expand Down

0 comments on commit 57e6a4d

Please sign in to comment.