-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpartials.ts
More file actions
100 lines (80 loc) · 2.71 KB
/
Copy pathpartials.ts
File metadata and controls
100 lines (80 loc) · 2.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import fs from 'fs';
import path from 'path';
import type { Plugin } from 'vite';
// {{> partial variable="example" }}
const PARTIAL_REGEX = /\{\{\>\s*(\w+)(?:\s+([^}]+))?\s*\}\}/g;
// {{ js expression }}
const JS_EXPR_REGEX = /\{\{\s*([\s\S]+?)\s*\}\}/g;
// variable="value"
const ATTR_REGEX = /(\w+)=["']([^"']+)["']/g;
const DIRECTORY = 'partials';
type Context = Record<string, any>;
function parse_attrs(attr_string: string | undefined): Context {
const attrs: Context = {};
if (!attr_string) return attrs;
let match: RegExpExecArray | null;
while ((match = ATTR_REGEX.exec(attr_string))) {
attrs[match[1]] = match[2];
}
return attrs;
}
function find_partial(name: string, root: string): string | null {
const dir = path.resolve(root, DIRECTORY);
if (!fs.existsSync(dir)) return null;
const files = fs.readdirSync(dir);
for (const file of files) {
if (path.parse(file).name === name) return path.join(dir, file);
}
return null;
}
function render_partials(content: string, root: string = process.cwd()): string {
return content.replace(PARTIAL_REGEX, (_, name, attr) => {
const file_path = find_partial(name, root);
if (!file_path) throw new Error(`Could not find ${name} in ${DIRECTORY}`);
const partial_content = fs.readFileSync(file_path, 'utf8');
const attrs = parse_attrs(attr);
return render(partial_content, attrs, root);
});
}
function evaluate_expressions(content: string, context: Context = {}): string {
return content.replace(JS_EXPR_REGEX, (_, expr) => {
// make all variables exist as undefined to allow `title ?? 'default'`
const safeContext = new Proxy(context, {
has: () => true,
get: (target, prop) => target[prop as string],
});
const fn = new Function('ctx', `with (ctx) { return (${expr}) }`);
return fn(safeContext);
});
}
function render(content: string, context: Context = {}, root: string = process.cwd()): string {
// substitute partials first
content = render_partials(content, root);
// then evaluate js expression
content = evaluate_expressions(content, context);
return content;
}
export default function partials(): Plugin {
// runs on main build input files
function transform_html(code: string, id: string): string | null {
if (!id.endsWith('.html')) return null;
return render(code);
}
// run on result in dev?
function transform_index(code: string): string {
return render(code);
}
// full reload on changes
function hot_update({ file, server }) {
if (!file.startsWith(path.resolve(process.cwd(), DIRECTORY))) return;
server.ws.send({ type: 'full-reload', path: '*' });
return [];
}
return {
name: 'vite-partials',
enforce: 'pre',
transform: transform_html,
transformIndexHtml: transform_index,
handleHotUpdate: hot_update,
};
}