vitempl 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +6 -8
- package/dist/engine-BqkJbOwH.d.ts +58 -0
- package/dist/engines/askama.d.ts +12 -0
- package/dist/engines/askama.js +28 -0
- package/dist/engines/handlebars.d.ts +5 -0
- package/dist/engines/handlebars.js +383 -0
- package/dist/engines/jinja.d.ts +11 -0
- package/dist/engines/jinja.js +27 -0
- package/dist/index.d.ts +60 -3
- package/dist/index.js +409 -0
- package/dist/jinja-like-BokTIprh.js +475 -0
- package/package.json +40 -26
- package/dist/index.cjs +0 -5
- package/dist/index.d.cts +0 -3
- package/dist/index.d.mts +0 -3
- package/dist/index.mjs +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizePath } from "vite";
|
|
4
|
+
import { hyphenate } from "@vue/shared";
|
|
5
|
+
import { babelParse, parse } from "vue/compiler-sfc";
|
|
6
|
+
//#region src/sfc.ts
|
|
7
|
+
/**
|
|
8
|
+
* Разбирает SFC и проверяет, что в нём нет ничего, что требует выполнения
|
|
9
|
+
* Vue: шаблоны выполняет шаблонизатор сервера.
|
|
10
|
+
*/
|
|
11
|
+
function parseSfc(source) {
|
|
12
|
+
const { descriptor, errors } = parse(source, { templateParseOptions: { whitespace: "preserve" } });
|
|
13
|
+
if (errors.length) return {
|
|
14
|
+
descriptor,
|
|
15
|
+
components: /* @__PURE__ */ new Map(),
|
|
16
|
+
errors: errors.map((error) => ({
|
|
17
|
+
message: error.message,
|
|
18
|
+
loc: "loc" in error && error.loc ? {
|
|
19
|
+
line: error.loc.start.line,
|
|
20
|
+
column: error.loc.start.column
|
|
21
|
+
} : {
|
|
22
|
+
line: 1,
|
|
23
|
+
column: 1
|
|
24
|
+
}
|
|
25
|
+
}))
|
|
26
|
+
};
|
|
27
|
+
const script = readScript(descriptor);
|
|
28
|
+
return {
|
|
29
|
+
descriptor,
|
|
30
|
+
components: script.components,
|
|
31
|
+
errors: [...checkBlocks(descriptor), ...script.errors]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Импортированные компоненты по именам тегов: во Vue компонент можно
|
|
36
|
+
* указать в шаблоне как `<AppHeader>` и как `<app-header>`
|
|
37
|
+
*/
|
|
38
|
+
function componentTags(sfc, templateName) {
|
|
39
|
+
const tags = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const [name, { source }] of sfc.components) {
|
|
41
|
+
const template = templateName(source);
|
|
42
|
+
tags.set(name, template).set(hyphenate(name), template);
|
|
43
|
+
}
|
|
44
|
+
return tags;
|
|
45
|
+
}
|
|
46
|
+
function checkBlocks(descriptor) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
const at = (loc) => ({
|
|
49
|
+
line: loc.start.line,
|
|
50
|
+
column: loc.start.column
|
|
51
|
+
});
|
|
52
|
+
if (!descriptor.template) errors.push({
|
|
53
|
+
message: "нет блока <template>",
|
|
54
|
+
loc: {
|
|
55
|
+
line: 1,
|
|
56
|
+
column: 1
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
else if (descriptor.template.lang) errors.push({
|
|
60
|
+
message: `<template lang="${descriptor.template.lang}"> не поддерживается`,
|
|
61
|
+
loc: at(descriptor.template.loc)
|
|
62
|
+
});
|
|
63
|
+
if (descriptor.script) errors.push({
|
|
64
|
+
message: "поддерживается только <script setup>",
|
|
65
|
+
loc: at(descriptor.script.loc)
|
|
66
|
+
});
|
|
67
|
+
for (const style of descriptor.styles) if (style.scoped || style.module) errors.push({
|
|
68
|
+
message: "<style scoped> и <style module> не поддерживаются: разметка не размечается классами Vue",
|
|
69
|
+
loc: at(style.loc)
|
|
70
|
+
});
|
|
71
|
+
for (const block of descriptor.customBlocks) errors.push({
|
|
72
|
+
message: `блок <${block.type}> не поддерживается`,
|
|
73
|
+
loc: at(block.loc)
|
|
74
|
+
});
|
|
75
|
+
return errors;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Читает `<script setup>`: в нём допускаются только импорты компонентов
|
|
79
|
+
* и типов, объявления типов и defineProps.
|
|
80
|
+
*/
|
|
81
|
+
function readScript(descriptor) {
|
|
82
|
+
const components = /* @__PURE__ */ new Map();
|
|
83
|
+
const errors = [];
|
|
84
|
+
const script = descriptor.scriptSetup;
|
|
85
|
+
if (!script) return {
|
|
86
|
+
components,
|
|
87
|
+
errors
|
|
88
|
+
};
|
|
89
|
+
const lineOffset = script.loc.start.line - 1;
|
|
90
|
+
let program;
|
|
91
|
+
try {
|
|
92
|
+
program = babelParse(script.content, {
|
|
93
|
+
sourceType: "module",
|
|
94
|
+
plugins: ["typescript"]
|
|
95
|
+
}).program;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const { loc } = error;
|
|
98
|
+
errors.push({
|
|
99
|
+
message: error.message,
|
|
100
|
+
loc: loc ? {
|
|
101
|
+
line: loc.line + lineOffset,
|
|
102
|
+
column: loc.column + 1
|
|
103
|
+
} : {
|
|
104
|
+
line: script.loc.start.line,
|
|
105
|
+
column: 1
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
components,
|
|
110
|
+
errors
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
for (const statement of program.body) {
|
|
114
|
+
const loc = {
|
|
115
|
+
line: (statement.loc?.start.line ?? 1) + lineOffset,
|
|
116
|
+
column: (statement.loc?.start.column ?? 0) + 1
|
|
117
|
+
};
|
|
118
|
+
switch (statement.type) {
|
|
119
|
+
case "ImportDeclaration": {
|
|
120
|
+
if (statement.importKind === "type" || statement.specifiers.length > 0 && statement.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type")) continue;
|
|
121
|
+
const [specifier] = statement.specifiers;
|
|
122
|
+
const source = statement.source.value;
|
|
123
|
+
if (source.endsWith(".vue") && statement.specifiers.length === 1 && specifier.type === "ImportDefaultSpecifier") {
|
|
124
|
+
components.set(specifier.local.name, {
|
|
125
|
+
source,
|
|
126
|
+
loc
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
errors.push({
|
|
131
|
+
message: `импорт "${source}": в <script setup> можно импортировать только компоненты (.vue) и типы`,
|
|
132
|
+
loc
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
case "TSInterfaceDeclaration":
|
|
137
|
+
case "TSTypeAliasDeclaration": continue;
|
|
138
|
+
case "ExpressionStatement":
|
|
139
|
+
if (isDefineProps(statement.expression)) continue;
|
|
140
|
+
break;
|
|
141
|
+
case "VariableDeclaration": if (statement.declarations.length === 1 && isDefineProps(statement.declarations[0].init)) continue;
|
|
142
|
+
}
|
|
143
|
+
errors.push({
|
|
144
|
+
message: "в <script setup> допускаются только импорты компонентов и типов, объявления типов и defineProps",
|
|
145
|
+
loc
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
components,
|
|
150
|
+
errors
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function isDefineProps(node) {
|
|
154
|
+
const call = node;
|
|
155
|
+
return call?.type === "CallExpression" && call.callee?.type === "Identifier" && call.callee.name === "defineProps";
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/utils.ts
|
|
159
|
+
/** Проверяет, что файл лежит внутри папки и не совпадает с ней */
|
|
160
|
+
function isInside(dir, file) {
|
|
161
|
+
const relative = path.relative(dir, file);
|
|
162
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/graph.ts
|
|
166
|
+
/**
|
|
167
|
+
* Обходит импорты компонентов от входов и разбирает используемые шаблоны:
|
|
168
|
+
* в сборку попадают только они.
|
|
169
|
+
*/
|
|
170
|
+
async function collectTemplates(entries, options) {
|
|
171
|
+
const { srcDir, load, display = (file) => file } = options;
|
|
172
|
+
const templates = /* @__PURE__ */ new Map();
|
|
173
|
+
const at = (file, loc) => `${display(file)}:${loc.line}:${loc.column}`;
|
|
174
|
+
async function visit(file, source, entry) {
|
|
175
|
+
const sfc = parseSfc(source);
|
|
176
|
+
templates.set(file, sfc);
|
|
177
|
+
for (const { source: specifier, loc } of sfc.components.values()) {
|
|
178
|
+
const target = path.resolve(path.dirname(file), specifier);
|
|
179
|
+
if (templates.has(target)) continue;
|
|
180
|
+
if (!isInside(srcDir, target)) throw new Error(`${at(file, loc)}: компонент "${specifier}" находится вне папки исходников ${display(srcDir)}`);
|
|
181
|
+
const targetSource = await load(target);
|
|
182
|
+
if (targetSource === void 0) {
|
|
183
|
+
const from = entry === file ? "" : ` (вход ${display(entry)})`;
|
|
184
|
+
throw new Error(`${at(file, loc)}: компонент "${specifier}" не найден: ${display(target)}${from}`);
|
|
185
|
+
}
|
|
186
|
+
await visit(target, targetSource, entry);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (templates.has(entry)) continue;
|
|
191
|
+
const source = await load(entry);
|
|
192
|
+
if (source === void 0) throw new Error(`${display(entry)}: файл не найден`);
|
|
193
|
+
await visit(entry, source, entry);
|
|
194
|
+
}
|
|
195
|
+
return templates;
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/check.ts
|
|
199
|
+
/**
|
|
200
|
+
* Проверяет исходник шаблона без сборки: ошибки разбора SFC и синтаксис,
|
|
201
|
+
* который не поддерживает шаблонизатор. Те же ошибки выдаёт сборка —
|
|
202
|
+
* функция нужна инструментам вроде линтера.
|
|
203
|
+
*/
|
|
204
|
+
function checkSource(source, engine) {
|
|
205
|
+
const sfc = parseSfc(source);
|
|
206
|
+
const { errors } = engine.generate(sfc.descriptor.template?.ast?.children ?? [], {
|
|
207
|
+
components: componentTags(sfc, (source) => source),
|
|
208
|
+
name: "",
|
|
209
|
+
entry: false,
|
|
210
|
+
templates: /* @__PURE__ */ new Map()
|
|
211
|
+
});
|
|
212
|
+
return [...sfc.errors, ...errors];
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/index.ts
|
|
216
|
+
/** Расширение исходников шаблонов */
|
|
217
|
+
const SOURCE_EXTENSION = ".vue";
|
|
218
|
+
/**
|
|
219
|
+
* Запрос к блоку `<style>` шаблона. Заканчивается на `lang.css`, чтобы Vite
|
|
220
|
+
* узнал в нём CSS — так же помечает свои стили плагин Vue.
|
|
221
|
+
*/
|
|
222
|
+
const STYLE_QUERY = "vitempl-style";
|
|
223
|
+
const styleRequestRE = new RegExp(`^(.+)\\?${STYLE_QUERY}&index=(\\d+)&lang\\.`);
|
|
224
|
+
/** Суффикс, по которому Vite распознаёт страницу как HTML */
|
|
225
|
+
const HTML_SUFFIX = ".html";
|
|
226
|
+
/**
|
|
227
|
+
* Собирает шаблоны для SSR на сервере.
|
|
228
|
+
*
|
|
229
|
+
* Исходники шаблонов — Vue SFC, от страниц и фрагментов обходятся импорты
|
|
230
|
+
* компонентов, и в сборку попадают только используемые. Каждый шаблон
|
|
231
|
+
* генерируется для заданного шаблонизатора, а страницы дополнительно
|
|
232
|
+
* подаются в Vite как HTML-входы: подключения скриптов и стилей
|
|
233
|
+
* заменяются на собранные ассеты.
|
|
234
|
+
*/
|
|
235
|
+
function vitempl(options) {
|
|
236
|
+
const { engine, srcDir = "src", pagesDir = "pages", fragmentsDir = "fragments", shell = "index.html", placeholder = "<!--app-html-->", outDir = "dist/templates" } = options;
|
|
237
|
+
let config;
|
|
238
|
+
let shellSource = "";
|
|
239
|
+
/** Страницы: HTML-модуль → файл исходника */
|
|
240
|
+
const pages = /* @__PURE__ */ new Map();
|
|
241
|
+
let fragments = [];
|
|
242
|
+
/** Разобранные исходники: файл → SFC */
|
|
243
|
+
let parsed = /* @__PURE__ */ new Map();
|
|
244
|
+
/** Сгенерированные шаблоны: файл исходника → шаблон */
|
|
245
|
+
const generated = /* @__PURE__ */ new Map();
|
|
246
|
+
/** Страницы после обработки Vite: файл исходника → HTML */
|
|
247
|
+
const output = /* @__PURE__ */ new Map();
|
|
248
|
+
const wrap = (html) => shellSource.replace(placeholder, () => html);
|
|
249
|
+
const display = (file) => normalizePath(path.relative(config.root, file));
|
|
250
|
+
/** Путь от srcDir без расширения — под этим именем шаблон регистрирует сервер */
|
|
251
|
+
const templateName = (file) => normalizePath(path.relative(path.resolve(config.root, srcDir), file)).slice(0, -4);
|
|
252
|
+
/** Шаблон и все шаблоны, которые он импортирует */
|
|
253
|
+
function reachable(file) {
|
|
254
|
+
const files = /* @__PURE__ */ new Set();
|
|
255
|
+
const visit = (current) => {
|
|
256
|
+
if (files.has(current)) return;
|
|
257
|
+
files.add(current);
|
|
258
|
+
for (const { source } of parsed.get(current)?.components.values() ?? []) visit(normalizePath(path.resolve(path.dirname(current), source)));
|
|
259
|
+
};
|
|
260
|
+
visit(normalizePath(file));
|
|
261
|
+
return files;
|
|
262
|
+
}
|
|
263
|
+
/** Ссылки на стили страницы и компонентов, которые она импортирует */
|
|
264
|
+
function styleTags(page) {
|
|
265
|
+
const tags = [];
|
|
266
|
+
for (const file of reachable(page)) {
|
|
267
|
+
const url = normalizePath(path.relative(config.root, file));
|
|
268
|
+
parsed.get(file)?.descriptor.styles.forEach((style, index) => {
|
|
269
|
+
tags.push({
|
|
270
|
+
tag: "link",
|
|
271
|
+
attrs: {
|
|
272
|
+
rel: "stylesheet",
|
|
273
|
+
href: `/${url}?${STYLE_QUERY}&index=${index}&lang.${style.lang ?? "css"}`
|
|
274
|
+
},
|
|
275
|
+
injectTo: "head"
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return tags;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
name: "vitempl",
|
|
283
|
+
apply: "build",
|
|
284
|
+
enforce: "pre",
|
|
285
|
+
configResolved(resolvedConfig) {
|
|
286
|
+
config = resolvedConfig;
|
|
287
|
+
},
|
|
288
|
+
async options(inputOptions) {
|
|
289
|
+
const pagesPath = path.resolve(config.root, srcDir, pagesDir);
|
|
290
|
+
const input = {};
|
|
291
|
+
pages.clear();
|
|
292
|
+
for (const file of await scan(pagesPath)) {
|
|
293
|
+
const id = normalizePath(file + HTML_SUFFIX);
|
|
294
|
+
const name = normalizePath(path.relative(pagesPath, file));
|
|
295
|
+
pages.set(id, file);
|
|
296
|
+
input[name.slice(0, -4)] = id;
|
|
297
|
+
}
|
|
298
|
+
if (pages.size === 0) throw new Error(`[templates] не найдено ни одной страницы ${SOURCE_EXTENSION} в ${display(pagesPath)}`);
|
|
299
|
+
fragments = await scan(path.resolve(config.root, srcDir, fragmentsDir));
|
|
300
|
+
return {
|
|
301
|
+
...inputOptions,
|
|
302
|
+
input
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
async buildStart() {
|
|
306
|
+
const templatesDir = path.resolve(config.root, outDir);
|
|
307
|
+
if (templatesDir === config.root || isInside(templatesDir, config.root)) this.error(`outDir ${outDir} не может содержать root`);
|
|
308
|
+
const shellPath = path.resolve(config.root, shell);
|
|
309
|
+
shellSource = await readFile(shellPath, "utf-8");
|
|
310
|
+
this.addWatchFile(shellPath);
|
|
311
|
+
if (!shellSource.includes(placeholder)) this.error(`${shell}: не найдено место для страницы ${placeholder}`);
|
|
312
|
+
const templates = await collectTemplates([...pages.values(), ...fragments], {
|
|
313
|
+
srcDir: path.resolve(config.root, srcDir),
|
|
314
|
+
load: loadTemplate,
|
|
315
|
+
display
|
|
316
|
+
}).catch((error) => this.error(error.message));
|
|
317
|
+
parsed = new Map([...templates].map(([file, sfc]) => [normalizePath(file), sfc]));
|
|
318
|
+
const onPages = new Set([...pages.values()].flatMap((page) => [...reachable(page)]));
|
|
319
|
+
for (const [file, sfc] of parsed) if (sfc.descriptor.styles.length > 0 && !onPages.has(file)) this.warn(`${display(file)}: блок <style> не попадёт в сборку — шаблон не используется ни на одной странице`);
|
|
320
|
+
generated.clear();
|
|
321
|
+
output.clear();
|
|
322
|
+
const entries = new Set([...pages.values(), ...fragments].map((file) => normalizePath(file)));
|
|
323
|
+
/** Разбор шаблонов: вызов компонента может зависеть от самого компонента */
|
|
324
|
+
const infos = /* @__PURE__ */ new Map();
|
|
325
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const [file, sfc] of templates) contexts.set(file, {
|
|
327
|
+
components: componentTags(sfc, (source) => templateName(path.resolve(path.dirname(file), source))),
|
|
328
|
+
name: templateName(file),
|
|
329
|
+
entry: entries.has(normalizePath(file)),
|
|
330
|
+
templates: infos
|
|
331
|
+
});
|
|
332
|
+
if (engine.analyze) for (const [file, sfc] of templates) infos.set(templateName(file), engine.analyze(sfc.descriptor.template?.ast?.children ?? [], contexts.get(file)));
|
|
333
|
+
const errors = [];
|
|
334
|
+
for (const [file, sfc] of templates) {
|
|
335
|
+
this.addWatchFile(file);
|
|
336
|
+
const result = engine.generate(sfc.descriptor.template?.ast?.children ?? [], contexts.get(file));
|
|
337
|
+
generated.set(file, result.code);
|
|
338
|
+
for (const { message, loc } of [...sfc.errors, ...result.errors]) errors.push(`${display(file)}:${loc.line}:${loc.column}: ${message}`);
|
|
339
|
+
}
|
|
340
|
+
if (errors.length) this.error(errors.join("\n"));
|
|
341
|
+
},
|
|
342
|
+
resolveId(id) {
|
|
343
|
+
return pages.has(id) ? id : null;
|
|
344
|
+
},
|
|
345
|
+
load(id) {
|
|
346
|
+
const style = styleRequestRE.exec(id);
|
|
347
|
+
if (style) return parsed.get(style[1])?.descriptor.styles[Number(style[2])]?.content ?? null;
|
|
348
|
+
const file = pages.get(id);
|
|
349
|
+
return file ? generated.get(file) ?? null : null;
|
|
350
|
+
},
|
|
351
|
+
transformIndexHtml: {
|
|
352
|
+
order: "pre",
|
|
353
|
+
handler(html, ctx) {
|
|
354
|
+
const file = pages.get(normalizePath(ctx.filename));
|
|
355
|
+
if (file) return {
|
|
356
|
+
html: wrap(html),
|
|
357
|
+
tags: styleTags(file)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
generateBundle: {
|
|
362
|
+
order: "post",
|
|
363
|
+
handler(_options, bundle) {
|
|
364
|
+
for (const [id, file] of pages) {
|
|
365
|
+
const fileName = normalizePath(path.relative(config.root, id));
|
|
366
|
+
const asset = bundle[fileName];
|
|
367
|
+
if (asset?.type !== "asset") return this.error(`Vite не собрал HTML страницы ${fileName}`);
|
|
368
|
+
output.set(file, typeof asset.source === "string" ? asset.source : new TextDecoder().decode(asset.source));
|
|
369
|
+
delete bundle[fileName];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
async writeBundle() {
|
|
374
|
+
const templatesDir = path.resolve(config.root, outDir);
|
|
375
|
+
if (config.build.emptyOutDir ?? isInside(config.root, templatesDir)) await rm(templatesDir, {
|
|
376
|
+
recursive: true,
|
|
377
|
+
force: true
|
|
378
|
+
});
|
|
379
|
+
for (const [file, code] of generated) {
|
|
380
|
+
const target = path.join(templatesDir, templateName(file) + engine.extension);
|
|
381
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
382
|
+
await writeFile(target, output.get(file) ?? code);
|
|
383
|
+
}
|
|
384
|
+
config.logger.info(`шаблоны (${engine.name}): ${generated.size} → ${display(templatesDir)}`);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** Находит исходники шаблонов в папке и её подпапках */
|
|
389
|
+
async function scan(dir) {
|
|
390
|
+
try {
|
|
391
|
+
return (await readdir(dir, {
|
|
392
|
+
recursive: true,
|
|
393
|
+
withFileTypes: true
|
|
394
|
+
})).filter((entry) => entry.isFile() && entry.name.endsWith(SOURCE_EXTENSION)).map((entry) => path.join(entry.parentPath, entry.name)).sort();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error.code === "ENOENT") return [];
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function loadTemplate(file) {
|
|
401
|
+
try {
|
|
402
|
+
return await readFile(file, "utf-8");
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error.code === "ENOENT") return;
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
export { checkSource, vitempl as default };
|