import {normalizePath, type Plugin} from "vite"; import color from 'picocolors' import fs from 'fs'; import ejs from 'ejs' import path from 'path' import history from 'connect-history-api-fallback'; // @ts-ignore import {name as pluginName} from '../package.json' export function viteMpaYdPlugin(): Plugin { let pageMap = {} let inputMap = {} // @ts-ignore const pathPrefixReg = /#{./|/}/ const template = 'index.html' const bodyInject = /<\/body>/; let root: string = '' function transform (code, id) { const page = pageMap[id] if (!page) return null return ejs.render(!page.entry ? code : code.replace(bodyInject, `\n`), {}, {filename: id, root: root}) } return { name: 'vite-plugin-mpa-yd', config(config, { command }) { // @ts-ignore const pageConfig = config.ydPages || {} let entries: string[] = [] for (const pageConfigKey in pageConfig) { // @ts-ignore const transformEntry = '/' + (pageConfig[pageConfigKey]?.entry || '').replace(pathPrefixReg, '') // @ts-ignore pageMap[`${pageConfigKey}.html`] = { name: pageConfigKey, entry: transformEntry } entries.push(transformEntry) // @ts-ignore inputMap[pageConfigKey] = `${pageConfigKey}.html` } // @ts-ignore return { appType: 'mpa', clearScreen: false, optimizeDeps: { entries, }, build: { rollupOptions: { input: inputMap, }, }, } }, resolveId (source, importer, options) { // @ts-ignore if (options.isEntry && pageMap[source]) { return source } }, load(id) { // @ts-ignore const page = pageMap[id] if (!page) return null return fs.readFileSync(template, 'utf-8'); }, transform, configureServer(server) { console.log('[configureServer]:', server); const { config, watcher, middlewares, pluginContainer, transformIndexHtml, } = server; const base = normalizePath(`/${config.base || '/'}/`); watcher.on('change', file => { if (file.endsWith('.html') && template === path.relative(config.root, file)) { server.ws.send({ type: 'full-reload', path: '*', }); } }) middlewares.use( history({ htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], rewrites: [ { from: new RegExp(normalizePath(`/${base}/(${Object.keys(inputMap).join('|')})`)), to: ctx => normalizePath(`/${inputMap[ctx.match[1]]}`), }, { from: /.*/, to: ctx => { const { parsedUrl: { pathname } } = ctx; return normalizePath(pathname?.endsWith('.html') ? pathname : `${pathname}/index.html`); }, }, ], }), ); middlewares.use(async (req, res, next) => { const accept = req.headers.accept; const url = req.url!; // Ignore request that are not html. if ( res.writableEnded || accept === '*/*' || !accept?.includes('text/html') ) { return next(); } // Uniform the request url, allows visiting files directly. const rewritten = url.startsWith(base) ? url : normalizePath(`/${base}/${url}`); const fileName = rewritten.replace(base, ''); // filename in page configuration can't start with '/', because the key of inputMap is relative path. // print rewriting log if verbose is true if (req.originalUrl !== url) { console.log( `[${pluginName}]: Rewriting ${color.blue(req.originalUrl)} to ${color.blue(rewritten)}`, ); } if (!pageMap[fileName]) { return next(); // This allows vite handling unmatched paths. } /** * The following 2 lines fixed #12. * When using cypress for e2e testing, we should manually set response header and status code. * Otherwise, it causes cypress testing process of cross-entry-page jumping hanging, which results in a timeout error. */ res.setHeader('Content-Type', 'text/html'); res.statusCode = 200; // load file let loadResult = await pluginContainer.load(fileName); if (!loadResult) { throw new Error(`Failed to load url ${fileName}`); } loadResult = typeof loadResult === 'string' ? loadResult : loadResult.code; res.end( await transformIndexHtml( url, // No transform applied, keep code as-is transform(loadResult, fileName) ?? loadResult, req.originalUrl, ), ); }); } } }