Fixes using React.lazy and Suspense (#3160)
* Revert "Revert "Fixes using React.lazy and Suspense""
This reverts commit e621c2f7d3
.
* Adds a changeset
* Fix ts errors
* Remove netlify metadata folder
This commit is contained in:
parent
3cdc5f1bfb
commit
ae9ac5cbdc
21 changed files with 183 additions and 14 deletions
7
.changeset/dirty-planes-dance.md
Normal file
7
.changeset/dirty-planes-dance.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
'@astrojs/deno': patch
|
||||
'@astrojs/netlify': patch
|
||||
'@astrojs/react': patch
|
||||
---
|
||||
|
||||
Allows using React.lazy, Suspense in SSR and with hydration
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,3 +18,4 @@ package-lock.json
|
|||
*.env
|
||||
|
||||
!packages/astro/vendor/vite/dist
|
||||
packages/integrations/**/.netlify/
|
||||
|
|
9
packages/astro/test/fixtures/react-component/src/components/LazyComponent.jsx
vendored
Normal file
9
packages/astro/test/fixtures/react-component/src/components/LazyComponent.jsx
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
export const LazyComponent = () => {
|
||||
return (
|
||||
<span id="lazy">inner content</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default LazyComponent;
|
14
packages/astro/test/fixtures/react-component/src/components/Suspense.jsx
vendored
Normal file
14
packages/astro/test/fixtures/react-component/src/components/Suspense.jsx
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React, { Suspense } from 'react';
|
||||
const LazyComponent = React.lazy(() => import('./LazyComponent.jsx'));
|
||||
|
||||
export const ParentComponent = () => {
|
||||
return (
|
||||
<div id="outer">
|
||||
<Suspense>
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParentComponent;
|
17
packages/astro/test/fixtures/react-component/src/pages/suspense.astro
vendored
Normal file
17
packages/astro/test/fixtures/react-component/src/pages/suspense.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import Suspense from '../components/Suspense.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="client">
|
||||
<Suspense client:load />
|
||||
</div>
|
||||
<div id="server">
|
||||
<Suspense />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai';
|
||||
import cheerio from 'cheerio';
|
||||
import { load as cheerioLoad } from 'cheerio';
|
||||
import { isWindows, loadFixture } from './test-utils.js';
|
||||
|
||||
let fixture;
|
||||
|
@ -18,7 +18,7 @@ describe('React Components', () => {
|
|||
|
||||
it('Can load React', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
// test 1: basic component renders
|
||||
expect($('#react-static').text()).to.equal('Hello static!');
|
||||
|
@ -51,13 +51,13 @@ describe('React Components', () => {
|
|||
|
||||
it('Can load Vue', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('#vue-h2').text()).to.equal('Hasta la vista, baby');
|
||||
});
|
||||
|
||||
it('Can use a pragma comment', async () => {
|
||||
const html = await fixture.readFile('/pragma-comment/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
// test 1: rendered the PragmaComment component
|
||||
expect($('.pragma-comment')).to.have.lengthOf(2);
|
||||
|
@ -66,7 +66,7 @@ describe('React Components', () => {
|
|||
// TODO: is this still a relevant test?
|
||||
it.skip('Includes reactroot on hydrating components', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
const div = $('#research');
|
||||
|
||||
|
@ -76,6 +76,13 @@ describe('React Components', () => {
|
|||
// test 2: renders correctly
|
||||
expect(div.html()).to.equal('foo bar <!-- -->1');
|
||||
});
|
||||
|
||||
it('Can load Suspense-using components', async () => {
|
||||
const html = await fixture.readFile('/suspense/index.html');
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('#client #lazy')).to.have.lengthOf(1);
|
||||
expect($('#server #lazy')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
if (isWindows) return;
|
||||
|
@ -93,7 +100,7 @@ describe('React Components', () => {
|
|||
|
||||
it('scripts proxy correctly', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
for (const script of $('script').toArray()) {
|
||||
const { src } = script.attribs;
|
||||
|
|
|
@ -23,6 +23,10 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
vite.resolve = vite.resolve || {};
|
||||
vite.resolve.alias = vite.resolve.alias || {};
|
||||
const alias = vite.resolve.alias as Record<string, string>;
|
||||
alias['react-dom/server'] = 'react-dom/server.browser'
|
||||
vite.ssr = {
|
||||
noExternal: true,
|
||||
};
|
||||
|
|
|
@ -13,6 +13,9 @@ Deno.test({
|
|||
assertEquals(resp.status, 200);
|
||||
const html = await resp.text();
|
||||
assert(html);
|
||||
const doc = new DOMParser().parseFromString(html, `text/html`);
|
||||
const div = doc.querySelector("#react");
|
||||
assert(div, 'div exists');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import deno from '@astrojs/deno';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: deno(),
|
||||
integrations: [react()],
|
||||
experimental: {
|
||||
ssr: true
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/deno": "workspace:*"
|
||||
"@astrojs/deno": "workspace:*",
|
||||
"@astrojs/react": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
7
packages/integrations/deno/test/fixtures/basics/src/components/React.jsx
vendored
Normal file
7
packages/integrations/deno/test/fixtures/basics/src/components/React.jsx
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function() {
|
||||
return (
|
||||
<div id="react">testing</div>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
|
||||
import ReactComponent from '../components/React.jsx';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
@ -8,5 +8,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Basic App on Deno</h1>
|
||||
<ReactComponent />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -86,6 +86,10 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
vite.resolve = vite.resolve || {};
|
||||
vite.resolve.alias = vite.resolve.alias || {};
|
||||
const alias = vite.resolve.alias as Record<string, string>;
|
||||
alias['react-dom/server'] = 'react-dom/server.browser'
|
||||
vite.ssr = {
|
||||
noExternal: true,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
export { fromFileUrl } from 'https://deno.land/std@0.110.0/path/mod.ts';
|
||||
export { assertEquals, assert } from 'https://deno.land/std@0.132.0/testing/asserts.ts';
|
||||
export * from 'https://deno.land/x/deno_dom/deno-dom-wasm.ts';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-ignore
|
||||
import { runBuild } from './test-utils.ts';
|
||||
// @ts-ignore
|
||||
import { assertEquals, assert } from './deps.ts';
|
||||
import { assertEquals, assert, DOMParser } from './deps.ts';
|
||||
|
||||
// @ts-ignore
|
||||
Deno.test({
|
||||
|
@ -9,12 +9,17 @@ Deno.test({
|
|||
async fn() {
|
||||
let close = await runBuild('./fixtures/edge-basic/');
|
||||
const { default: handler } = await import(
|
||||
'./fixtures/edge-basic/dist/edge-functions/entry.mjs'
|
||||
'./fixtures/edge-basic/dist/edge-functions/entry.js'
|
||||
);
|
||||
const response = await handler(new Request('http://example.com/'));
|
||||
assertEquals(response.status, 200);
|
||||
const html = await response.text();
|
||||
assert(html, 'got some html');
|
||||
|
||||
const doc = new DOMParser().parseFromString(html, `text/html`)!;
|
||||
const div = doc.querySelector('#react');
|
||||
assert(div, 'div exists');
|
||||
|
||||
await close();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import { netlifyEdgeFunctions } from '@astrojs/netlify';
|
||||
import react from "@astrojs/react";
|
||||
|
||||
export default defineConfig({
|
||||
adapter: netlifyEdgeFunctions({
|
||||
dist: new URL('./dist/', import.meta.url),
|
||||
}),
|
||||
integrations: [react()],
|
||||
experimental: {
|
||||
ssr: true
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@astrojs/netlify": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function() {
|
||||
return (
|
||||
<div id="react">testing</div>
|
||||
)
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
---
|
||||
import ReactComponent from '../components/React.jsx';
|
||||
---
|
||||
<html>
|
||||
<head><title>Testing</title></head>
|
||||
<body>
|
||||
|
@ -6,5 +9,6 @@
|
|||
<ul>
|
||||
<li><a href="/two/">Two</a></li>
|
||||
</ul>
|
||||
<ReactComponent />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -12,7 +12,7 @@ function errorIsComingFromPreactComponent(err) {
|
|||
);
|
||||
}
|
||||
|
||||
function check(Component, props, children) {
|
||||
async function check(Component, props, children) {
|
||||
// Note: there are packages that do some unholy things to create "components".
|
||||
// Checking the $$typeof property catches most of these patterns.
|
||||
if (typeof Component === 'object') {
|
||||
|
@ -42,7 +42,7 @@ function check(Component, props, children) {
|
|||
return React.createElement('div');
|
||||
}
|
||||
|
||||
renderToStaticMarkup(Tester, props, children, {});
|
||||
await renderToStaticMarkup(Tester, props, children, {});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
|
@ -50,7 +50,13 @@ function check(Component, props, children) {
|
|||
return isReactComponent;
|
||||
}
|
||||
|
||||
function renderToStaticMarkup(Component, props, children, metadata) {
|
||||
async function getNodeWritable() {
|
||||
let nodeStreamBuiltinModuleName = 'stream';
|
||||
let { Writable } = await import(nodeStreamBuiltinModuleName);
|
||||
return Writable;
|
||||
}
|
||||
|
||||
async function renderToStaticMarkup(Component, props, children, metadata) {
|
||||
delete props['class'];
|
||||
const vnode = React.createElement(Component, {
|
||||
...props,
|
||||
|
@ -59,12 +65,74 @@ function renderToStaticMarkup(Component, props, children, metadata) {
|
|||
let html;
|
||||
if (metadata && metadata.hydrate) {
|
||||
html = ReactDOM.renderToString(vnode);
|
||||
if('renderToReadableStream' in ReactDOM) {
|
||||
html = await renderToReadableStreamAsync(vnode);
|
||||
} else {
|
||||
html = await renderToPipeableStreamAsync(vnode);
|
||||
}
|
||||
} else {
|
||||
html = ReactDOM.renderToStaticMarkup(vnode);
|
||||
if('renderToReadableStream' in ReactDOM) {
|
||||
html = await renderToReadableStreamAsync(vnode);
|
||||
} else {
|
||||
html = await renderToStaticNodeStreamAsync(vnode);
|
||||
}
|
||||
|
||||
}
|
||||
return { html };
|
||||
}
|
||||
|
||||
async function renderToPipeableStreamAsync(vnode) {
|
||||
const Writable = await getNodeWritable();
|
||||
let html = '';
|
||||
return new Promise((resolve, reject) => {
|
||||
let error = undefined;
|
||||
let stream = ReactDOM.renderToPipeableStream(vnode, {
|
||||
onError(err) {
|
||||
error = err;
|
||||
reject(error);
|
||||
},
|
||||
onAllReady() {
|
||||
stream.pipe(new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
html += chunk.toString('utf-8');
|
||||
callback();
|
||||
},
|
||||
destroy() {
|
||||
resolve(html);
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function renderToStaticNodeStreamAsync(vnode) {
|
||||
const Writable = await getNodeWritable();
|
||||
let html = '';
|
||||
return new Promise((resolve) => {
|
||||
let stream = ReactDOM.renderToStaticNodeStream(vnode);
|
||||
stream.pipe(new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
html += chunk.toString('utf-8');
|
||||
callback();
|
||||
},
|
||||
destroy() {
|
||||
resolve(html);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async function renderToReadableStreamAsync(vnode) {
|
||||
const decoder = new TextDecoder();
|
||||
const stream = await ReactDOM.renderToReadableStream(vnode);
|
||||
let html = '';
|
||||
for await(const chunk of stream) {
|
||||
html += decoder.decode(chunk);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup,
|
||||
|
|
|
@ -1238,9 +1238,11 @@ importers:
|
|||
packages/integrations/deno/test/fixtures/basics:
|
||||
specifiers:
|
||||
'@astrojs/deno': workspace:*
|
||||
'@astrojs/react': workspace:*
|
||||
astro: workspace:*
|
||||
dependencies:
|
||||
'@astrojs/deno': link:../../..
|
||||
'@astrojs/react': link:../../../../react
|
||||
astro: link:../../../../../astro
|
||||
|
||||
packages/integrations/lit:
|
||||
|
@ -1270,9 +1272,11 @@ importers:
|
|||
packages/integrations/netlify/test/edge-functions/fixtures/edge-basic:
|
||||
specifiers:
|
||||
'@astrojs/netlify': workspace:*
|
||||
'@astrojs/react': workspace:*
|
||||
astro: workspace:*
|
||||
dependencies:
|
||||
'@astrojs/netlify': link:../../../..
|
||||
'@astrojs/react': link:../../../../../react
|
||||
astro: link:../../../../../../astro
|
||||
|
||||
packages/integrations/node:
|
||||
|
|
Loading…
Reference in a new issue