Skip to content

Commit

Permalink
Merge pull request #185 from cher-ami/generate-temp-folder
Browse files Browse the repository at this point in the history
Generate html files in _temp folder, then move it to outDir if no errors.
  • Loading branch information
cherami-tech authored Jul 3, 2024
2 parents 1bf9cb8 + 0cff278 commit 6d39959
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 72 deletions.
1 change: 1 addition & 0 deletions apps/front/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
outDirSsrClient: resolve("dist/ssr/client"),
outDirSpa: resolve("dist/spa"),
outDirStaticClient: resolve("dist/static/client"),
outDirStaticClientTemp: resolve("dist/static/_temp"),
outDirStaticScripts: resolve("dist/static/scripts"),

// Input entry files array
Expand Down
9 changes: 4 additions & 5 deletions apps/front/prerender/exe-prerender-server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import express from "express"
import { prerender } from "./prerender"
import chalk from "chalk"
import { fetchAvailableUrls } from "./urls"
import config from "../config/config"
import express from "express"
import * as process from "process"
import { loadEnv } from "vite"
import { prerender } from "./prerender"
import { fetchAvailableUrls } from "./urls"

const envs = loadEnv("", process.cwd(), "")
const port = envs.PRERENDER_SERVER_NODE_PORT || process.env.PRERENDER_SERVER_NODE_PORT
Expand All @@ -26,7 +25,7 @@ app.get("/generate", async (req, res) => {

// second arg "./static" is matching cher-ami deploy conf
// need to be edited if we want to start this server locally
await prerender(urlsArray, config.outDirStaticClient)
await prerender(urlsArray)
res?.send("Generated static pages: " + urlsArray.join(", "))
})

Expand Down
125 changes: 125 additions & 0 deletions apps/front/prerender/helpers/filesManipulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import path from "path"
import * as mfs from "@cher-ami/mfs"
import * as fs from "fs/promises"
import { JSXElementConstructor, ReactElement } from "react"
import { isRouteIndex } from "./isRouteIndex"
import chalk from "chalk"
import { renderToString } from "react-dom/server"

/**
* Create a single HTML file
* @param urls: All urls to generate
* @param url: Current URL to generate
* @param outDir: Generation destination directory
* @param dom: React DOM from index-server.tsx
*/
export const createHtmlFile = async (
urls: string[],
url: string,
outDir: string,
dom: ReactElement<any, string | JSXElementConstructor<any>>
): Promise<void> => {
// Prepare file
if (isRouteIndex(url, urls)) url = `${url}/index`
const routePath = path.resolve(`${outDir}/${url}`)
const htmlFilePath = `${routePath}.html`

// Create file
await mfs.createFile(htmlFilePath, htmlReplacement(renderToString(dom)))
console.log(chalk.green(` → ${htmlFilePath.split("static")[1]}`))
}

/**
* Render string patch middleware
* @param[string] render html string to parse
* @returns
*/
const htmlReplacement = (render: string): string => {
return render
.replace("<html", `<!DOCTYPE html><html`)
.replaceAll('<script nomodule=""', "<script nomodule")
.replaceAll('crossorigin="anonymous"', "crossorigin")
}

/**
* Copy file to destination
* @param[string] source source file
* @param[string] destination destination file
*/
export async function copyFile(source, destination) {
await fs.mkdir(path.dirname(destination), { recursive: true })
await fs.copyFile(source, destination)
}

/**
* Copy folder to destination and remove source afterwards
* @param[string] source source folder
* @param[string] destination desitnation folder
*/
export async function moveFolder(source, destination) {
try {
// Crée le dossier de destination s'il n'existe pas
await fs.mkdir(destination, { recursive: true })

// Lire le contenu du dossier source
const entries = await fs.readdir(source, { withFileTypes: true })

for (const entry of entries) {
const sourcePath = path.join(source, entry.name)
const destinationPath = path.join(destination, entry.name)

if (entry.isDirectory()) {
// Appel récursif pour les sous-dossiers
await moveFolder(sourcePath, destinationPath)
} else {
// Copier le fichier
await copyFile(sourcePath, destinationPath)
await fs.unlink(sourcePath) // Supprimer le fichier source après copie
}
}

// Supprimer le dossier source après déplacement de son contenu
await fs.rmdir(source)
// console.log(`Folder ${source} moved to ${destination}`)
} catch (err) {
console.error(`Erreur lors du déplacement du dossier : ${err}`)
}
}

/**
* Copy only html files from one folder to another, delete them afterwards
* @param[string] source source folder
* @param[string] destination destination folder
*/
export async function moveHTML(source: string, destination: string): Promise<void> {
try {
// Crée le dossier de destination s'il n'existe pas
await fs.mkdir(destination, { recursive: true })

// Lire le contenu du dossier source
const entries = await fs.readdir(source, { withFileTypes: true })

for (const entry of entries) {
const sourcePath = path.join(source, entry.name)
const destinationPath = path.join(destination, entry.name)

if (entry.isDirectory()) {
// Appel récursif pour les sous-dossiers
await moveHTML(sourcePath, destinationPath)
} else if (path.extname(entry.name).toLowerCase() === ".html") {
// Copier le fichier HTML
await fs.copyFile(sourcePath, destinationPath)
await fs.unlink(sourcePath) // Supprimer le fichier source après copie
// console.log(`File ${entry.name} moved from ${source} to ${destination}`)
}
}

// Vérifier si le dossier source est vide après déplacement des fichiers HTML
const remainingEntries = await fs.readdir(source)
if (remainingEntries.length === 0) {
await fs.rmdir(source) // Supprimer le dossier source s'il est vide
}
} catch (err) {
console.error(`Erreur lors du déplacement des fichiers HTML : ${err}`)
}
}
133 changes: 67 additions & 66 deletions apps/front/prerender/prerender.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,87 @@
// @ts-ignore
import { render } from "~/index-server"
import * as mfs from "@cher-ami/mfs"
import path from "path"
import chalk from "chalk"
import { renderToPipeableStream } from "react-dom/server"
import { loadEnv } from "vite"
import { render } from "~/index-server"
import config from "../config/config.js"
import { isRouteIndex } from "./helpers/isRouteIndex"
import { createHtmlFile, moveFolder, moveHTML } from "./helpers/filesManipulation.js"
import { ManifestParser } from "./helpers/ManifestParser"
import { renderToPipeableStream, renderToString } from "react-dom/server"
import { JSXElementConstructor, ReactElement } from "react"

/**
* Prerender
* Create static HTML files from react render DOM
* @param urls: Urls to generate
* @param outDirStatic: Generation destination directory
*/
export const prerender = async (
urls: string[],
outDirStatic = config.outDirStaticClient
) => {
const indexTemplateSrc = `${outDirStatic}/index-template.html`
export const prerender = async (urls: string[]) => {
try {
// Define output folders (_temp & client)
const outDirStatic = config.outDirStaticClient
const outDirStaticTemp = config.outDirStaticClientTemp

// copy index as template to avoid the override with the generated static index.html bellow
if (!(await mfs.fileExists(indexTemplateSrc))) {
await mfs.copyFile(`${outDirStatic}/index.html`, indexTemplateSrc)
}
// Define if comes from :
// - build (npm run build:static)
// - generate (server /generate or npm run generate)
const isGenerate = !(await mfs.fileExists(`${outDirStaticTemp}/.vite/manifest.json`))

// get script tags to inject in render
const base = loadEnv("", process.cwd(), "").VITE_APP_BASE || process.env.VITE_APP_BASE
let manifest = null

// get script tags to inject in render
const base = loadEnv("", process.cwd(), "").VITE_APP_BASE || process.env.VITE_APP_BASE
const manifest = (await mfs.readFile(`${outDirStatic}/.vite/manifest.json`)) as string
const scriptTags = ManifestParser.getScriptTagFromManifest(manifest, base)
// If from build, use manifest file from _temp/
if (!isGenerate) {
manifest = await mfs.readFile(`${outDirStaticTemp}/.vite/manifest.json`)
} else {
// Else from client/
manifest = await mfs.readFile(`${outDirStatic}/.vite/manifest.json`)
}

const scriptTags = ManifestParser.getScriptTagFromManifest(manifest, base)
let errorOnRender = false
const renderPromises: Promise<void>[] = []

// pre-render each route
for (let url of urls) {
url = url.startsWith("/") ? url : `/${url}`
// pre-render each route
for (let url of urls) {
let formattedURL = url.startsWith("/") ? url : `/${url}`

try {
// Request DOM
const dom = await render(url, scriptTags, true, base)
// create stream and generate current file when all DOM is ready
renderToPipeableStream(dom, {
onAllReady() {
createHtmlFile(urls, url, outDirStatic, dom)
},
onError(x) {
console.error(x)
}
})
} catch (e) {
console.log(e)
try {
// Request DOM
const dom = await render(formattedURL, scriptTags, true, base)
// create stream and generate current file when all DOM is ready
renderPromises.push(
new Promise<void>((resolve, rejects) => {
renderToPipeableStream(dom, {
onAllReady: async () => {
await createHtmlFile(urls, formattedURL, outDirStaticTemp, dom)
resolve()
},
onError(x) {
errorOnRender = true
console.error(x)
rejects(new Error("Error on renderToPipeableStream"))
}
})
})
)
} catch (e) {
console.log(e)
}
}
}
}
await Promise.all(renderPromises)

/**
* Create a single HTML file
* @param urls: All urls to generate
* @param url: Current URL to generate
* @param outDir: Generation destination directory
* @param dom: React DOM from index-server.tsx
*/
const createHtmlFile = async (
urls: string[],
url: string,
outDir: string,
dom: ReactElement<any, string | JSXElementConstructor<any>>
): Promise<void> => {
// Prepare file
if (isRouteIndex(url, urls)) url = `${url}/index`
const routePath = path.resolve(`${outDir}/${url}`)
const htmlFilePath = `${routePath}.html`
// Create file
await mfs.createFile(htmlFilePath, htmlReplacement(renderToString(dom)))
console.log(chalk.green(` → ${htmlFilePath.split("static")[1]}`))
if (errorOnRender) {
console.error(chalk.red("Error on render"))
process.exit(1)
} else if (!isGenerate) {
// If from build, move whole folder
await moveFolder(outDirStatic, "dist/static/old")
await moveFolder(outDirStaticTemp, outDirStatic)
} else {
// If from generate, move html files only
await moveHTML(outDirStaticTemp, outDirStatic)
}
} catch (e) {
console.error(e)
}
}

/**
* Render string patch middleware
*/
const htmlReplacement = (render: string): string =>
render
.replace("<html", `<!DOCTYPE html><html`)
.replaceAll('<script nomodule=""', "<script nomodule")
.replaceAll('crossorigin="anonymous"', "crossorigin")
2 changes: 1 addition & 1 deletion apps/front/prerender/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const fetchAvailableUrls = async (): Promise<string[]> => {
"/work",
"/work/first",
"/work/second",
"/404"
"/404",
])
})
}

0 comments on commit 6d39959

Please sign in to comment.