From 558062e18da18e348263442708c9444c6ca17d63 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 12:00:47 +0200 Subject: [PATCH 01/25] Improve 404 page detection (covers more use-cases) + Don't serve pages under a locale that hasn't been enabled --- .../components/MultiversalAppBootstrap.tsx | 41 +------------------ src/layouts/demo/demoLayoutSSG.ts | 24 +++++++++++ src/pages/404.tsx | 2 + 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index d50bce5f1..536896548 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -24,6 +24,7 @@ import { configureSentryI18n } from '@/modules/core/sentry/sentry'; import deserializeSafe from '@/modules/core/serializeSafe/deserializeSafe'; import { detectCypress } from '@/modules/core/testing/cypress'; import { initCustomerTheme } from '@/modules/core/theming/theme'; +import { NotFound404PageName } from '@/pages/404'; import ErrorPage from '@/pages/_error'; import { NO_AUTO_PREVIEW_MODE_KEY } from '@/pages/api/preview'; import { ThemeProvider } from '@emotion/react'; @@ -225,44 +226,6 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele console.debug('dataset.customer', customer); } - // Force redirect to an allowed locale page, if the locale used to display the page isn't available for this customer - // TODO This should be replaced by something better, ideally the pages for non-available locales shouldn't be generated at all and then this wouldn't be needed. - // Using redirects in the app bootstrap can easily lead to infinite redirects, if not handled carefully. - // XXX Be extra careful with this kind of redirects based on remote data! - // It's easy to create an infinite redirect loop when the data aren't shaped as expected, edge cases (e.g "404"), etc. - // XXX Doing this isn't recommended and has been disabled because it breaks 404 pages by redirecting them to the homepage. - // Feel free to enable it if you wish. It should be implemented differently (by not generating non-allowed i18n pages), but might help anyway. - /*if (!includes(availableLanguages, locale) && size(availableLanguages) > 0 && isBrowser()) { - Sentry.captureEvent({ - message: `Unauthorized locale used "${locale}" (allowed: "${availableLanguages.join(', ')}") when loading page "${location.href}", user will be redirected.`, - level: Sentry.Severity.Warning, - }); - - // Edge case where the default locale isn't available for this customer, and the resolved user locale is wrong (e.g: 404 page where there is no locale detection) - // Redirect to the home page using the first allowed language (instead of redirecting to the same page, which would result in infinite loop for 404 pages, etc.) - if (locale === DEFAULT_LOCALE) { - const redirectTo = `/${availableLanguages?.[0] || ''}`; - location.href = redirectTo; - - if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { - return ( -
Locale not allowed. Redirecting to "{redirectTo}"...
- ); - } - } else { - // Otherwise, redirect to the same page using another locale (using the first available locale) - i18nRedirect(availableLanguages?.[0] || DEFAULT_LOCALE, router); - - if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { - return ( -
Locale not allowed. Redirecting...
- ); - } - } - - return null; - }*/ - let isPreviewModeEnabled; let previewData; let isQuickPreviewPage; @@ -299,7 +262,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } // Don't treat 404 pages like errors, it's expected in 404 pages not to have all the props the app needs - if (router?.route !== '/404') { + if (props?.Component?.name !== NotFound404PageName) { // If the app is misconfigured, simulate a native Next.js error to catch the misconfiguration early if (!customer || !i18nTranslations || !lang || !locale) { let error = props.err || null; diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 8a15062db..ba78598cb 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -8,6 +8,8 @@ import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consoli import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; import { DEFAULT_LOCALE, @@ -19,8 +21,11 @@ import { I18nextResources, } from '@/modules/core/i18n/i18nextLocize'; import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; +import find from 'lodash.find'; +import includes from 'lodash.includes'; import map from 'lodash.map'; import { GetStaticPaths, @@ -29,6 +34,11 @@ import { GetStaticPropsResult, } from 'next'; +const fileLabel = 'layouts/demo/demoLayoutSSG'; +const logger = createLogger({ + fileLabel, +}); + /** * Only executed on the server side at build time. * Computes all static paths that should be available for all SSG pages. @@ -46,6 +56,10 @@ import { * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getDemoStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { + // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, + // to only generate the pages for the locales the customer has explicitly enabled + // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption + // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { return { params: { @@ -89,6 +103,16 @@ export const getDemoStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + + // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") + if (!includes(customer?.availableLanguages, locale)) { + logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + + return { + notFound: true, + }; + } return { // Props returned here will be available as page properties (pageProps) diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 080ca0ae9..c576a28ee 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -135,4 +135,6 @@ const NotFound404Page: NextPage = (props): JSX.Element => { ); }; +export const NotFound404PageName = NotFound404Page.name; + export default NotFound404Page; From c4cf97bcbffef8345f7e569e8be71d5506650f79 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 12:04:18 +0200 Subject: [PATCH 02/25] Copy changes to core --- src/layouts/core/coreLayoutSSG.ts | 26 +++++++++++++++++++++++++- src/layouts/demo/demoLayoutSSG.ts | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index b547f880f..85271b58c 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -2,11 +2,14 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; +import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; import { DEFAULT_LOCALE, @@ -18,8 +21,11 @@ import { I18nextResources, } from '@/modules/core/i18n/i18nextLocize'; import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; +import find from 'lodash.find'; +import includes from 'lodash.includes'; import map from 'lodash.map'; import { GetStaticPaths, @@ -27,7 +33,11 @@ import { GetStaticProps, GetStaticPropsResult, } from 'next'; -import { SSGPageProps } from './types/SSGPageProps'; + +const fileLabel = 'layouts/demo/demoLayoutSSG'; +const logger = createLogger({ + fileLabel, +}); /** * Only executed on the server side at build time. @@ -46,6 +56,10 @@ import { SSGPageProps } from './types/SSGPageProps'; * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getCoreStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { + // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, + // to only generate the pages for the locales the customer has explicitly enabled + // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption + // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { return { params: { @@ -89,6 +103,16 @@ export const getCoreStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + + // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") + if (!includes(customer?.availableLanguages, locale)) { + logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + + return { + notFound: true, + }; + } return { // Props returned here will be available as page properties (pageProps) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index ba78598cb..8b72ab827 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -84,7 +84,7 @@ export const getDemoStaticPaths: GetStaticPaths = async * Meant to avoid code duplication * Can be overridden for per-page customisation (e.g: deepmerge) * - * XXX Demo component, not meant to be modified. It's a copy of the baseSSG implementation, so the demo keep working even if you change the base implementation. + * XXX Demo component, not meant to be modified. It's a copy of the coreLayoutSSG implementation, so the demo keep working even if you change the base implementation. * @return Props (as "SSGPageProps") that will be passed to the Page component, as props (known as "pageProps" in _app). * From e7e6ce76dff1f2e61adb869870b00993f36f5b4a Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 12:07:51 +0200 Subject: [PATCH 03/25] Apply same changes to SSR layouts --- src/layouts/core/coreLayoutSSR.ts | 24 ++++++++++++++++++++-- src/layouts/demo/demoLayoutSSR.ts | 34 ++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/layouts/core/coreLayoutSSR.ts b/src/layouts/core/coreLayoutSSR.ts index 3794fe74d..cdf8b45d2 100644 --- a/src/layouts/core/coreLayoutSSR.ts +++ b/src/layouts/core/coreLayoutSSR.ts @@ -1,4 +1,6 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; +import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; +import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; @@ -6,6 +8,8 @@ import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; import { @@ -17,6 +21,7 @@ import { fetchTranslations, I18nextResources, } from '@/modules/core/i18n/i18nextLocize'; +import { createLogger } from '@/modules/core/logging/logger'; import { isQuickPreviewRequest } from '@/modules/core/quickPreview/quickPreview'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; import { UserSemiPersistentSession } from '@/modules/core/userSession/types/UserSemiPersistentSession'; @@ -24,14 +29,19 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; +import find from 'lodash.find'; +import includes from 'lodash.includes'; import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult, } from 'next'; import NextCookies from 'next-cookies'; -import { PublicHeaders } from './types/PublicHeaders'; -import { SSRPageProps } from './types/SSRPageProps'; + +const fileLabel = 'layouts/demo/demoLayoutSSR'; +const logger = createLogger({ + fileLabel, +}); /** * "getCoreServerSideProps" returns only part of the props expected in SSRPageProps. @@ -97,6 +107,16 @@ export const getCoreServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + + // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") + if (!includes(customer?.availableLanguages, locale)) { + logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + + return { + notFound: true, + }; + } // Most props returned here will be necessary for the app to work properly (see "SSRPageProps") // Some props are meant to be helpful to the consumer and won't be passed down to the _app.render (e.g: apolloClient, layoutQueryOptions) diff --git a/src/layouts/demo/demoLayoutSSR.ts b/src/layouts/demo/demoLayoutSSR.ts index b732b0283..1cba58f65 100644 --- a/src/layouts/demo/demoLayoutSSR.ts +++ b/src/layouts/demo/demoLayoutSSR.ts @@ -8,6 +8,8 @@ import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; import { @@ -19,6 +21,7 @@ import { fetchTranslations, I18nextResources, } from '@/modules/core/i18n/i18nextLocize'; +import { createLogger } from '@/modules/core/logging/logger'; import { isQuickPreviewRequest } from '@/modules/core/quickPreview/quickPreview'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; import { UserSemiPersistentSession } from '@/modules/core/userSession/types/UserSemiPersistentSession'; @@ -26,6 +29,8 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; +import find from 'lodash.find'; +import includes from 'lodash.includes'; import { GetServerSideProps, GetServerSidePropsContext, @@ -33,9 +38,14 @@ import { } from 'next'; import NextCookies from 'next-cookies'; +const fileLabel = 'layouts/demo/demoLayoutSSR'; +const logger = createLogger({ + fileLabel, +}); + /** - * getDemoServerSideProps returns only part of the props expected in SSRPageProps - * To avoid TS issue, we omit those that we don't return, and add those necessary to the getServerSideProps function + * "getDemoServerSideProps" returns only part of the props expected in SSRPageProps. + * To avoid TS errors, we omit those that we don't return, and add those necessary to the "getServerSideProps" function. */ export type GetDemoServerSidePropsResults = SSRPageProps & { headers: PublicHeaders; @@ -43,14 +53,14 @@ export type GetDemoServerSidePropsResults = SSRPageProps & { /** * Only executed on the server side, for every request. - * Computes some dynamic props that should be available for all SSR pages that use getServerSideProps + * Computes some dynamic props that should be available for all SSR pages that use getServerSideProps. * - * Because the exact GQL query will depend on the consumer (AKA "caller"), this helper doesn't run any query by itself, but rather return all necessary props to allow the consumer to perform its own queries - * This improves performances, by only running one GQL query instead of many (consumer's choice) + * Because the exact GQL query will depend on the consumer (AKA "caller"), this helper doesn't run any query by itself, but rather return all necessary props to allow the consumer to perform its own queries. + * This improves performances, by only running one GQL query instead of many (consumer's choice). * - * Meant to avoid code duplication + * Meant to avoid code duplication. * - * XXX Demo component, not meant to be modified. It's a copy of the baseSSR implementation, so the demo keep working even if you change the base implementation. + * XXX Demo component, not meant to be modified. It's a copy of the coreLayoutSSR implementation, so the demo keep working even if you change the base implementation. * * @see https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering */ @@ -97,6 +107,16 @@ export const getDemoServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + + // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") + if (!includes(customer?.availableLanguages, locale)) { + logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + + return { + notFound: true, + }; + } // Most props returned here will be necessary for the app to work properly (see "SSRPageProps") // Some props are meant to be helpful to the consumer and won't be passed down to the _app.render (e.g: apolloClient, layoutQueryOptions) From dcd7926bd7c05f6d4e47f6a2fa136f437edebec3 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 12:37:13 +0200 Subject: [PATCH 04/25] Add TODO --- src/modules/core/i18n/middlewares/localeMiddleware.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/core/i18n/middlewares/localeMiddleware.ts b/src/modules/core/i18n/middlewares/localeMiddleware.ts index 5a8c1e869..939ca2040 100644 --- a/src/modules/core/i18n/middlewares/localeMiddleware.ts +++ b/src/modules/core/i18n/middlewares/localeMiddleware.ts @@ -35,6 +35,10 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse detections.forEach((language) => { if (localeFound || typeof language !== 'string') return; + // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, + // to only redirect the pages for the locales the customer has explicitly enabled + // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption + // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too const lookedUpLocale = supportedLocales.find( (allowedLocale) => allowedLocale.name === language, ); From 526fd789d123791ed035d747b8251ed62ad0037e Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 15:19:37 +0200 Subject: [PATCH 05/25] Use next-plugin-preval to preload the app-wide shared data at build time once instead of doing it for every page --- next.config.js | 6 +- package.json | 3 + src/layouts/demo/demoLayoutSSG.ts | 15 ++-- .../core/preval/fetchAirtableDataset.ts | 22 ++++++ yarn.lock | 75 ++++++++++++++++++- 5 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 src/modules/core/preval/fetchAirtableDataset.ts diff --git a/next.config.js b/next.config.js index ac0be5f09..eb33f205e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,10 @@ const bundleAnalyzer = require('@next/bundle-analyzer'); const nextSourceMaps = require('@zeit/next-source-maps'); +const createNextPluginPreval = require('next-plugin-preval/config'); const packageJson = require('./package'); const i18nConfig = require('./src/modules/core/i18n/i18nConfig'); +const withNextPluginPreval = createNextPluginPreval(); const withSourceMaps = nextSourceMaps(); const withBundleAnalyzer = bundleAnalyzer({ // Run with "yarn analyse:bundle" - See https://www.npmjs.com/package/@next/bundle-analyzer enabled: process.env.ANALYZE_BUNDLE === 'true', @@ -40,7 +42,7 @@ console.debug(`Release version resolved from tags: "${APP_RELEASE_TAG}" (matchin * * @see https://nextjs.org/docs/api-reference/next.config.js/introduction */ -module.exports = withBundleAnalyzer(withSourceMaps({ +module.exports = withNextPluginPreval(withBundleAnalyzer(withSourceMaps({ // basepath: '', // If you want Next.js to cover only a subsection of the domain. See https://nextjs.org/docs/api-reference/next.config.js/basepath // target: 'serverless', // Automatically enabled on Vercel, you may need to manually opt-in if you're not using Vercel. See https://nextjs.org/docs/api-reference/next.config.js/build-target#serverless-target // trailingSlash: false, // By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash. See https://nextjs.org/docs/api-reference/next.config.js/trailing-slash @@ -319,4 +321,4 @@ module.exports = withBundleAnalyzer(withSourceMaps({ // }, poweredByHeader: false, // See https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by -})); +}))); diff --git a/package.json b/package.json index 3c39691e7..b766d27b8 100644 --- a/package.json +++ b/package.json @@ -156,10 +156,12 @@ "lodash.size": "4.2.0", "lodash.some": "4.6.0", "lodash.startswith": "4.2.1", + "lodash.uniq": "4.5.0", "lodash.xorby": "4.7.0", "markdown-to-jsx": "7.1.2", "next": "10.2.0", "next-cookies": "2.0.3", + "next-plugin-preval": "1.0.1", "prop-types": "15.7.2", "rc-tooltip": "5.1.1", "react": "17.0.2", @@ -210,6 +212,7 @@ "@types/lodash.size": "4.2.6", "@types/lodash.some": "4.6.6", "@types/lodash.startswith": "4.2.6", + "@types/lodash.uniq": "4.5.6", "@types/lodash.xorby": "4.7.6", "@types/popper.js": "1.11.0", "@types/react": "17.0.5", diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 8b72ab827..9422f4791 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -3,11 +3,6 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -93,18 +88,20 @@ export const getDemoStaticPaths: GetStaticPaths = async */ export const getDemoStaticProps: GetStaticProps = async (props: StaticPropsInput): Promise> => { const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF; - const preview: boolean = props?.preview || false; + const preview: boolean = props?.preview || false; // TODO do something different if preview mode enabled const previewData: PreviewData = props?.previewData || null; const hasLocaleFromUrl = !!props?.params?.locale; const locale: string = hasLocaleFromUrl ? props?.params?.locale : DEFAULT_LOCALE; // If the locale isn't found (e.g: 404 page) const lang: string = locale.split('-')?.[0]; const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)]; const i18nTranslations: I18nextResources = await fetchTranslations(lang); // Pre-fetches translations from Locize API - const airtableSchema: AirtableSchema = getAirtableSchema(); - const datasets: AirtableDatasets = await fetchAndSanitizeAirtableDatasets(airtableSchema, bestCountryCodes); - const dataset: SanitizedAirtableDataset = consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); + const dataset: SanitizedAirtableDataset = await (await import('@/modules/core/preval/fetchAirtableDataset')).default; const customer: AirtableRecord = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + console.log('dataset', dataset); + console.log('customer', customer); + console.log('customer?.availableLanguages', customer?.availableLanguages); + // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); diff --git a/src/modules/core/preval/fetchAirtableDataset.ts b/src/modules/core/preval/fetchAirtableDataset.ts new file mode 100644 index 000000000..f3630194d --- /dev/null +++ b/src/modules/core/preval/fetchAirtableDataset.ts @@ -0,0 +1,22 @@ +import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; +import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; +import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import uniq from 'lodash.uniq'; +import preval from 'next-plugin-preval'; + +const fetchAirtableDataset = async (): Promise => { + const airtableSchema: AirtableSchema = getAirtableSchema(); + const supportedLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const datasets: AirtableDatasets = await fetchAndSanitizeAirtableDatasets(airtableSchema, supportedLanguages); + + return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); +}; + +export const dataset = preval(fetchAirtableDataset()) + +export default dataset; diff --git a/yarn.lock b/yarn.lock index 0c0353203..7ec54bbb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4546,6 +4546,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -4684,6 +4689,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.uniq@4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.uniq/-/lodash.uniq-4.5.6.tgz#adb052f6c7eeb38b920c13166e7a972dd960b4c5" + integrity sha512-XHNMXBtiwsWZstZMyxOYjr0e8YYWv0RgPlzIHblTuwBBiWo2MzWVaTBihtBpslb5BglgAWIeBv69qt1+RTRW1A== + dependencies: + "@types/lodash" "*" + "@types/lodash.xorby@4.7.6": version "4.7.6" resolved "https://registry.yarnpkg.com/@types/lodash.xorby/-/lodash.xorby-4.7.6.tgz#709c3d6994cc95fc0a2892cb60c7cbee5259f8d0" @@ -6198,6 +6210,17 @@ babel-plugin-macros@^3.0.1: cosmiconfig "^7.0.0" resolve "^1.19.0" +babel-plugin-module-resolver@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz#22a4f32f7441727ec1fbf4967b863e1e3e9f33e2" + integrity sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA== + dependencies: + find-babel-config "^1.2.0" + glob "^7.1.6" + pkg-up "^3.1.0" + reselect "^4.0.0" + resolve "^1.13.1" + babel-plugin-named-asset-import@^0.3.1: version "0.3.7" resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz#156cd55d3f1228a5765774340937afc8398067dd" @@ -9804,6 +9827,14 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-babel-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2" + integrity sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA== + dependencies: + json5 "^0.5.1" + path-exists "^3.0.0" + find-cache-dir@3.3.1, find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" @@ -12416,6 +12447,11 @@ json5@2.x, json5@^2.1.0: dependencies: minimist "^1.2.0" +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -13676,6 +13712,21 @@ next-cookies@2.0.3: dependencies: universal-cookie "^4.0.2" +next-plugin-preval@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/next-plugin-preval/-/next-plugin-preval-1.0.1.tgz#b3329d6aa22c23bcad7269d97da22950b1c56f1a" + integrity sha512-jKxueEzov+qR7vLMbIykAvCi2DczbBgtUp0QNV1wmagL7wKeRXgvc+f3gGXncefkHRHnZ3Ll6G8R19hZ6Bk7dA== + dependencies: + "@babel/core" "^7.12.10" + "@babel/preset-env" "^7.12.11" + babel-plugin-module-resolver "^4.1.0" + loader-utils "^2.0.0" + lodash "^4.17.20" + pirates "^4.0.1" + regenerator-runtime "^0.13.7" + require-from-string "^2.0.2" + tsconfig-paths "^3.9.0" + next-unused@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/next-unused/-/next-unused-0.0.6.tgz#dbefa300bf5586e33d5bfde909130fb19ab04a64" @@ -14688,7 +14739,7 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkg-up@3.1.0: +pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== @@ -16230,6 +16281,11 @@ requirejs@^2.3.5: resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -16287,7 +16343,7 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.8.1: dependencies: path-parse "^1.0.6" -resolve@^1.14.2, resolve@^1.19.0: +resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -17380,6 +17436,11 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -18044,6 +18105,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.11.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.0.tgz#f1f3528301621a53220d58373ae510ff747a66bc" From f72d3c64fe94e3dd9af69272cfa4abdf044bd64e Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 15:41:21 +0200 Subject: [PATCH 06/25] Doc + refactoring --- src/layouts/demo/demoLayoutSSG.ts | 2 +- .../fetchAirtableDataset.preval.ts} | 4 ++-- .../{fetchAirtableDS.ts => fetchAirtableDataset.ts} | 4 ++-- .../airtable/fetchAndSanitizeAirtableDatasets.ts | 13 +++++++++++-- 4 files changed, 16 insertions(+), 7 deletions(-) rename src/modules/core/{preval/fetchAirtableDataset.ts => airtable/fetchAirtableDataset.preval.ts} (88%) rename src/modules/core/airtable/{fetchAirtableDS.ts => fetchAirtableDataset.ts} (97%) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 9422f4791..24b33962c 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -95,7 +95,7 @@ export const getDemoStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; console.log('dataset', dataset); diff --git a/src/modules/core/preval/fetchAirtableDataset.ts b/src/modules/core/airtable/fetchAirtableDataset.preval.ts similarity index 88% rename from src/modules/core/preval/fetchAirtableDataset.ts rename to src/modules/core/airtable/fetchAirtableDataset.preval.ts index f3630194d..321a9d4a2 100644 --- a/src/modules/core/preval/fetchAirtableDataset.ts +++ b/src/modules/core/airtable/fetchAirtableDataset.preval.ts @@ -9,7 +9,7 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import uniq from 'lodash.uniq'; import preval from 'next-plugin-preval'; -const fetchAirtableDataset = async (): Promise => { +const fetchAirtableDatasetPreval = async (): Promise => { const airtableSchema: AirtableSchema = getAirtableSchema(); const supportedLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); const datasets: AirtableDatasets = await fetchAndSanitizeAirtableDatasets(airtableSchema, supportedLanguages); @@ -17,6 +17,6 @@ const fetchAirtableDataset = async (): Promise => { return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); }; -export const dataset = preval(fetchAirtableDataset()) +export const dataset = preval(fetchAirtableDatasetPreval()); export default dataset; diff --git a/src/modules/core/airtable/fetchAirtableDS.ts b/src/modules/core/airtable/fetchAirtableDataset.ts similarity index 97% rename from src/modules/core/airtable/fetchAirtableDS.ts rename to src/modules/core/airtable/fetchAirtableDataset.ts index 65f221aaf..fba43f9fd 100644 --- a/src/modules/core/airtable/fetchAirtableDS.ts +++ b/src/modules/core/airtable/fetchAirtableDataset.ts @@ -46,7 +46,7 @@ const VERCEL_DISK_CACHE_TTL = 180; // In seconds * Whether you use locales or languages is up to you, as it depends how you name your Airtable fields. * Tip: "Underscore" is recommended if using localized locales. (i.e: 'en_gb', not 'en-gb') */ -export const fetchAirtableDS = async (airtableSchema: AirtableSchema, localesOfLanguagesToFetch: string[]): Promise => { +export const fetchAirtableDataset = async (airtableSchema: AirtableSchema, localesOfLanguagesToFetch: string[]): Promise => { const promises: Promise[] = []; const rawAirtableRecordsSets: RawAirtableRecordsSet[] = []; const tableSchemaKeys: AirtableDBTable[] = Object.keys(airtableSchema) as AirtableDBTable[]; @@ -122,4 +122,4 @@ export const fetchAirtableDS = async (airtableSchema: AirtableSchema, localesOfL return rawAirtableRecordsSets; }; -export default fetchAirtableDS; +export default fetchAirtableDataset; diff --git a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts b/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts index 18ec463d8..fa7de9f10 100644 --- a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts +++ b/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts @@ -1,10 +1,13 @@ +import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import groupBy from 'lodash.groupby'; import map from 'lodash.map'; import size from 'lodash.size'; +import uniq from 'lodash.uniq'; import { AirtableDatasets } from '../data/types/AirtableDatasets'; import { RawAirtableDataset } from '../data/types/RawAirtableDataset'; -import fetchAirtableDS from './fetchAirtableDS'; +import fetchAirtableDataset from './fetchAirtableDataset'; import prepareAirtableDS from './prepareAirtableDS'; import sanitizeRawAirtableDS from './sanitizeRawAirtableDS'; import { AirtableSchema } from './types/AirtableSchema'; @@ -41,10 +44,16 @@ const printAirtableDatasetStatistics = (airtableDataset: RawAirtableDataset, lab * See https://lodash.com/docs/4.17.15#filter */ export const fetchAndSanitizeAirtableDatasets = async (airtableSchema: AirtableSchema, preferredLocalesOrLanguages: string[], filterByPredicate?: any): Promise => { - const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDS(airtableSchema, preferredLocalesOrLanguages); + // Resolves the languages we want to fetch the fields for (all supported languages configured in the app) + // We want to fetch all fields (for all language variations) during the initial dataset fetch + const localesOfLanguagesToFetch = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, localesOfLanguagesToFetch); + const airtableDatasets: AirtableDatasets = prepareAirtableDS(rawAirtableRecordsSets); printAirtableDatasetStatistics(airtableDatasets.raw, 'Raw dataset metadata:'); + // Sanitizes the dataset, during sanitization we will fallback (when a record isn't translated) + // using lang/locale depending on the order defined in preferredLocalesOrLanguages airtableDatasets.sanitized = sanitizeRawAirtableDS(airtableSchema, airtableDatasets, preferredLocalesOrLanguages, filterByPredicate); return airtableDatasets; From a89bba6ebde706a5db1ef11f6beb15b2f772b7ad Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 16:47:35 +0200 Subject: [PATCH 07/25] Simplifies fetching dataset/customer from pages (getStaticProps/getServerSideProps) using "next-plugin-preval" --- .storybook/preview.js | 38 ++++++------ .../components/MultiversalAppBootstrap.tsx | 4 +- src/layouts/core/coreLayoutSSG.ts | 16 ++--- src/layouts/core/coreLayoutSSR.ts | 16 ++--- src/layouts/demo/demoLayoutSSG.ts | 14 ++++- src/layouts/demo/demoLayoutSSR.ts | 16 ++--- src/modules/core/airtable/airtableSchema.ts | 4 +- .../airtable/fetchAirtableDataset.preval.ts | 22 ------- .../fetchRawAirtableDataset.preval.ts | 6 ++ .../core/airtable/fetchRawAirtableDataset.ts | 29 +++++++++ .../core/airtable/getSharedAirtableDataset.ts | 46 ++++++++++++++ .../core/airtable/prepareAirtableDS.ts | 31 ---------- ...s => prepareAndSanitizeAirtableDataset.ts} | 60 ++++++++++++------- .../example-with-ssg-and-revalidate.tsx | 3 +- 14 files changed, 176 insertions(+), 129 deletions(-) delete mode 100644 src/modules/core/airtable/fetchAirtableDataset.preval.ts create mode 100644 src/modules/core/airtable/fetchRawAirtableDataset.preval.ts create mode 100644 src/modules/core/airtable/fetchRawAirtableDataset.ts create mode 100644 src/modules/core/airtable/getSharedAirtableDataset.ts delete mode 100644 src/modules/core/airtable/prepareAirtableDS.ts rename src/modules/core/airtable/{fetchAndSanitizeAirtableDatasets.ts => prepareAndSanitizeAirtableDataset.ts} (56%) diff --git a/.storybook/preview.js b/.storybook/preview.js index 7ac86f816..996a0668a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,30 +1,30 @@ -import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; -import { ThemeProvider } from '@emotion/react'; -import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console -import { withTests } from '@storybook/addon-jest'; -import { addDecorator } from '@storybook/react'; -import { themes } from '@storybook/theming'; -import find from 'lodash.find'; -import React from 'react'; -import { withNextRouter } from 'storybook-addon-next-router'; -import { withPerformance } from 'storybook-addon-performance'; import '@/app/components/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) import MultiversalGlobalStyles from '@/app/components/MultiversalGlobalStyles'; -import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import '@/common/utils/ignoreNoisyWarningsHacks'; +import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; import customerContext from '@/modules/core/data/contexts/customerContext'; -import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import '@/modules/core/fontAwesome/fontAwesome'; import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; -import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; -import '@/common/utils/ignoreNoisyWarningsHacks'; -import { initCustomerTheme } from '@/modules/core/theming/theme'; -import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; -import '@/modules/core/fontAwesome/fontAwesome'; +import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; +import { ThemeProvider } from '@emotion/react'; +import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console +import { withTests } from '@storybook/addon-jest'; +import { addDecorator } from '@storybook/react'; +import { themes } from '@storybook/theming'; +import React from 'react'; +import { withNextRouter } from 'storybook-addon-next-router'; +import { withPerformance } from 'storybook-addon-performance'; import dataset from './mock/sb-dataset'; // Loads translations from local file cache (Locize) @@ -179,7 +179,7 @@ export const decorators = [ // Although, they are configured in the same way as the Next.js app during development mode i18nextLocize(lang, i18nTranslations); - const customer = find(dataset, { __typename: 'Customer' }); + const customer = getCustomer(dataset); const customerTheme = initCustomerTheme(customer); // console.log('customer', customer) // console.log('customerTheme', customerTheme) @@ -290,6 +290,6 @@ try { }), ); } catch (e) { - console.log(`Couldn't find ../.jest-test-results.json, Jest tests might not work properly.`) + console.log(`Couldn't find ../.jest-test-results.json, Jest tests might not work properly.`); } diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 536896548..8a4b401c0 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -1,6 +1,7 @@ import Loader from '@/components/animations/Loader'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; +import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; import customerContext from '@/modules/core/data/contexts/customerContext'; import datasetContext from '@/modules/core/data/contexts/datasetContext'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; @@ -31,7 +32,6 @@ import { ThemeProvider } from '@emotion/react'; import * as Sentry from '@sentry/node'; import { isBrowser } from '@unly/utils'; import { i18n } from 'i18next'; -import find from 'lodash.find'; import isEmpty from 'lodash.isempty'; import size from 'lodash.size'; import React, { useState } from 'react'; @@ -210,7 +210,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } const dataset: SanitizedAirtableDataset = deserializeSafe(serializedDataset); - const customer: AirtableRecord = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const customer: AirtableRecord = getCustomer(dataset); let availableLanguages: string[] = customer?.availableLanguages; if (isEmpty(availableLanguages)) { diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index 85271b58c..47b845513 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -3,11 +3,10 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getSharedAirtableDataset'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -24,7 +23,6 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import map from 'lodash.map'; import { @@ -100,10 +98,8 @@ export const getCoreStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { diff --git a/src/layouts/core/coreLayoutSSR.ts b/src/layouts/core/coreLayoutSSR.ts index cdf8b45d2..58483d44f 100644 --- a/src/layouts/core/coreLayoutSSR.ts +++ b/src/layouts/core/coreLayoutSSR.ts @@ -1,13 +1,12 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getSharedAirtableDataset'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; @@ -29,7 +28,6 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import { GetServerSideProps, @@ -104,10 +102,8 @@ export const getCoreServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 24b33962c..e0c80c9af 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -3,6 +3,16 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; +import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getSharedAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; +import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -95,8 +105,8 @@ export const getDemoStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); console.log('dataset', dataset); console.log('customer', customer); diff --git a/src/layouts/demo/demoLayoutSSR.ts b/src/layouts/demo/demoLayoutSSR.ts index 1cba58f65..9df852a59 100644 --- a/src/layouts/demo/demoLayoutSSR.ts +++ b/src/layouts/demo/demoLayoutSSR.ts @@ -1,13 +1,12 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getSharedAirtableDataset'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; @@ -29,7 +28,6 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import { GetServerSideProps, @@ -104,10 +102,8 @@ export const getDemoServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { diff --git a/src/modules/core/airtable/airtableSchema.ts b/src/modules/core/airtable/airtableSchema.ts index 368429a89..47f33339e 100644 --- a/src/modules/core/airtable/airtableSchema.ts +++ b/src/modules/core/airtable/airtableSchema.ts @@ -1,7 +1,7 @@ import { AirtableSchema } from './types/AirtableSchema'; import { GenericPostConsolidationTransformationValueInputProps } from './types/FieldSchema'; -type Props = {} +export type GetAirtableSchemaProps = {} /** * Airtable database schema used thorough the whole app. @@ -23,7 +23,7 @@ type Props = {} * - e.g: fields based on a computed property "isExpired" will be correct when fetched, but will become stale afterwards, and aren't reliable. * This is a concern for the production environment only, as staging/development use real-time API requests and thus use up-to-date content. */ -export const getAirtableSchema = (props?: Props): AirtableSchema => { +export const getAirtableSchema = (props?: GetAirtableSchemaProps): AirtableSchema => { return { Customer: { diff --git a/src/modules/core/airtable/fetchAirtableDataset.preval.ts b/src/modules/core/airtable/fetchAirtableDataset.preval.ts deleted file mode 100644 index 321a9d4a2..000000000 --- a/src/modules/core/airtable/fetchAirtableDataset.preval.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; -import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; -import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; -import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; -import uniq from 'lodash.uniq'; -import preval from 'next-plugin-preval'; - -const fetchAirtableDatasetPreval = async (): Promise => { - const airtableSchema: AirtableSchema = getAirtableSchema(); - const supportedLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); - const datasets: AirtableDatasets = await fetchAndSanitizeAirtableDatasets(airtableSchema, supportedLanguages); - - return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); -}; - -export const dataset = preval(fetchAirtableDatasetPreval()); - -export default dataset; diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts new file mode 100644 index 000000000..6385d2962 --- /dev/null +++ b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts @@ -0,0 +1,6 @@ +import fetchRawAirtableDataset from '@/modules/core/airtable/fetchRawAirtableDataset'; +import preval from 'next-plugin-preval'; + +export const dataset = preval(fetchRawAirtableDataset()); + +export default dataset; diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.ts b/src/modules/core/airtable/fetchRawAirtableDataset.ts new file mode 100644 index 000000000..11ec3cbb4 --- /dev/null +++ b/src/modules/core/airtable/fetchRawAirtableDataset.ts @@ -0,0 +1,29 @@ +import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; +import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { createLogger } from '@/modules/core/logging/logger'; +import uniq from 'lodash.uniq'; +import fetchAirtableDataset from './fetchAirtableDataset'; +import { AirtableSchema } from './types/AirtableSchema'; +import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; + +const fileLabel = 'modules/core/airtable/fetchRawAirtableDataset'; +const logger = createLogger({ + fileLabel, +}); + +/** + * XXX used by preval + * XXX Must be a single export file otherwise it can cause issues + */ +export const fetchRawAirtableDataset = async (): Promise => { + const airtableSchema: AirtableSchema = getAirtableSchema(); + + // Resolves the languages we want to fetch the fields for (all supported languages configured in the app) + // We want to fetch all fields (for all language variations) during the initial dataset fetch + const localesOfLanguagesToFetch = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + + return await fetchAirtableDataset(airtableSchema, localesOfLanguagesToFetch); +}; + +export default fetchRawAirtableDataset; diff --git a/src/modules/core/airtable/getSharedAirtableDataset.ts b/src/modules/core/airtable/getSharedAirtableDataset.ts new file mode 100644 index 000000000..5ee4e0c46 --- /dev/null +++ b/src/modules/core/airtable/getSharedAirtableDataset.ts @@ -0,0 +1,46 @@ +import { + getAirtableSchema, + GetAirtableSchemaProps, +} from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; +import { createLogger } from '@/modules/core/logging/logger'; +import find from 'lodash.find'; +import { AirtableSchema } from './types/AirtableSchema'; +import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; + +const fileLabel = 'modules/core/airtable/getSharedAirtableDataset'; +const logger = createLogger({ + fileLabel, +}); + +/** + * Finds the customer within the dataset. + * + * @param dataset + */ +export const getCustomer = (dataset: SanitizedAirtableDataset): AirtableRecord => { + return find(dataset, { __typename: 'Customer' }) as AirtableRecord; +}; + +/** + * Returns the whole dataset (sanitized). + * + * Uses the + * + * @param preferredLocalesOrLanguages + * @param props + */ +export const getSharedAirtableDataset = async (preferredLocalesOrLanguages: string[], props?: GetAirtableSchemaProps): Promise => { + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; + const airtableSchema: AirtableSchema = getAirtableSchema(props); + const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); + + return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); +}; + +export default getSharedAirtableDataset; diff --git a/src/modules/core/airtable/prepareAirtableDS.ts b/src/modules/core/airtable/prepareAirtableDS.ts deleted file mode 100644 index b65e15121..000000000 --- a/src/modules/core/airtable/prepareAirtableDS.ts +++ /dev/null @@ -1,31 +0,0 @@ -import map from 'lodash.map'; -import { AirtableDatasets } from '../data/types/AirtableDatasets'; -import { RawAirtableRecord } from './types/RawAirtableRecord'; -import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; - -/** - * Prepare the raw and sanitized airtable datasets. - * - * Each RawAirtableRecordsSet is basically the result of a full table fetch (a list of records sharing the same data structure). - * - * @param rawAirtableRecordsSets - */ -export const prepareAirtableDS = (rawAirtableRecordsSets: RawAirtableRecordsSet[]): AirtableDatasets => { - const datasets: AirtableDatasets = { - raw: {}, - sanitized: {}, - }; - - map(rawAirtableRecordsSets, (rawRecordsSet: RawAirtableRecordsSet) => { - map(rawRecordsSet.records, (record: RawAirtableRecord) => { - datasets.raw[record.id] = { - ...record, - __typename: rawRecordsSet.__typename, - }; - }); - }); - - return datasets; -}; - -export default prepareAirtableDS; diff --git a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts b/src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts similarity index 56% rename from src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts rename to src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts index fa7de9f10..663b1e494 100644 --- a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts +++ b/src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts @@ -1,54 +1,74 @@ -import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; -import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { RawAirtableRecord } from '@/modules/core/airtable/types/RawAirtableRecord'; import { createLogger } from '@/modules/core/logging/logger'; import groupBy from 'lodash.groupby'; import map from 'lodash.map'; import size from 'lodash.size'; -import uniq from 'lodash.uniq'; import { AirtableDatasets } from '../data/types/AirtableDatasets'; import { RawAirtableDataset } from '../data/types/RawAirtableDataset'; -import fetchAirtableDataset from './fetchAirtableDataset'; -import prepareAirtableDS from './prepareAirtableDS'; -import sanitizeRawAirtableDS from './sanitizeRawAirtableDS'; +import { sanitizeRawAirtableDS } from './sanitizeRawAirtableDS'; import { AirtableSchema } from './types/AirtableSchema'; import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; import { TableSchema } from './types/TableSchema'; -const fileLabel = 'modules/core/airtable/fetchAndSanitizeAirtableDatasets'; +const fileLabel = 'modules/core/airtable/prepareAndSanitizeAirtableDataset'; const logger = createLogger({ fileLabel, }); +/** + * Prints stats metadata about the dataset. + * Useful for debugging. + * + * @param airtableDataset + * @param label + */ const printAirtableDatasetStatistics = (airtableDataset: RawAirtableDataset, label: string): void => { const rawDatasetGroupedByTable = groupBy(airtableDataset, '__typename'); - logger.log(label, // eslint-disable-line no-console + logger.info(label, `size: ${size(airtableDataset)}`, map(rawDatasetGroupedByTable, (tableSchemas: TableSchema[], tableName) => `${tableName}: ${size(tableSchemas)}`), ); }; /** - * Fetches the Airtable API (once per table) - * Main entry point of the Airtable Dataset consolidation. + * Prepare the raw and sanitized airtable datasets. * + * Each RawAirtableRecordsSet is basically the result of a full table fetch (a list of records sharing the same data structure). + * + * @param rawAirtableRecordsSets + */ +export const prepareAirtableDS = (rawAirtableRecordsSets: RawAirtableRecordsSet[]): AirtableDatasets => { + const datasets: AirtableDatasets = { + raw: {}, + sanitized: {}, + }; + + map(rawAirtableRecordsSets, (rawRecordsSet: RawAirtableRecordsSet) => { + map(rawRecordsSet.records, (record: RawAirtableRecord) => { + datasets.raw[record.id] = { + ...record, + __typename: rawRecordsSet.__typename, + }; + }); + }); + + return datasets; +}; + +/** * Performs the consolidation in the following order: - * 1) Fetches all tables and their records as described in the schema. - * 2) Prepare the datasets (raw and sanitized). - * 3) Sanitize the raw dataset. + * 1) Prepare the datasets (raw and sanitized). + * 2) Sanitize the raw dataset. * + * @param rawAirtableRecordsSets * @param airtableSchema * @param preferredLocalesOrLanguages Those locales will be used to fetch i18n variant fields and resolve the missing translations * @param filterByPredicate Optional lodash.filter predicate to filter the whole raw dataset (after it's been fetched). * Different than per-table filterByFormula which is applied directly by the Airtable API). * See https://lodash.com/docs/4.17.15#filter */ -export const fetchAndSanitizeAirtableDatasets = async (airtableSchema: AirtableSchema, preferredLocalesOrLanguages: string[], filterByPredicate?: any): Promise => { - // Resolves the languages we want to fetch the fields for (all supported languages configured in the app) - // We want to fetch all fields (for all language variations) during the initial dataset fetch - const localesOfLanguagesToFetch = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); - const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, localesOfLanguagesToFetch); - +export const prepareAndSanitizeAirtableDataset = (rawAirtableRecordsSets: RawAirtableRecordsSet[], airtableSchema: AirtableSchema, preferredLocalesOrLanguages: string[], filterByPredicate?: any): AirtableDatasets => { const airtableDatasets: AirtableDatasets = prepareAirtableDS(rawAirtableRecordsSets); printAirtableDatasetStatistics(airtableDatasets.raw, 'Raw dataset metadata:'); @@ -59,4 +79,4 @@ export const fetchAndSanitizeAirtableDatasets = async (airtableSchema: AirtableS return airtableDatasets; }; -export default fetchAndSanitizeAirtableDatasets; +export default prepareAndSanitizeAirtableDataset; diff --git a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx index 0339a85ac..ed81cac3c 100644 --- a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx +++ b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx @@ -11,6 +11,7 @@ import { getDemoStaticPaths, getDemoStaticProps, } from '@/layouts/demo/demoLayoutSSG'; +import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; @@ -63,7 +64,7 @@ export const getStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const customer: AirtableRecord = getCustomer(dataset); return deepmerge(commonStaticProps, { props: { From 090831a8dd6ba447b0f1c677b564a45853cdc4b5 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 16:52:34 +0200 Subject: [PATCH 08/25] Misc logs --- src/layouts/demo/demoLayoutSSG.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index e0c80c9af..c9ecf451b 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -108,8 +108,6 @@ export const getDemoStaticProps: GetStaticProps = getCustomer(dataset); - console.log('dataset', dataset); - console.log('customer', customer); console.log('customer?.availableLanguages', customer?.availableLanguages); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") From bb35c369c19e8c89b2d0f50c87ab54a8b0367887 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 16:53:08 +0200 Subject: [PATCH 09/25] Misc reformat --- src/layouts/demo/demoLayoutSSG.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index c9ecf451b..26422b336 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -3,16 +3,10 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; import { getCustomer, getSharedAirtableDataset, } from '@/modules/core/airtable/getSharedAirtableDataset'; -import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; -import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -29,7 +23,6 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import map from 'lodash.map'; import { From 0052e17a13f9f288f6042c54a9c8330a9d8f50e6 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 17:13:34 +0200 Subject: [PATCH 10/25] Doc --- .../fetchRawAirtableDataset.preval.ts | 28 +++++++++++++++++++ .../core/airtable/getSharedAirtableDataset.ts | 7 +++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts index 6385d2962..50f0b5582 100644 --- a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts +++ b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts @@ -1,6 +1,34 @@ import fetchRawAirtableDataset from '@/modules/core/airtable/fetchRawAirtableDataset'; import preval from 'next-plugin-preval'; +/** + * Pre-fetches the Airtable dataset and stores the result in an cached internal JSON file. + * Overall, this approach allows us to have some static app-wide data that will never update, and have real-time data wherever we want. + * + * This is very useful to avoid fetching the same data for each page during the build step. + * By default, Next.js would call the Airtable API once per page built. + * This was a huge pain for many reasons, because our app uses mostly static pages and we don't want those static pages to be updated. + * + * Also, even considering built time only, it was very inefficient, because Next was triggering too many API calls: + * - More than 120 fetch attempts (locales * pages) + * - 3 locales (in supportedLocales) + * - lots of static pages (40+ demo pages) + * - Our in-memory cache was helping but wouldn't completely conceal the over-fetching caused by Next.js + * - Airtable API requires 1 API request per table (we fetch 9 tables for the demo) + * + * The shared/static dataset is accessible to: + * - All components + * - All pages (both getStaticProps and getStaticPaths, and even in getServerSideProps is you wish to!) + * - All API endpoints + * + * XXX The data are therefore STALE, they're not fetched in real-time. + * They won't update (the app won't display up-to-date data until the next deployment, for static pages). + * + * @example await import('@/modules/core/airtable/fetchRawAirtableDataset.preval') + * @example const rawAirtableRecordsSets: RawAirtableRecordsSet[] = (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; + * + * @see https://github.com/ricokahler/next-plugin-preval + */ export const dataset = preval(fetchRawAirtableDataset()); export default dataset; diff --git a/src/modules/core/airtable/getSharedAirtableDataset.ts b/src/modules/core/airtable/getSharedAirtableDataset.ts index 5ee4e0c46..a61fe748b 100644 --- a/src/modules/core/airtable/getSharedAirtableDataset.ts +++ b/src/modules/core/airtable/getSharedAirtableDataset.ts @@ -28,9 +28,12 @@ export const getCustomer = (dataset: SanitizedAirtableDataset): AirtableRecord Date: Wed, 26 May 2021 17:32:58 +0200 Subject: [PATCH 11/25] Refactor + only generate pages for languages that have been enabled --- src/layouts/core/coreLayoutSSG.ts | 25 ++++++----------- src/layouts/demo/demoLayoutSSG.ts | 27 ++++++------------- .../fetchRawAirtableDataset.preval.ts | 2 ++ .../core/airtable/getSharedAirtableDataset.ts | 11 +++++++- 4 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index 47b845513..d2bb054b2 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -23,8 +23,8 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import includes from 'lodash.includes'; import map from 'lodash.map'; +import uniq from 'lodash.uniq'; import { GetStaticPaths, GetStaticPathsContext, @@ -54,14 +54,15 @@ const logger = createLogger({ * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getCoreStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only generate the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = getCustomer(dataset); + + // Generate only pages for languages that have been allowed by the customer + const paths: StaticPath[] = map(customer?.availableLanguages, (availableLanguage: string): StaticPath => { return { params: { - locale: supportedLocale.name, + locale: availableLanguage, }, }; }); @@ -99,16 +100,6 @@ export const getCoreStaticProps: GetStaticProps = getCustomer(dataset); - - // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") - if (!includes(customer?.availableLanguages, locale)) { - logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); - - return { - notFound: true, - }; - } return { // Props returned here will be available as page properties (pageProps) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 26422b336..2a1504579 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -23,8 +23,8 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import includes from 'lodash.includes'; import map from 'lodash.map'; +import uniq from 'lodash.uniq'; import { GetStaticPaths, GetStaticPathsContext, @@ -54,14 +54,15 @@ const logger = createLogger({ * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getDemoStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only generate the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = getCustomer(dataset); + + // Generate only pages for languages that have been allowed by the customer + const paths: StaticPath[] = map(customer?.availableLanguages, (availableLanguage: string): StaticPath => { return { params: { - locale: supportedLocale.name, + locale: availableLanguage, }, }; }); @@ -99,18 +100,6 @@ export const getDemoStaticProps: GetStaticProps = getCustomer(dataset); - - console.log('customer?.availableLanguages', customer?.availableLanguages); - - // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") - if (!includes(customer?.availableLanguages, locale)) { - logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); - - return { - notFound: true, - }; - } return { // Props returned here will be available as page properties (pageProps) diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts index 50f0b5582..20bf846b9 100644 --- a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts +++ b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts @@ -15,6 +15,8 @@ import preval from 'next-plugin-preval'; * - lots of static pages (40+ demo pages) * - Our in-memory cache was helping but wouldn't completely conceal the over-fetching caused by Next.js * - Airtable API requires 1 API request per table (we fetch 9 tables for the demo) + * - We were generating pages for all supportedLocales, even if the customer hadn't enabled some languages (longer build + undesired pages leading to bad UX) + * - We weren't able to auto-redirect only to one of the enabled customer locales, not without fetching Airtable (which is slow and has strong rate limits) * * The shared/static dataset is accessible to: * - All components diff --git a/src/modules/core/airtable/getSharedAirtableDataset.ts b/src/modules/core/airtable/getSharedAirtableDataset.ts index a61fe748b..2c933b7ac 100644 --- a/src/modules/core/airtable/getSharedAirtableDataset.ts +++ b/src/modules/core/airtable/getSharedAirtableDataset.ts @@ -27,6 +27,15 @@ export const getCustomer = (dataset: SanitizedAirtableDataset): AirtableRecord; }; +/** + * Returns the whole dataset (raw), based on the app-wide static/shared/stale data fetched at build time. + * + * @example const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await getSharedRawAirtableDataset(); + */ +export const getSharedRawAirtableDataset = async (): Promise => { + return (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; +}; + /** * Returns the whole dataset (sanitized), based on the app-wide static/shared/stale data fetched at build time. * @@ -39,7 +48,7 @@ export const getCustomer = (dataset: SanitizedAirtableDataset): AirtableRecord => { - const rawAirtableRecordsSets: RawAirtableRecordsSet[] = (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await getSharedRawAirtableDataset(); const airtableSchema: AirtableSchema = getAirtableSchema(props); const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); From e24b0e4fe7c995e1577ce62c330494a1083a8d39 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 17:35:42 +0200 Subject: [PATCH 12/25] Fix bad link (i18n) --- src/layouts/demo/components/IntroductionSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/demo/components/IntroductionSection.tsx b/src/layouts/demo/components/IntroductionSection.tsx index 8c52c4f46..70f24beea 100644 --- a/src/layouts/demo/components/IntroductionSection.tsx +++ b/src/layouts/demo/components/IntroductionSection.tsx @@ -71,7 +71,7 @@ const IntroductionSection: React.FunctionComponent = (props): JSX.Element Nav/Footer component are localised, as well as dynamic content and i18n examples.

You can switch locale from the footer or by clicking on{' '} - fr-FR or en-US. + fr or en or en-US. From 4ed5c4c9a6c1871253083bcee4b058b54febd4e1 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 18:09:25 +0200 Subject: [PATCH 13/25] Attempt to handle preview mode, commented out because failing --- .storybook/preview.js | 2 +- .../components/MultiversalAppBootstrap.tsx | 2 +- src/layouts/core/coreLayoutSSG.ts | 2 +- src/layouts/core/coreLayoutSSR.ts | 2 +- src/layouts/demo/demoLayoutSSG.ts | 5 ++-- src/layouts/demo/demoLayoutSSR.ts | 2 +- .../core/airtable/fetchRawAirtableDataset.ts | 10 +++++-- ...rtableDataset.ts => getAirtableDataset.ts} | 28 +++++++++++++++---- .../example-with-ssg-and-revalidate.tsx | 2 +- 9 files changed, 39 insertions(+), 16 deletions(-) rename src/modules/core/airtable/{getSharedAirtableDataset.ts => getAirtableDataset.ts} (60%) diff --git a/.storybook/preview.js b/.storybook/preview.js index 996a0668a..8d286c905 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,7 +1,7 @@ import '@/app/components/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) import MultiversalGlobalStyles from '@/app/components/MultiversalGlobalStyles'; import '@/common/utils/ignoreNoisyWarningsHacks'; -import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; import customerContext from '@/modules/core/data/contexts/customerContext'; diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 8a4b401c0..7c66b3f62 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -1,7 +1,7 @@ import Loader from '@/components/animations/Loader'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; -import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; import customerContext from '@/modules/core/data/contexts/customerContext'; import datasetContext from '@/modules/core/data/contexts/datasetContext'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index d2bb054b2..82672caf2 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -6,7 +6,7 @@ import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { getCustomer, getSharedAirtableDataset, -} from '@/modules/core/airtable/getSharedAirtableDataset'; +} from '@/modules/core/airtable/getAirtableDataset'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; diff --git a/src/layouts/core/coreLayoutSSR.ts b/src/layouts/core/coreLayoutSSR.ts index 58483d44f..92f6ac094 100644 --- a/src/layouts/core/coreLayoutSSR.ts +++ b/src/layouts/core/coreLayoutSSR.ts @@ -4,7 +4,7 @@ import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; import { getCustomer, getSharedAirtableDataset, -} from '@/modules/core/airtable/getSharedAirtableDataset'; +} from '@/modules/core/airtable/getAirtableDataset'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 2a1504579..71f4faedd 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -4,9 +4,10 @@ import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { + getAirtableDataset, getCustomer, getSharedAirtableDataset, -} from '@/modules/core/airtable/getSharedAirtableDataset'; +} from '@/modules/core/airtable/getAirtableDataset'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -99,7 +100,7 @@ export const getDemoStaticProps: GetStaticProps => { - const airtableSchema: AirtableSchema = getAirtableSchema(); +export const fetchRawAirtableDataset = async (airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); // Resolves the languages we want to fetch the fields for (all supported languages configured in the app) // We want to fetch all fields (for all language variations) during the initial dataset fetch diff --git a/src/modules/core/airtable/getSharedAirtableDataset.ts b/src/modules/core/airtable/getAirtableDataset.ts similarity index 60% rename from src/modules/core/airtable/getSharedAirtableDataset.ts rename to src/modules/core/airtable/getAirtableDataset.ts index 2c933b7ac..ffe438b7c 100644 --- a/src/modules/core/airtable/getSharedAirtableDataset.ts +++ b/src/modules/core/airtable/getAirtableDataset.ts @@ -13,7 +13,7 @@ import find from 'lodash.find'; import { AirtableSchema } from './types/AirtableSchema'; import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; -const fileLabel = 'modules/core/airtable/getSharedAirtableDataset'; +const fileLabel = 'modules/core/airtable/getAirtableDataset'; const logger = createLogger({ fileLabel, }); @@ -45,14 +45,32 @@ export const getSharedRawAirtableDataset = async (): Promise => { +export const getSharedAirtableDataset = async (preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await getSharedRawAirtableDataset(); - const airtableSchema: AirtableSchema = getAirtableSchema(props); + const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); }; -export default getSharedAirtableDataset; +export const getLiveAirtableDataset = async (preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); + // XXX importing fetchAirtableDataset crashes the app + // const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, preferredLocalesOrLanguages); + // const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); + + // return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); + return {}; +}; + +export const getAirtableDataset = async (isPreviewMode: boolean, preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + if (isPreviewMode) { + // When preview mode is enabled, we want to make real-time API requests to get up-to-date data + return await getLiveAirtableDataset(preferredLocalesOrLanguages, airtableSchemaProps); + } else { + // When preview mode is not enabled, we fallback to the app-wide shared/static data (stale) + return await getSharedAirtableDataset(preferredLocalesOrLanguages); + } +}; diff --git a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx index ed81cac3c..6d9f20cb7 100644 --- a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx +++ b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx @@ -11,7 +11,7 @@ import { getDemoStaticPaths, getDemoStaticProps, } from '@/layouts/demo/demoLayoutSSG'; -import { getCustomer } from '@/modules/core/airtable/getSharedAirtableDataset'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; From a7406f771db431507685493b8e7b4c4e63e3ffc1 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 18:29:17 +0200 Subject: [PATCH 14/25] Moving implementation to the page fixes the webpack issue --- src/layouts/demo/demoLayoutSSG.ts | 21 ++++++++++++++++++- .../core/airtable/getAirtableDataset.ts | 7 ++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 71f4faedd..087aafe4c 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -3,11 +3,18 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; +import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset'; import { getAirtableDataset, getCustomer, getSharedAirtableDataset, } from '@/modules/core/airtable/getAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; +import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -100,7 +107,19 @@ export const getDemoStaticProps: GetStaticProps => { - const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); - // XXX importing fetchAirtableDataset crashes the app + // const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); + // XXX Importing fetchAirtableDataset in the file causes a crash, while doing exactly the same from the Next.js page works fine (claiming "fs" module cannot be found) + // This is most likely related to the "next-plugin-preval" package, which messes up with the Webpack configuration // const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, preferredLocalesOrLanguages); // const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); - + // // return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); return {}; }; From 44ccdeddc35454b6c152361f428f21bea4a4f651 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 18:30:43 +0200 Subject: [PATCH 15/25] Copy code to core --- src/layouts/core/coreLayoutSSG.ts | 22 +++++++++++++++++++++- src/layouts/demo/demoLayoutSSG.ts | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index 82672caf2..d261f497f 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -3,10 +3,18 @@ import { StaticPath } from '@/app/types/StaticPath'; import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; +import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset'; import { + getAirtableDataset, getCustomer, getSharedAirtableDataset, } from '@/modules/core/airtable/getAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; +import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; @@ -99,7 +107,19 @@ export const getCoreStaticProps: GetStaticProps = async */ export const getDemoStaticProps: GetStaticProps = async (props: StaticPropsInput): Promise> => { const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF; - const preview: boolean = props?.preview || false; // TODO do something different if preview mode enabled + const preview: boolean = props?.preview || false; const previewData: PreviewData = props?.previewData || null; const hasLocaleFromUrl = !!props?.params?.locale; const locale: string = hasLocaleFromUrl ? props?.params?.locale : DEFAULT_LOCALE; // If the locale isn't found (e.g: 404 page) From 245031a9f6dc84769a1c3463f13c9a2a6da7ddf7 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 18:33:17 +0200 Subject: [PATCH 16/25] Doc --- src/layouts/core/coreLayoutSSG.ts | 2 +- src/layouts/demo/demoLayoutSSG.ts | 3 +-- src/modules/core/airtable/getAirtableDataset.ts | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index d261f497f..adb6264a1 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -118,7 +118,7 @@ export const getCoreStaticProps: GetStaticProps => { // const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); // XXX Importing fetchAirtableDataset in the file causes a crash, while doing exactly the same from the Next.js page works fine (claiming "fs" module cannot be found) @@ -66,6 +72,15 @@ export const getLiveAirtableDataset = async (preferredLocalesOrLanguages: string return {}; }; +/** + * WIP not used because getLiveAirtableDataset isn't working + * + * Meant to make code more reusable and avoid bloating pages with too much logic + * + * @param isPreviewMode + * @param preferredLocalesOrLanguages + * @param airtableSchemaProps + */ export const getAirtableDataset = async (isPreviewMode: boolean, preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { if (isPreviewMode) { // When preview mode is enabled, we want to make real-time API requests to get up-to-date data From 678effe9f1ac7e7c85af88688c8a38d0d2e2999f Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 19:06:29 +0200 Subject: [PATCH 17/25] Auto-detect and redirect to the right localized page based on the customer's available languages --- .../core/airtable/fetchRawAirtableDataset.ts | 7 ++--- .../core/i18n/middlewares/localeMiddleware.ts | 27 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.ts b/src/modules/core/airtable/fetchRawAirtableDataset.ts index 4cfab5bd1..6cc9654f2 100644 --- a/src/modules/core/airtable/fetchRawAirtableDataset.ts +++ b/src/modules/core/airtable/fetchRawAirtableDataset.ts @@ -16,9 +16,10 @@ const logger = createLogger({ }); /** - * XXX used by preval - * XXX Must be a single export file otherwise it can cause issues - * XXX Must not be called by other scripts + * Fetches the airtable dataset. + * Invoked by fetchRawAirtableDataset.preval.ts file at build time (during Webpack bundling). + * + * XXX Must be a single export file otherwise it can cause issues - See https://github.com/ricokahler/next-plugin-preval/issues/19#issuecomment-848799473 */ export const fetchRawAirtableDataset = async (airtableSchemaProps?: GetAirtableSchemaProps): Promise => { const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); diff --git a/src/modules/core/i18n/middlewares/localeMiddleware.ts b/src/modules/core/i18n/middlewares/localeMiddleware.ts index 939ca2040..65a06d9a4 100644 --- a/src/modules/core/i18n/middlewares/localeMiddleware.ts +++ b/src/modules/core/i18n/middlewares/localeMiddleware.ts @@ -1,6 +1,16 @@ +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import redirect from '@/utils/redirect'; +import includes from 'lodash.includes'; import size from 'lodash.size'; +import uniq from 'lodash.uniq'; import { NextApiRequest, NextApiResponse, @@ -30,21 +40,16 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse logger.debug('Detecting browser locale...'); const detections: string[] = acceptLanguageHeaderLookup(req) || []; let localeFound; // Will contain the most preferred browser locale (e.g: fr-FR, fr, en-US, en, etc.) + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = getCustomer(dataset); if (detections && !!size(detections)) { detections.forEach((language) => { if (localeFound || typeof language !== 'string') return; - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only redirect the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const lookedUpLocale = supportedLocales.find( - (allowedLocale) => allowedLocale.name === language, - ); - - if (lookedUpLocale) { - localeFound = lookedUpLocale.lang; + if (includes(customer?.availableLanguages, language)) { + localeFound = language; } }); @@ -54,7 +59,7 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse } if (!localeFound) { - localeFound = DEFAULT_LOCALE; + localeFound = customer?.availableLanguages?.[0] || DEFAULT_LOCALE; } logger.debug(`Locale applied: "${localeFound}", for url "${req.url}"`); From cdac3503d5f076dc894168298aba8344e2f12547 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 19:16:34 +0200 Subject: [PATCH 18/25] Misc --- src/layouts/core/coreLayoutSSG.ts | 1 - src/layouts/demo/demoLayoutSSG.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index adb6264a1..378fe15e8 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -7,7 +7,6 @@ import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; import fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset'; import { - getAirtableDataset, getCustomer, getSharedAirtableDataset, } from '@/modules/core/airtable/getAirtableDataset'; diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 8a85a135d..175d693f1 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -82,13 +82,13 @@ export const getDemoStaticPaths: GetStaticPaths = async /** * Only executed on the server side at build time. - * Computes all static props that should be available for all SSG pages + * Computes all static props that should be available for all SSG pages. * * Note that when a page uses "getStaticProps", then "_app:getInitialProps" is executed (if defined) but not actually used by the page, * only the results from getStaticProps are actually injected into the page (as "SSGPageProps"). * - * Meant to avoid code duplication - * Can be overridden for per-page customisation (e.g: deepmerge) + * Meant to avoid code duplication. + * Can be overridden for per-page customisation (e.g: deepmerge). * * XXX Demo component, not meant to be modified. It's a copy of the coreLayoutSSG implementation, so the demo keep working even if you change the base implementation. From 7f59a7061e23114407e75360dde55d78d977ab39 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 19:36:11 +0200 Subject: [PATCH 19/25] Remove logs --- .storybook/preview.js | 2 -- src/pages/[locale]/demo/privacy.tsx | 2 -- src/pages/[locale]/demo/terms.tsx | 1 - 3 files changed, 5 deletions(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index 8d286c905..3af2903b3 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -181,8 +181,6 @@ export const decorators = [ const customer = getCustomer(dataset); const customerTheme = initCustomerTheme(customer); - // console.log('customer', customer) - // console.log('customerTheme', customerTheme) const customerRef = 'storybook'; // Fake customer ref const amplitudeApiKey = ''; // Use invalid amplitude tracking key to force disable all amplitude analytics const userConsent = { diff --git a/src/pages/[locale]/demo/privacy.tsx b/src/pages/[locale]/demo/privacy.tsx index 048c10d53..febc7f4cd 100644 --- a/src/pages/[locale]/demo/privacy.tsx +++ b/src/pages/[locale]/demo/privacy.tsx @@ -63,8 +63,6 @@ const PrivacyPage: NextPage = (props): JSX.Element => { serviceLabel, } = customer || {}; - console.log('customer', customer) - // Replace dynamic values (like "{customerLabel}") by their actual value const privacy = replaceAllOccurrences(privacyDescription, { serviceLabel: `**${serviceLabel}**`, diff --git a/src/pages/[locale]/demo/terms.tsx b/src/pages/[locale]/demo/terms.tsx index 3cff34f86..290aa72a2 100644 --- a/src/pages/[locale]/demo/terms.tsx +++ b/src/pages/[locale]/demo/terms.tsx @@ -58,7 +58,6 @@ type Props = {} & SSGPageProps>; */ const TermsPage: NextPage = (props): JSX.Element => { const customer: Customer = useCustomer(); - console.log('customer', customer); const { termsDescription, serviceLabel, From 8ca709310cf5896625a3ed94838b0d0dafc6992a Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 21:04:11 +0200 Subject: [PATCH 20/25] Simplifies debugging of pages crashing during build --- src/app/components/MultiversalAppBootstrap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 7c66b3f62..511886b90 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -275,12 +275,12 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele if (process.env.NEXT_PUBLIC_CUSTOMER_REF !== customer?.ref) { error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? `Fatal error - An error happened, the page cannot be displayed. (customer doesn't match)` : - `Fatal error when bootstrapping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`, + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}").`, ); } else { error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? `Fatal error - An error happened, the page cannot be displayed.` : - `Fatal error when bootstrapping the app. It might happen when lang/locale/translations couldn't be resolved.`, + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). It might happen when lang/locale/translations couldn't be resolved.`, ); } } else { From eb557b3afcd05914dce15096e42683f023a361d7 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 21:07:49 +0200 Subject: [PATCH 21/25] Rename PagePublicTemplateSSG > ExamplePublicPage --- src/layouts/public/pagePublicTemplateSSG.tsx | 14 ++++++------- src/pages/[locale]/public/index.tsx | 22 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/layouts/public/pagePublicTemplateSSG.tsx b/src/layouts/public/pagePublicTemplateSSG.tsx index 88155d0d8..a05a5ae07 100644 --- a/src/layouts/public/pagePublicTemplateSSG.tsx +++ b/src/layouts/public/pagePublicTemplateSSG.tsx @@ -23,15 +23,15 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- }); /** - * Only executed on the server side at build time - * Necessary when a page has dynamic routes and uses "getStaticProps" + * Only executed on the server side at build time. + * Necessary when a page has dynamic routes and uses "getStaticProps". */ export const getStaticPaths: GetStaticPaths = getPublicLayoutStaticPaths; /** * Only executed on the server side at build time. * - * @return Props (as "SSGPageProps") that will be passed to the Page component, as props + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props. * * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation @@ -39,12 +39,12 @@ export const getStaticPaths: GetStaticPaths = getPublicL export const getStaticProps: GetStaticProps = getPublicLayoutStaticProps; /** - * SSG pages are first rendered by the server (during static bundling) - * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps) - * Because this last case is the most common (server bundle only happens during development stage), we consider it a default + * SSG pages are first rendered by the server (during static bundling). + * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps). + * Because this last case is the most common (server bundle only happens during development stage), we consider it a d.efault. * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional * - * Beware props in OnlyBrowserPageProps are not available on the server + * Beware props in OnlyBrowserPageProps are not available on the server. */ type Props = {} & SSGPageProps>; diff --git a/src/pages/[locale]/public/index.tsx b/src/pages/[locale]/public/index.tsx index 09dc1638c..9af7c2eec 100644 --- a/src/pages/[locale]/public/index.tsx +++ b/src/pages/[locale]/public/index.tsx @@ -24,15 +24,15 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- }); /** - * Only executed on the server side at build time - * Necessary when a page has dynamic routes and uses "getStaticProps" + * Only executed on the server side at build time. + * Necessary when a page has dynamic routes and uses "getStaticProps". */ export const getStaticPaths: GetStaticPaths = getPublicLayoutStaticPaths; /** * Only executed on the server side at build time. * - * @return Props (as "SSGPageProps") that will be passed to the Page component, as props + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props. * * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation @@ -40,19 +40,19 @@ export const getStaticPaths: GetStaticPaths = getPublicL export const getStaticProps: GetStaticProps = getPublicLayoutStaticProps; /** - * SSG pages are first rendered by the server (during static bundling) - * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps) - * Because this last case is the most common (server bundle only happens during development stage), we consider it a default - * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional + * SSG pages are first rendered by the server (during static bundling). + * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps). + * Because this last case is the most common (server bundle only happens during development stage), we consider it a default. + * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional. * - * Beware props in OnlyBrowserPageProps are not available on the server + * Beware props in OnlyBrowserPageProps are not available on the server. */ type Props = {} & SSGPageProps>; /** - * Public template for SSG pages + * Example of a public page. */ -const PagePublicTemplateSSG: NextPage = (props): JSX.Element => { +const ExamplePublicPage: NextPage = (props): JSX.Element => { const customer: Customer = useCustomer(); return ( @@ -83,4 +83,4 @@ const PagePublicTemplateSSG: NextPage = (props): JSX.Element => { ); }; -export default PagePublicTemplateSSG; +export default ExamplePublicPage; From 51711855bcf6105575c36d1f3bc61a43813c12fc Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 21:17:08 +0200 Subject: [PATCH 22/25] Fix error with Public layout (customer wasn't found due to lack of __typename) --- src/layouts/public/publicLayoutSSG.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/layouts/public/publicLayoutSSG.ts b/src/layouts/public/publicLayoutSSG.ts index e50df44da..dec208e25 100644 --- a/src/layouts/public/publicLayoutSSG.ts +++ b/src/layouts/public/publicLayoutSSG.ts @@ -80,7 +80,8 @@ export const getPublicLayoutStaticProps: GetStaticProps Date: Wed, 26 May 2021 21:18:21 +0200 Subject: [PATCH 23/25] Improved logger, shows less logs during build, shows more logs in production (on the server side) --- src/modules/core/logging/logger.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/modules/core/logging/logger.ts b/src/modules/core/logging/logger.ts index 551a5b6e6..c94602f94 100644 --- a/src/modules/core/logging/logger.ts +++ b/src/modules/core/logging/logger.ts @@ -1,9 +1,12 @@ import createSimpleLogger, { SimpleLogger } from '@unly/simple-logger'; +import { isBrowser } from '@unly/utils'; /** * Custom logger proxy. * - * Customize the @unly/simple-logger library by providing app-wide default behavior. + * Customizes the @unly/simple-logger library by providing app-wide default behavior. + * + * Optimized to avoid logging in the browser in production, to reduce the noise and to avoid leaking debug information publicly. * * @param fileLabel */ @@ -16,7 +19,13 @@ export const createLogger = ({ fileLabel }: { fileLabel: string }): SimpleLogger return createSimpleLogger({ prefix: fileLabel, shouldPrint: (mode) => { - return process.env.NEXT_PUBLIC_APP_STAGE !== 'production'; + // When bundling with Webpack, only print errors/warnings to avoid printing too much noise on Vercel + if (process.env.IS_SERVER_INITIAL_BUILD === '1') { + return mode === 'error' || mode === 'warn' || mode === 'debug'; + } + + // Otherwise, only hide browser errors in production + return !(process.env.NEXT_PUBLIC_APP_STAGE === 'production' && isBrowser()); }, }); }; From e9313944e97c5715776296effdcc23f44d940fa3 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 21:23:41 +0200 Subject: [PATCH 24/25] Fix logger in dev mode --- src/modules/core/logging/logger.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/core/logging/logger.ts b/src/modules/core/logging/logger.ts index c94602f94..612391972 100644 --- a/src/modules/core/logging/logger.ts +++ b/src/modules/core/logging/logger.ts @@ -20,7 +20,8 @@ export const createLogger = ({ fileLabel }: { fileLabel: string }): SimpleLogger prefix: fileLabel, shouldPrint: (mode) => { // When bundling with Webpack, only print errors/warnings to avoid printing too much noise on Vercel - if (process.env.IS_SERVER_INITIAL_BUILD === '1') { + // IS_SERVER_INITIAL_BUILD is always "1" during development, and is being ignored + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'development' && process.env.IS_SERVER_INITIAL_BUILD === '1') { return mode === 'error' || mode === 'warn' || mode === 'debug'; } From 390f483080ab5321d902b41243400321d7a3edc4 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Wed, 26 May 2021 21:26:21 +0200 Subject: [PATCH 25/25] Ignore noise on Vercel --- src/app/components/MultiversalAppBootstrap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 511886b90..bcb96b4b0 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -200,7 +200,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } } - if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && !process.env.IS_SERVER_INITIAL_BUILD) { // XXX It's too cumbersome to do proper typings when type changes // The "customer" was forwarded as a JSON-ish string (using Flatten) in order to avoid circular dependencies issues (SSG/SSR) // It now being converted back into an object to be actually usable on all pages