UNPKG

@aem-vite/vite-aem-plugin

Version:

A proxy server and starter kit for using Vite with Adobe Experience Manager.

249 lines (245 loc) 8.73 kB
// src/index.ts import { bundlesImportRewriter } from "@aem-vite/import-rewriter"; // src/helpers.ts import _debug from "debug"; import zlib from "node:zlib"; import viteReact from "@vitejs/plugin-react"; var prefix = "[vite-aem-plugin]"; var bundleEntries; var resolvedConfig; var debug = _debug("vite-aem-plugin"); function isObject(value) { return Object.prototype.toString.call(value) === "[object Object]"; } function getViteScripts() { const entries = []; entries.push('<script type="module" src="/@vite/client"></script>'); const isUsingReact = resolvedConfig.plugins.find(({ name }) => name === "vite:react-refresh"); if (isUsingReact) { entries.push(` <script type="module"> ${viteReact.preambleCode.replace("__BASE__", resolvedConfig.base)} </script> `); } for (const source of bundleEntries) { if (/\.(js|ts)x?/.test(source)) { entries.push(`<script type="module" src="/${source}"></script>`); } else if (/\.(css|less|sass|scss|postcss)/.test(source)) { entries.push(`<link rel="stylesheet" href="/${source}"/>`); } } return entries.join("\n"); } function replaceUrl(input, aemUrl) { return (input || "").replace(aemUrl, `http://${resolvedConfig.server.host}:${resolvedConfig.server.port}`); } function setBundleEntries(entries) { if (!bundleEntries) { bundleEntries = entries; } } function setResolvedConfig(config) { if (!resolvedConfig) { resolvedConfig = config; } } function configureAemProxy(aemUrl, options) { const clientlibsExpression = new RegExp( `<(?:script|link).*(?:src|href)="${options.clientlibsExpression ?? options.publicPath}.(?:(lc-\\w{32}-lc(.min)?)|((min.)?ACSHASH\\w{32})|(\\w{32}(.min)?))?.?(?:css|js)"(([\\w+])=['"]([^'"]*)['"][^>]*>|[^>]*(?:></script>|>))`, "g" ); debug("clientlibs (custom) expression", options.clientlibsExpression); debug("clientlibs expression", clientlibsExpression); return (proxy) => { proxy.on("proxyRes", (proxyRes, req, res) => { const requestUrl = req.url; const proxyHeaders = proxyRes && proxyRes.headers; const isHtmlRequest = proxyHeaders && proxyHeaders["content-type"] && proxyHeaders["content-type"].match(/(text\/html|application\/xhtml+xml)/); debug("is html request?", requestUrl, isHtmlRequest); const isGzipedRequest = proxyHeaders && proxyHeaders["content-encoding"] && proxyHeaders["content-encoding"].includes("gzip"); let cookieHeader = proxyHeaders && proxyHeaders["set-cookie"]; res.statusCode = proxyRes.statusCode || 200; if (isHtmlRequest) { const body = []; proxyRes.on("data", (chunk) => body.push(chunk)); proxyRes.on("end", () => { const data = Buffer.concat(body); const html = isGzipedRequest ? zlib.unzipSync(data).toString() : data.toString(); debug("parsing request for:", requestUrl); debug("content length", html.length); const matches = html.match(clientlibsExpression); debug("total clientlib matches:", matches); let replacedHtml = html; if (matches) { debug("stripping matched clientlibs:", matches); matches.forEach((match, index) => { replacedHtml = replacedHtml.replace(match, index === matches.length - 1 ? getViteScripts() : ""); }); } const isHtmlModified = replacedHtml.length !== html.length; debug("has content changed?", isHtmlModified ? "yes" : "no"); if (isHtmlModified) { try { res.setHeader("content-encoding", ""); res.setHeader("content-type", proxyHeaders["content-type"] ?? "text/html;charset=utf-8"); res.removeHeader("content-length"); res.end(replacedHtml); debug(`proxy ${requestUrl} with Vite DevServer entries`); } catch (err) { console.error("Something went wrong!\n\n", err.message); } } else { res.end(data.toString("binary")); debug(`proxy ${requestUrl} without changes.`); } }); } else { proxyRes.pipe(res); } if (cookieHeader) { cookieHeader = cookieHeader.map((val) => val.replace("Secure;", "")); } for (const header in proxyHeaders) { const headerValue = proxyHeaders[header]; if (Array.isArray(headerValue)) { res.setHeader( header, headerValue.map((h) => replaceUrl(h, aemUrl)) ); } else { res.setHeader(header, replaceUrl(headerValue, aemUrl)); } } }); proxy.on("error", (err, _req, res) => { res.writeHead(500, { "Content-Type": "text/plain" }); res.end(`${prefix} Something went wrong! ${err.message}`); }); }; } // src/index.ts function viteForAem(options) { if (!options) { throw new Error("No options were provided."); } const aemOptions = options.aem; const aemUrl = `http://${aemOptions?.host ?? "localhost"}:${aemOptions?.port ?? 4502}`; if (!options.publicPath || !options.publicPath.length) { throw new Error("A public path is required for the proxy server to find and inject Vite DevServer!"); } debug("using AEM URL: %s", aemUrl); debug("options:", aemOptions); const aemProxySegments = [ ...options.aemProxySegments ?? [], "aem", "apps", "bin", "conf", "content", "crx", "etc", "etc.clientlibs", "home", "libs", "login", "mnt", "system", "var", "(assets|editor|sites|screens)" ]; const aemProxySegmentsExp = new RegExp(`^/(${aemProxySegments.join("|")}(.html)?)/.*`).source; const aemContentPathsExp = `^/content/(${options.contentPaths.join("|")})(/.*)?`; debug("aem content paths:", aemContentPathsExp); debug("aem request segments:", aemProxySegmentsExp); const plugins = [ { enforce: "pre", name: "aem-vite:vite-aem-plugin", config(config) { const baseProxyOptions = { autoRewrite: true, changeOrigin: true, preserveHeaderKeyCase: true, secure: false, target: aemUrl, // These headers makes AEM believe that all requests are been made internally. This is important // to ensure that redirects and such behave correctly. headers: { Host: aemUrl.replace(/(^\w+:|^)\/\//, ""), Origin: aemUrl, Referer: aemUrl } }; debug("proxy options:", baseProxyOptions); config.build = { ...config.build || {}, // Always prefer maximum browser compatibility target: "es2015" }; config.server = { ...config.server || {}, open: config.server?.open ?? true, strictPort: true, proxy: { [aemContentPathsExp]: { ...baseProxyOptions, protocolRewrite: "http", selfHandleResponse: true, // Use a proxy response handler to dynamically change the response content for specific pages configure: configureAemProxy(aemUrl, options) }, // Handle all other AEM based requests [aemProxySegmentsExp]: { ...baseProxyOptions }, // Handle the initial interaction between the Vite DevServer and AEM "^/(index.html)?$": { ...baseProxyOptions } } }; return config; }, configResolved(config) { setResolvedConfig(config); const buildInput = config.build.rollupOptions?.input; let bundleEntries2 = []; if (buildInput) { if (typeof buildInput === "string") { bundleEntries2 = [buildInput]; } else if (Array.isArray(buildInput)) { bundleEntries2 = [...new Set(buildInput)]; } else if (isObject(buildInput)) { bundleEntries2 = Object.values(buildInput); } else { throw new Error( "Invalid value detected for rollupOptions.input. Please ensure it is a string, array or alias object." ); } } else { throw new Error("No input option(s) was provided via rollupOptions.input."); } setBundleEntries(bundleEntries2); } } ]; if (options.rewriterOptions) { const { caching, minify, resourcesPath } = options.rewriterOptions; plugins.push( bundlesImportRewriter({ caching, publicPath: options.publicPath, minify, resourcesPath }) ); } return plugins; } export { viteForAem };