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 | } |