Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: handle nested hydration better #258

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/partialHydration/__tests__/partialHydration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import partialHydration from '../partialHydration';
import partialHydration, { partialHydrationClient } from '../partialHydration';

describe('#partialHydration', () => {
it('replaces as expected', async () => {
Expand Down Expand Up @@ -110,3 +110,15 @@ describe('#partialHydration', () => {
);
});
});

describe('partialHydrationClient', () => {
it('replaces as expected', async () => {
expect(
(
await partialHydrationClient.markup({
content: '<DatePicker hydrate-client={{ a: "b" }} />',
})
).code,
).toMatchInlineSnapshot(`"<DatePicker {...{ a: \\"b\\" }}/>"`);
});
});
5 changes: 5 additions & 0 deletions src/partialHydration/inlineSvelteComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ type InputParamsInlinePreprocessedSvelteComponent = {
name?: string;
props?: string;
options?: string;
mode?: 'inline' | 'wrapper';
};

export function inlinePreprocessedSvelteComponent({
name = '',
props = '',
options = '',
mode = 'wrapper',
}: InputParamsInlinePreprocessedSvelteComponent): string {
if (mode === 'inline') {
return `<${name} {...${props}}/>`;
}
// FIXME: don't output default options into the component to reduce file size.
const hydrationOptionsString =
options.length > 0
Expand Down
21 changes: 5 additions & 16 deletions src/partialHydration/mountComponentsInHtml.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import svelteComponent from '../utils/svelteComponent';
import type { HydrateOptions } from '../utils/types';

export const replaceSpecialCharacters = (str) =>
str
Expand All @@ -9,7 +10,7 @@ export const replaceSpecialCharacters = (str) =>
.replace(/&#039;/gim, "'")
.replace(/&amp;/gim, '&');

export default function mountComponentsInHtml({ page, html, hydrateOptions }): string {
export default function mountComponentsInHtml({ page, html, isHydrated = false }): string {
let outputHtml = html;
// sometimes svelte adds a class to our inlining.
const matches = outputHtml.matchAll(
Expand All @@ -18,8 +19,8 @@ export default function mountComponentsInHtml({ page, html, hydrateOptions }): s

for (const match of matches) {
const hydrateComponentName = match[2];
let hydrateComponentProps;
let hydrateComponentOptions;
let hydrateComponentProps: any;
let hydrateComponentOptions: HydrateOptions;

try {
hydrateComponentProps = JSON.parse(replaceSpecialCharacters(match[3]));
Expand All @@ -32,23 +33,11 @@ export default function mountComponentsInHtml({ page, html, hydrateOptions }): s
throw new Error(`Failed to JSON.parse props for ${hydrateComponentName} ${replaceSpecialCharacters(match[4])}`);
}

if (hydrateOptions) {
throw new Error(
`Client side hydrated component is attempting to hydrate another sub component "${hydrateComponentName}." This isn't supported. \n
Debug: ${JSON.stringify({
hydrateOptions,
hydrateComponentName,
hydrateComponentProps,
hydrateComponentOptions,
})}
`,
);
}

const hydratedHtml = svelteComponent(hydrateComponentName)({
page,
props: hydrateComponentProps,
hydrateOptions: hydrateComponentOptions,
isHydrated,
});

outputHtml = outputHtml.replace(match[0], hydratedHtml);
Expand Down
14 changes: 10 additions & 4 deletions src/partialHydration/partialHydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ const extractHydrateOptions = (htmlString) => {
return '';
};

const createReplacementString = ({ input, name, props }) => {
const createReplacementString = ({ input, name, props, mode }) => {
const options = extractHydrateOptions(input);
return inlinePreprocessedSvelteComponent({ name, props, options });
return inlinePreprocessedSvelteComponent({ name, props, options, mode });
};

export const preprocessSvelteContent = (content) => {
export const preprocessSvelteContent = (content, mode = 'wrapper') => {
// Note: this regex only supports self closing components.
// Slots aren't supported for client hydration either.
const hydrateableComponentPattern = /<([a-zA-Z]+)[^>]+hydrate-client={([^]*?})}[^/>]*\/>/gim;
const matches = [...content.matchAll(hydrateableComponentPattern)];

const output = matches.reduce((out, match) => {
const [wholeMatch, name, props] = match;
const replacement = createReplacementString({ input: wholeMatch, name, props });
const replacement = createReplacementString({ input: wholeMatch, name, props, mode });
return out.replace(wholeMatch, replacement);
}, content);

Expand All @@ -48,4 +48,10 @@ const partialHydration = {
},
};

export const partialHydrationClient = {
markup: async ({ content }) => {
return { code: preprocessSvelteContent(content, 'inline') };
},
};

export default partialHydration;
7 changes: 4 additions & 3 deletions src/rollup/rollupPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import del from 'del';
import { fork, ChildProcess } from 'child_process';
import chokidar from 'chokidar';

import partialHydration from '../partialHydration/partialHydration';
import partialHydration, { partialHydrationClient } from '../partialHydration/partialHydration';
import windowsPathFix from '../utils/windowsPathFix';
import { SettingsOptions } from '../utils/types';

Expand Down Expand Up @@ -87,11 +87,12 @@ export function transformFn({
type: 'ssr' | 'client';
}) {
const compilerOptions = getCompilerOptions({ type });
const hydrationPreprocessor = type === 'ssr' ? partialHydration : partialHydrationClient;

const preprocessors =
svelteConfig && Array.isArray(svelteConfig.preprocess)
? [...svelteConfig.preprocess, partialHydration]
: [partialHydration];
? [...svelteConfig.preprocess, hydrationPreprocessor]
: [hydrationPreprocessor];

return async (code, id) => {
const extensions = (svelteConfig && svelteConfig.extensions) || ['.svelte'];
Expand Down
2 changes: 1 addition & 1 deletion src/utils/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const buildPage = async (page) => {
await page.runHook('shortcodes', page);

// shortcodes can add svelte components, so we have to process the resulting html accordingly.
page.layoutHtml = mountComponentsInHtml({ page, html: page.layoutHtml, hydrateOptions: false });
page.layoutHtml = mountComponentsInHtml({ page, html: page.layoutHtml });

hydrateComponents(page);

Expand Down
7 changes: 4 additions & 3 deletions src/utils/svelteComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getComponentName = (str) => {

const svelteComponent =
(componentName: String, folder: String = 'components') =>
({ page, props, hydrateOptions }: ComponentPayload): string => {
({ page, props, hydrateOptions, isHydrated = false }: ComponentPayload): string => {
const { ssr, client } = page.settings.$$internal.findComponent(componentName, folder);

const cleanComponentName = getComponentName(componentName);
Expand All @@ -39,11 +39,12 @@ const svelteComponent =
const innerHtml = mountComponentsInHtml({
html: htmlOutput,
page,
hydrateOptions,
isHydrated: isHydrated || (hydrateOptions && hydrateOptions.loading !== 'none'),
});

// hydrateOptions.loading=none for server only rendered injected into html
if (!hydrateOptions || hydrateOptions.loading === 'none') {
if (isHydrated || !hydrateOptions || hydrateOptions.loading === 'none') {
// if parent component is hydrated or
// if a component isn't hydrated we don't need to wrap it in a unique div.
return innerHtml;
}
Expand Down
1 change: 1 addition & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export interface ComponentPayload {
page: Page;
props: any;
hydrateOptions?: HydrateOptions;
isHydrated?: boolean;
}

export interface RollupDevOptions {
Expand Down