From 7ea32c7fbf4fc74a3c0fa7319561243c4e262e99 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:12:00 +0200 Subject: [PATCH 1/4] feat: pass our domains and remote config to the Vercel config (#8452) * feat: pass our domains and remote config to Vercel'S * chore: changeset * test: update test to test for this * docs: update README --- .changeset/forty-hotels-itch.md | 5 ++++ packages/integrations/vercel/README.md | 2 ++ .../integrations/vercel/src/image/shared.ts | 23 +++++++++++-------- .../vercel/src/serverless/adapter.ts | 21 ++++++++++++++--- .../integrations/vercel/src/static/adapter.ts | 21 ++++++++++++++--- .../test/fixtures/image/astro.config.mjs | 5 ++++ .../integrations/vercel/test/image.test.js | 8 ++++++- 7 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 .changeset/forty-hotels-itch.md diff --git a/.changeset/forty-hotels-itch.md b/.changeset/forty-hotels-itch.md new file mode 100644 index 0000000000000..b13c6db35b5ae --- /dev/null +++ b/.changeset/forty-hotels-itch.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': patch +--- + +Fix Astro's `domains` and `remotePatterns` not being used by Vercel when using Vercel Image Optimization diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index e16d98390ebe1..00c2a18cfe0bf 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -114,6 +114,8 @@ export default defineConfig({ Configuration options for [Vercel's Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel's image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters. +The `domains` and `remotePatterns` properties will automatically be filled using [the Astro corresponding `image` settings](https://docs.astro.build/en/reference/configuration-reference/#image-options). + ```js // astro.config.mjs import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts index ad6b45bd0a7c4..f6cace2a24ee2 100644 --- a/packages/integrations/vercel/src/image/shared.ts +++ b/packages/integrations/vercel/src/image/shared.ts @@ -1,9 +1,13 @@ -import type { ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; - -export const defaultImageConfig: VercelImageConfig = { - sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], - domains: [], -}; +import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; + +export function getDefaultImageConfig(astroImageConfig: AstroConfig['image']): VercelImageConfig { + return { + sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + domains: astroImageConfig.domains ?? [], + // Cast is necessary here because Vercel's types are slightly different from ours regarding allowed protocols. Behavior should be the same, however. + remotePatterns: (astroImageConfig.remotePatterns as VercelImageConfig['remotePatterns']) ?? [], + }; +} export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { return typeof src === 'object'; @@ -56,10 +60,11 @@ export const qualityTable: Record = { max: 100, }; -export function getImageConfig( +export function getAstroImageConfig( images: boolean | undefined, imagesConfig: VercelImageConfig | undefined, - command: string + command: string, + astroImageConfig: AstroConfig['image'] ) { if (images) { return { @@ -69,7 +74,7 @@ export function getImageConfig( command === 'dev' ? '@astrojs/vercel/dev-image-service' : '@astrojs/vercel/build-image-service', - config: imagesConfig ? imagesConfig : defaultImageConfig, + config: imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig), }, }, }; diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index a1d8b18bf3acc..1c0eb9530d8bf 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -9,7 +9,11 @@ import { AstroError } from 'astro/errors'; import glob from 'fast-glob'; import { basename } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { defaultImageConfig, getImageConfig, type VercelImageConfig } from '../image/shared.js'; +import { + getAstroImageConfig, + getDefaultImageConfig, + type VercelImageConfig, +} from '../image/shared.js'; import { exposeEnv } from '../lib/env.js'; import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; import { copyDependenciesToFunction } from '../lib/nft.js'; @@ -143,7 +147,7 @@ export default function vercelServerless({ external: ['@vercel/nft'], }, }, - ...getImageConfig(imageService, imagesConfig, command), + ...getAstroImageConfig(imageService, imagesConfig, command, config.image), }); }, 'astro:config:done': ({ setAdapter, config, logger }) => { @@ -250,7 +254,18 @@ You can set functionPerRoute: false to prevent surpassing the limit.` ...routeDefinitions, ], ...(imageService || imagesConfig - ? { images: imagesConfig ? imagesConfig : defaultImageConfig } + ? { + images: imagesConfig + ? { + ...imagesConfig, + domains: [...imagesConfig.domains, ..._config.image.domains], + remotePatterns: [ + ...(imagesConfig.remotePatterns ?? []), + ..._config.image.remotePatterns, + ], + } + : getDefaultImageConfig(_config.image), + } : {}), }); diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 0a63dc33368a3..2908dbf585294 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,10 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; -import { defaultImageConfig, getImageConfig, type VercelImageConfig } from '../image/shared.js'; +import { + getAstroImageConfig, + getDefaultImageConfig, + type VercelImageConfig, +} from '../image/shared.js'; import { exposeEnv } from '../lib/env.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { isServerLikeOutput } from '../lib/prerender.js'; @@ -59,7 +63,7 @@ export default function vercelStatic({ vite: { define: viteDefine, }, - ...getImageConfig(imageService, imagesConfig, command), + ...getAstroImageConfig(imageService, imagesConfig, command, config.image), }); }, 'astro:config:done': ({ setAdapter, config }) => { @@ -91,7 +95,18 @@ export default function vercelStatic({ { handle: 'filesystem' }, ], ...(imageService || imagesConfig - ? { images: imagesConfig ? imagesConfig : defaultImageConfig } + ? { + images: imagesConfig + ? { + ...imagesConfig, + domains: [...imagesConfig.domains, ..._config.image.domains], + remotePatterns: [ + ...(imagesConfig.remotePatterns ?? []), + ..._config.image.remotePatterns, + ], + } + : getDefaultImageConfig(_config.image), + } : {}), }); }, diff --git a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs index 2a343d03577b5..78923f2cbb9bb 100644 --- a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs +++ b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs @@ -6,5 +6,10 @@ export default defineConfig({ adapter: vercel({imageService: true}), image: { service: testImageService(), + domains: ['astro.build'], + remotePatterns: [{ + protocol: 'https', + hostname: '**.amazonaws.com', + }], }, }); diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js index 834b6d69b1811..c5153cc6e622d 100644 --- a/packages/integrations/vercel/test/image.test.js +++ b/packages/integrations/vercel/test/image.test.js @@ -32,7 +32,13 @@ describe('Image', () => { expect(vercelConfig.images).to.deep.equal({ sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], - domains: [], + domains: ['astro.build'], + remotePatterns: [ + { + protocol: 'https', + hostname: '**.amazonaws.com', + }, + ], }); }); From f66053a1ea0a4e3bdb0b0df12bb1bf56e1ea2618 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:21:57 +0200 Subject: [PATCH 2/4] Transition only between pages where both have ViewTransitions enabled (#8441) * added e2e test regarding loss of router * only navigate to pages from which we can navigate back * location does not change before deferred pushState * initialize history state * test cases adapted to new semantics (only traverse to pages w/ ViewTransigs) * type URL instead of Location * + changeset --- .changeset/curvy-dolls-thank.md | 5 ++ .../astro/components/ViewTransitions.astro | 72 ++++++++++-------- .../view-transitions/src/pages/five.astro | 11 +++ .../view-transitions/src/pages/three.astro | 2 + packages/astro/e2e/view-transitions.test.js | 76 ++++++++++++------- 5 files changed, 106 insertions(+), 60 deletions(-) create mode 100644 .changeset/curvy-dolls-thank.md create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/pages/five.astro diff --git a/.changeset/curvy-dolls-thank.md b/.changeset/curvy-dolls-thank.md new file mode 100644 index 0000000000000..47ea8ad322a3a --- /dev/null +++ b/.changeset/curvy-dolls-thank.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Only transition between pages where both have ViewTransitions enabled diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index 74c338333752f..049355461b8f2 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -19,7 +19,9 @@ const { fallback = 'animate' } = Astro.props as Props; }; type Events = 'astro:page-load' | 'astro:after-swap'; - const persistState = (state: State) => history.replaceState(state, ''); + // only update history entries that are managed by us + // leave other entries alone and do not accidently add state. + const persistState = (state: State) => history.state && history.replaceState(state, ''); const supportsViewTransitions = !!document.startViewTransition; const transitionEnabledOnThisPage = () => !!document.querySelector('[name="astro-view-transitions-enabled"]'); @@ -32,11 +34,13 @@ const { fallback = 'animate' } = Astro.props as Props; // can use that to determine popstate if going forward or back. let currentHistoryIndex = 0; if (history.state) { - // we reloaded a page with history state (e.g. back button or browser reload) + // we reloaded a page with history state + // (e.g. history navigation from non-transition page or browser reload) currentHistoryIndex = history.state.index; scrollTo({ left: 0, top: history.state.scrollY }); + } else if (transitionEnabledOnThisPage()) { + history.replaceState({index: currentHistoryIndex, scrollY}, ''); } - const throttle = (cb: (...args: any[]) => any, delay: number) => { let wait = false; // During the waiting time additional events are lost. @@ -109,9 +113,7 @@ const { fallback = 'animate' } = Astro.props as Props; const parser = new DOMParser(); - async function updateDOM(html: string, state?: State, fallback?: Fallback) { - const doc = parser.parseFromString(html, 'text/html'); - + async function updateDOM(doc: Document, loc: URL, state?: State, fallback?: Fallback) { // Check for a head element that should persist, either because it has the data // attribute or is a link el. const persistedHeadElement = (el: Element): Element | null => { @@ -189,19 +191,21 @@ const { fallback = 'animate' } = Astro.props as Props; // Chromium based browsers (Chrome, Edge, Opera, ...) scrollTo({ left: 0, top: 0, behavior: 'instant' }); - if (state?.scrollY === 0 && location.hash) { - const id = decodeURIComponent(location.hash.slice(1)); + let initialScrollY = 0; + if (!state && loc.hash) { + const id = decodeURIComponent(loc.hash.slice(1)); const elem = document.getElementById(id); // prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account - if (elem) { - state.scrollY = elem.offsetTop; - persistState(state); // first guess, later updated by scroll handler - elem.scrollIntoView(); // for Firefox, this should better be {behavior: 'instant'} - } + elem && (initialScrollY = elem.offsetTop) && elem.scrollIntoView(); } else if (state && state.scrollY !== 0) { scrollTo(0, state.scrollY); // usings default scrollBehavior } - + !state && + history.pushState( + { index: ++currentHistoryIndex, scrollY: initialScrollY }, + '', + loc.href + ); triggerEvent('astro:after-swap'); }; @@ -247,19 +251,26 @@ const { fallback = 'animate' } = Astro.props as Props; } } - async function navigate(dir: Direction, href: string, state?: State) { + async function navigate(dir: Direction, loc: URL, state?: State) { let finished: Promise; + const href=loc.href; const { html, ok } = await getHTML(href); // If there is a problem fetching the new page, just do an MPA navigation to it. if (!ok) { location.href = href; return; } + const doc = parser.parseFromString(html, 'text/html'); + if (!doc.querySelector('[name="astro-view-transitions-enabled"]')) { + location.href = href; + return; + } + document.documentElement.dataset.astroTransition = dir; if (supportsViewTransitions) { - finished = document.startViewTransition(() => updateDOM(html, state)).finished; + finished = document.startViewTransition(() => updateDOM(doc, loc, state)).finished; } else { - finished = updateDOM(html, state, getFallback()); + finished = updateDOM(doc, loc, state, getFallback()); } try { await finished; @@ -311,11 +322,11 @@ const { fallback = 'animate' } = Astro.props as Props; ev.shiftKey || // new window ev.defaultPrevented || !transitionEnabledOnThisPage() - ) + ) { // No page transitions in these cases, // Let the browser standard action handle this return; - + } // We do not need to handle same page links because there are no page transitions // Same page means same path and same query params (but different hash) if (location.pathname === link.pathname && location.search === link.search) { @@ -341,10 +352,8 @@ const { fallback = 'animate' } = Astro.props as Props; // these are the cases we will handle: same origin, different page ev.preventDefault(); - navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 }); - const newState: State = { index: currentHistoryIndex, scrollY }; - persistState({ index: currentHistoryIndex - 1, scrollY }); - history.pushState(newState, '', link.href); + persistState({ index: currentHistoryIndex, scrollY }); + navigate('forward', new URL(link.href)); }); addEventListener('popstate', (ev) => { @@ -374,11 +383,11 @@ const { fallback = 'animate' } = Astro.props as Props; history.scrollRestoration = 'manual'; } - const state: State | undefined = history.state; - const nextIndex = state?.index ?? currentHistoryIndex + 1; + const state: State = history.state; + const nextIndex = state.index; const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back'; - navigate(direction, location.href, state); currentHistoryIndex = nextIndex; + navigate(direction, new URL(location.href), state); }); ['mouseenter', 'touchstart', 'focus'].forEach((evName) => { @@ -402,13 +411,10 @@ const { fallback = 'animate' } = Astro.props as Props; addEventListener('load', onPageLoad); // There's not a good way to record scroll position before a back button. // So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position. - const updateState = () => { - // only update history entries that are managed by us - // leave other entries alone and do not accidently add state. - if (history.state) { - persistState({ ...history.state, scrollY }); - } - }; + const updateState = () => { + persistState({ ...history.state, scrollY }); + } + if ('onscrollend' in window) addEventListener('scrollend', updateState); else addEventListener('scroll', throttle(updateState, 300)); } diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/five.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/five.astro new file mode 100644 index 0000000000000..f17dd42b3f1ca --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/five.astro @@ -0,0 +1,11 @@ + + + Page 5 + + +
+

Page 5

+ go to 3 +
+ + diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro index eddc049a80a23..8324a9c72614a 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro @@ -7,6 +7,8 @@

Page 3

go to 2
+ go to 5 +
hash target

Long paragraph

diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 4b4d6491525e4..f5a63c67c4315 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -83,7 +83,7 @@ test.describe('View Transitions', () => { expect(loads.length, 'There should only be 1 page load').toEqual(1); }); - test('Moving from a page without ViewTransitions triggers a full page navigation', async ({ + test('Moving to a page without ViewTransitions triggers a full page navigation', async ({ page, astro, }) => { @@ -102,10 +102,6 @@ test.describe('View Transitions', () => { p = page.locator('#three'); await expect(p, 'should have content').toHaveText('Page 3'); - await page.click('#click-two'); - p = page.locator('#two'); - await expect(p, 'should have content').toHaveText('Page 2'); - expect( loads.length, 'There should be 2 page loads. The original, then going from 3 to 2' @@ -142,8 +138,8 @@ test.describe('View Transitions', () => { expect( loads.length, - 'There should be only 1 page load. The original, but no additional loads for the hash change' - ).toEqual(1); + 'There should be only 2 page loads (for page one & three), but no additional loads for the hash change' + ).toEqual(2); }); test('Moving from a page without ViewTransitions w/ back button', async ({ page, astro }) => { @@ -501,25 +497,51 @@ test.describe('View Transitions', () => { await page.click('#click-logo'); await downloadPromise; }); -}); -test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({ - page, - astro, -}) => { - // Go to middle of long page - await page.goto(astro.resolveUrl('/long-page#click-external')); - - let locator = page.locator('#click-external'); - await expect(locator).toBeInViewport(); - - // Go to a page that has not enabled ViewTransistions - await page.click('#click-external'); - locator = page.locator('#three'); - await expect(locator).toHaveText('Page 3'); - - // Scroll back to long page - await page.goBack(); - locator = page.locator('#click-external'); - await expect(locator).toBeInViewport(); + test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({ + page, + astro, + }) => { + // Go to middle of long page + await page.goto(astro.resolveUrl('/long-page#click-external')); + + let locator = page.locator('#click-external'); + await expect(locator).toBeInViewport(); + + // Go to a page that has not enabled ViewTransistions + await page.click('#click-external'); + locator = page.locator('#three'); + await expect(locator).toHaveText('Page 3'); + + // Scroll back to long page + await page.goBack(); + locator = page.locator('#click-external'); + await expect(locator).toBeInViewport(); + }); + + test("Non transition navigation doesn't loose handlers", async ({ page, astro }) => { + // Go to page 1 + await page.goto(astro.resolveUrl('/one')); + let p = page.locator('#one'); + await expect(p, 'should have content').toHaveText('Page 1'); + + // go to page 3 + await page.click('#click-three'); + p = page.locator('#three'); + await expect(p, 'should have content').toHaveText('Page 3'); + + // go to page 5 + await page.click('#click-five'); + p = page.locator('#five'); + await expect(p, 'should have content').toHaveText('Page 5'); + + await page.goBack(); + p = page.locator('#three'); + await expect(p, 'should have content').toHaveText('Page 3'); + + await page.goBack(); + p = page.locator('#one'); + await expect(p, 'should have content').toHaveText('Page 1'); + }); }); + From 5c23bf1c901006ea9615827430642c2041a6cf8d Mon Sep 17 00:00:00 2001 From: matthewp Date: Thu, 7 Sep 2023 18:24:12 +0000 Subject: [PATCH 3/4] [ci] format --- packages/astro/components/ViewTransitions.astro | 16 ++++++---------- packages/astro/e2e/view-transitions.test.js | 1 - 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index 049355461b8f2..7b556c252770c 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -34,12 +34,12 @@ const { fallback = 'animate' } = Astro.props as Props; // can use that to determine popstate if going forward or back. let currentHistoryIndex = 0; if (history.state) { - // we reloaded a page with history state + // we reloaded a page with history state // (e.g. history navigation from non-transition page or browser reload) currentHistoryIndex = history.state.index; scrollTo({ left: 0, top: history.state.scrollY }); } else if (transitionEnabledOnThisPage()) { - history.replaceState({index: currentHistoryIndex, scrollY}, ''); + history.replaceState({ index: currentHistoryIndex, scrollY }, ''); } const throttle = (cb: (...args: any[]) => any, delay: number) => { let wait = false; @@ -201,11 +201,7 @@ const { fallback = 'animate' } = Astro.props as Props; scrollTo(0, state.scrollY); // usings default scrollBehavior } !state && - history.pushState( - { index: ++currentHistoryIndex, scrollY: initialScrollY }, - '', - loc.href - ); + history.pushState({ index: ++currentHistoryIndex, scrollY: initialScrollY }, '', loc.href); triggerEvent('astro:after-swap'); }; @@ -253,7 +249,7 @@ const { fallback = 'animate' } = Astro.props as Props; async function navigate(dir: Direction, loc: URL, state?: State) { let finished: Promise; - const href=loc.href; + const href = loc.href; const { html, ok } = await getHTML(href); // If there is a problem fetching the new page, just do an MPA navigation to it. if (!ok) { @@ -411,9 +407,9 @@ const { fallback = 'animate' } = Astro.props as Props; addEventListener('load', onPageLoad); // There's not a good way to record scroll position before a back button. // So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position. - const updateState = () => { + const updateState = () => { persistState({ ...history.state, scrollY }); - } + }; if ('onscrollend' in window) addEventListener('scrollend', updateState); else addEventListener('scroll', throttle(updateState, 300)); diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index f5a63c67c4315..34f1a4e027b56 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -544,4 +544,3 @@ test.describe('View Transitions', () => { await expect(p, 'should have content').toHaveText('Page 1'); }); }); - From 85fe213fe0e8de3227ac80a41119800c374214f6 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 7 Sep 2023 15:54:39 -0500 Subject: [PATCH 4/4] fix(react): support void children in experimentalReactChildren (#8455) --- .changeset/cool-jokes-unite.md | 5 +++++ packages/integrations/react/vnode-children.js | 20 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 .changeset/cool-jokes-unite.md diff --git a/.changeset/cool-jokes-unite.md b/.changeset/cool-jokes-unite.md new file mode 100644 index 0000000000000..57f0bedafd53b --- /dev/null +++ b/.changeset/cool-jokes-unite.md @@ -0,0 +1,5 @@ +--- +'@astrojs/react': patch +--- + +Update `experimentalReactChildren` behavior to support void tags diff --git a/packages/integrations/react/vnode-children.js b/packages/integrations/react/vnode-children.js index 9c7abe64418c1..ea5bc0869ee68 100644 --- a/packages/integrations/react/vnode-children.js +++ b/packages/integrations/react/vnode-children.js @@ -1,9 +1,12 @@ import { parse, walkSync, DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'; import { createElement, Fragment } from 'react'; +let ids = 0; export default function convert(children) { const nodeMap = new WeakMap(); let doc = parse(children.toString().trim()); + let id = ids++; + let key = 0; let root = createElement(Fragment, { children: [] }); walkSync(doc, (node, parent, index) => { @@ -12,23 +15,18 @@ export default function convert(children) { nodeMap.set(node, root); } else if (node.type === ELEMENT_NODE) { const { class: className, ...props } = node.attributes; - newNode = createElement(node.name, { ...props, className, children: [] }); + // NOTE: do not manually pass `children`, React handles this internally + newNode = createElement(node.name, { ...props, className, key: `${id}-${key++}` }); nodeMap.set(node, newNode); if (parent) { const newParent = nodeMap.get(parent); newParent.props.children[index] = newNode; } } else if (node.type === TEXT_NODE) { - newNode = node.value.trim(); - if (newNode.trim()) { - if (parent) { - const newParent = nodeMap.get(parent); - if (parent.children.length === 1) { - newParent.props.children[0] = newNode; - } else { - newParent.props.children[index] = newNode; - } - } + newNode = node.value; + if (newNode.trim() && parent) { + const newParent = nodeMap.get(parent); + newParent.props.children[index] = newNode; } } });