Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preload app-wide shared data #334

Merged
merged 26 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
558062e
Improve 404 page detection (covers more use-cases) + Don't serve page…
Vadorequest May 26, 2021
c4cf97b
Copy changes to core
Vadorequest May 26, 2021
e7e6ce7
Apply same changes to SSR layouts
Vadorequest May 26, 2021
dcd7926
Add TODO
Vadorequest May 26, 2021
526fd78
Use next-plugin-preval to preload the app-wide shared data at build t…
Vadorequest May 26, 2021
f72d3c6
Doc + refactoring
Vadorequest May 26, 2021
a89bba6
Simplifies fetching dataset/customer from pages (getStaticProps/getSe…
Vadorequest May 26, 2021
090831a
Misc logs
Vadorequest May 26, 2021
bb35c36
Misc reformat
Vadorequest May 26, 2021
0052e17
Doc
Vadorequest May 26, 2021
8453255
Merge branch 'v2-mst-aptd-at-lcz-sty' into prefetch-shared-data
Vadorequest May 26, 2021
0156bde
Refactor + only generate pages for languages that have been enabled
Vadorequest May 26, 2021
e24b0e4
Fix bad link (i18n)
Vadorequest May 26, 2021
4ed5c4c
Attempt to handle preview mode, commented out because failing
Vadorequest May 26, 2021
a7406f7
Moving implementation to the page fixes the webpack issue
Vadorequest May 26, 2021
44ccded
Copy code to core
Vadorequest May 26, 2021
245031a
Doc
Vadorequest May 26, 2021
678effe
Auto-detect and redirect to the right localized page based on the cus…
Vadorequest May 26, 2021
cdac350
Misc
Vadorequest May 26, 2021
7f59a70
Remove logs
Vadorequest May 26, 2021
8ca7093
Simplifies debugging of pages crashing during build
Vadorequest May 26, 2021
eb557b3
Rename PagePublicTemplateSSG > ExamplePublicPage
Vadorequest May 26, 2021
5171185
Fix error with Public layout (customer wasn't found due to lack of __…
Vadorequest May 26, 2021
8ea139f
Improved logger, shows less logs during build, shows more logs in pro…
Vadorequest May 26, 2021
e931394
Fix logger in dev mode
Vadorequest May 26, 2021
390f483
Ignore noise on Vercel
Vadorequest May 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 19 additions & 21 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -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/getAirtableDataset';
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)
Expand Down Expand Up @@ -179,10 +179,8 @@ 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)
const customerRef = 'storybook'; // Fake customer ref
const amplitudeApiKey = ''; // Use invalid amplitude tracking key to force disable all amplitude analytics
const userConsent = {
Expand Down Expand Up @@ -290,6 +288,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.`);
}

6 changes: 4 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -319,4 +321,4 @@ module.exports = withBundleAnalyzer(withSourceMaps({
// },

poweredByHeader: false, // See https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by
}));
})));
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions src/app/components/MultiversalAppBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -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/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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -200,7 +200,7 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (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
Expand All @@ -210,7 +210,7 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
}

const dataset: SanitizedAirtableDataset = deserializeSafe(serializedDataset);
const customer: AirtableRecord<Customer> = find(dataset, { __typename: 'Customer' }) as AirtableRecord<Customer>;
const customer: AirtableRecord<Customer> = getCustomer(dataset);
let availableLanguages: string[] = customer?.availableLanguages;

if (isEmpty(availableLanguages)) {
Expand Down Expand Up @@ -275,12 +275,12 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (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 {
Expand Down
44 changes: 25 additions & 19 deletions src/layouts/core/coreLayoutSSG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ 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 fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset';
import {
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';
Expand All @@ -24,9 +30,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 find from 'lodash.find';
import includes from 'lodash.includes';
import map from 'lodash.map';
import uniq from 'lodash.uniq';
import {
GetStaticPaths,
GetStaticPathsContext,
Expand Down Expand Up @@ -56,14 +61,15 @@ const logger = createLogger({
* @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation
*/
export const getCoreStaticPaths: GetStaticPaths<CommonServerSideParams> = async (context: GetStaticPathsContext): Promise<StaticPathsOutput> => {
// 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<string>(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang));
const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages);
const customer: AirtableRecord<Customer> = 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,
},
};
});
Expand Down Expand Up @@ -100,18 +106,18 @@ export const getCoreStaticProps: GetStaticProps<SSGPageProps, CommonServerSidePa
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 customer: AirtableRecord<Customer> = find(dataset, { __typename: 'Customer' }) as AirtableRecord<Customer>;
let dataset: SanitizedAirtableDataset;

// 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.`);
if (preview) {
// When preview mode is enabled, we want to make real-time API requests to get up-to-date data
const airtableSchema: AirtableSchema = getAirtableSchema();
const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, bestCountryCodes);
const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, bestCountryCodes);

return {
notFound: true,
};
dataset = consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized);
} else {
// When preview mode is not enabled, we fallback to the app-wide shared/static data (stale)
dataset = await getSharedAirtableDataset(bestCountryCodes);
}

return {
Expand Down
16 changes: 6 additions & 10 deletions src/layouts/core/coreLayoutSSR.ts
Original file line number Diff line number Diff line change
@@ -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/getAirtableDataset';
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';
Expand All @@ -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,
Expand Down Expand Up @@ -104,10 +102,8 @@ export const getCoreServerSideProps: GetServerSideProps<GetCoreServerSidePropsRe
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 customer: AirtableRecord<Customer> = find(dataset, { __typename: 'Customer' }) as AirtableRecord<Customer>;
const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes);
const customer: AirtableRecord<Customer> = 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)) {
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/demo/components/IntroductionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const IntroductionSection: React.FunctionComponent<Props> = (props): JSX.Element
Nav/Footer component are localised, as well as dynamic content and i18n examples.<br />
<br />
You can switch locale from the footer or by clicking on{' '}
<I18nLink href={`/`} locale={'fr-FR'}>fr-FR</I18nLink> or <I18nLink href={`/`} locale={'en-US'}>en-US</I18nLink>.
<I18nLink href={`/`} locale={'fr'}>fr</I18nLink> or <I18nLink href={`/`} locale={'en'}>en</I18nLink> or <I18nLink href={`/`} locale={'en-US'}>en-US</I18nLink>.
</Alert>

</Jumbotron>
Expand Down
Loading