From 1e01251454c13dfcaeaba67bdfdb6b807ba96e5c Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 9 Jul 2021 09:00:32 -0400 Subject: [PATCH] Use correct React rendering API or hydration (#636) * Use correct React rendering API or hydration * Add changeset --- .changeset/rude-deers-know.md | 6 + packages/astro/src/@types/astro.ts | 14 +- packages/astro/src/config_manager.ts | 3 +- .../astro/src/internal/__astro_component.ts | 32 ++--- .../astro/src/internal/element-registry.ts | 12 +- .../src/components/Research.jsx | 7 + .../react-component/src/pages/index.astro | 3 + packages/astro/test/react-component.test.js | 11 ++ packages/renderers/renderer-react/server.js | 14 +- yarn.lock | 127 ++++++++++++++---- 10 files changed, 171 insertions(+), 58 deletions(-) create mode 100644 .changeset/rude-deers-know.md create mode 100644 packages/astro/test/fixtures/react-component/src/components/Research.jsx diff --git a/.changeset/rude-deers-know.md b/.changeset/rude-deers-know.md new file mode 100644 index 000000000..c09b381fa --- /dev/null +++ b/.changeset/rude-deers-know.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/renderer-react': patch +--- + +Fixes bug with React renderer that would not hydrate correctly diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 009eb26ff..026603c05 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -178,7 +178,19 @@ export interface ComponentInfo { export type Components = Map; -type AsyncRendererComponentFn = (Component: any, props: any, children: string | undefined, options?: any) => Promise; +export interface AstroComponentMetadata { + displayName: string; + hydrate?: 'load' | 'idle' | 'visible'; + componentUrl?: string; + componentExport?: { value: string; namespace?: boolean }; +} + +type AsyncRendererComponentFn = ( + Component: any, + props: any, + children: string | undefined, + metadata?: AstroComponentMetadata +) => Promise; export interface Renderer { check: AsyncRendererComponentFn; diff --git a/packages/astro/src/config_manager.ts b/packages/astro/src/config_manager.ts index d70cbcd51..8087f58c8 100644 --- a/packages/astro/src/config_manager.ts +++ b/packages/astro/src/config_manager.ts @@ -140,8 +140,7 @@ let rendererInstances = [${renderers .map( (r, i) => `{ source: ${rendererClientPackages[i] ? `"${rendererClientPackages[i]}"` : 'null'}, - renderer: __renderer_${i}, - options: ${r.options ? JSON.stringify(r.options) : 'null'}, + renderer: typeof __renderer_${i} === 'function' ? __renderer_${i}(${r.options ? JSON.stringify(r.options) : 'null'}) : __renderer_${i}, polyfills: ${JSON.stringify(rendererPolyfills[i])}, hydrationPolyfills: ${JSON.stringify(rendererHydrationPolyfills[i])} }` diff --git a/packages/astro/src/internal/__astro_component.ts b/packages/astro/src/internal/__astro_component.ts index 2581b2471..873c1b7d4 100644 --- a/packages/astro/src/internal/__astro_component.ts +++ b/packages/astro/src/internal/__astro_component.ts @@ -1,4 +1,4 @@ -import type { Renderer } from '../@types/astro'; +import type { Renderer, AstroComponentMetadata } from '../@types/astro'; import hash from 'shorthash'; import { valueToEstree, Value } from 'estree-util-value-to-estree'; import { generate } from 'astring'; @@ -12,7 +12,6 @@ const serialize = (value: Value) => generate(valueToEstree(value)); export interface RendererInstance { source: string | null; renderer: Renderer; - options: any; polyfills: string[]; hydrationPolyfills: string[]; } @@ -20,7 +19,6 @@ export interface RendererInstance { const astroRendererInstance: RendererInstance = { source: '', renderer: astro as Renderer, - options: null, polyfills: [], hydrationPolyfills: [], }; @@ -28,7 +26,6 @@ const astroRendererInstance: RendererInstance = { const astroHtmlRendererInstance: RendererInstance = { source: '', renderer: astroHtml as Renderer, - options: null, polyfills: [], hydrationPolyfills: [], }; @@ -53,13 +50,13 @@ async function resolveRenderer(Component: any, props: any = {}, children?: strin const errors: Error[] = []; for (const instance of rendererInstances) { - const { renderer, options } = instance; + const { renderer } = instance; // Yes, we do want to `await` inside of this loop! // __renderer.check can't be run in parallel, it // returns the first match and skips any subsequent checks try { - const shouldUse: boolean = await renderer.check(Component, props, children, options); + const shouldUse: boolean = await renderer.check(Component, props, children); if (shouldUse) { rendererCache.set(Component, instance); @@ -76,13 +73,6 @@ async function resolveRenderer(Component: any, props: any = {}, children?: strin } } -export interface AstroComponentProps { - displayName: string; - hydrate?: 'load' | 'idle' | 'visible'; - componentUrl?: string; - componentExport?: { value: string; namespace?: boolean }; -} - interface HydrateScriptOptions { instance: RendererInstance; astroId: string; @@ -90,7 +80,7 @@ interface HydrateScriptOptions { } /** For hydrated components, generate a `).join(''); @@ -164,14 +154,14 @@ export const __astro_component = (Component: any, componentProps: AstroComponent } // If we're NOT hydrating this component, just return the HTML - if (!componentProps.hydrate) { + if (!metadata.hydrate) { // It's safe to remove , static content doesn't need the wrapper return html.replace(/\<\/?astro-fragment\>/g, ''); } // If we ARE hydrating this component, let's generate the hydration script const astroId = hash.unique(html); - const script = await generateHydrateScript({ instance, astroId, props }, componentProps as Required); + const script = await generateHydrateScript({ instance, astroId, props }, metadata as Required); const astroRoot = `${html}`; return [astroRoot, script].join('\n'); }; diff --git a/packages/astro/src/internal/element-registry.ts b/packages/astro/src/internal/element-registry.ts index 2b9ac1cbf..a0124523f 100644 --- a/packages/astro/src/internal/element-registry.ts +++ b/packages/astro/src/internal/element-registry.ts @@ -1,4 +1,4 @@ -import type { AstroComponentProps } from './__astro_component'; +import type { AstroComponentMetadata } from '../@types/astro'; type ModuleCandidates = Map; @@ -35,13 +35,13 @@ class AstroElementRegistry { return specifier; } - astroComponentArgs(tagName: string, props: AstroComponentProps) { + astroComponentArgs(tagName: string, metadata: AstroComponentMetadata) { const specifier = this.findCached(tagName); - const outProps: AstroComponentProps = { - ...props, - componentUrl: specifier || props.componentUrl, + const outMeta: AstroComponentMetadata = { + ...metadata, + componentUrl: specifier || metadata.componentUrl, }; - return [tagName, outProps]; + return [tagName, outMeta]; } } diff --git a/packages/astro/test/fixtures/react-component/src/components/Research.jsx b/packages/astro/test/fixtures/react-component/src/components/Research.jsx new file mode 100644 index 000000000..9ab83e5f3 --- /dev/null +++ b/packages/astro/test/fixtures/react-component/src/components/Research.jsx @@ -0,0 +1,7 @@ +import * as React from 'react' + +export function Research2() { + const [value] = React.useState(1) + + return
foo bar {value}
+} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/pages/index.astro b/packages/astro/test/fixtures/react-component/src/pages/index.astro index 8fe0e593f..6ec7004f6 100644 --- a/packages/astro/test/fixtures/react-component/src/pages/index.astro +++ b/packages/astro/test/fixtures/react-component/src/pages/index.astro @@ -2,6 +2,7 @@ import Hello from '../components/Hello.jsx'; import Later from '../components/Goodbye.vue'; // use different specifier import ArrowFunction from '../components/ArrowFunction.jsx'; +import {Research2} from '../components/Research.jsx'; --- @@ -12,5 +13,7 @@ import ArrowFunction from '../components/ArrowFunction.jsx'; + + diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index 93f464d3c..b5e01e638 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -39,9 +39,20 @@ React('Can load React', async () => { const $ = doc(result.contents); assert.equal($('#react-h2').text(), 'Hello world!'); + assert.equal($('#react-h2').attr('data-reactroot'), undefined, 'no reactroot'); assert.equal($('#arrow-fn-component').length, 1, 'Can use function components'); }); +React('Includes reactroot on hydrating components', async () => { + const result = await runtime.load('/'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + const div = $('#research'); + assert.equal(div.attr('data-reactroot'), '', 'Has the hydration attr'); + assert.equal(div.html(), 'foo bar 1'); +}) + React('Throws helpful error message on window SSR', async () => { const result = await runtime.load('/window'); assert.match( diff --git a/packages/renderers/renderer-react/server.js b/packages/renderers/renderer-react/server.js index d4d9f0cf8..e4f1cf690 100644 --- a/packages/renderers/renderer-react/server.js +++ b/packages/renderers/renderer-react/server.js @@ -1,5 +1,5 @@ import { Component as BaseComponent, createElement as h } from 'react'; -import { renderToStaticMarkup as renderToString } from 'react-dom/server.js'; +import { renderToStaticMarkup as reactRenderToStaticMarkup, renderToString } from 'react-dom/server.js'; import StaticHtml from './static-html.js'; const reactTypeof = Symbol.for('react.element'); @@ -26,7 +26,7 @@ function check(Component, props, children) { return h('div'); } - renderToStaticMarkup(Tester, props, children); + renderToStaticMarkup(Tester, props, children, {}); if (error) { throw error; @@ -34,8 +34,14 @@ function check(Component, props, children) { return isReactComponent; } -function renderToStaticMarkup(Component, props, children) { - const html = renderToString(h(Component, { ...props, children: h(StaticHtml, { value: children }), innerHTML: children })); +function renderToStaticMarkup(Component, props, children, metadata) { + const vnode = h(Component, { ...props, children: h(StaticHtml, { value: children }), innerHTML: children }); + let html; + if(metadata.hydrate) { + html = renderToString(vnode); + } else { + html = reactRenderToStaticMarkup(vnode); + } return { html }; } diff --git a/yarn.lock b/yarn.lock index 65a98c158..177ce0c9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1140,6 +1140,20 @@ semver "^7.3.5" which "^2.0.2" +"@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + "@npmcli/installed-package-contents@^1.0.6": version "1.0.7" resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz" @@ -2396,7 +2410,7 @@ bytes@3.1.0, bytes@^3.0.0: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^15.0.5: +cacache@^15.0.5, cacache@^15.2.0: version "15.2.0" resolved "https://registry.npmjs.org/cacache/-/cacache-15.2.0.tgz" integrity sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw== @@ -3313,6 +3327,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" @@ -3796,9 +3815,9 @@ esbuild@^0.11.16, esbuild@^0.11.17: resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.11.23.tgz" integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q== -esbuild@^0.9.3: +esbuild@~0.9.0: version "0.9.7" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.9.7.tgz" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.9.7.tgz#ea0d639cbe4b88ec25fbed4d6ff00c8d788ef70b" integrity sha512-VtUf6aQ89VTmMLKrWHYG50uByMF4JQlVysb8dmg6cOgW8JnFCipmz7p+HNBl+RR3LLCuBxFGVauAe2wfnF9bLg== escalade@^3.1.1: @@ -5267,9 +5286,9 @@ is-decimal@^1.0.0: resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== -is-docker@^2.0.0: +is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-dotfile@^1.0.0: @@ -5542,9 +5561,9 @@ is-wsl@^1.1.0: resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.1: +is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" @@ -6153,6 +6172,28 @@ make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" +make-fetch-happen@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.0.3.tgz#57bbfb5b859807cd28005ca85aa6a72568675e24" + integrity sha512-uZ/9Cf2vKqsSWZyXhZ9wHHyckBrkntgbnqV68Bfe8zZenlf7D6yuGMXvHZQ+jSnzPkjosuNP1HGasj1J4h8OlQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + map-cache@^0.2.0: version "0.2.2" resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" @@ -6831,7 +6872,7 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: +negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== @@ -7062,6 +7103,18 @@ npm-registry-fetch@^10.0.0: minizlib "^2.0.0" npm-package-arg "^8.0.0" +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + npm-registry-fetch@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz" @@ -7193,13 +7246,14 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^7.0.4: - version "7.4.2" - resolved "https://registry.npmjs.org/open/-/open-7.4.2.tgz" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== +open@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/open/-/open-8.2.1.tgz#82de42da0ccbf429bc12d099dad2e0975e14e8af" + integrity sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ== dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" openurl@1.1.1: version "1.1.1" @@ -7376,7 +7430,7 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -pacote@^11.2.6, pacote@^11.3.1: +pacote@^11.2.6: version "11.3.3" resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.3.tgz" integrity sha512-GQxBX+UcVZrrJRYMK2HoG+gPeSUX/rQhnbPkkGrCYa4n2F/bgClFPaMm0nsdnYrxnmUy85uMHoFXZ0jTD0drew== @@ -7401,6 +7455,31 @@ pacote@^11.2.6, pacote@^11.3.1: ssri "^8.0.1" tar "^6.1.0" +pacote@^11.3.4: + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -7623,7 +7702,7 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -9091,18 +9170,18 @@ smartwrap@^1.2.3: wcwidth "^1.0.1" yargs "^15.1.0" -snowpack@^3.6.2: - version "3.6.2" - resolved "https://registry.npmjs.org/snowpack/-/snowpack-3.6.2.tgz" - integrity sha512-F+Q3qFLPLp+vJZtisnJG8dmJyXaZ2xePKU0VC1u7LDVD55xxh/h8NFHB7zBgstY33EyCkGrxmHZ4kZQu3FTTlw== +snowpack@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/snowpack/-/snowpack-3.7.1.tgz#2ecee14018a84a748d7628253d90b7166e9435e6" + integrity sha512-i7yj8zywKvg0Z/v2FfkKW1kROjnqIp+kkg/rU9rtaIOly2gsgwe0jSXh5/8SiAXw6XszOid2eJ6j9SmrJFPxRQ== dependencies: cli-spinners "^2.5.0" default-browser-id "^2.0.0" - esbuild "^0.9.3" + esbuild "~0.9.0" fdir "^5.0.0" - open "^7.0.4" - pacote "^11.3.1" - picomatch "^2.2.2" + open "^8.2.1" + pacote "^11.3.4" + picomatch "^2.3.0" resolve "^1.20.0" rollup "~2.37.1" optionalDependencies: