-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #185 from cher-ami/generate-temp-folder
Generate html files in _temp folder, then move it to outDir if no errors.
- Loading branch information
Showing
5 changed files
with
198 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters