-
-
Save andrewcourtice/748a6cb52e75b58a63727d712bb3b817 to your computer and use it in GitHub Desktop.
Vite search index plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs'; | |
import path from 'path'; | |
import getPagesPlugin from 'vite-plugin-pages'; | |
import generateSitemap from 'vite-plugin-pages-sitemap'; | |
import merge from 'lodash/merge.js'; | |
import startCase from 'lodash/startCase.js'; | |
import parseFrontmatter from '@yankeeinlondon/gray-matter'; | |
import markdownIt from 'markdown-it'; | |
import { | |
parse as parseVue, | |
} from '@vue/compiler-sfc'; | |
import { | |
parse as parseHTML, | |
} from 'node-html-parser'; | |
const markdownParser = markdownIt('commonmark'); | |
// Remove code blocks from search results | |
markdownParser.use(md => { | |
md.renderer.rules.fence = () => ''; | |
}); | |
/** | |
* @typedef FileRoutesPluginOptions | |
* @property { string? } dir | |
*/ | |
/** | |
* @typedef FileRouteMeta | |
* @property { string? } title | |
* @property { string? } description | |
* @property { object } search | |
*/ | |
const EXTENSIONS = { | |
vue: 'vue', | |
markdown: 'md', | |
}; | |
const ROUTE_INFO_PARSER = { | |
[EXTENSIONS.vue]: parseVueRouteInfo, | |
[EXTENSIONS.markdown]: parseMarkdownRouteInfo, | |
}; | |
const CONTENT_PARSER = { | |
[EXTENSIONS.vue]: parseVueContent, | |
[EXTENSIONS.markdown]: parseMarkdownContent, | |
}; | |
/** | |
* @param { string } value | |
*/ | |
function titleCase(value) { | |
return startCase(value.toLocaleLowerCase()); | |
} | |
/** | |
* @param { import('vite-plugin-pages').VueRoute } route | |
* @param { Record<string, any> } meta | |
*/ | |
function assignBasicMeta(route, meta) { | |
return merge({}, route, { | |
meta, | |
}); | |
} | |
/** | |
* @param { import('vite-plugin-pages').VueRoute } route | |
* @param { string } source | |
* | |
* @returns { import('vite-plugin-pages').VueRoute } | |
*/ | |
function parseVueRouteInfo(route, source) { | |
return assignBasicMeta(route, { | |
isMarkdownComponent: false, | |
}); | |
} | |
/** | |
* @param { import('vite-plugin-pages').VueRoute } route | |
* @param { string } source | |
* | |
* @returns { import('vite-plugin-pages').VueRoute } | |
*/ | |
function parseMarkdownRouteInfo(route, source) { | |
const { | |
data, | |
} = parseFrontmatter(source); | |
return assignBasicMeta(data, { | |
isMarkdownComponent: true, | |
}); | |
} | |
/** | |
* @param { string } source | |
*/ | |
function parseVueContent(source) { | |
return parseVue(source).descriptor.template.content; | |
} | |
/** | |
* @param { string } source | |
*/ | |
function parseMarkdownContent(source) { | |
const { | |
content, | |
} = parseFrontmatter(source); | |
return markdownParser.render(content); | |
} | |
/** | |
* @param { FileRoutesPluginOptions } options | |
* | |
* @returns { import('vite').Plugin } | |
*/ | |
export default function fileRoutesPlugin(options) { | |
const searchIndex = []; | |
let root = process.cwd(); | |
const pagesPlugin = getPagesPlugin({ | |
dirs: ['src/routes'], | |
resolver: 'vue', | |
extensions: ['vue', 'md'], | |
moduleId: 'virtual:file-routes', | |
/** | |
* @param { import('vite-plugin-pages').VueRoute } route | |
*/ | |
extendRoute(route) { | |
const filePath = path.join(root, route.component); | |
let { | |
name: fileName, | |
ext: fileExtension, | |
} = path.parse(filePath); | |
fileExtension = fileExtension.replace('.', ''); | |
const routeInfoParser = ROUTE_INFO_PARSER[fileExtension]; | |
const contentParser = CONTENT_PARSER[fileExtension]; | |
const source = fs.readFileSync(filePath, { | |
encoding: 'utf-8', | |
}); | |
const { | |
meta = {}, | |
...routeInfo | |
} = routeInfoParser(route, source, filePath) || {}; | |
const output = { | |
...route, | |
...routeInfo, | |
meta: { | |
title: titleCase(fileName), | |
description: '', | |
tags: [], | |
...meta, | |
}, | |
}; | |
if (output.meta?.search?.indexed === false) { | |
return output; | |
} | |
try { | |
const content = parseHTML(contentParser(source)) | |
.structuredText | |
.trim() | |
.replace(/\{\{.*\}\}/g, ''); | |
if (content) { | |
searchIndex.push({ | |
content, | |
title: output.meta.title, | |
description: output.meta.description, | |
tags: output.meta.tags.join(), | |
route: { | |
name: route.name, | |
path: route.path, | |
}, | |
}); | |
} | |
} catch (error) { | |
console.warn(error); | |
} | |
return output; | |
}, | |
onRoutesGenerated(routes) { | |
return generateSitemap({ | |
routes, | |
changefreq: 'weekly', | |
// hostname TODO | |
}); | |
}, | |
}); | |
return { | |
...pagesPlugin, | |
name: 'fathom:pages-plugin', | |
async configResolved(config) { | |
root = config.root; | |
return pagesPlugin.configResolved(config); | |
}, | |
generateBundle(options, bundle) { | |
this.emitFile({ | |
type: 'asset', | |
fileName: 'search-index.json', | |
source: JSON.stringify(searchIndex), | |
}); | |
}, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment