UNPKG

htmldocs

Version:

<h1 align="center"> <img src="https://github.com/user-attachments/assets/655fa7f9-98e7-42ee-8cd0-bb9193f100e9" alt="htmldocs" width="100%" /> </h1>

133 lines (118 loc) 4.57 kB
import path from "node:path"; import fs from "node:fs"; import type { Loader, PluginBuild, ResolveOptions } from "esbuild"; import { Documentation, parse } from "react-docgen"; import * as tsj from "ts-json-schema-generator"; import { DOCUMENT_SCHEMAS_DIR } from "./paths"; /** * Made to export the `renderAsync` function out of the user's email template * so that issues like https://github.com/resend/react-email/issues/649 don't * happen. * * This avoids multiple versions of React being involved, i.e., the version * in the CLI vs. the version the user has on their document. * * Also, this plugin generates the schema for document components and saves it to .next */ export const htmldocsPlugin = (documentTemplates: string[], isBuild: boolean) => ({ name: "htmldocs-plugin", setup: (b: PluginBuild) => { b.onLoad( { filter: new RegExp(documentTemplates.join("|")) }, async ({ path: pathToFile }) => { let contents = await fs.promises.readFile(pathToFile, "utf8"); await generateAndWriteSchema(contents, pathToFile); if (isBuild) { // Replace all occurrences of /static with ./static contents = contents.replace(/\/static/g, './static'); } return { contents: `${contents}; export { renderAsync } from 'htmldocs-module-that-will-export-render' `, loader: path.extname(pathToFile).slice(1) as Loader, }; } ); b.onResolve( { filter: /^htmldocs-module-that-will-export-render$/ }, async (args) => { const options: ResolveOptions = { kind: "import-statement", importer: args.importer, resolveDir: args.resolveDir, namespace: args.namespace, }; let result = await b.resolve("@htmldocs/render", options); if (result.errors.length === 0) { return result; } if (result.errors.length > 0 && result.errors[0]) { result.errors[0].text = "Failed trying to import `renderAsync` from `@htmldocs/render` to be able to render your document template.\n Maybe you don't have `@htmldocs/render` installed?"; } return result; } ); }, }); async function generateAndWriteSchema(contents: string, filePath: string): Promise<void> { const componentProps = await parseFileToProps(contents, filePath); const componentInterfaceName = `ComponentProps`; let interfaceContent = `export interface ${componentInterfaceName} {\n`; for (const propName in componentProps) { const prop = componentProps[propName]; if (!prop || !prop.tsType) continue; const required = prop.required ? "" : "?"; const tsType = "raw" in prop.tsType ? prop.tsType.raw : prop.tsType?.name; interfaceContent += ` ${propName}${required}: ${tsType};\n`; } interfaceContent += `}\n`; const fileContents = contents + "\n" + interfaceContent; const tempFilePath = createTempFilePath(filePath); await fs.writeFileSync(tempFilePath, fileContents); const config = { path: tempFilePath, tsconfig: process.env.NEXT_PUBLIC_USER_PROJECT_LOCATION + "/tsconfig.json", type: componentInterfaceName, }; const schema = tsj.createGenerator(config).createSchema(config.type); fs.unlinkSync(tempFilePath); const schemaString = JSON.stringify(schema, null, 2); if (!fs.existsSync(DOCUMENT_SCHEMAS_DIR)) { fs.mkdirSync(DOCUMENT_SCHEMAS_DIR, { recursive: true }); } const baseName = path.basename(filePath, path.extname(filePath)); const schemaFilePath = path.join( DOCUMENT_SCHEMAS_DIR, baseName, `${baseName}.schema.json` ); // Ensure the directory exists before writing fs.mkdirSync(path.dirname(schemaFilePath), { recursive: true }); fs.writeFileSync(schemaFilePath, schemaString); } function createTempFilePath(filePath: string): string { const baseName = path.basename(filePath, path.extname(filePath)); const dirName = path.dirname(filePath); const extension = path.extname(filePath).replace(".", ""); const tempFileName = `.${baseName}.${extension}`; const tempFilePath = path.join(dirName, tempFileName); return tempFilePath; } const parseFileToProps = async ( contents: string, filePath: string ): Promise<Documentation["props"] | undefined> => { const componentsInfo = parse(contents, { babelOptions: { filename: filePath, babelrc: false, }, }); if (componentsInfo.length > 0 && componentsInfo[0]) { return componentsInfo[0].props; } else { return undefined; } };