146 lines
6.2 KiB
TypeScript
146 lines
6.2 KiB
TypeScript
import type {PluginOption, HtmlTagDescriptor, ResolvedConfig} from 'vite';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import sirv from 'sirv';
|
|
|
|
const GHOST_ADMIN_PATH = path.resolve(__dirname, '../../ghost/core/core/built/admin');
|
|
const GHOST_ADMIN_DIST = path.resolve(__dirname, '../../ghost/admin/dist');
|
|
|
|
function isAbsoluteUrl(url: string): boolean {
|
|
return url.startsWith('http://') ||
|
|
url.startsWith('https://') ||
|
|
url.startsWith('/');
|
|
}
|
|
|
|
function prefixUrl(url: string, base: string): string {
|
|
if (isAbsoluteUrl(url)) return url;
|
|
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
return `${normalizedBase}/${url}`;
|
|
}
|
|
|
|
// Vite plugin to extract styles and scripts from Ghost admin index.html
|
|
export function emberAssetsPlugin() {
|
|
let config: ResolvedConfig;
|
|
|
|
return {
|
|
name: 'ember-assets',
|
|
configResolved(resolvedConfig) {
|
|
config = resolvedConfig;
|
|
},
|
|
transformIndexHtml: {
|
|
order: 'post',
|
|
handler() {
|
|
// Read from Ember's own build output (not the combined output
|
|
// in built/admin which gets overwritten by closeBundle and would
|
|
// accumulate duplicate path prefixes on repeated builds)
|
|
const indexPath = path.resolve(GHOST_ADMIN_DIST, 'index.html');
|
|
try {
|
|
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
const base = config.base || '/';
|
|
|
|
// Extract stylesheets
|
|
const styleRegex = /<link[^>]*rel="stylesheet"[^>]*href="([^"]*)"[^>]*>/g;
|
|
const styles: HtmlTagDescriptor[] = [];
|
|
let styleMatch;
|
|
while ((styleMatch = styleRegex.exec(indexContent)) !== null) {
|
|
styles.push({
|
|
tag: 'link',
|
|
attrs: {
|
|
rel: 'stylesheet',
|
|
href: prefixUrl(styleMatch[1], base)
|
|
}
|
|
});
|
|
}
|
|
// Extract scripts
|
|
const scriptRegex = /<script[^>]*src="([^"]*)"[^>]*><\/script>/g;
|
|
const scripts: HtmlTagDescriptor[] = [];
|
|
let scriptMatch;
|
|
while ((scriptMatch = scriptRegex.exec(indexContent)) !== null) {
|
|
scripts.push({
|
|
tag: 'script',
|
|
injectTo: 'body',
|
|
attrs: {
|
|
src: prefixUrl(scriptMatch[1], base)
|
|
}
|
|
});
|
|
}
|
|
|
|
// Extract meta tags
|
|
const metaRegex = /<meta name="ghost-admin\/config\/environment" content="([^"]*)"[^>]*>/g;
|
|
const metaTags: HtmlTagDescriptor[] = [];
|
|
let metaMatch;
|
|
while ((metaMatch = metaRegex.exec(indexContent)) !== null) {
|
|
metaTags.push({
|
|
tag: 'meta',
|
|
attrs: {
|
|
name: 'ghost-admin/config/environment',
|
|
content: metaMatch[1]
|
|
}
|
|
});
|
|
}
|
|
|
|
// Generate the virtual module content
|
|
return [...styles, ...scripts, ...metaTags];
|
|
} catch (error) {
|
|
console.warn('Failed to read Ghost admin index.html:', error);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
configureServer(server) {
|
|
// Serve Ember assets from the filesystem in development
|
|
const assetsMiddleware = sirv(path.resolve(GHOST_ADMIN_PATH, 'assets'), {
|
|
dev: true,
|
|
etag: true
|
|
});
|
|
|
|
const base = (server.config.base ?? '/ghost').replace(/\/$/, '');
|
|
const assetsPrefix = `${base}/assets/`;
|
|
|
|
server.middlewares.use((req, res, next) => {
|
|
if (req.url?.startsWith(assetsPrefix)) {
|
|
const originalUrl = req.url;
|
|
req.url = req.url.replace(assetsPrefix, '/');
|
|
assetsMiddleware(req, res, () => {
|
|
req.url = originalUrl;
|
|
next();
|
|
});
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
},
|
|
closeBundle() {
|
|
// Only copy assets during production builds
|
|
if (config.command === 'build') {
|
|
try {
|
|
// All legacy admin assets gets copied to the Ghost core
|
|
// admin assets folder by the Ember build
|
|
const ghostAssetsDir = path.resolve(GHOST_ADMIN_PATH, 'assets');
|
|
|
|
// React admin build output (apps/admin/dist/)
|
|
const reactAssetsDir = path.resolve(config.build.outDir, 'assets');
|
|
const reactIndexFile = path.resolve(config.build.outDir, 'index.html');
|
|
|
|
// Copy Ember assets to React build output to enable use of
|
|
// vite preview. This also prevents stale Ember assets from
|
|
// overwriting fresh ones in the next step.
|
|
fs.cpSync(ghostAssetsDir, reactAssetsDir, { recursive: true });
|
|
|
|
// Copy combined assets back to Ghost core admin assets folder
|
|
fs.cpSync(reactAssetsDir, ghostAssetsDir, {
|
|
recursive: true,
|
|
force: true
|
|
});
|
|
|
|
// Copy React index.html, overwriting the existing index.html
|
|
const forwardIndexFile = path.resolve(GHOST_ADMIN_PATH, 'index.html');
|
|
fs.copyFileSync(reactIndexFile, forwardIndexFile);
|
|
} catch (error) {
|
|
throw new Error(`Failed to copy admin assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
}
|
|
} as const satisfies PluginOption;
|
|
}
|