astro-lunr
/ plugin.mjs
158 lines
(150 sloc)
|
4.7 KB
| 1 | import fs from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import {rehype} from 'rehype' |
| 4 | import lunr from 'lunr'; |
| 5 | import crypto from 'node:crypto'; |
| 6 | |
| 7 | function traverseReplace(replace, tree) { |
| 8 | return transform(tree, null, null); |
| 9 | function transform(node, index, parent) { |
| 10 | let replacement = replace(node, index, parent); |
| 11 | if(replacement){ |
| 12 | return replacement; |
| 13 | } else { |
| 14 | if ('children' in node) { |
| 15 | return { |
| 16 | ...node, |
| 17 | children: node.children.flatMap( |
| 18 | (child, index) => transform(child, index, node) |
| 19 | ) |
| 20 | }; |
| 21 | } return node; |
| 22 | } |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | function parseLunrDocument(lunrTree){ |
| 27 | let doc = {}; |
| 28 | traverseReplace((node => { |
| 29 | if(node.type === "element"){ |
| 30 | switch(node.tagName){ |
| 31 | case "lunr-field": { |
| 32 | doc[node.properties.name] = node.properties.value; |
| 33 | break; |
| 34 | } |
| 35 | case "lunr-text": { |
| 36 | doc[node.properties.name] = node.children.filter(n => n.type === "text").map(n => n.value).join("\n"); |
| 37 | break; |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | }), lunrTree) |
| 42 | return doc; |
| 43 | } |
| 44 | |
| 45 | function indexLunrDocuments(canonicalUrl, addToBulk){ |
| 46 | return () => traverseReplace.bind(null, (node) => { |
| 47 | if(node.type === "element" && node.tagName === "lunr-document"){ |
| 48 | let doc = parseLunrDocument(node); |
| 49 | doc["canonicalUrl"] = canonicalUrl; |
| 50 | doc["__index__"] = node.properties.index; |
| 51 | addToBulk(doc); |
| 52 | return [] |
| 53 | } |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | |
| 58 | export function createVitePlugin({ config }) { |
| 59 | return { |
| 60 | name: '@siverv/astro-lunr:dev-server', |
| 61 | configureServer(viteServer) { |
| 62 | viteServer.middlewares.use((req, res, next) => { |
| 63 | if(req.url.endsWith("/idx.json") || req.url.endsWith("/docs.json")){ |
| 64 | let path = req.url; |
| 65 | if(config.base && path.startsWith(config.base)){ |
| 66 | path = path.replace(config.base, ""); |
| 67 | } |
| 68 | let preBuiltPath = new URL(path, config.outDir); |
| 69 | try { |
| 70 | var stat = fs.statSync(preBuiltPath); |
| 71 | } catch(err){ |
| 72 | err.message = "Could not find pre-built lunr-files - search is not available without first building your astro-pages at least once. " + err.toString(); |
| 73 | throw err; |
| 74 | } |
| 75 | res.writeHead(200, { |
| 76 | 'Content-Type': 'application/json', |
| 77 | 'Content-Length': stat.size |
| 78 | }); |
| 79 | return fs.createReadStream(preBuiltPath).pipe(res); |
| 80 | } |
| 81 | return next(); |
| 82 | }); |
| 83 | }, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | function getViteConfiguration(config) { |
| 88 | return { |
| 89 | plugins: [createVitePlugin(config)] |
| 90 | }; |
| 91 | } |
| 92 | |
| 93 | |
| 94 | export default function createPlugin({pathFilter, subDir, documentFilter, initialize, mapDocument, verbose}){ |
| 95 | let config = {}; |
| 96 | let pathsToIndex = [] |
| 97 | return { |
| 98 | name: '@siverv/astro-lunr:plugin', |
| 99 | hooks: { |
| 100 | 'astro:config:setup': (options) => { |
| 101 | if(options.command === "dev"){ |
| 102 | options.addRenderer({ |
| 103 | name: '@siverv/astro-lunr:renderer', |
| 104 | serverEntrypoint: '@siverv/astro-lunr/server/renderer.js', |
| 105 | }); |
| 106 | options.updateConfig({ vite: getViteConfiguration(options) }); |
| 107 | } |
| 108 | }, |
| 109 | 'astro:build:done': async ({pages, dir}) => { |
| 110 | let indexMap = new Map(); |
| 111 | const addToIndexMap = (doc) => { |
| 112 | if(documentFilter && !documentFilter(doc)){ |
| 113 | return; |
| 114 | } |
| 115 | const {__index__: index, ...rest} = doc; |
| 116 | if(!indexMap.has(index)){ |
| 117 | indexMap.set(index, []); |
| 118 | } |
| 119 | indexMap.get(index).push({ |
| 120 | ...rest, |
| 121 | id: doc["id"] || crypto.createHash('md5').update(doc["canonicalUrl"]).digest('hex') |
| 122 | }); |
| 123 | } |
| 124 | let documents = []; |
| 125 | for(let {pathname} of pages) { |
| 126 | if(pathFilter && !pathFilter(pathname)){ |
| 127 | continue; |
| 128 | } |
| 129 | let url = new URL((pathname ? pathname + "/" : "") + "index.html", dir); |
| 130 | let content = fs.readFileSync(url, "utf-8"); |
| 131 | let newDocuments = []; |
| 132 | let hyped = await rehype() |
| 133 | .use(indexLunrDocuments(pathname, (doc) => newDocuments.push(doc))) |
| 134 | .process(content); |
| 135 | if(newDocuments.length > 0) { |
| 136 | if(verbose){ |
| 137 | console.log(`Indexing ${newDocuments.length} doc(s) from ${pathname}`); |
| 138 | } |
| 139 | fs.writeFileSync(url, String(hyped)); |
| 140 | newDocuments.forEach(addToIndexMap) |
| 141 | } |
| 142 | } |
| 143 | for(let [index, documents] of indexMap){ |
| 144 | const idx = lunr(function () { |
| 145 | initialize(this, lunr); |
| 146 | documents.forEach(doc => this.add(doc)); |
| 147 | }) |
| 148 | if(mapDocument){ |
| 149 | documents = documents.map(mapDocument); |
| 150 | } |
| 151 | fs.mkdirSync(new URL(path.join(subDir || "", index || ""), dir), { recursive: true }); |
| 152 | fs.writeFileSync(new URL(path.join(subDir || "", index || "", 'idx.json'), dir), JSON.stringify(idx)); |
| 153 | fs.writeFileSync(new URL(path.join(subDir || "", index || "", 'docs.json'), dir), JSON.stringify(documents)); |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | } |