fix: stream render (#5956)

* fix: resolve after render

* fix: basename without /

* fix: request path

* fix: error order

* feat: support pass manifest

* feat: exports analyze

* chore: update lock

* fix: should keep state when rerender

* feat: request ctx for suspense

* fix: type

* feat: support pass server data

* revert: version

* chore: add changeset

* fix: lint

* fix: type

* chore: add comments

* chore: add comments

* chore: print error separately

* refactor: support runtime public path

* revert: export assets manifest
This commit is contained in:
水澜 2023-03-01 18:02:39 +08:00 committed by GitHub
parent dae91e218c
commit 7d7296975c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 126 additions and 52 deletions

View File

@ -0,0 +1,12 @@
---
'@ice/runtime': patch
'@ice/app': patch
---
feat: support pass server data
feat: support change public path runtime
feat: support get request ctx in suspense
feat: export code analyzer for reuse in other plugin
fix: suspense error handle
fix: duplicate data request in suspense csr
fix: support await render to response

View File

@ -24,7 +24,8 @@ const fakeData = [
'I like marshmallows',
];
async function getData() {
async function getData(ctx) {
console.log(ctx);
console.log('load comments');
await new Promise<any>((resolve) => {

View File

@ -8,7 +8,7 @@ export default function Home() {
<div>
<h2>Home Page</h2>
<SuspenseComments id="comments" fallback={<Loading />} />
<Footer />
<Footer id="footer" />
</div>
);
}
@ -43,7 +43,7 @@ const fakeData = [
'I like marshmallows',
];
async function getCommentsData() {
async function getCommentsData(ctx) {
console.log('load comments');
if (process.env.ICE_CORE_SSR === 'true') {
@ -51,6 +51,7 @@ async function getCommentsData() {
setTimeout(() => reject('get data error'), 100);
});
} else {
console.log('client ctx', ctx);
await new Promise<any>((resolve) => {
setTimeout(() => resolve(null), 100);
});

View File

@ -6,7 +6,8 @@
"main": "./esm/index.js",
"exports": {
".": "./esm/index.js",
"./types": "./esm/types/index.js"
"./types": "./esm/types/index.js",
"./analyze": "./esm/service/analyze.js"
},
"bin": {
"ice": "./bin/ice-cli.mjs"

View File

@ -219,6 +219,7 @@ interface FileOptions {
type CachedRouteExports = { hash: string; exports: string[] };
// Exports for other plugin to get exports info.
export async function getFileExports(options: FileOptions): Promise<CachedRouteExports['exports']> {
const { rootDir, file } = options;
const filePath = path.join(rootDir, file);

View File

@ -40,6 +40,8 @@ interface RenderOptions {
routePath?: string;
disableFallback?: boolean;
distType?: DistType;
publicPath?: string;
serverData?: any;
}
export async function renderToHTML(requestContext, options: RenderOptions = {}) {
@ -55,7 +57,7 @@ export async function renderToResponse(requestContext, options: RenderOptions =
setRuntimeEnv(renderMode);
const mergedOptions = mergeOptions(options);
runtime.renderToResponse(requestContext, mergedOptions);
return runtime.renderToResponse(requestContext, mergedOptions);
}
<% if (jsOutput) { -%>
@ -69,7 +71,11 @@ export async function renderToEntry(requestContext, options: RenderOptions = {})
<% } -%>
function mergeOptions(options) {
const { documentOnly, renderMode = 'SSR', basename, serverOnlyBasename, routePath, disableFallback, distType } = options;
const { documentOnly, renderMode = 'SSR', basename, serverOnlyBasename, routePath, disableFallback, distType, serverData, publicPath } = options;
if (publicPath) {
assetsManifest.publicPath = publicPath;
}
return {
app,
@ -85,6 +91,7 @@ function mergeOptions(options) {
disableFallback,
routesConfig,
distType,
serverData,
<% if (runtimeOptions.exports) { -%>
runtimeOptions: {
<%- runtimeOptions.exports %>

View File

@ -146,7 +146,7 @@ export type DataType = (props: DataProps) => JSX.Element;
// use app context separately
export const Data: DataType = (props: DataProps) => {
const { routesData, documentOnly, matches, routesConfig, downgrade, renderMode } = useAppContext();
const { routesData, documentOnly, matches, routesConfig, downgrade, renderMode, serverData } = useAppContext();
const appData = useAppData();
const {
ScriptElement = 'script',
@ -163,6 +163,7 @@ export const Data: DataType = (props: DataProps) => {
matchedIds,
documentOnly,
renderMode,
serverData,
};
return (
@ -191,7 +192,6 @@ export const Main: MainType = (props: React.HTMLAttributes<HTMLDivElement>) => {
* merge assets info for matched route
*/
export function getPageAssets(matches: RouteMatch[], assetsManifest: AssetsManifest): string[] {
// TODOpublicPath from runtime
const { pages, publicPath } = assetsManifest;
let result = [];

View File

@ -1,5 +1,7 @@
import * as React from 'react';
import type { ReactNode } from 'react';
import { useAppContext } from './AppContext.js';
import type { RequestContext } from './types.js';
const LOADER = '__ICE_SUSPENSE_LOADER__';
const isClient = typeof window !== 'undefined' && 'onload' in window;
@ -13,40 +15,44 @@ interface SuspenseState {
update: Function;
}
type Request = () => Promise<any>;
type Request = (ctx: RequestContext) => Promise<any>;
const SuspenseContext = React.createContext<SuspenseState | undefined>(undefined);
export function useSuspenseData(request?: Request) {
const appContext = useAppContext();
const { requestContext } = appContext;
const suspenseState = React.useContext(SuspenseContext);
const { data, done, promise, update, error, id } = suspenseState;
// use data from server side directly when hydrate.
// 1. Use data from server side directly when hydrate.
if (isClient && (window[LOADER] as Map<string, any>) && window[LOADER].has(id)) {
return window[LOADER].get(id);
}
if (done) {
return data;
}
// 2. Check data request error, if error throw it to react.
if (error) {
throw error;
}
// request is pending.
// 3. If request is done, return data.
if (done) {
return data;
}
// 4. If request is pending, throw the promise to react.
if (promise) {
throw promise;
}
// when called by Data, request is null.
// 5. If no request, return null.
if (!request) {
return null;
}
// send request and throw promise
const thenable = request();
// 6. Create a promise for the request and throw it to react.
const thenable = request(requestContext);
thenable.then((response) => {
update({
@ -79,16 +85,21 @@ export function withSuspense(Component) {
return (props: SuspenseProps) => {
const { fallback, id, ...componentProps } = props;
const suspenseState = {
const [suspenseState, updateSuspenseData] = React.useState({
id: id,
data: null,
done: false,
promise: null,
error: null,
update: (value) => {
Object.assign(suspenseState, value);
},
};
update,
});
function update(value) {
// For SSR, setState is not working, so here we need to update the state manually.
const newState = Object.assign(suspenseState, value);
// For CSR.
updateSuspenseData(newState);
}
return (
<React.Suspense fallback={fallback || null}>

View File

@ -55,6 +55,7 @@ export default async function runClientApp(options: RunClientAppOptions) {
downgrade,
documentOnly,
renderMode,
serverData,
} = windowContext;
const requestContext = getRequestContext(window.location);
@ -79,6 +80,8 @@ export default async function runClientApp(options: RunClientAppOptions) {
basename,
routePath,
renderMode,
requestContext,
serverData,
};
const runtime = new Runtime(appContext, runtimeOptions);

View File

@ -48,6 +48,7 @@ interface RenderOptions {
};
runtimeOptions?: Record<string, any>;
distType?: Array<'html' | 'javascript'>;
serverData?: any;
}
interface Piper {
@ -140,22 +141,31 @@ export async function renderToResponse(requestContext: ServerContext, renderOpti
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// Send stream result to ServerResponse.
pipe(res, {
onShellError: async (err) => {
if (renderOptions.disableFallback) {
throw err;
}
return new Promise<void>((resolve, reject) => {
// Send stream result to ServerResponse.
pipe(res, {
onShellError: async (err) => {
if (renderOptions.disableFallback) {
reject(err);
}
// downgrade to CSR.
console.error('PipeToResponse onShellError, downgrade to CSR.', err);
const result = await fallback();
sendResult(res, result);
},
onError: async (err) => {
// onError triggered after shell ready, should not downgrade to csr.
console.error('PipeToResponse error.', err);
},
// downgrade to CSR.
console.error('PipeToResponse onShellError, downgrade to CSR.');
console.error(err);
const result = await fallback();
sendResult(res, result);
resolve();
},
onError: async (err) => {
// onError triggered after shell ready, should not downgrade to csr
// and should not be throw to break the render process
console.error('PipeToResponse error.');
console.error(err);
},
onAllReady: () => {
resolve();
},
});
});
}
}
@ -182,6 +192,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
runtimeModules,
renderMode,
runtimeOptions,
serverData,
} = renderOptions;
const finalBasename = serverOnlyBasename || basename;
const location = getLocation(req.url);
@ -201,6 +212,8 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
assetsManifest,
basename: finalBasename,
matches: [],
requestContext,
serverData,
};
const runtime = new Runtime(appContext, runtimeOptions);
runtime.setAppRouter(DefaultAppRouter);
@ -350,6 +363,7 @@ function renderDocument(options: RenderDocumentOptions): RenderResult {
Document,
basename,
routesConfig = {},
serverData,
} = renderOptions;
const routesData = null;
@ -376,13 +390,13 @@ function renderDocument(options: RenderDocumentOptions): RenderResult {
routePath,
basename,
downgrade,
serverData,
};
const documentContext = {
main: null,
};
const htmlStr = ReactDOMServer.renderToString(
<AppContextProvider value={appContext}>
<DocumentContextProvider value={documentContext}>

View File

@ -25,6 +25,9 @@ export function renderToNodeStream(
onError(error) {
options?.onError && options?.onError(error);
},
onAllReady() {
options?.onAllReady && options?.onAllReady();
},
});
};
}

View File

@ -75,6 +75,7 @@ export interface RoutesData {
export interface AppContext {
appConfig: AppConfig;
appData: any;
serverData?: any;
assetsManifest?: AssetsManifest;
routesData?: RoutesData;
routesConfig?: RoutesConfig;
@ -88,11 +89,12 @@ export interface AppContext {
basename?: string;
downgrade?: boolean;
renderMode?: string;
requestContext?: RequestContext;
}
export type WindowContext = Pick<
AppContext,
'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode'
'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode' | 'serverData'
>;
export type Renderer = (

View File

@ -5965,7 +5965,6 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
@ -5975,7 +5974,6 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
@ -5985,7 +5983,6 @@ packages:
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
@ -5995,7 +5992,6 @@ packages:
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
@ -6564,6 +6560,7 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@swc/core-darwin-arm64/1.3.3:
@ -6581,6 +6578,7 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@swc/core-darwin-x64/1.3.3:
@ -6609,6 +6607,7 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@swc/core-linux-arm-gnueabihf/1.3.3:
@ -6627,8 +6626,8 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
/@swc/core-linux-arm64-gnu/1.3.3:
@ -6636,7 +6635,6 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -6646,8 +6644,8 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
/@swc/core-linux-arm64-musl/1.3.3:
@ -6655,7 +6653,6 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: true
optional: true
@ -6665,8 +6662,8 @@ packages:
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
/@swc/core-linux-x64-gnu/1.3.3:
@ -6674,7 +6671,6 @@ packages:
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -6684,8 +6680,8 @@ packages:
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
/@swc/core-linux-x64-musl/1.3.3:
@ -6693,7 +6689,6 @@ packages:
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: true
optional: true
@ -6704,6 +6699,7 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@swc/core-win32-arm64-msvc/1.3.3:
@ -6723,6 +6719,7 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@swc/core-win32-ia32-msvc/1.3.3:
@ -6742,6 +6739,7 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@swc/core-win32-x64-msvc/1.3.3:
@ -6769,6 +6767,7 @@ packages:
'@swc/core-win32-arm64-msvc': 1.3.19
'@swc/core-win32-ia32-msvc': 1.3.19
'@swc/core-win32-x64-msvc': 1.3.19
dev: false
/@swc/core/1.3.3:
resolution: {integrity: sha512-OGx3Qpw+czNSaea1ojP2X2wxrGtYicQxH1QnzX4F3rXGEcSUFIllmrae6iJHW91zS4SNcOocnQoRz1IYnrILYw==}
@ -9481,6 +9480,7 @@ packages:
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
dependencies:
is-what: 3.14.1
dev: false
/copy-text-to-clipboard/3.0.1:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
@ -10649,6 +10649,7 @@ packages:
requiresBuild: true
dependencies:
prr: 1.0.1
dev: false
optional: true
/error-ex/1.3.2:
@ -13007,6 +13008,7 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
requiresBuild: true
dev: false
optional: true
/image-size/1.0.2:
@ -13029,6 +13031,7 @@ packages:
/immutable/4.1.0:
resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==}
dev: false
/import-cwd/3.0.0:
resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==}
@ -13538,6 +13541,7 @@ packages:
/is-what/3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
dev: false
/is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
@ -14839,6 +14843,7 @@ packages:
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
dev: false
/leven/3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -15159,6 +15164,7 @@ packages:
dependencies:
pify: 4.0.1
semver: 5.7.1
dev: false
optional: true
/make-dir/3.1.0:
@ -15537,6 +15543,7 @@ packages:
sax: 1.2.4
transitivePeerDependencies:
- supports-color
dev: false
optional: true
/negotiator/0.6.3:
@ -16013,6 +16020,7 @@ packages:
/parse-node-version/1.0.1:
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
engines: {node: '>= 0.10'}
dev: false
/parse-numeric-range/1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
@ -17859,6 +17867,7 @@ packages:
/prr/1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
dev: false
optional: true
/pseudomap/1.0.2:
@ -18666,6 +18675,7 @@ packages:
object-assign: 4.1.1
react: 17.0.2
scheduler: 0.20.2
dev: false
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
@ -18779,6 +18789,7 @@ packages:
/react-refresh/0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'}
dev: false
/react-router-config/5.1.1_2dl5roaqnyqqppnjni7uetnb3a:
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
@ -18876,6 +18887,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
@ -19461,6 +19473,7 @@ packages:
chokidar: 3.5.3
immutable: 4.1.0
source-map-js: 1.0.2
dev: false
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@ -19477,6 +19490,7 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: false
/scheduler/0.21.0:
resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==}
@ -20527,6 +20541,7 @@ packages:
serialize-javascript: 6.0.0
terser: 5.16.2
webpack: 5.75.0_esbuild@0.16.10
dev: true
/terser-webpack-plugin/5.3.6_peqg53aznxdu6ibj7wrnfxrfiq:
resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
@ -20552,6 +20567,7 @@ packages:
serialize-javascript: 6.0.0
terser: 5.16.2
webpack: 5.75.0_ncbsfugu56ddhgadp34k4kpsue
dev: true
/terser-webpack-plugin/5.3.6_webpack@5.75.0:
resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
@ -21966,6 +21982,7 @@ packages:
- '@swc/core'
- esbuild
- uglify-js
dev: true
/webpack/5.75.0_ncbsfugu56ddhgadp34k4kpsue:
resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==}
@ -22005,6 +22022,7 @@ packages:
- '@swc/core'
- esbuild
- uglify-js
dev: true
/webpackbar/5.0.2_webpack@5.75.0:
resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==}