From 4eac3be8292e75a7d3c603c249198a8923699aa4 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Tue, 12 Sep 2023 09:37:17 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A9organise=20les=20fichiers=20pour=20le=20?= =?UTF-8?q?build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/build/.eslintrc.yaml | 5 + site/{scripts => build}/build-sitemap.ts | 0 site/{scripts => build}/build-yaml-to-dts.ts | 0 site/build/multiple-SPA.ts | 119 ++++++++++++ site/{ => build}/prerender-worker.js | 8 +- site/{ => build}/prerender.ts | 2 +- .../vite-build-simulation-data.config.ts | 17 +- site/{ => build}/vite-iframe-script.config.ts | 0 site/{ => build}/vite-pwa-options.ts | 0 site/package.json | 29 +-- .../execOnFileChange.ts | 0 .../index.ts | 2 +- site/tsconfig.json | 5 +- site/vite.config.ts | 180 ++++-------------- 14 files changed, 201 insertions(+), 166 deletions(-) create mode 100644 site/build/.eslintrc.yaml rename site/{scripts => build}/build-sitemap.ts (100%) rename site/{scripts => build}/build-yaml-to-dts.ts (100%) create mode 100644 site/build/multiple-SPA.ts rename site/{ => build}/prerender-worker.js (78%) rename site/{ => build}/prerender.ts (97%) rename site/{ => build}/vite-build-simulation-data.config.ts (85%) rename site/{ => build}/vite-iframe-script.config.ts (100%) rename site/{ => build}/vite-pwa-options.ts (100%) rename site/scripts/{runScriptOnFileChange => compileEconomieColllaborativeYaml}/execOnFileChange.ts (100%) rename site/scripts/{runScriptOnFileChange => compileEconomieColllaborativeYaml}/index.ts (92%) diff --git a/site/build/.eslintrc.yaml b/site/build/.eslintrc.yaml new file mode 100644 index 000000000..bde82d6ae --- /dev/null +++ b/site/build/.eslintrc.yaml @@ -0,0 +1,5 @@ +# We overwrite the default ESLint configuration for NodeJS scripts +env: + node: true +rules: + no-console: 0 diff --git a/site/scripts/build-sitemap.ts b/site/build/build-sitemap.ts similarity index 100% rename from site/scripts/build-sitemap.ts rename to site/build/build-sitemap.ts diff --git a/site/scripts/build-yaml-to-dts.ts b/site/build/build-yaml-to-dts.ts similarity index 100% rename from site/scripts/build-yaml-to-dts.ts rename to site/build/build-yaml-to-dts.ts diff --git a/site/build/multiple-SPA.ts b/site/build/multiple-SPA.ts new file mode 100644 index 000000000..148dcab53 --- /dev/null +++ b/site/build/multiple-SPA.ts @@ -0,0 +1,119 @@ +import fs from 'fs/promises' + +import serveStatic from 'serve-static' +import { Plugin } from 'vite' + +type MultipleSPAOptions = { + defaultSite: string + templatePath: string + sites: Record> +} + +/** + * A custom plugin to create multiple virtual html files from a template. Will + * generate distinct entry points and single-page application outputs. + */ +export function multipleSPA(options: MultipleSPAOptions): Plugin { + const fillTemplate = async (siteName: string) => { + const siteData = options.sites[siteName] + const template = await fs.readFile(options.templatePath, 'utf-8') + const filledTemplate = template + .toString() + .replace(/\{\{(.+)\}\}/g, (_match, p1) => siteData[(p1 as string).trim()]) + + return filledTemplate + } + + return { + name: 'multiple-spa', + enforce: 'pre', + + configureServer(vite) { + // TODO: this middleware is specific to the "mon-entreprise" app and + // shouldn't be in the "multipleSPA" plugin. It allows to serve the + // iframe integration script (already built) from the same server as the app. + vite.middlewares.use( + '/simulateur-iframe-integration.js', + serveStatic(new URL('./dist', import.meta.url).pathname, { + index: 'simulateur-iframe-integration.js', + }) + ) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + vite.middlewares.use(async (req, res, next) => { + const url = req.originalUrl?.replace(/^\/%2F/, '/') + + const firstLevelDir = url?.slice(1).split('/')[0] + + if (url && /\?.*html-proxy/.test(url)) { + return next() + } + + if (url && ['/', '/index.html'].includes(url)) { + res.writeHead(302, { Location: '/' + options.defaultSite }).end() + } + // this condition is for the start:netlify script to match /mon-entreprise or /infrance + else if ( + firstLevelDir && + url && + Object.keys(options.sites) + .map((site) => `/${site}.html`) + .includes(url) + ) { + const siteName = firstLevelDir.replace('.html', '') + const content = await vite.transformIndexHtml( + '/' + siteName, + await fillTemplate(siteName) + ) + res.end(content) + } else if ( + firstLevelDir && + Object.keys(options.sites).some((name) => firstLevelDir === name) + ) { + const siteName = firstLevelDir + const content = await vite.transformIndexHtml( + url, + await fillTemplate(siteName) + ) + res.end(content) + } else { + next() + } + }) + }, + + config(config, { command }) { + if (command === 'build' && !config.build?.ssr) { + config.build = { + ...config.build, + rollupOptions: { + ...config.build?.rollupOptions, + input: Object.fromEntries( + Object.keys(options.sites).map((name) => [ + name, + `virtual:${name}.html`, + ]) + ), + }, + } + } + }, + + resolveId(id) { + const pathname = id.split('/').slice(-1)[0] + if (pathname?.startsWith('virtual:')) { + return pathname.replace('virtual:', '') + } + + return null + }, + + async load(id) { + if ( + Object.keys(options.sites).some((name) => id.endsWith(name + '.html')) + ) { + return await fillTemplate(id.replace(/\.html$/, '')) + } + }, + } +} diff --git a/site/prerender-worker.js b/site/build/prerender-worker.js similarity index 78% rename from site/prerender-worker.js rename to site/build/prerender-worker.js index 5b71428f6..3fd954106 100644 --- a/site/prerender-worker.js +++ b/site/build/prerender-worker.js @@ -2,7 +2,7 @@ import { promises as fs, readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'url' -import { render } from './dist/ssr/entry-server.js' +import { render } from '../dist/ssr/entry-server.js' const dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -22,7 +22,7 @@ export default async ({ site, url, lang }) => { const template = cache[site] ?? - readFileSync(path.join(dirname, `./dist/${site}.html`), 'utf-8') + readFileSync(path.join(dirname, `../dist/${site}.html`), 'utf-8') cache[site] = template @@ -31,10 +31,10 @@ export default async ({ site, url, lang }) => { .replace('', styleTags) .replace(regexHelmet, helmet.title.toString() + helmet.meta.toString()) - const dir = path.join(dirname, 'dist/prerender', site, decodeURI(url)) + const dir = path.join(dirname, '../dist/prerender', site, decodeURI(url)) await fs.mkdir(dir, { recursive: true }) await fs.writeFile(path.join(dir, 'index.html'), page) - return path.relative(path.join(dirname, 'dist'), path.join(dir, 'index.html')) + return path.relative(path.join(dirname, '../dist'), path.join(dir, 'index.html')) } diff --git a/site/prerender.ts b/site/build/prerender.ts similarity index 97% rename from site/prerender.ts rename to site/build/prerender.ts index 743965343..d7c3cccdc 100644 --- a/site/prerender.ts +++ b/site/build/prerender.ts @@ -4,7 +4,7 @@ import { argv } from 'node:process' import Tinypool from 'tinypool' -import { absoluteSitePaths } from './source/sitePaths.js' +import { absoluteSitePaths } from '../source/sitePaths.js' const filename = new URL('./prerender-worker.js', import.meta.url).href const pool = new Tinypool({ filename }) diff --git a/site/vite-build-simulation-data.config.ts b/site/build/vite-build-simulation-data.config.ts similarity index 85% rename from site/vite-build-simulation-data.config.ts rename to site/build/vite-build-simulation-data.config.ts index b7369eadf..314fb078a 100644 --- a/site/vite-build-simulation-data.config.ts +++ b/site/build/vite-build-simulation-data.config.ts @@ -1,11 +1,11 @@ -import { unlinkSync, writeFileSync } from 'fs' -import path from 'path' +import { writeFileSync } from 'fs' +import { join, resolve } from 'path' import { defineConfig } from 'vite' import { PageConfig } from '@/pages/simulateurs/_configs/types' -import { objectTransform } from './source/utils' +import { objectTransform } from '../source/utils' const filterOgImage = (obj: Record>) => objectTransform(obj, (entries) => { @@ -25,10 +25,10 @@ const filterOgImage = (obj: Record>) => export default defineConfig({ resolve: { - alias: [{ find: '@', replacement: path.resolve('./source') }], + alias: [{ find: '@', replacement: resolve('./source') }], }, build: { - outDir: './', + outDir: './dist', target: 'esnext', emptyOutDir: false, lib: { @@ -60,13 +60,16 @@ export default defineConfig({ closeBundle: () => { // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { - const path = './builded-simulation-data.js' + const path = join( + import.meta.url, + '../../dist/builded-simulation-data.js' + ) + console.log('path', path) type PageConfigType = { default: Record> } const algoliaUpdate = ((await import(path)) as PageConfigType).default - unlinkSync(path) writeFileSync( './source/public/simulation-data.json', JSON.stringify(filterOgImage(algoliaUpdate)) diff --git a/site/vite-iframe-script.config.ts b/site/build/vite-iframe-script.config.ts similarity index 100% rename from site/vite-iframe-script.config.ts rename to site/build/vite-iframe-script.config.ts diff --git a/site/vite-pwa-options.ts b/site/build/vite-pwa-options.ts similarity index 100% rename from site/vite-pwa-options.ts rename to site/build/vite-pwa-options.ts diff --git a/site/package.json b/site/package.json index 92689370d..008687b76 100644 --- a/site/package.json +++ b/site/package.json @@ -18,35 +18,42 @@ "not ie < 11" ], "scripts": { - "build:yaml-to-dts": "ts-node-esm scripts/build-yaml-to-dts.ts", - "postinstall": "node scripts/prepare.js", "start": "vite dev", "start:axe-debugging": "VITE_AXE_CORE_ENABLED=true vite dev", "start:netlify": "sed 's|:SITE_EN|/infrance|g' netlify.base.toml | sed 's|:SITE_FR|/mon-entreprise|g' | sed 's|:API_URL|http://localhost:3004|g' | sed 's|\\[\\[redirects\\]\\]|\\[\\[redirects\\]\\]\\n force = true|g' > netlify.toml && HMR_CLIENT_PORT=8888 netlify dev", + "start:storybook": "storybook dev -p 6006", + + "postinstall": "node scripts/prepare.js", + "build": "NODE_OPTIONS='--max-old-space-size=6144'; yarn build:sitemap && yarn build:simulator-data && vite build && yarn build:iframe-script", - "build:ssr": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --ssr ./source/entries/entry-server.tsx --outDir ./dist/ssr --emptyOutDir && echo '{\"type\": \"module\"}' > dist/package.json", - "build:prerender": "ts-node-esm prerender.ts", - "build:simulator-data": "vite build --config vite-build-simulation-data.config.ts", - "build:iframe-script": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --config vite-iframe-script.config.ts", - "build:sitemap": "ts-node-esm scripts/build-sitemap.ts", + "build:iframe-script": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --config build/vite-iframe-script.config.ts", + "build:prerender": "ts-node-esm build/prerender.ts", "build:preview": "VITE_FR_BASE_URL=http://localhost:8888; VITE_EN_BASE_URL=http://localhost:8889; yarn build && yarn build:ssr && cp ./netlify.base.toml ./netlify.preview.toml && yarn build:prerender --dev --netlify-toml-path ./netlify.preview.toml", + "build:simulator-data": "vite build --config build/vite-build-simulation-data.config.ts", + "build:sitemap": "ts-node-esm build/build-sitemap.ts", + "build:ssr": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --ssr ./source/entries/entry-server.tsx --outDir ./dist/ssr --emptyOutDir && echo '{\"type\": \"module\"}' > dist/package.json", + "build:storybook": "NODE_OPTIONS='--max-old-space-size=6144'; storybook build", + "build:yaml-to-dts": "ts-node-esm scripts/build-yaml-to-dts.ts", + "preview:mon-entreprise": "sed 's|:SITE_EN|_|g' netlify.preview.toml | sed 's|:SITE_FR||g' | sed 's|:API_URL|http://localhost:3004|g' > dist/netlify.toml && cd dist && netlify dev -d ./ -p 8888", "preview:infrance": " sed 's|:SITE_EN||g' netlify.preview.toml | sed 's|:SITE_FR|_|g' | sed 's|:API_URL|http://localhost:3004|g' > dist/netlify.toml && cd dist && netlify dev -d ./ -p 8889", + "typecheck:watch": "tsc --skipLibCheck --noEmit --watch", + "test": "vitest", "test:dev-e2e:mon-entreprise": "cypress open --e2e --config \"baseUrl=http://localhost:8888\"", "test:dev-e2e:mycompanyinfrance": "cypress open --e2e --config \"baseUrl=http://localhost:8889,specPattern=cypress/integration/mon-entreprise/english/**/*.{js,jsx,ts,tsx}\" --env language=en", "test:record-http-calls:mon-entreprise": "cypress run --env record_http=", - "algolia:update": "yarn build:simulator-data && ts-node-esm ./scripts/search/update-data.ts", + "algolia:clean": "node scripts/search/clean.js", + "algolia:update": "yarn build:simulator-data && ts-node-esm ./scripts/search/update-data.ts", + "i18n:check": "yarn i18n:rules:check && yarn i18n:ui:check", "i18n:translate": "yarn i18n:rules:translate && yarn i18n:ui:translate", "i18n:rules:check": "node scripts/i18n/check-missing-rule-translation.js", "i18n:rules:translate": "node scripts/i18n/translate-rules.js", "i18n:ui:check": "i18next -c scripts/i18n/parser.config.js && node scripts/i18n/check-missing-UI-translation.js", - "i18n:ui:translate": "rm -rf source/locales/static-analysis-fr.json && i18next -c scripts/i18n/parser.config.js && node scripts/i18n/translate-ui.js", - "start:storybook": "storybook dev -p 6006", - "build:storybook": "NODE_OPTIONS='--max-old-space-size=6144'; storybook build" + "i18n:ui:translate": "rm -rf source/locales/static-analysis-fr.json && i18next -c scripts/i18n/parser.config.js && node scripts/i18n/translate-ui.js" }, "dependencies": { "@atomik-color/component": "^1.0.17", diff --git a/site/scripts/runScriptOnFileChange/execOnFileChange.ts b/site/scripts/compileEconomieColllaborativeYaml/execOnFileChange.ts similarity index 100% rename from site/scripts/runScriptOnFileChange/execOnFileChange.ts rename to site/scripts/compileEconomieColllaborativeYaml/execOnFileChange.ts diff --git a/site/scripts/runScriptOnFileChange/index.ts b/site/scripts/compileEconomieColllaborativeYaml/index.ts similarity index 92% rename from site/scripts/runScriptOnFileChange/index.ts rename to site/scripts/compileEconomieColllaborativeYaml/index.ts index fad5e6ab7..a105d9548 100644 --- a/site/scripts/runScriptOnFileChange/index.ts +++ b/site/scripts/compileEconomieColllaborativeYaml/index.ts @@ -1,6 +1,6 @@ import { execOnFileChange } from './execOnFileChange.js' -export const runScriptOnFileChange = async () => { +export const compileEconomieCollaborativeYaml = async () => { console.log('Search for changed file...') const results = await execOnFileChange({ diff --git a/site/tsconfig.json b/site/tsconfig.json index 95472235f..c61dc38a3 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -31,12 +31,13 @@ "include": [ "source", "scripts", + "build", "test/**/*.ts", "vite.config.ts", "cypress.config.ts", "vite-iframe-script.config.ts", - "vite-build-simulation-data.config.ts", - "prerender.ts", + "build/vite-build-simulation-data.config.ts", + "build/prerender.ts", "vite-pwa-options.ts" ] } diff --git a/site/vite.config.ts b/site/vite.config.ts index ee7564c6c..8c6170a0c 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -1,17 +1,17 @@ /* eslint-disable no-console */ -import fs from 'fs/promises' + import path from 'path' import replace from '@rollup/plugin-replace' import yaml, { ValidYamlType } from '@rollup/plugin-yaml' import legacy from '@vitejs/plugin-legacy' import react from '@vitejs/plugin-react-swc' -import serveStatic from 'serve-static' -import { defineConfig, loadEnv, Plugin } from 'vite' +import { defineConfig, loadEnv } from 'vite' import { VitePWA } from 'vite-plugin-pwa' -import { runScriptOnFileChange } from './scripts/runScriptOnFileChange' -import { pwaOptions } from './vite-pwa-options' +import { multipleSPA } from './build/multiple-SPA' +import { pwaOptions } from './build/vite-pwa-options' +import { compileEconomieCollaborativeYaml } from './scripts/compileEconomieColllaborativeYaml' const env = (mode: string) => loadEnv(mode, process.cwd(), '') @@ -47,18 +47,30 @@ export default defineConfig(({ command, mode }) => ({ apply: 'serve', buildStart() { if (mode === 'development') { - void runScriptOnFileChange() + void compileEconomieCollaborativeYaml() } }, }, + command === 'build' && replace({ __SENTRY_DEBUG__: false, preventAssignment: false, }), + react({ - plugins: [['@swc/plugin-styled-components', { pure: true }]], + plugins: [ + [ + '@swc/plugin-styled-components', + { + pure: true, + displayName: true, + transpileTemplateLiterals: false, + }, + ], + ], }), + yaml({ transform(data, filePath) { return filePath.endsWith('/rules-en.yaml') @@ -66,6 +78,7 @@ export default defineConfig(({ command, mode }) => ({ : data }, }), + multipleSPA({ defaultSite: 'mon-entreprise', templatePath: './source/entries/template.html', @@ -84,11 +97,14 @@ export default defineConfig(({ command, mode }) => ({ }, }, }), + VitePWA(pwaOptions), + legacy({ targets: ['defaults', 'not IE 11'], }), ], + server: { port: 3000, hmr: { @@ -116,150 +132,22 @@ export default defineConfig(({ command, mode }) => ({ }, }, }, + optimizeDeps: { entries: ['./source/entries/entry-fr.tsx', './source/entries/entry-en.tsx'], include: ['publicodes-react > react/jsx-runtime'], exclude: ['publicodes-react', 'publicodes'], }, + ssr: { /** * Prevent listed dependencies from being externalized for SSR build cause some - * packages are not esm ready or package.json setup seems wrong, wait this pr to be merge: - * markdown-to-jsx: https://github.com/probablyup/markdown-to-jsx/pull/414 - * styled-components: https://github.com/styled-components/styled-components/issues/3601 (wait v6 release) + * packages are not esm ready or package.json setup seems wrong: */ - noExternal: [/styled-components|emotion/, /tslib/], + noExternal: [/tslib/], }, })) -type MultipleSPAOptions = { - defaultSite: string - templatePath: string - sites: Record> -} - -/** - * A custom plugin to create multiple virtual html files from a template. Will - * generate distinct entry points and single-page application outputs. - */ -function multipleSPA(options: MultipleSPAOptions): Plugin { - const fillTemplate = async (siteName: string) => { - const siteData = options.sites[siteName] - const template = await fs.readFile(options.templatePath, 'utf-8') - const filledTemplate = template - .toString() - .replace(/\{\{(.+)\}\}/g, (_match, p1) => siteData[(p1 as string).trim()]) - - return filledTemplate - } - - return { - name: 'multiple-spa', - enforce: 'pre', - - configureServer(vite) { - // TODO: this middleware is specific to the "mon-entreprise" app and - // shouldn't be in the "multipleSPA" plugin - vite.middlewares.use( - '/simulateur-iframe-integration.js', - serveStatic(new URL('./dist', import.meta.url).pathname, { - index: 'simulateur-iframe-integration.js', - }) - ) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - vite.middlewares.use(async (req, res, next) => { - const url = req.originalUrl?.replace(/^\/%2F/, '/') - - const firstLevelDir = url?.slice(1).split('/')[0] - - if (url && /\?.*html-proxy/.test(url)) { - return next() - } - - if (url && ['/', '/index.html'].includes(url)) { - res.writeHead(302, { Location: '/' + options.defaultSite }).end() - } - // this condition is for the start:netlify script to match /mon-entreprise or /infrance - else if ( - firstLevelDir && - url && - Object.keys(options.sites) - .map((site) => `/${site}.html`) - .includes(url) - ) { - const siteName = firstLevelDir.replace('.html', '') - const content = await vite.transformIndexHtml( - '/' + siteName, - await fillTemplate(siteName) - ) - res.end(content) - } else if ( - firstLevelDir && - Object.keys(options.sites).some((name) => firstLevelDir === name) - ) { - const siteName = firstLevelDir - const content = await vite.transformIndexHtml( - url, - await fillTemplate(siteName) - ) - res.end(content) - } else { - next() - } - }) - }, - - config(config, { command }) { - if (command === 'build' && !config.build?.ssr) { - config.build = { - ...config.build, - rollupOptions: { - ...config.build?.rollupOptions, - input: Object.fromEntries( - Object.keys(options.sites).map((name) => [ - name, - `virtual:${name}.html`, - ]) - ), - }, - } - } - }, - - resolveId(id) { - const pathname = id.split('/').slice(-1)[0] - if (pathname?.startsWith('virtual:')) { - return pathname.replace('virtual:', '') - } - - return null - }, - - async load(id) { - if ( - Object.keys(options.sites).some((name) => id.endsWith(name + '.html')) - ) { - return await fillTemplate(id.replace(/\.html$/, '')) - } - }, - } -} - -/** - * Git branch name - */ -export const getBranch = (mode: string) => { - let branch: string | undefined = env(mode) - .VITE_GITHUB_REF?.split('/') - ?.slice(-1)?.[0] - - if (branch === 'merge') { - branch = env(mode).VITE_GITHUB_HEAD_REF - } - - return branch ?? '' -} - /** * We use this function to hide some features in production while keeping them * in feature-branches. In case we do A/B testing with several branches served @@ -268,10 +156,22 @@ export const getBranch = (mode: string) => { * be build in production mode (with the NODE_ENV) but we may still want to show * or hide some features. */ -export const isProductionBranch = (mode: string) => { +const isProductionBranch = (mode: string) => { return ['master', 'next'].includes(getBranch(mode)) } +const getBranch = (mode: string) => { + let branch: string | undefined = env(mode) + .VITE_GITHUB_REF?.split('/') + ?.slice(-1)?.[0] + + if (branch === 'merge') { + branch = env(mode).VITE_GITHUB_HEAD_REF + } + + return branch ?? '' +} + const cleanAutomaticTag = (data: ValidYamlType): ValidYamlType => { if (typeof data === 'string' && data.startsWith('[automatic] ')) { return data.replace('[automatic] ', '')