Files
mygit/apps/admin/vite-backend-proxy.ts
DuckQ1u 93d1b7c3d3
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
first commit
2026-04-22 19:51:20 +07:00

142 lines
4.7 KiB
TypeScript

import type { Plugin, ProxyOptions } from "vite";
import type { IncomingMessage } from "http";
import { getSubdir, GHOST_URL } from "./vite.config";
/**
* Resolves the configured Ghost site URL by calling the admin api site endpoint
* with retries (up to 20 seconds).
*/
async function resolveGhostSiteUrl() {
const MAX_ATTEMPTS = 20;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
const siteEndpoint = new URL('ghost/api/admin/site/', GHOST_URL);
const response = await fetch(siteEndpoint);
const data = (await response.json()) as { site: { url: string } };
return {
url: data.site.url,
host: new URL(data.site.url).host,
};
} catch (error) {
if (attempt === MAX_ATTEMPTS) throw error;
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
}
}
throw new Error("Failed to resolve Ghost site URL");
}
/**
* Creates proxy configuration for Ghost Admin API requests. Rewrites cookies
* and headers to work with Ghost's security middleware.
*/
function createAdminApiProxy(site: {
url: string;
host: string;
}): Record<string, ProxyOptions> {
// When running the dev server against the backend on HTTPS, we need to
// remove the same site and secure flags from the cookie. Otherwise, the
// browser won't set it correctly since the dev server is running on HTTP.
const rewriteCookies = (proxyRes: IncomingMessage) => {
const cookies = proxyRes.headers["set-cookie"];
if (Array.isArray(cookies)) {
proxyRes.headers["set-cookie"] = cookies.map((cookie) => {
return cookie
.split(";")
.filter((v) => v.trim().toLowerCase() !== "secure")
.filter((v) => v.trim().toLowerCase() !== "samesite=none")
.join("; ");
});
}
};
const subdir = getSubdir();
return {
[`^${subdir}/ghost/api/.*`]: {
target: site.url,
changeOrigin: true,
followRedirects: true,
autoRewrite: true,
cookieDomainRewrite: {
"*": site.host,
},
configure(proxy) {
proxy.on("proxyRes", rewriteCookies);
},
},
};
}
/**
* Creates proxy configuration for Ember CLI live reload script.
*/
function createEmberLiveReloadProxy(): Record<string, ProxyOptions> {
return {
"^/ember-cli-live-reload.js": {
target: "http://localhost:4200",
changeOrigin: true,
},
};
}
/**
* Vite plugin that injects proxy configurations for:
* 1. Ghost Admin API - proxies /ghost/api requests to the Ghost backend
* 2. Ember Live Reload - proxies ember-cli-live-reload.js to Ember dev server
*/
export function ghostBackendProxyPlugin(): Plugin {
let siteUrl!: { url: string; host: string };
return {
name: "ghost-backend-proxy",
async configResolved(config) {
// Only resolve backend URL for dev/preview, not for builds or tests
if (config.command !== 'serve' || config.mode === 'test') return;
try {
// We expect this to succeed immediately, but if the backend
// server is getting started, it might need some time.
// In that case, this lets the user know in case we're barking
// up the wrong tree (aka the GHOST_URL is wrong.)
const timeout = setTimeout(() => {
config.logger.info(`Trying to reach Ghost Admin API at ${GHOST_URL}...`);
}, 1000);
siteUrl = await resolveGhostSiteUrl();
clearTimeout(timeout);
config.logger.info(`👻 Using backend url: ${siteUrl.url}`);
} catch (error) {
config.logger
.error(`Could not reach Ghost Admin API at: ${GHOST_URL}
Ensure the Ghost backend is running. If needed, set the GHOST_URL environment variable to the correct URL.
`);
throw error;
}
},
configureServer(server) {
if (!siteUrl) return;
server.config.server.proxy = {
...server.config.server.proxy,
...createAdminApiProxy(siteUrl),
...createEmberLiveReloadProxy(),
};
},
configurePreviewServer(server) {
if (!siteUrl) return;
server.config.preview.proxy = {
...server.config.preview.proxy,
...createAdminApiProxy(siteUrl),
};
},
} as const satisfies Plugin;
}