diff --git a/.changeset/olive-bikes-carry.md b/.changeset/olive-bikes-carry.md new file mode 100644 index 0000000000..80d87c52f1 --- /dev/null +++ b/.changeset/olive-bikes-carry.md @@ -0,0 +1,6 @@ +--- +'@ice/runtime': patch +'@ice/app': patch +--- + +feat: support API useDocumentData diff --git a/examples/basic-project/src/document.tsx b/examples/basic-project/src/document.tsx index 5a3d61e38d..79ee7f4ef1 100644 --- a/examples/basic-project/src/document.tsx +++ b/examples/basic-project/src/document.tsx @@ -1,8 +1,24 @@ -import { Meta, Title, Links, Main, Scripts, useAppData } from 'ice'; +// eslint-disable-next-line +import { Meta, Title, Links, Main, Scripts, useAppData, defineDataLoader, unstable_useDocumentData } from 'ice'; import type { AppData } from '@/types'; +export const dataLoader = defineDataLoader(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + title: 'documentData', + }); + // ATTENTION: This async call will pause rendering document. + }, 1000); + }); +}); + function Document() { const appData = useAppData(); + // Get document data when fallback to document only. + const documentData = unstable_useDocumentData(); + + console.log('document data', documentData); return ( @@ -21,6 +37,12 @@ function Document() {
+
+

Document Data

+ +
{JSON.stringify(documentData, null, 2)}
+
+
diff --git a/packages/ice/src/constant.ts b/packages/ice/src/constant.ts index 42158a8131..65321bd026 100644 --- a/packages/ice/src/constant.ts +++ b/packages/ice/src/constant.ts @@ -67,6 +67,7 @@ export const RUNTIME_EXPORTS = [ 'defineServerDataLoader', 'defineStaticDataLoader', 'usePageLifecycle', + 'unstable_useDocumentData', ], alias: { usePublicAppContext: 'useAppContext', diff --git a/packages/ice/templates/core/entry.server.ts.ejs b/packages/ice/templates/core/entry.server.ts.ejs index 48b7a4eb0f..26f9376976 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -4,7 +4,7 @@ import * as runtime from '@ice/runtime/server'; import { commons, statics } from './runtimeModules'; <% }-%> import * as app from '@/app'; -import Document from '@/document'; +import * as Document from '@/document'; import type { RenderMode, DistType } from '@ice/runtime'; import type { RenderToPipeableStreamOptions } from 'react-dom/server'; // @ts-ignore @@ -85,7 +85,8 @@ function mergeOptions(options) { assetsManifest, createRoutes, runtimeModules, - Document, + documentDataLoader: Document.dataLoader, + Document: Document.default, basename: basename || getRouterBasename(), renderMode, routesConfig, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 22c0e1ea88..52c19c6ed4 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -50,6 +50,7 @@ ], "dependencies": { "@ice/jsx-runtime": "^0.2.1", + "@ice/shared": "^1.0.1", "@remix-run/router": "1.7.2", "abortcontroller-polyfill": "1.7.5", "ejs": "^3.1.6", diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 67c46b42b4..f5ad7663c9 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -80,6 +80,15 @@ function usePublicAppContext(): PublicAppContext { }; } +function useDocumentData() { + const context = useInternalAppContext(); + return context.documentData; +} + +// @TODO: remove unstable prefix or refactor. +// eslint-disable-next-line +export const unstable_useDocumentData = useDocumentData; + export { getAppConfig, defineAppConfig, diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 56a80a4f60..eb92a9354d 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -1,9 +1,10 @@ import type { ServerResponse, IncomingMessage } from 'http'; import * as React from 'react'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; import * as ReactDOMServer from 'react-dom/server'; -import { parsePath } from 'history'; import type { Location } from 'history'; -import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { parsePath } from 'history'; +import { isFunction } from '@ice/shared'; import type { AppContext, RouteItem, ServerContext, AppExport, AssetsManifest, @@ -13,7 +14,7 @@ import type { DocumentComponent, RuntimeModules, AppData, - ServerAppRouterProps, + ServerAppRouterProps, DataLoaderConfig, } from './types.js'; import Runtime from './runtime.js'; import { AppContextProvider } from './AppContext.js'; @@ -31,11 +32,13 @@ import ServerRouter from './ServerRouter.js'; import { renderHTMLToJS } from './renderHTMLToJS.js'; import addLeadingSlash from './utils/addLeadingSlash.js'; + interface RenderOptions { app: AppExport; assetsManifest: AssetsManifest; createRoutes: (options: Pick) => RouteItem[]; runtimeModules: RuntimeModules; + documentDataLoader?: DataLoaderConfig; Document: DocumentComponent; documentOnly?: boolean; renderMode?: RenderMode; @@ -262,7 +265,20 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean)); } - // don't need to execute getAppData in CSR + // Execute document dataLoader. + let documentData: any; + if (renderOptions.documentDataLoader) { + const { loader } = renderOptions.documentDataLoader; + if (isFunction(loader)) { + documentData = await loader(requestContext); + // @TODO: document should have it's own context, not shared with app. + appContext.documentData = documentData; + } else { + console.warn('Document dataLoader only accepts function.'); + } + } + + // Not to execute [getAppData] when CSR. if (!documentOnly) { try { appData = await getAppData(app, requestContext); @@ -273,13 +289,13 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio // HashRouter loads route modules by the CSR. if (appConfig?.router?.type === 'hash') { - return renderDocument({ matches: [], routes, renderOptions }); + return renderDocument({ matches: [], routes, renderOptions, documentData }); } const matches = matchRoutes(routes, location, finalBasename); const routePath = getCurrentRoutePath(matches); if (documentOnly) { - return renderDocument({ matches, routePath, routes, renderOptions }); + return renderDocument({ matches, routePath, routes, renderOptions, documentData }); } else if (!matches.length) { return handleNotFoundResponse(); } @@ -339,7 +355,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio throw err; } console.error('Warning: render server entry error, downgrade to csr.', err); - return renderDocument({ matches, routePath, renderOptions, routes, downgrade: true }); + return renderDocument({ matches, routePath, renderOptions, routes, downgrade: true, documentData }); } } @@ -399,7 +415,14 @@ async function renderServerEntry( const pipe = renderToNodeStream(element); const fallback = () => { - return renderDocument({ matches, routePath, renderOptions, routes, downgrade: true }); + return renderDocument({ + matches, + routePath, + renderOptions, + routes, + downgrade: true, + documentData: appContext.documentData, + }); }; return { @@ -414,6 +437,7 @@ interface RenderDocumentOptions { matches: RouteMatch[]; renderOptions: RenderOptions; routes: RouteItem[]; + documentData: any; routePath?: string; downgrade?: boolean; } @@ -428,6 +452,7 @@ function renderDocument(options: RenderDocumentOptions): Response { routePath, downgrade, routes, + documentData, }: RenderDocumentOptions = options; const { @@ -465,6 +490,7 @@ function renderDocument(options: RenderDocumentOptions): Response { basename, downgrade, serverData, + documentData, }; const documentContext = { diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 6cc510d3f5..6ac6110b2e 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -99,6 +99,7 @@ export interface LoaderData { export interface AppContext { appConfig: AppConfig; appData: any; + documentData?: any; serverData?: any; assetsManifest?: AssetsManifest; loaderData?: LoadersData; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be4e8223cf..27c5a19226 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1582,6 +1582,7 @@ importers: packages/runtime: specifiers: '@ice/jsx-runtime': ^0.2.1 + '@ice/shared': ^1.0.1 '@remix-run/router': 1.7.2 '@remix-run/web-fetch': ^4.3.3 '@types/react': ^18.0.8 @@ -1599,6 +1600,7 @@ importers: source-map: ^0.7.4 dependencies: '@ice/jsx-runtime': link:../jsx-runtime + '@ice/shared': link:../shared '@remix-run/router': 1.7.2 abortcontroller-polyfill: 1.7.5 ejs: 3.1.8