/** * This triple-slash directive ensures the virtual module declarations are loaded when consumers * import the integration entrypoint in their Astro config, matching Starlight's package pattern. */ /// import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import UnoCSS from '@unocss/astro'; import type { AstroIntegration } from 'astro'; import type { DefaultTreeAdapterTypes } from 'parse5'; import { parseFragment } from 'parse5'; import type { ViteDevServer } from 'vite'; import pkg from '../package.json' with { type: 'json' }; import { createRecoveryConfigLoader } from './config-loader'; import { aboutLink, archiveLink, blogLink, type CommentsConfig, defineConfig, type ExternalLinkIndicator, friendsLink, type GiscusOptions, homeLink, type KomorebiFriend, type KomorebiNavLink, type KomorebiThemeLabels, type KomorebiThemeOptions, navLinks, type ResolvedKomorebiThemeOptions, resolveThemeOptions, } from './options'; import { createKomorebiUnoOptions } from './unocss'; const THEME_CONFIG_MODULE_ID = 'virtual:komorebi-theme/config'; const USER_CSS_MODULE_ID = 'virtual:komorebi-theme/user-css'; const routesDir = new URL('./routes/', import.meta.url); function route(pattern: string, file: string) { return { pattern, entrypoint: new URL(file, routesDir) }; } const THEME_ROUTES = [ route('/', 'index.astro'), route('/blog/[...id]', 'blog/[...id].astro'), route('/blog/[...page]', 'blog/[...page].astro'), route('/archive', 'archive.astro'), route('/about', 'about.astro'), route('/friends', 'friends.astro'), route('/rss.xml', 'rss.xml.ts'), route('/rss/styles.xsl', 'rss/styles.xsl.ts'), route('/giscus.css', 'giscus/styles.css.ts'), ]; export type { CommentsConfig, ExternalLinkIndicator, GiscusOptions, KomorebiFriend, KomorebiNavLink, KomorebiThemeLabels, KomorebiThemeOptions, ResolvedKomorebiThemeOptions, }; export { aboutLink, archiveLink, blogLink, defineConfig, friendsLink, homeLink, navLinks, }; function userCssVitePlugin(customCss: string[], root: URL) { const resolvedId = `\0${USER_CSS_MODULE_ID}`; const code = customCss .map((id) => { const resolved = id.startsWith('.') ? resolve(fileURLToPath(root), id) : id; return `import ${JSON.stringify(resolved)};`; }) .join('\n'); return { name: 'komorebi-theme/user-css', resolveId(id: string) { if (id === USER_CSS_MODULE_ID) return resolvedId; return undefined; }, load(id: string) { if (id === resolvedId) return code; return undefined; }, }; } export default function komorebi( inlineOptions?: KomorebiThemeOptions, ): AstroIntegration { let resolved: ResolvedKomorebiThemeOptions | undefined; let configFileList: string[] = []; let root: URL | undefined; let generatedConfigUrl: URL | undefined; const loadConfig = createRecoveryConfigLoader(); const hasInlineOptions = inlineOptions !== undefined && Object.keys(inlineOptions).length > 0; async function loadConfigOnce(configRoot: URL): Promise { if (hasInlineOptions) { resolved = resolveThemeOptions(inlineOptions); return; } const cwd = fileURLToPath(configRoot); const result = await loadConfig(cwd); resolved = resolveThemeOptions(result.config); configFileList = result.sources; } async function reloadConfig(): Promise { if (!root || hasInlineOptions || !generatedConfigUrl) return; const cwd = fileURLToPath(root); const result = await loadConfig(cwd); resolved = resolveThemeOptions(result.config); configFileList = result.sources; writeRuntimeConfig(generatedConfigUrl, resolved); } function getConfigHMRPlugin() { return { name: 'komorebi-theme:config-hmr', configureServer(server: ViteDevServer) { if (configFileList.length === 0) return; server.watcher.add(configFileList); server.watcher.on('change', async (path: string) => { if (configFileList.includes(path)) { await reloadConfig(); const module = server.moduleGraph.getModuleById( THEME_CONFIG_MODULE_ID, ); if (module) { server.moduleGraph.invalidateModule(module); server.reloadModule(module); } } }); }, }; } const themeContentGlobs = createThemeContentGlobs([ fileURLToPath(new URL('./runtime', import.meta.url)), fileURLToPath(new URL('./routes', import.meta.url)), ]); return { name: 'komorebi-theme', hooks: { 'astro:config:setup': async ({ addMiddleware, config, createCodegenDir, injectRoute, updateConfig, }) => { root = config.root; await loadConfigOnce(config.root); if (!resolved) { resolved = resolveThemeOptions({}); } const codegenDir = createCodegenDir(); generatedConfigUrl = new URL('config.mjs', codegenDir); writeRuntimeConfig(generatedConfigUrl, resolved); // const comments = resolved.comments; // const giscusTheme = // comments && 'theme' in comments ? comments.theme : undefined; // const customCssText = // typeof giscusTheme === 'string' ? giscusTheme : undefined; // let giscusCssEntrypoint: URL; // if (customCssText) { // const cssFile = new URL('giscus.css', codegenDir); // writeFileSync(cssFile, customCssText, 'utf-8'); // giscusCssEntrypoint = new URL('giscus.css.ts', codegenDir); // writeFileSync( // giscusCssEntrypoint, // `import css from './giscus.css?raw';\n` + // `export const GET = () => new Response(css, { headers: { 'Content-Type': 'text/css; charset=utf-8' } });\n`, // 'utf-8', // ); // } else { // giscusCssEntrypoint = new URL('giscus/styles.css.ts', routesDir); // } const vitePlugins = [ userCssVitePlugin(resolved.customCss, config.root), getConfigHMRPlugin(), ]; const indicatorSafelist = resolved.externalLinks === false ? [] : computeIndicatorSafelist(resolved.externalLinks.indicator); updateConfig({ integrations: [ UnoCSS( createKomorebiUnoOptions(themeContentGlobs, indicatorSafelist), ), ], markdown: { shikiConfig: { theme: 'github-light', }, }, vite: { define: { __KOMOREBI_THEME_VERSION__: `'${pkg.version}'`, }, plugins: vitePlugins, resolve: { alias: { [THEME_CONFIG_MODULE_ID]: fileURLToPath(generatedConfigUrl), }, }, }, }); for (const route of THEME_ROUTES) { injectRoute(route); } addMiddleware({ entrypoint: new URL( './middleware/external-links.ts', import.meta.url, ), order: 'pre', }); }, }, }; } function createThemeContentGlobs(dirs: string[]) { return dirs.map((dir) => `${normalizeGlobDir(dir)}/**/*.{astro,ts}`); } function normalizeGlobDir(dir: string) { return dir.replace(/\\/g, '/'); } function writeRuntimeConfig(file: URL, config: ResolvedKomorebiThemeOptions) { writeFileSync( file, `export default ${JSON.stringify(config, null, 2)};\n`, 'utf-8', ); } function extractClasses(node: DefaultTreeAdapterTypes.Node): string[] { const classes: string[] = []; if ('attrs' in node) { const classAttr = (node as DefaultTreeAdapterTypes.Element).attrs.find( (a) => a.name === 'class', ); if (classAttr) { classes.push(...classAttr.value.split(/\s+/).filter(Boolean)); } } if ('childNodes' in node) { for (const child of ( node as | DefaultTreeAdapterTypes.DocumentFragment | DefaultTreeAdapterTypes.Element ).childNodes) { classes.push(...extractClasses(child)); } } return classes; } function computeIndicatorSafelist(indicator: ExternalLinkIndicator): string[] { if (indicator === false) return []; if (typeof indicator === 'string') return [`i-${indicator}`]; const fragment = parseFragment(indicator.html); return extractClasses(fragment); }