import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; export const PRODUCT_DIRS = ['smartbuddy', 'smartinput', 'smarthanzi', 'massiveedit', 'html']; const SKIP_NAMES = new Set([ '.DS_Store', '.texpadtmp', 'smartbuddy_readme', ]); const SKIP_EXT = new Set([ '.tex', '.aux', '.log', '.toc', '.out', '.synctex.gz', ]); const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8', '.md': 'text/plain; charset=utf-8', '.apk': 'application/vnd.android.package-archive', '.deb': 'application/vnd.debian.binary-package', '.pkg': 'application/octet-stream', '.exe': 'application/octet-stream', '.zip': 'application/zip', }; export function shouldCopy(src) { const base = path.basename(src); if (SKIP_NAMES.has(base)) return false; if (base.startsWith('.')) return false; if (SKIP_EXT.has(path.extname(base))) return false; return true; } export function copyProductSites(destRoot, sourceRoot = process.cwd()) { for (const dir of PRODUCT_DIRS) { const from = path.join(sourceRoot, dir); if (!fs.existsSync(from)) continue; fs.cpSync(from, path.join(destRoot, dir), { recursive: true, filter: shouldCopy, }); } } function resolveProductFile(root, urlPath) { const clean = urlPath.split('?')[0]; const product = PRODUCT_DIRS.find( (name) => clean === `/${name}` || clean === `/${name}/` || clean.startsWith(`/${name}/`), ); if (!product) return null; let rel = clean.slice(product.length + 2); if (!rel || rel.endsWith('/')) rel = `${rel}index.html`; const file = path.join(root, product, rel); if (fs.existsSync(file) && fs.statSync(file).isFile()) return file; const asIndex = path.join(root, product, rel, 'index.html'); if (fs.existsSync(asIndex) && fs.statSync(asIndex).isFile()) return asIndex; return null; } function serveProduct(root, req, res, next) { const file = resolveProductFile(root, req.url ?? ''); if (!file) return next(); res.statusCode = 200; res.setHeader('Content-Type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream'); fs.createReadStream(file).pipe(res); } export function productSites() { const root = process.cwd(); return { name: 'product-sites', hooks: { 'astro:config:setup'({ updateConfig }) { updateConfig({ vite: { plugins: [ { name: 'serve-product-sites', configureServer(server) { server.middlewares.use((req, res, next) => serveProduct(root, req, res, next)); }, }, ], }, }); }, 'astro:build:done'({ dir }) { copyProductSites(fileURLToPath(dir)); }, }, }; }