@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
JavaScript
// 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
};