diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index 304a38b6f..160da323c 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -1,13 +1,9 @@ function readPackage(pkg) { const versions = { - react: '19.0.0-rc-7771d3a7-20240827', - 'react-dom': '19.0.0-rc-7771d3a7-20240827', - '@types/react': '18.3.3', - '@types/react-dom': '18.3.0', - 'react-server-dom-webpack': '19.0.0-rc-7771d3a7-20240827', + react: '19.0.0', + 'react-dom': '19.0.0', typescript: '5.4.5', graphql: '16.8.1', - waku: '0.21.1', sharp: '0.33.1', 'vite-imagetools': '6.2.9', }; diff --git a/apps/cms/src/index.ts b/apps/cms/src/index.ts index 1627056b5..41baed94a 100644 --- a/apps/cms/src/index.ts +++ b/apps/cms/src/index.ts @@ -1,10 +1,17 @@ import type { AnyOperationId, OperationVariables } from '@custom/schema'; +const cache: Record = {}; + export function createDrupalExecutor(host: string, frontendUrl: string) { return async function ( id: OperationId, variables?: OperationVariables, ) { + const key = `${id}:${JSON.stringify(variables)}`; + if (cache[key]) { + return cache[key]; + } + const url = new URL(`${host}/graphql`); const isMutation = id.includes('Mutation:'); const publicUrl = @@ -53,6 +60,9 @@ export function createDrupalExecutor(host: string, frontendUrl: string) { throw new Error('GraphQL error: ' + JSON.stringify(errors)); } } + if (!isMutation) { + cache[key] = data; + } return data; } catch (error) { console.error('Fetch error:', error); diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx index b1f3aacb0..b364d90dd 100644 --- a/apps/preview/src/App.tsx +++ b/apps/preview/src/App.tsx @@ -2,6 +2,7 @@ import { createDrupalExecutor } from '@custom/cms'; import { OperationExecutorsProvider } from '@custom/schema'; import { Frame } from '@custom/ui/routes/Frame'; import { Preview, usePreviewRefresh } from '@custom/ui/routes/Preview'; +import { Providers } from '@custom/ui/routes/Providers'; import { useEffect } from 'react'; import { retry } from 'rxjs'; import { webSocket } from 'rxjs/webSocket'; @@ -29,20 +30,22 @@ function App() { return sub.unsubscribe; }, [refresh]); return ( - - - - - + + + + + + + ); } diff --git a/apps/website/netlify/functions/strangler.ts b/apps/website/netlify/functions/strangler.ts index 7f020086b..faba53ac3 100644 --- a/apps/website/netlify/functions/strangler.ts +++ b/apps/website/netlify/functions/strangler.ts @@ -14,17 +14,17 @@ function encodeRSCUrl(inputUrl: string) { function decodeRSCUrl(inputUrl: string) { const url = new URL(inputUrl, 'http://localhost'); - url.pathname = url.pathname.replace(/^\/RSC/, '').replace(/\.txt$/, ''); + url.pathname = url.pathname.replace(/^\/RSC\/R/, '').replace(/\.txt$/, ''); return url; } -const notFoundRSC = fs.readFileSync('dist/public/RSC/404.txt').toString(); +const notFoundRSC = fs.readFileSync('dist/public/RSC/R/404.txt').toString(); export const handler = createStrangler( [ { url: drupalUrl, - applies: (url) => url.pathname.startsWith('/RSC/'), + applies: (url) => url.pathname.startsWith('/RSC/R/'), preprocess: (event) => { // Before handling, turn the RSC url into the corresponding Drupal url. event.rawUrl = decodeRSCUrl(event.rawUrl).toString(); diff --git a/apps/website/package.json b/apps/website/package.json index 765d157ec..12f1106e8 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -16,14 +16,14 @@ "@custom/ui": "workspace:*", "image-dimensions": "^2.3.0", "netlify-cli": "^17.29.0", - "react": "19.0.0-rc.0", - "react-dom": "19.0.0-rc.0", + "react": "19.0.0", + "react-dom": "19.0.0", "react-error-boundary": "^4.0.13", - "react-server-dom-webpack": "19.0.0-rc.0", + "react-server-dom-webpack": "19.0.0", "sharp": "^0.33.4", "start-server-and-test": "^2.0.3", "tsx": "^4.7.1", - "waku": "0.21.0-alpha.2" + "waku": "0.21.17" }, "devDependencies": { "@custom/eslint-config": "workspace:*", diff --git a/apps/website/src/bridge.tsx b/apps/website/src/bridge.tsx index b393e6c81..776533c62 100644 --- a/apps/website/src/bridge.tsx +++ b/apps/website/src/bridge.tsx @@ -6,5 +6,11 @@ import { Link as WakuLink, useRouter_UNSTABLE } from 'waku'; export { LocationProvider } from '@amazeelabs/bridge-waku'; -export const useLocation = createUseLocationHook(useRouter_UNSTABLE); +export const useLocation = () => { + const [loc, navigate] = createUseLocationHook(useRouter_UNSTABLE)(); + if (Array.isArray(loc.pathname)) { + loc.pathname = `/${loc.pathname.join('/')}`; + } + return [loc, navigate]; +}; export const Link = createLinkComponent(WakuLink); diff --git a/apps/website/src/build/redirects.ts b/apps/website/src/build/redirects.ts index d7ee86a5c..73a17d1b1 100644 --- a/apps/website/src/build/redirects.ts +++ b/apps/website/src/build/redirects.ts @@ -140,7 +140,7 @@ function writeRedirectsNetlify(config: RedirectsOutputConfig) { redirectEntry += '!'; } if ([301, 302].includes(value.statusCode)) { - redirectEntry += `\n/RSC${value.source}.txt /RSC${value.destination}.txt ${value.statusCode}`; + redirectEntry += `\n/RSC/R${value.source}.txt /RSC/R${value.destination}.txt ${value.statusCode}`; } fs.appendFileSync(`${config.outputFile}`, redirectEntry); diff --git a/apps/website/src/entries.tsx b/apps/website/src/entries.tsx index 6a59a911d..318cbf359 100644 --- a/apps/website/src/entries.tsx +++ b/apps/website/src/entries.tsx @@ -16,10 +16,10 @@ import { HomePage } from '@custom/ui/routes/HomePage'; import { Inquiry } from '@custom/ui/routes/Inquiry'; import { NotFoundPage } from '@custom/ui/routes/NotFoundPage'; import { Page } from '@custom/ui/routes/Page'; -import React from 'react'; +import { Providers } from '@custom/ui/routes/Providers'; +import React, { PropsWithChildren } from 'react'; import { createPages } from 'waku'; -import { BrokenLinkHandler } from './broken-link-handler.js'; import { ClientExecutors } from './executors-client.js'; import { ServerExecutors, serverExecutors } from './executors-server.js'; import { query } from './query.js'; @@ -36,58 +36,44 @@ async function queryAll( ); } -export default createPages(async ({ createPage, createLayout }) => { - createLayout({ - render: 'static', - path: '/', - component: ({ children, path }) => ( - - - - - src.replace(frontendUrl, drupalUrl)}> - {children} - - - - - - ), - }); - - Object.values(Locale).forEach((lang) => { - createPage({ - render: 'static', - path: `/${lang}`, - component: () => , - }); - - createPage({ - render: 'static', - path: `/${lang}/content-hub`, - component: () => , - }); - - createPage({ - render: 'static', - path: `/${lang}/inquiry`, - component: () => , - }); - }); +function Layout({ children }: PropsWithChildren) { + return ( + + + src.replace(frontendUrl, drupalUrl)}> + {children} + + + + ); +} - createPage({ - render: 'static', - path: '/404', - component: () => , - }); +function withPageWrapper(Component: React.FC) { + return function PageWrapper({ path }: { path: string }) { + return ( + + + + src.replace(frontendUrl, drupalUrl)}> + + + + + + + + ); + }; +} +export default createPages(async ({ createPage, createLayout }) => { // Initialise a map for the homepages, since we want to exclude them from // creating a page for their internal path. const homePages = await query(HomePageQuery, {}); @@ -119,10 +105,46 @@ export default createPages(async ({ createPage, createLayout }) => { }); } - createPage({ - render: 'static', - path: '/[...path]', - staticPaths: [...pagePaths].map((path) => path.substring(1).split('/')), - component: Page, - }); + return [ + createLayout({ + render: 'static', + path: '/', + component: Layout, + }), + + ...Object.values(Locale) + .map((lang) => [ + createPage({ + render: 'static', + path: `/${lang}`, + component: withPageWrapper(HomePage), + }), + + createPage({ + render: 'static', + path: `/${lang}/content-hub`, + component: withPageWrapper(() => ), + }), + + createPage({ + render: 'static', + path: `/${lang}/inquiry`, + component: withPageWrapper(Inquiry), + }), + ]) + .reduce((acc, val) => [...acc, ...val]), + + createPage({ + render: 'static', + path: '/404', + component: withPageWrapper(NotFoundPage), + }), + + createPage({ + render: 'static', + path: '/[...path]', + staticPaths: [...pagePaths].map((path) => path.substring(1).split('/')), + component: withPageWrapper(Page), + }), + ]; }); diff --git a/packages/schema/package.json b/packages/schema/package.json index 504521419..f3a50c96f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -59,7 +59,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@amazeelabs/executors": "^3.1.0", + "@amazeelabs/executors": "^3.1.14", "@amazeelabs/gatsby-silverback-cloudinary": "^1.2.7", "@amazeelabs/gatsby-source-silverback": "^1.14.0", "@amazeelabs/scalars": "^1.6.13", diff --git a/packages/ui/src/components/Routes/Frame.tsx b/packages/ui/src/components/Routes/Frame.tsx index e9ce90e40..6826b1726 100644 --- a/packages/ui/src/components/Routes/Frame.tsx +++ b/packages/ui/src/components/Routes/Frame.tsx @@ -1,79 +1,17 @@ -import { ImageSettings } from '@amazeelabs/image'; -import { IntlProvider } from '@amazeelabs/react-intl'; -import { FrameQuery, Locale, Operation } from '@custom/schema'; -import React, { ComponentProps, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; -import translationSources from '../../../build/translatables.json'; -import { useLocale } from '../../utils/locale'; import { TranslationsProvider } from '../../utils/translations'; import { PageTransitionWrapper } from '../Molecules/PageTransition'; import { Footer } from '../Organisms/Footer'; import { Header } from '../Organisms/Header'; -function filterByLocale(locale: Locale) { - return (str: Exclude[number]) => - str.language === locale; -} - -function translationsMap( - translatables: Required['stringTranslations'], -) { - return Object.fromEntries( - translatables - .filter((tr) => tr.translation) - .map((tr) => [tr.source, tr.translation]), - ); -} - -export function Frame({ - children, - ...imageSettings -}: PropsWithChildren>) { - const locale = useLocale(); +export function Frame({ children }: PropsWithChildren) { return ( - - {(result) => { - if (result.state === 'success') { - const rawTranslations = result.data - .map((res) => res.stringTranslations || []) - .reduce((acc, val) => [...acc, ...val], []); - const translations = { - ...translationsMap( - rawTranslations?.filter(filterByLocale('en')) || [], - ), - ...translationsMap( - rawTranslations?.filter(filterByLocale(locale)) || [], - ), - }; - const messages = Object.fromEntries( - Object.keys(translationSources).map((key) => [ - key, - translations[ - translationSources[key as keyof typeof translationSources] - .defaultMessage - ] || - translationSources[key as keyof typeof translationSources] - .defaultMessage, - ]), - ); - return ( - - - - -
- {children} -