prerender

engine-in-web-worker
Jérémy Rialland 2023-09-05 16:48:08 +02:00
parent d33d7db5ed
commit e8fa858eb6
15 changed files with 347 additions and 219 deletions

View File

@ -6,7 +6,7 @@ import { render } from '../dist/ssr/entry-server.js'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const cache = {}
const cache: { [k: string]: string } = {}
const htmlBodyStart = '<!--app-html:start-->'
const htmlBodyEnd = '<!--app-html:end-->'
@ -16,15 +16,35 @@ const headTagsEnd = '<!--app-helmet-tags:end-->'
const regexHTML = new RegExp(htmlBodyStart + '[\\s\\S]+' + htmlBodyEnd, 'm')
const regexHelmet = new RegExp(headTagsStart + '[\\s\\S]+' + headTagsEnd, 'm')
export default async ({ site, url, lang }) => {
// TODO: Add CI test to enforce meta tags on SSR pages
const { html, styleTags, helmet } = render(url, lang)
interface Params {
site: string
url: string
lang: string
}
const template =
// const vite = await createViteServer({
// server: {},
// appType: 'mpa',
// })
export default async ({ site, url, lang }: Params) => {
const fileTemplate =
cache[site] ??
readFileSync(path.join(dirname, `../dist/${site}.html`), 'utf-8')
cache[site] = template
cache[site] ??= fileTemplate
// const template = await vite.transformIndexHtml(url, fileTemplate)
const template = fileTemplate
// const { render } = await vite.ssrLoadModule(
// './source/entries/entry-server.tsx'
// )
// TODO: Add CI test to enforce meta tags on SSR pages
const { html, styleTags, helmet } = await render(url, lang)
console.log({ html, styleTags, helmet })
const page = template
.replace(regexHTML, html)

View File

@ -6,8 +6,12 @@ import Tinypool from 'tinypool'
import { absoluteSitePaths } from '../source/sitePaths.js'
const filename = new URL('./prerender-worker.js', import.meta.url).href
const pool = new Tinypool({ filename })
const filename = new URL('./prerender-worker.ts', import.meta.url).href
const pool = new Tinypool({
filename,
execArgv: ['--loader', 'ts-node/esm'],
idleTimeout: 2000,
})
const sitePathFr = absoluteSitePaths.fr
const sitePathEn = absoluteSitePaths.en
@ -17,33 +21,34 @@ export const pagesToPrerender: {
infrance: string[]
} = {
'mon-entreprise': [
'/iframes/pamc',
'/iframes/simulateur-embauche',
'/iframes/simulateur-independant',
sitePathFr.assistants['choix-du-statut'].index,
'/documentation/artisteauteur/cotisations/CSGCRDS/abattement',
// '/iframes/pamc',
// '/iframes/simulateur-embauche',
// '/iframes/simulateur-independant',
// sitePathFr.assistants['choix-du-statut'].index,
sitePathFr.index,
sitePathFr.simulateursEtAssistants,
sitePathFr.simulateurs.index,
sitePathFr.simulateurs.comparaison,
sitePathFr.simulateurs.dividendes,
sitePathFr.simulateurs.eurl,
sitePathFr.simulateurs.indépendant,
sitePathFr.simulateurs.is,
sitePathFr.simulateurs.salarié,
sitePathFr.simulateurs.sasu,
sitePathFr.simulateurs['artiste-auteur'],
// sitePathFr.simulateursEtAssistants,
// sitePathFr.simulateurs.index,
// sitePathFr.simulateurs.comparaison,
// sitePathFr.simulateurs.dividendes,
// sitePathFr.simulateurs.eurl,
// sitePathFr.simulateurs.indépendant,
// sitePathFr.simulateurs.is,
// sitePathFr.simulateurs.salarié,
// sitePathFr.simulateurs.sasu,
// sitePathFr.simulateurs['artiste-auteur'],
sitePathFr.simulateurs['auto-entrepreneur'],
sitePathFr.simulateurs['chômage-partiel'],
sitePathFr.simulateurs['coût-création-entreprise'],
sitePathFr.simulateurs['entreprise-individuelle'],
sitePathFr.simulateurs['profession-libérale'].avocat,
sitePathFr.simulateurs['profession-libérale']['chirurgien-dentiste'],
sitePathFr.simulateurs['profession-libérale'].index,
// sitePathFr.simulateurs['chômage-partiel'],
// sitePathFr.simulateurs['coût-création-entreprise'],
// sitePathFr.simulateurs['entreprise-individuelle'],
// sitePathFr.simulateurs['profession-libérale'].avocat,
// sitePathFr.simulateurs['profession-libérale']['chirurgien-dentiste'],
// sitePathFr.simulateurs['profession-libérale'].index,
].map((val) => encodeURI(val)),
infrance: [
sitePathEn.index,
sitePathEn.simulateurs.salarié,
'/iframes/simulateur-embauche',
// sitePathEn.simulateurs.salarié,
// '/iframes/simulateur-embauche',
].map((val) => encodeURI(val)),
}
@ -51,15 +56,17 @@ const dev = argv.findIndex((val) => val === '--dev') > -1
const redirects = await Promise.all(
Object.entries(pagesToPrerender).flatMap(([site, urls]) =>
urls.map((url) =>
pool
.run({
site,
url,
lang: site === 'mon-entreprise' ? 'fr' : 'en',
})
.then((path: string) => {
return `
urls.map(async (url) => {
const path = await (pool.run({
site,
url,
lang: site === 'mon-entreprise' ? 'fr' : 'en',
}) as Promise<string>)
// eslint-disable-next-line no-console
console.log(`preredering ${url} done, adding redirect`)
return `
[[redirects]]
from = ":SITE_${site === 'mon-entreprise' ? 'FR' : 'EN'}${
dev ? decodeURI(url) : url
@ -67,8 +74,7 @@ const redirects = await Promise.all(
to = "/${path}"
status = 200
${dev ? ' force = true\n' : ''}`
})
)
})
)
)

View File

@ -1,6 +1,22 @@
import {
SuspensePromise,
useAsyncGetRule,
useAsyncParsedRules,
useAsyncShallowCopy,
useLazyPromise,
usePromise,
useWorkerEngine,
WorkerEngine,
} from '@publicodes/worker-react'
import { ErrorBoundary } from '@sentry/react'
import { FallbackRender } from '@sentry/react/types/errorboundary'
import { ComponentProps, StrictMode, useEffect, useState } from 'react'
import React, {
ComponentProps,
StrictMode,
Suspense,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { Route, Routes } from 'react-router-dom'
import { css, styled } from 'styled-components'
@ -11,13 +27,14 @@ import { Container } from '@/design-system/layout'
import { useAxeCoreAnalysis } from '@/hooks/useAxeCoreAnalysis'
import { useGetFullURL } from '@/hooks/useGetFullURL'
import { useIsEmbedded } from '@/hooks/useIsEmbedded'
import { useLazyPromise, usePromise } from '@/hooks/usePromise'
import { useSaveAndRestoreScrollPosition } from '@/hooks/useSaveAndRestoreScrollPosition'
import Landing from '@/pages/_landing/Landing'
import Page404 from '@/pages/404'
import Accessibilité from '@/pages/Accessibilité'
import Budget from '@/pages/budget/index'
import IntegrationTest from '@/pages/dev/IntegrationTest'
import Documentation from '@/pages/Documentation'
import Iframes from '@/pages/iframes'
import Integration from '@/pages/integration/index'
import Nouveautés from '@/pages/nouveautés/index'
import Offline from '@/pages/Offline'
@ -26,33 +43,26 @@ import Simulateurs from '@/pages/simulateurs'
import SimulateursEtAssistants from '@/pages/simulateurs-et-assistants'
import Stats from '@/pages/statistiques/LazyStats'
import { useSitePaths } from '@/sitePaths'
import {
useAsyncGetRule,
useAsyncParsedRules,
useShallowCopy,
useWorkerEngine,
} from '@/worker/workerEngineClientReact'
import Provider, { ProviderProps } from './Provider'
import Redirections from './Redirections'
type RootProps = {
basename: ProviderProps['basename']
// rulesPreTransform?: (rules: Rules) => Rules
}
const TestWorkerEngine = () => {
const [refresh, setRefresh] = useState(0)
const workerEngine = useWorkerEngine()
// const workerEngineCtx = useWorkerEngineContext()
const [, trigger] = useLazyPromise(
async () => workerEngine?.asyncSetSituation({ SMIC: '1000€/mois' }),
async () => workerEngine.asyncSetSituation({ SMIC: '1000€/mois' }),
[workerEngine],
{ defaultValue: 'loading...' }
)
const date = useAsyncGetRule('date', { defaultValue: 'loading...' })
const SMIC = useAsyncGetRule('SMIC', { defaultValue: 'loading...' })
const parsedRules = useAsyncParsedRules()
@ -65,12 +75,10 @@ const TestWorkerEngine = () => {
const [resultLazySmic, triggerLazySmic] = useLazyPromise(
() => workerEngine.asyncEvaluate('SMIC'),
[workerEngine],
'wait 2sec...'
'wait 3sec...'
)
useEffect(() => {
console.log('??? useEffect')
void (async () => {
await workerEngine.isWorkerReady
setTimeout(() => {
@ -79,65 +87,72 @@ const TestWorkerEngine = () => {
})()
}, [triggerLazySmic, workerEngine.isWorkerReady])
const workerEngineCopy = useShallowCopy(workerEngine)
// // const workerEngineCopy = workerEngine
console.log('=========>', workerEngine, workerEngineCopy)
// const workerEngineCopy = useAsyncShallowCopy(workerEngine)
// // // const workerEngineCopy = workerEngine
// console.log('=========>', workerEngine, workerEngineCopy)
const [, triggerCopy] = useLazyPromise(async () => {
// console.log('+++++++++>', workerEngineCopy)
// const [, triggerCopy] = useLazyPromise(async () => {
// console.log('+++++++++>', workerEngineCopy)
await workerEngineCopy?.asyncSetSituation({
SMIC: '2000€/mois',
})
}, [workerEngineCopy])
// await workerEngineCopy?.asyncSetSituation({
// SMIC: '2000€/mois',
// })
// }, [workerEngineCopy])
const dateCopy = useAsyncGetRule('date', {
defaultValue: 'loading...',
// workerEngine: workerEngineCopy,
})
// const dateCopy = useAsyncGetRule('date', {
// defaultValue: 'loading...',
// workerEngine: workerEngineCopy,
// })
const parsedRulesCopy = useAsyncParsedRules({
workerEngine: workerEngineCopy,
})
// const parsedRulesCopy = useAsyncParsedRules({
// workerEngine: workerEngineCopy,
// })
const resultSmicCopy = usePromise(
async () => workerEngineCopy?.asyncEvaluate('SMIC'),
[workerEngineCopy],
'loading...'
)
// const resultSmicCopy = usePromise(
// async () =>
// !workerEngineCopy
// ? 'still loading...'
// : workerEngineCopy.asyncEvaluate('SMIC'),
// [workerEngineCopy],
// 'loading...'
// )
const [resultLazySmicCopy, triggerLazySmicCopy] = useLazyPromise(
async () => workerEngineCopy?.asyncEvaluate('SMIC'),
[workerEngineCopy],
'wait 2sec...'
)
// const [resultLazySmicCopy, triggerLazySmicCopy] = useLazyPromise(
// async () =>
// !workerEngineCopy
// ? 'still loading...'
// : workerEngineCopy.asyncEvaluate('SMIC'),
// [workerEngineCopy],
// 'wait 3sec...'
// )
useEffect(() => {
// console.log('useEffect')
// useEffect(() => {
// // console.log('useEffect')
void (async () => {
await workerEngine.isWorkerReady
setTimeout(() => {
void triggerLazySmicCopy()
}, 3000)
})()
}, [triggerLazySmicCopy, workerEngine.isWorkerReady])
// void (async () => {
// await workerEngine.isWorkerReady
// setTimeout(() => {
// void triggerLazySmicCopy()
// }, 3000)
// })()
// }, [triggerLazySmicCopy, workerEngine.isWorkerReady])
const { asyncSetSituation } = workerEngineCopy ?? {}
usePromise(async () => {
// console.log('**************>', workerEngineCopy, resultSmic)
// const { asyncSetSituation } = workerEngineCopy ?? {}
// usePromise(async () => {
// // console.log('**************>', workerEngineCopy, resultSmic)
if (
typeof resultSmic !== 'string' &&
typeof resultSmic.nodeValue === 'number'
) {
// console.log('ooooooooooooooooooo', resultSmic)
// if (
// resultSmic &&
// typeof resultSmic !== 'string' &&
// typeof resultSmic.nodeValue === 'number'
// ) {
// // console.log('ooooooooooooooooooo', resultSmic)
await asyncSetSituation?.({
SMIC: resultSmic.nodeValue + '€/mois',
})
}
}, [asyncSetSituation, resultSmic])
// await asyncSetSituation?.({
// SMIC: resultSmic.nodeValue + '€/mois',
// })
// }
// }, [asyncSetSituation, resultSmic])
return (
<div>
@ -146,12 +161,16 @@ const TestWorkerEngine = () => {
Refresh {refresh}
</button>
<button onClick={() => void trigger()}>trigger</button>
<button onClick={() => void triggerCopy()}>trigger copy</button>
{/* <button onClick={() => void triggerCopy()}>trigger copy</button> */}
<p>
date title:{' '}
{JSON.stringify(typeof date === 'string' ? date : date?.title)}
</p>
<p>
SMIC title:{' '}
{JSON.stringify(typeof SMIC === 'string' ? SMIC : SMIC?.title)}
</p>
<p>
parsedRules length:{' '}
{JSON.stringify(Object.entries(parsedRules ?? {}).length)}
@ -171,7 +190,7 @@ const TestWorkerEngine = () => {
)}
</p>
<p>workerEngineCopy: {JSON.stringify(workerEngineCopy?.engineId)}</p>
{/* <p>workerEngineCopy: {JSON.stringify(workerEngineCopy?.engineId)}</p>
<p>
dateCopy title:{' '}
@ -198,7 +217,7 @@ const TestWorkerEngine = () => {
? resultLazySmicCopy
: resultLazySmicCopy?.nodeValue
)}
</p>
</p> */}
</div>
)
}
@ -221,13 +240,11 @@ RootProps) {
return (
<StrictMode>
{/* <EngineProvider value={engine}> */}
<Provider basename={basename}>
<Redirections>
<Router />
</Redirections>
</Provider>
{/* </EngineProvider> */}
</StrictMode>
)
}
@ -277,9 +294,21 @@ const Router = () => {
exemple d'execution manuel : {JSON.stringify(exampleAsyncValue)} */}
{/* */}
<Routes>
<Route path="test-worker" element={<TestWorkerEngine />} />
<Route
path="test-worker"
element={
<>
<SuspensePromise isSSR={import.meta.env.SSR}>
{/* <TestWorkerEngine /> */}
<div>
<TestWorkerEngine />
</div>
</SuspensePromise>
</>
}
/>
{/* <Route path="/iframes/*" element={<Iframes />} /> */}
<Route path="/iframes/*" element={<Iframes />} />
<Route path="*" element={<App />} />
</Routes>
</>
@ -300,6 +329,7 @@ const App = () => {
const fullURL = useGetFullURL()
useSaveAndRestoreScrollPosition()
const isEmbedded = useIsEmbedded()
const workerEngine = useWorkerEngine()
if (!import.meta.env.PROD && import.meta.env.VITE_AXE_CORE_ENABLED) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useAxeCoreAnalysis()
@ -345,15 +375,17 @@ const App = () => {
path={relativeSitePaths.simulateursEtAssistants + '/*'}
element={<SimulateursEtAssistants />}
/>
{/* <Route
path={relativeSitePaths.documentation.index + '/*'}
element={
<Route
path={relativeSitePaths.documentation.index + '/*'}
element={
<SuspensePromise isSSR={import.meta.env.SSR}>
<Documentation
documentationPath={documentationPath}
engine={engine}
engine={workerEngine}
/>
}
/> */}
</SuspensePromise>
}
/>
<Route
path={relativeSitePaths.développeur.index + '/*'}
element={<Integration />}

View File

@ -1,3 +1,6 @@
import NodeWorker from '@eshaz/web-worker'
import { createWorkerEngineClient } from '@publicodes/worker'
import { useWorkerEngine, WorkerEngineProvider } from '@publicodes/worker-react'
import { OverlayProvider } from '@react-aria/overlays'
import { ErrorBoundary } from '@sentry/react'
import i18next from 'i18next'
@ -20,8 +23,6 @@ import { Body, Intro } from '@/design-system/typography/paragraphs'
import { EmbededContextProvider } from '@/hooks/useIsEmbedded'
import { Actions } from '@/worker/socialWorkerEngine.worker'
import SocialeWorkerEngine from '@/worker/socialWorkerEngine.worker?worker'
import { createWorkerEngineClient } from '@/worker/workerEngineClient'
import { WorkerEngineProvider } from '@/worker/workerEngineClientReact'
import { Message } from '../design-system'
import * as safeLocalStorage from '../storage/safeLocalStorage'
@ -31,40 +32,21 @@ import { createTracker } from './ATInternetTracking/Tracker'
import { IframeResizer } from './IframeResizer'
import { ServiceWorker } from './ServiceWorker'
import { DarkModeProvider } from './utils/DarkModeContext'
import { useSetupSafeSituation } from './utils/EngineContext'
const workerClient = createWorkerEngineClient<Actions>(
typeof Worker === 'undefined'
? ({ postMessage: () => {} } as unknown as Worker)
: new SocialeWorkerEngine(),
// () => {},
// () =>
// startTransition(() => {
// setSituationVersion((situationVersion) => {
// // console.log('??? setSituationVersion original')
console.time('start!')
// // situationVersion[engineId] =
// // typeof situationVersion[engineId] !== 'number'
// // ? 0
// // : situationVersion[engineId]++
export const worker = import.meta.env.SSR
? // Node doesn't support web worker :( upvote issue here: https://github.com/nodejs/node/issues/43583
new NodeWorker(
new URL('../worker/socialWorkerEngine.worker.js', import.meta.url),
{ type: 'module' }
)
: new SocialeWorkerEngine()
// // return situationVersion
// return situationVersion + 1
// })
// }),
//
{
initParams: [{ basename: 'mon-entreprise' }],
// onSituationChange: function () {
// console.log('update *****************')
// startTransition(() => {
// setSituationVersion((situationVersion) => {
// return situationVersion + 1
// })
// })
// },
}
)
const workerClient = createWorkerEngineClient<Actions>(worker, {
initParams: [{ basename: 'mon-entreprise' }],
})
type SiteName = 'mon-entreprise' | 'infrance'
@ -75,6 +57,14 @@ export type ProviderProps = {
children: ReactNode
}
const SituationSynchronize = ({ children }: { children: ReactNode }) => {
const workerEngine = useWorkerEngine()
useSetupSafeSituation(workerEngine)
return children
}
export default function Provider({
basename,
children,
@ -91,26 +81,28 @@ export default function Provider({
<ReduxProvider store={store}>
<BrowserRouterProvider basename={basename}>
<WorkerEngineProvider workerClient={workerClient}>
<ErrorBoundary
fallback={(errorData) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ErrorFallback {...errorData} />
)}
>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator && <ServiceWorker />}
<IframeResizer />
<OverlayProvider>
<ThemeColorsProvider>
<DisableAnimationOnPrintProvider>
<SiteNameContext.Provider value={basename}>
{children}
</SiteNameContext.Provider>
</DisableAnimationOnPrintProvider>
</ThemeColorsProvider>
</OverlayProvider>
</ErrorBoundary>
<SituationSynchronize>
<ErrorBoundary
fallback={(errorData) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ErrorFallback {...errorData} />
)}
>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&
'serviceWorker' in navigator && <ServiceWorker />}
<IframeResizer />
<OverlayProvider>
<ThemeColorsProvider>
<DisableAnimationOnPrintProvider>
<SiteNameContext.Provider value={basename}>
{children}
</SiteNameContext.Provider>
</DisableAnimationOnPrintProvider>
</ThemeColorsProvider>
</OverlayProvider>
</ErrorBoundary>
</SituationSynchronize>
</WorkerEngineProvider>
</BrowserRouterProvider>
</ReduxProvider>

View File

@ -1,10 +1,9 @@
import { useWorkerEngine } from '@publicodes/worker-react'
import { usePromise, useWorkerEngine } from '@publicodes/worker-react'
import { DottedName } from 'modele-social'
import { RuleLink as EngineRuleLink } from 'publicodes-react'
import React from 'react'
import { Link } from '@/design-system/typography/link'
import { usePromise } from '@/hooks/usePromise'
import { useSitePaths } from '@/sitePaths'
// TODO : quicklink -> en cas de variations ou de somme avec un seul élément actif, faire un lien vers cet élément
@ -15,20 +14,23 @@ export default function RuleLink(
documentationPath?: string
} & Omit<React.ComponentProps<typeof Link>, 'to' | 'children'>
) {
const { dottedName, documentationPath, ...linkProps } = props
const { dottedName, documentationPath, children, ...linkProps } = props
const { absoluteSitePaths } = useSitePaths()
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(false)
const workerEngine = useWorkerEngine()
usePromise(() => {
usePromise(async () => {
setLoading(true)
setError(false)
return workerEngine
.asyncGetRule(dottedName)
.catch(() => setError(true))
.then(() => setLoading(false))
try {
const rule = await workerEngine.asyncGetRule(dottedName)
} catch (error) {
setError(true)
}
setLoading(false)
}, [dottedName, workerEngine])
if (loading || error) {
@ -41,9 +43,12 @@ export default function RuleLink(
// @ts-ignore
linkComponent={Link}
engine={workerEngine}
dottedName={dottedName}
documentationPath={
documentationPath ?? absoluteSitePaths.documentation.index
}
/>
>
{children}
</EngineRuleLink>
)
}

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react'
import { useWorkerEngine, WorkerEngine } from '@publicodes/worker-react'
import { useCallback, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useNextQuestions } from '@/hooks/useNextQuestion'
@ -12,7 +13,6 @@ import {
currentQuestionSelector,
useMissingVariables,
} from '@/store/selectors/simulationSelectors'
import { useWorkerEngine, WorkerEngine } from '@/worker/workerEngineClientReact'
export function useNavigateQuestions(workerEngines?: WorkerEngine[]) {
const dispatch = useDispatch()
@ -25,22 +25,27 @@ export function useNavigateQuestions(workerEngines?: WorkerEngine[]) {
const previousAnswers = useSelector(answeredQuestionsSelector)
const goToPrevious = () => {
const goToPrevious = useCallback(() => {
dispatch(updateShouldFocusField(true))
dispatch(goToQuestion(previousAnswers.slice(-1)[0]))
}
const goToNext = () => {
}, [dispatch, previousAnswers])
const goToNext = useCallback(() => {
dispatch(updateShouldFocusField(true))
if (currentQuestion) {
dispatch(stepAction(currentQuestion))
// dispatch(goToQuestion(nextQuestions[0]))
nextQuestions.length > 1 && dispatch(goToQuestion(nextQuestions[1]))
}
}
}, [currentQuestion, dispatch, nextQuestions])
const init = useRef(false)
useEffect(() => {
if (!currentQuestion && nextQuestions[0]) {
if (!init.current && !currentQuestion && nextQuestions.length) {
dispatch(goToQuestion(nextQuestions[0]))
init.current = true
}
}, [nextQuestions, currentQuestion, dispatch])
}, [currentQuestion, dispatch, nextQuestions])
return {
currentQuestion: currentQuestion ?? nextQuestions[0],

View File

@ -122,7 +122,7 @@ export const FadeIn = ({
)
}
export function Appear({
function AppearAnim({
children,
className,
unless = false,
@ -158,3 +158,7 @@ export function Appear({
</animated.div>
)
}
export const Appear = (props: Parameters<typeof AppearAnim>[0]) =>
// eslint-disable-next-line react/jsx-props-no-spreading
import.meta.env.SSR ? props.children : <AppearAnim {...props} />

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'
import { ThemeProvider, useTheme } from 'styled-components'
import { createGlobalStyle, ThemeProvider, useTheme } from 'styled-components'
import { useIsEmbedded } from '@/hooks/useIsEmbedded'
import { hexToHSL } from '@/utils/hexToHSL'
@ -42,31 +42,39 @@ const IFRAME_COLOR = iframeColor
// the full palette generation that happen here. This is to prevent a UI
// flash, cf. #1786.
const GlobalCssVar = createGlobalStyle<{
$hue: number
$saturation: number
}>`
html {
--${HUE_CSS_VARIABLE_NAME}: ${({ $hue }) => $hue}deg;
--${SATURATION_CSS_VARIABLE_NAME}: ${({ $saturation }) => $saturation}%;
}
`
export function ThemeColorsProvider({ children }: ProviderProps) {
const divRef = useRef<HTMLDivElement>(null)
const [themeColor, setThemeColor] = useState(IFRAME_COLOR)
useEffect(() => {
window.addEventListener('message', (evt: MessageEvent) => {
if (evt.data.kind === 'change-theme-color') {
setThemeColor(hexToHSL(evt.data.value))
window.addEventListener(
'message',
(evt: MessageEvent<{ kind: string; value: string }>) => {
if (evt.data.kind === 'change-theme-color') {
console.log('change-theme-color', evt.data.value)
setThemeColor(hexToHSL(evt.data.value))
}
}
})
}, [])
const [hue, saturation] = themeColor
useEffect(() => {
const root = document.querySelector(':root') as HTMLElement | undefined
root?.style.setProperty(`--${HUE_CSS_VARIABLE_NAME}`, `${hue}deg`)
root?.style.setProperty(
`--${SATURATION_CSS_VARIABLE_NAME}`,
`${saturation}%`
)
}, [hue, saturation])
}, [])
const isEmbeded = useIsEmbedded()
const defaultTheme = useTheme()
if (!themeColor && !isEmbeded) {
return <>{children}</>
}
const [hue, saturation] = themeColor
return (
<ThemeProvider
theme={{
@ -77,6 +85,7 @@ export function ThemeColorsProvider({ children }: ProviderProps) {
},
}}
>
<GlobalCssVar $hue={hue} $saturation={saturation} />
{/* This div is only used to set the CSS variables */}
<div
ref={divRef}

View File

@ -1,20 +1,46 @@
import { SSRProvider } from '@react-aria/ssr'
import ReactDOMServer from 'react-dom/server'
import { lazy } from 'react'
import ReactDomServer, { type renderToReadableStream } from 'react-dom/server'
import { FilledContext, HelmetProvider } from 'react-helmet-async'
import { StaticRouter } from 'react-router-dom/server'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
import i18next from '../locales/i18n'
import { AppEn } from './entry-en'
import { AppFr } from './entry-fr'
export function render(url: string, lang: 'fr' | 'en') {
function streamToString(stream: ReadableStream<Uint8Array>) {
return new Response(stream).text()
}
const AppFrLazy = lazy(async () => ({
default: (await import('./entry-fr')).AppFr,
}))
const AppEnLazy = lazy(async () => ({
default: (await import('./entry-en')).AppEn,
}))
// @ts-ignore
global.window = {
// @ts-ignore
location: {},
}
interface Result {
html: string
styleTags: string
helmet: FilledContext['helmet']
}
export async function render(url: string, lang: 'fr' | 'en'): Promise<Result> {
global.window.location.href = url
global.window.location.search = ''
console.log({ url, lang })
const sheet = new ServerStyleSheet()
const helmetContext = {} as FilledContext
const App = lang === 'fr' ? AppFr : AppEn
i18next.changeLanguage(lang).catch((err) =>
// eslint-disable-next-line no-console
console.error(err)
console.error('Error', err)
)
const element = (
@ -22,20 +48,39 @@ export function render(url: string, lang: 'fr' | 'en') {
<SSRProvider>
<StyleSheetManager sheet={sheet.instance}>
<StaticRouter location={url}>
<App />
[prerender] window: {JSON.stringify(window)}
{lang === 'fr' ? <AppFrLazy /> : <AppEnLazy />}
</StaticRouter>
</StyleSheetManager>
</SSRProvider>
</HelmetProvider>
)
// Render to initialize redux store (via useSimulationConfig)
ReactDOMServer.renderToString(element)
console.log('!!! STARTING !!!')
// Render with redux store configured
const html = ReactDOMServer.renderToString(element)
try {
const stream = await (
ReactDomServer.renderToReadableStream as unknown as typeof renderToReadableStream
)(element, {
onError(error, errorInfo) {
console.error({ error, errorInfo })
},
})
const styleTags = sheet.getStyleTags()
console.log('!!! LOADING !!!')
return { html, styleTags, helmet: helmetContext.helmet }
await stream.allReady
console.log('!!! DONE !!!')
const html = await streamToString(stream)
const styleTags = sheet.getStyleTags()
return { html, styleTags, helmet: helmetContext.helmet }
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -43,6 +43,9 @@
<meta name="theme-color" content="#2975d1" />
<!--app-helmet-tags:start-->
<!--app-helmet-tags:end-->
<style>
/* CSS Loader */
#loading {

View File

@ -3,6 +3,7 @@ import { DependencyList, useCallback, useEffect, useState } from 'react'
/**
* Execute an asynchronous function and return its result (Return default value if the promise is not finished).
* The function is executed each time the dependencies change.
* @deprecated use `import { usePromise } from '@publicodes/worker-react'`
*/
export const usePromise = <T, Default = undefined>(
promise: () => Promise<T>,
@ -47,6 +48,7 @@ const tuple = <T extends unknown[]>(args: [...T]): T => args
/**
* Execute an asynchronous function and return its result (Return default value if the promise is not finished).
* Use this hook if you want to fire the promise manually.
* @deprecated use `import { useLazyPromise } from '@publicodes/worker-react'`
*/
export const useLazyPromise = <
T,

View File

@ -146,6 +146,7 @@ function DocumentationPageBody({
return (
<StyledDocumentation>
<RulePage
isSSR={import.meta.env.SSR}
language={i18n.language as 'fr' | 'en'}
rulePath={params['*'] ?? ''}
engine={engine}

View File

@ -260,6 +260,10 @@ const LazyBlobProvider = lazy<typeof BlobProvider>(
// From https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886
function isOnTouchDevice() {
if (import.meta.env.SSR) {
return false
}
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
const mq = function (query: string) {
return window.matchMedia(query).matches

View File

@ -39,12 +39,11 @@ export const goToQuestion = (question: DottedName) =>
step: question,
}) as const
export const stepAction = (step: DottedName, source?: string) =>
export const stepAction = (step: DottedName) =>
({
type: 'STEP_ACTION',
name: 'fold',
step,
source,
}) as const
export const setSimulationConfig = (

View File

@ -2,9 +2,9 @@
"compilerOptions": {
"lib": ["ESNext", "DOM", "WebWorker"],
"baseUrl": "source",
"moduleResolution": "node",
"module": "esnext",
"target": "esnext",
"moduleResolution": "Node",
"module": "ESNext",
"target": "ESNext",
"esModuleInterop": true,
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
@ -38,6 +38,7 @@
"vite-iframe-script.config.ts",
"build/vite-build-simulation-data.config.ts",
"build/prerender.ts",
"build/prerender-worker.ts",
"vite-pwa-options.ts"
]
}