From 3449d14eff5d24adde4f59f2ed0e9eb0a35aff81 Mon Sep 17 00:00:00 2001 From: eight04 Date: Thu, 10 Nov 2022 02:55:37 +0800 Subject: [PATCH 1/2] Fix: handle nested hydration better --- .../__tests__/partialHydration.spec.ts | 14 ++++++++++++- src/partialHydration/inlineSvelteComponent.ts | 5 +++++ src/partialHydration/mountComponentsInHtml.ts | 20 ++++--------------- src/partialHydration/partialHydration.ts | 14 +++++++++---- src/rollup/rollupPlugin.ts | 7 ++++--- src/utils/Page.ts | 2 +- src/utils/svelteComponent.ts | 1 - 7 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/partialHydration/__tests__/partialHydration.spec.ts b/src/partialHydration/__tests__/partialHydration.spec.ts index 7972e3dc..709c5b2d 100644 --- a/src/partialHydration/__tests__/partialHydration.spec.ts +++ b/src/partialHydration/__tests__/partialHydration.spec.ts @@ -1,4 +1,4 @@ -import partialHydration from '../partialHydration'; +import partialHydration, { partialHydrationClient } from '../partialHydration'; describe('#partialHydration', () => { it('replaces as expected', async () => { @@ -110,3 +110,15 @@ describe('#partialHydration', () => { ); }); }); + +describe('partialHydrationClient', () => { + it('replaces as expected', async () => { + expect( + ( + await partialHydrationClient.markup({ + content: '', + }) + ).code, + ).toMatchInlineSnapshot(`""`); + }); +}); diff --git a/src/partialHydration/inlineSvelteComponent.ts b/src/partialHydration/inlineSvelteComponent.ts index 747fd286..45339736 100644 --- a/src/partialHydration/inlineSvelteComponent.ts +++ b/src/partialHydration/inlineSvelteComponent.ts @@ -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 diff --git a/src/partialHydration/mountComponentsInHtml.ts b/src/partialHydration/mountComponentsInHtml.ts index 0ef208d2..73b61ec2 100644 --- a/src/partialHydration/mountComponentsInHtml.ts +++ b/src/partialHydration/mountComponentsInHtml.ts @@ -1,4 +1,5 @@ import svelteComponent from '../utils/svelteComponent'; +import type { HydrateOptions } from '../utils/types'; export const replaceSpecialCharacters = (str) => str @@ -9,7 +10,7 @@ export const replaceSpecialCharacters = (str) => .replace(/'/gim, "'") .replace(/&/gim, '&'); -export default function mountComponentsInHtml({ page, html, hydrateOptions }): string { +export default function mountComponentsInHtml({ page, html }): string { let outputHtml = html; // sometimes svelte adds a class to our inlining. const matches = outputHtml.matchAll( @@ -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])); @@ -32,19 +33,6 @@ 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, diff --git a/src/partialHydration/partialHydration.ts b/src/partialHydration/partialHydration.ts index 45c821fe..a7d64370 100644 --- a/src/partialHydration/partialHydration.ts +++ b/src/partialHydration/partialHydration.ts @@ -10,12 +10,12 @@ 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; @@ -23,7 +23,7 @@ export const preprocessSvelteContent = (content) => { 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); @@ -48,4 +48,10 @@ const partialHydration = { }, }; +export const partialHydrationClient = { + markup: async ({ content }) => { + return { code: preprocessSvelteContent(content, 'inline') }; + }, +}; + export default partialHydration; diff --git a/src/rollup/rollupPlugin.ts b/src/rollup/rollupPlugin.ts index f68454ae..3dc38880 100644 --- a/src/rollup/rollupPlugin.ts +++ b/src/rollup/rollupPlugin.ts @@ -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'; @@ -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']; diff --git a/src/utils/Page.ts b/src/utils/Page.ts index 084de7a5..2a142e9d 100644 --- a/src/utils/Page.ts +++ b/src/utils/Page.ts @@ -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); diff --git a/src/utils/svelteComponent.ts b/src/utils/svelteComponent.ts index 2ddc007b..5e4a46b7 100644 --- a/src/utils/svelteComponent.ts +++ b/src/utils/svelteComponent.ts @@ -39,7 +39,6 @@ const svelteComponent = const innerHtml = mountComponentsInHtml({ html: htmlOutput, page, - hydrateOptions, }); // hydrateOptions.loading=none for server only rendered injected into html From 77e4d5c2b7713854be8cd864f563a4bb7527ebda Mon Sep 17 00:00:00 2001 From: eight04 Date: Thu, 10 Nov 2022 03:55:17 +0800 Subject: [PATCH 2/2] Fix: isHydrated flag --- src/partialHydration/mountComponentsInHtml.ts | 3 ++- src/utils/svelteComponent.ts | 6 ++++-- src/utils/types.ts | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/partialHydration/mountComponentsInHtml.ts b/src/partialHydration/mountComponentsInHtml.ts index 73b61ec2..2db672fd 100644 --- a/src/partialHydration/mountComponentsInHtml.ts +++ b/src/partialHydration/mountComponentsInHtml.ts @@ -10,7 +10,7 @@ export const replaceSpecialCharacters = (str) => .replace(/'/gim, "'") .replace(/&/gim, '&'); -export default function mountComponentsInHtml({ page, html }): 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( @@ -37,6 +37,7 @@ export default function mountComponentsInHtml({ page, html }): string { page, props: hydrateComponentProps, hydrateOptions: hydrateComponentOptions, + isHydrated, }); outputHtml = outputHtml.replace(match[0], hydratedHtml); diff --git a/src/utils/svelteComponent.ts b/src/utils/svelteComponent.ts index 5e4a46b7..97b65ef6 100644 --- a/src/utils/svelteComponent.ts +++ b/src/utils/svelteComponent.ts @@ -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); @@ -39,10 +39,12 @@ const svelteComponent = const innerHtml = mountComponentsInHtml({ html: htmlOutput, page, + 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; } diff --git a/src/utils/types.ts b/src/utils/types.ts index cdc03738..339a129d 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -183,6 +183,7 @@ export interface ComponentPayload { page: Page; props: any; hydrateOptions?: HydrateOptions; + isHydrated?: boolean; } export interface RollupDevOptions {