UNPKG

@wroud/vite-plugin-ssg

Version:

A Vite plugin for static site generation (SSG) with React. Renders React applications to static HTML for faster load times and improved SEO.

314 lines (312 loc) 15.9 kB
import nodePath from "node:path"; import { createRunnableDevEnvironment } from "vite"; import { cleanUrl } from "./utils/cleanUrl.js"; import { createSsgId, isSsgId, removeSsgQuery } from "./modules/isSsgId.js"; import { createSsgServerEntryId, isSsgServerEntryId, } from "./modules/isSsgServerEntryId.js"; import { createSsgClientEntryId, isSsgClientEntryId, } from "./modules/isSsgClientEntryId.js"; import { addMainQuery, isMainId, removeMainQuery, } from "./modules/mainQuery.js"; import { addQueryParam, parseQueryParams } from "./utils/queryParam.js"; import { cleanSsgAssetId } from "./modules/isSsgAssetId.js"; import { isSsgHtmlTagsId } from "./utils/ssgHtmlTags.js"; import { createSsgPageUrlId, isSsgPageUrlId, removeSsgPageUrlId, } from "./modules/isSsgPageUrlId.js"; import { createSsgUrl, isSsgUrl, removeSsgUrl } from "./modules/isSsgUrl.js"; import { getPathsToLookup } from "./utils/getPathsToLookup.js"; import { existsSync } from "node:fs"; import { loadServerApi } from "./api/loadServerApi.js"; import { createSsgEntryQuery, isSsgEntryQuery, removeSsgEntryQuery, } from "./modules/ssgEntryQuery.js"; import { createVirtualHtmlEntry } from "./modules/isVirtualHtmlEntry.js"; import { createSsgComponentId } from "./modules/isSsgComponentId.js"; import { getPageName } from "./utils/getPageName.js"; import { ssgComponentResolution } from "./resolvers/ssgComponentResolution.js"; import { ssgAssetsResolutionPlugin } from "./resolvers/ssgAssetsResolution.js"; import { viteFsFallbackResolutionPlugin } from "./resolvers/viteFsFallbackResolution.js"; import { changePathExt } from "./utils/changePathExt.js"; import { pagesMiddleware } from "./server/pages-middleware.js"; import { getHrefFromPath } from "./utils/getHrefFromPath.js"; import { mapBaseToUrl } from "./utils/mapBaseToUrl.js"; import { ssrBundlePlugin } from "./resolvers/ssrBundlePlugin.js"; import { clientBundlePlugin } from "./resolvers/clientBundlePlugin.js"; import { stripBase } from "./utils/stripBase.js"; export * from "./react/IndexComponent.js"; export const ssgPlugin = (pluginOptions = { renderTimeout: 10000, }) => { const emittedPages = new Set(); return [ viteFsFallbackResolutionPlugin(), ssgAssetsResolutionPlugin(), ssrBundlePlugin(), clientBundlePlugin(pluginOptions.renderTimeout), { name: "@wroud/vite-plugin-ssg", enforce: "post", config(userConfig, env) { userConfig.environments = { ...userConfig.environments, ssr: { ...userConfig.environments?.["ssr"], dev: { ...userConfig.environments?.["ssr"]?.dev, createEnvironment(name, config) { return createRunnableDevEnvironment(name, config, { runnerOptions: { hmr: { logger: false } }, }); }, }, // resolve: { // dedupe: ["@wroud/vite-plugin-ssg"], // }, build: { ...userConfig.environments?.["ssr"]?.build, ssr: true, rollupOptions: { ...userConfig.environments?.["ssr"]?.build?.rollupOptions, // external: // env.command === "build" // ? [/^@wroud\/vite-plugin-ssg.*/] // : undefined, }, outDir: (userConfig.environments?.["ssr"]?.build?.outDir || userConfig.build?.outDir || "dist") + "-server", }, }, }; // userConfig.resolve = { // ...userConfig.resolve, // dedupe: [ // ...(userConfig.resolve?.dedupe || []), // "@wroud/vite-plugin-ssg", // ], // }; userConfig.builder = { async buildApp(builder) { await builder.build(builder.environments["ssr"]); await builder.build(builder.environments["client"]); }, }; }, buildStart: { handler() { emittedPages.clear(); }, }, configureServer: { order: "pre", async handler(server) { return () => { server.middlewares.use(pagesMiddleware(server, pluginOptions)); }; }, }, resolveId: { order: "pre", async handler(source, importer, options) { const config = this.environment.config; if (isSsgClientEntryId(source) || isSsgServerEntryId(source) || isSsgHtmlTagsId(source) || (importer && isMainId(source) && (isSsgClientEntryId(importer) || isSsgServerEntryId(importer)))) { const params = parseQueryParams(source); const componentResolved = await this.resolve(createSsgComponentId(cleanUrl(source)), source, options); if (!componentResolved) { return null; } return addQueryParam(cleanUrl(componentResolved.id), Object.keys(params)[0]); } if (isSsgId(source)) { if (this.environment.name === "ssr") { source = createSsgServerEntryId(removeSsgQuery(source)); } else { source = createSsgClientEntryId(removeSsgQuery(source)); } return await this.resolve(source, importer, { ...options, skipSelf: false, }); } if (isSsgPageUrlId(source)) { const alreadyResolved = options.custom?.["@wroud/vite-plugin-ssg:page-url-id"]?.resolved; if (alreadyResolved) { return alreadyResolved; } const resolved = await this.resolve(removeSsgPageUrlId(source), importer, options); if (!resolved) { this.error(`Failed to resolve SSG page URL: ${source}`); //@ts-ignore return null; } if (config.command === "build") { const name = nodePath.posix.relative(config.root, changePathExt(resolved.id, "")); this.emitFile({ id: createSsgEntryQuery(changePathExt(resolved.id, "")), name, fileName: this.environment.name === "ssr" ? changePathExt(name, ".js") : undefined, type: "chunk", importer: source, preserveSignature: "strict", }); } return this.resolve(changePathExt(createSsgPageUrlId(resolved.id), ""), importer, { ...options, custom: { ...options.custom, "@wroud/vite-plugin-ssg:page-url-id": { resolved: changePathExt(createSsgPageUrlId(resolved.id), ""), }, }, }); } if (isSsgEntryQuery(source)) { source = nodePath.posix.resolve(config.root, source); let resolvedId = await this.resolve(createSsgId(removeSsgEntryQuery(source)), importer, { ...options, skipSelf: false, }); if (!resolvedId) { this.error(`Failed to resolve SSG entry query: ${source}`); } if (config.command === "build" && this.environment.name === "client") { let name = nodePath.posix.relative(config.root, changePathExt(removeSsgEntryQuery(source), "")); this.emitFile({ id: createVirtualHtmlEntry(nodePath.posix.join(config.root, name)), name, type: "chunk", }); try { const ssrConfig = config.environments["ssr"]; const lookUpPaths = getPathsToLookup(name); let serverModulePath; for (const possiblePath of lookUpPaths) { const exists = existsSync(nodePath.join(config.root, ssrConfig.build.outDir, possiblePath + ".js")); if (exists) { serverModulePath = possiblePath; break; } } if (!serverModulePath) { this.error(`No SSG chunk found for: ${name}`); } const serverApiProvider = await loadServerApi(nodePath.join(config.root, ssrConfig.build.outDir, serverModulePath + `.js`)); const serverApi = await serverApiProvider.create({ base: mapBaseToUrl("/", config), href: getHrefFromPath(name, config), }); const routes = await serverApi.getPathsToPrerender(); await serverApi.dispose(); await serverApiProvider.dispose(); for (let route of routes) { route = stripBase(route, config.base); const id = createSsgUrl(route); if (emittedPages.has(id)) { continue; } emittedPages.add(id); const name = getPageName(route); this.emitFile({ id, name, type: "chunk", }); } } catch (error) { this.error(`Failed to import routes prerender: ${error}`); } } return resolvedId; } if (isSsgUrl(source)) { return this.resolve(createSsgEntryQuery(removeSsgUrl(source)), importer, { ...options, skipSelf: false, }); } return undefined; }, }, load: { order: "pre", async handler(id) { const config = this.environment.config; if (isMainId(id)) { if (config.command === "serve") { if (id.startsWith(config.root)) { id = nodePath.posix.relative(config.root, id); this.debug(`Transformed to server URL: ${id}`); } else { id = mapBaseToUrl("/@fs" + id, config); this.debug(`Transformed to fs path: ${id}`); } id = JSON.stringify(createSsgClientEntryId(removeMainQuery(id))); } else { id = `import.meta.ROLLUP_FILE_URL_${this.emitFile({ type: "chunk", id: createSsgClientEntryId(removeMainQuery(id)), })}`; } return { code: ` export default ${id}; `, moduleType: "js", }; } if (isSsgServerEntryId(id)) { return { code: ` import { create as createServer } from "@wroud/vite-plugin-ssg/react/server"; import Index from "${addQueryParam(cleanUrl(id), "server")}"; import mainScriptUrl from "${addMainQuery(cleanUrl(id))}"; export async function create(context) { return await createServer(Index, context, mainScriptUrl); } `, moduleType: "js", }; } if (isSsgClientEntryId(id)) { return { code: ` import { create } from "@wroud/vite-plugin-ssg/react/client"; import htmlTags from "${addQueryParam(cleanUrl(id), "ssg-html-tags")}"; import Index from "${addQueryParam(cleanUrl(id), "client")}"; import mainScriptUrl from "${addMainQuery(cleanUrl(id))}"; const context = {} const api = await create(Index, context, mainScriptUrl); await api.hydrate(htmlTags); `, moduleSideEffects: true, moduleType: "js", }; } if (isSsgHtmlTagsId(id)) { return { code: `export default __VITE_SSG_HTML_TAGS__;`, moduleType: "js", }; } if (isSsgPageUrlId(id)) { id = removeSsgPageUrlId(cleanSsgAssetId(id)); id = changePathExt(nodePath.posix.relative(config.root, id), ".html"); return { code: `export default ${JSON.stringify(id)};`, moduleType: "js", }; } return undefined; }, }, }, ssgComponentResolution(), ]; }; //# sourceMappingURL=ssgPlugin.js.map