@rodavel/vite-plugin-content-tree 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 +21 -0
- package/README.md +66 -0
- package/dist/index.cjs +537 -0
- package/dist/index.d.cts +277 -0
- package/dist/index.d.ts +277 -0
- package/dist/index.js +484 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var INDEX_SUFFIX = "/index";
|
|
3
|
+
var DEFAULT_META_FILE = "_meta.json";
|
|
4
|
+
var DEFAULT_PAGE_FILE = "index.mdx";
|
|
5
|
+
var DEFAULT_ORDER = 999;
|
|
6
|
+
|
|
7
|
+
// src/generator.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { mkdir, rename, unlink, writeFile } from "fs/promises";
|
|
10
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/scanner.ts
|
|
13
|
+
import { readdir, readFile } from "fs/promises";
|
|
14
|
+
import { dirname, join, relative, resolve } from "path";
|
|
15
|
+
import matter from "gray-matter";
|
|
16
|
+
var BASE_FIELDS = /* @__PURE__ */ new Set(["title", "description", "order", "nav"]);
|
|
17
|
+
var META_BASE_FIELDS = /* @__PURE__ */ new Set(["name", "order"]);
|
|
18
|
+
var defaultMapFrontmatter = (raw) => {
|
|
19
|
+
const result = {
|
|
20
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
21
|
+
description: typeof raw.description === "string" ? raw.description : void 0,
|
|
22
|
+
nav: typeof raw.nav === "string" ? raw.nav : void 0,
|
|
23
|
+
order: typeof raw.order === "number" ? raw.order : void 0
|
|
24
|
+
};
|
|
25
|
+
const extra = {};
|
|
26
|
+
let hasExtra = false;
|
|
27
|
+
for (const key of Object.keys(raw)) {
|
|
28
|
+
if (!BASE_FIELDS.has(key)) {
|
|
29
|
+
extra[key] = raw[key];
|
|
30
|
+
hasExtra = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (hasExtra) {
|
|
34
|
+
result.extra = extra;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
async function readDirectoryMeta(docsDir, dirPath, metaFile) {
|
|
39
|
+
const metaPath = join(docsDir, dirPath, metaFile);
|
|
40
|
+
const resolvedMeta = resolve(metaPath);
|
|
41
|
+
const resolvedRoot = resolve(docsDir);
|
|
42
|
+
if (!resolvedMeta.startsWith(`${resolvedRoot}/`) && resolvedMeta !== resolvedRoot) {
|
|
43
|
+
throw new Error(`Metadata path escapes content root: ${metaPath}`);
|
|
44
|
+
}
|
|
45
|
+
let content;
|
|
46
|
+
try {
|
|
47
|
+
content = await readFile(resolvedMeta, "utf-8");
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.code === "ENOENT") {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Failed to read metadata file: ${metaPath}`, { cause: err });
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(content);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`Failed to parse metadata file: ${metaPath}`, { cause: err });
|
|
59
|
+
}
|
|
60
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
61
|
+
const actual = Array.isArray(parsed) ? "array" : typeof parsed;
|
|
62
|
+
throw new Error(`Metadata file must contain a JSON object, got ${actual}: ${metaPath}`);
|
|
63
|
+
}
|
|
64
|
+
const obj = parsed;
|
|
65
|
+
const meta = {};
|
|
66
|
+
if (typeof obj.name === "string") meta.name = obj.name;
|
|
67
|
+
if (typeof obj.order === "number" && Number.isFinite(obj.order)) meta.order = obj.order;
|
|
68
|
+
const extra = {};
|
|
69
|
+
let hasExtra = false;
|
|
70
|
+
for (const key of Object.keys(obj)) {
|
|
71
|
+
if (!META_BASE_FIELDS.has(key)) {
|
|
72
|
+
extra[key] = obj[key];
|
|
73
|
+
hasExtra = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (hasExtra) {
|
|
77
|
+
meta.extra = extra;
|
|
78
|
+
}
|
|
79
|
+
return meta;
|
|
80
|
+
}
|
|
81
|
+
async function scanPages(docsDir, pageFile, mapFrontmatter = defaultMapFrontmatter) {
|
|
82
|
+
const pages = [];
|
|
83
|
+
async function walk(dir) {
|
|
84
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
85
|
+
const subdirs = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const full = join(dir, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
subdirs.push(full);
|
|
90
|
+
} else if (entry.name === pageFile) {
|
|
91
|
+
const relPath = relative(docsDir, full).replace(/\\/g, "/");
|
|
92
|
+
const dirRel = dirname(relPath);
|
|
93
|
+
const key = dirRel === "." ? "" : dirRel;
|
|
94
|
+
const segments = key ? key.split("/") : [];
|
|
95
|
+
let content;
|
|
96
|
+
try {
|
|
97
|
+
content = await readFile(full, "utf-8");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new Error(`Failed to read page file: ${full}`, { cause: err });
|
|
100
|
+
}
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = matter(content).data;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error(`Failed to parse frontmatter in: ${full}`, { cause: err });
|
|
106
|
+
}
|
|
107
|
+
const result = mapFrontmatter(data, relPath);
|
|
108
|
+
pages.push({
|
|
109
|
+
key,
|
|
110
|
+
title: result.title,
|
|
111
|
+
description: result.description,
|
|
112
|
+
nav: result.nav,
|
|
113
|
+
order: result.order ?? DEFAULT_ORDER,
|
|
114
|
+
file: relPath,
|
|
115
|
+
segments,
|
|
116
|
+
extra: result.extra
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await Promise.all(subdirs.map(walk));
|
|
121
|
+
}
|
|
122
|
+
await walk(docsDir);
|
|
123
|
+
return pages;
|
|
124
|
+
}
|
|
125
|
+
function resolveModules(pages, pageByKey) {
|
|
126
|
+
for (const page of pages) {
|
|
127
|
+
if (page.extra?.module) continue;
|
|
128
|
+
if (page.segments.length === 0) continue;
|
|
129
|
+
const moduleRoot = pageByKey.get(page.segments[0]);
|
|
130
|
+
const mod = moduleRoot?.extra?.module;
|
|
131
|
+
if (mod) {
|
|
132
|
+
page.extra = { ...page.extra, module: mod };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/tree-builder.ts
|
|
138
|
+
function buildPageIndex(pages) {
|
|
139
|
+
const directChildren = /* @__PURE__ */ new Map();
|
|
140
|
+
const subDirNames = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const page of pages) {
|
|
142
|
+
if (page.key === "") continue;
|
|
143
|
+
const parentPath = page.segments.slice(0, -1).join("/") || "";
|
|
144
|
+
let children = directChildren.get(parentPath);
|
|
145
|
+
if (!children) {
|
|
146
|
+
children = [];
|
|
147
|
+
directChildren.set(parentPath, children);
|
|
148
|
+
}
|
|
149
|
+
children.push(page);
|
|
150
|
+
for (let depth = 0; depth < page.segments.length - 1; depth++) {
|
|
151
|
+
const ancestor = page.segments.slice(0, depth).join("/") || "";
|
|
152
|
+
let dirs = subDirNames.get(ancestor);
|
|
153
|
+
if (!dirs) {
|
|
154
|
+
dirs = /* @__PURE__ */ new Set();
|
|
155
|
+
subDirNames.set(ancestor, dirs);
|
|
156
|
+
}
|
|
157
|
+
dirs.add(page.segments[depth]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { directChildren, subDirNames };
|
|
161
|
+
}
|
|
162
|
+
function humanize(slug, acronyms) {
|
|
163
|
+
return slug.split("-").map((w) => acronyms.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
164
|
+
}
|
|
165
|
+
var defaultResolveDirectoryLabel = (dirName, meta, indexPage, acronyms) => {
|
|
166
|
+
const mod = indexPage?.extra?.module;
|
|
167
|
+
if (mod) return mod;
|
|
168
|
+
if (meta.name) return meta.name;
|
|
169
|
+
return humanize(dirName, acronyms);
|
|
170
|
+
};
|
|
171
|
+
function buildChildren(parentPath, ctx) {
|
|
172
|
+
const parentSegments = parentPath ? parentPath.split("/") : [];
|
|
173
|
+
const directPages = ctx.pageIndex.directChildren.get(parentPath) ?? [];
|
|
174
|
+
const dirNames = ctx.pageIndex.subDirNames.get(parentPath) ?? /* @__PURE__ */ new Set();
|
|
175
|
+
const pageBySegment = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const page of directPages) {
|
|
177
|
+
pageBySegment.set(page.segments[parentSegments.length], page);
|
|
178
|
+
}
|
|
179
|
+
const results = [];
|
|
180
|
+
for (const page of directPages) {
|
|
181
|
+
const lastSegment = page.segments[page.segments.length - 1];
|
|
182
|
+
if (dirNames.has(lastSegment)) continue;
|
|
183
|
+
const pageNode = {
|
|
184
|
+
type: "page",
|
|
185
|
+
name: page.nav ?? page.title,
|
|
186
|
+
id: page.key,
|
|
187
|
+
url: `${ctx.urlPrefix}/${page.key}`,
|
|
188
|
+
order: page.order
|
|
189
|
+
};
|
|
190
|
+
results.push(pageNode);
|
|
191
|
+
}
|
|
192
|
+
for (const dirName of dirNames) {
|
|
193
|
+
const dirPath = parentPath ? `${parentPath}/${dirName}` : dirName;
|
|
194
|
+
const indexPage = pageBySegment.get(dirName);
|
|
195
|
+
const meta = ctx.metas.get(dirPath) ?? {};
|
|
196
|
+
const children = buildChildren(dirPath, ctx);
|
|
197
|
+
if (children.length === 0 && !indexPage) continue;
|
|
198
|
+
const dirLabel = ctx.resolveDirectoryLabel(dirName, meta, indexPage, ctx.acronyms);
|
|
199
|
+
const directory = {
|
|
200
|
+
type: "directory",
|
|
201
|
+
name: dirLabel,
|
|
202
|
+
id: dirPath,
|
|
203
|
+
children,
|
|
204
|
+
order: indexPage?.order ?? meta.order ?? DEFAULT_ORDER
|
|
205
|
+
};
|
|
206
|
+
if (indexPage) {
|
|
207
|
+
directory.index = {
|
|
208
|
+
type: "page",
|
|
209
|
+
name: indexPage.nav ?? indexPage.title,
|
|
210
|
+
url: `${ctx.urlPrefix}/${indexPage.key}`,
|
|
211
|
+
id: `${indexPage.key}${INDEX_SUFFIX}`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
results.push(directory);
|
|
215
|
+
}
|
|
216
|
+
results.sort((a, b) => {
|
|
217
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
218
|
+
return a.name.localeCompare(b.name);
|
|
219
|
+
});
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
function stripOrder(node) {
|
|
223
|
+
if (node.type === "page") {
|
|
224
|
+
const { order: _2, ...rest2 } = node;
|
|
225
|
+
return rest2;
|
|
226
|
+
}
|
|
227
|
+
const { order: _, children, index, ...rest } = node;
|
|
228
|
+
return {
|
|
229
|
+
...rest,
|
|
230
|
+
children: children.map(stripOrder),
|
|
231
|
+
...index && { index }
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/generator.ts
|
|
236
|
+
var SAFE_IDENTIFIER = /^[a-zA-Z_$][\w$]*$/;
|
|
237
|
+
var SAFE_IMPORT_PATH = /^(?:\.\.?\/[\w@._$/-]+|@[\w-]+\/[\w._-]+(?:\/[\w._-]+)*|[a-zA-Z][\w._-]*(?:\/[\w._-]+)*)$/;
|
|
238
|
+
function normalizeAcronyms(acronyms) {
|
|
239
|
+
if (acronyms instanceof Set) return acronyms;
|
|
240
|
+
return new Set(acronyms ?? []);
|
|
241
|
+
}
|
|
242
|
+
var defaultMapPageEntry = (page) => {
|
|
243
|
+
const entry = { ...page.extra };
|
|
244
|
+
entry.title = page.title;
|
|
245
|
+
if (page.description) entry.description = page.description;
|
|
246
|
+
entry.file = page.file;
|
|
247
|
+
return entry;
|
|
248
|
+
};
|
|
249
|
+
async function preloadMetas(docsDir, metaFile, subDirNames) {
|
|
250
|
+
const dirPaths = /* @__PURE__ */ new Set();
|
|
251
|
+
for (const [parentPath, dirs] of subDirNames) {
|
|
252
|
+
for (const dir of dirs) {
|
|
253
|
+
dirPaths.add(parentPath ? `${parentPath}/${dir}` : dir);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const metas = /* @__PURE__ */ new Map();
|
|
257
|
+
await Promise.all(
|
|
258
|
+
[...dirPaths].map(async (dirPath) => {
|
|
259
|
+
const meta = await readDirectoryMeta(docsDir, dirPath, metaFile);
|
|
260
|
+
if (meta.name !== void 0 || meta.order !== void 0 || meta.extra !== void 0) {
|
|
261
|
+
metas.set(dirPath, meta);
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
return metas;
|
|
266
|
+
}
|
|
267
|
+
async function generateData(opts) {
|
|
268
|
+
const metaFile = opts.metaFile ?? DEFAULT_META_FILE;
|
|
269
|
+
const pageFile = opts.pageFile ?? DEFAULT_PAGE_FILE;
|
|
270
|
+
const acronyms = normalizeAcronyms(opts.acronyms);
|
|
271
|
+
const pages = await scanPages(opts.docsDir, pageFile, opts.mapFrontmatter);
|
|
272
|
+
const pageByKey = new Map(pages.map((p) => [p.key, p]));
|
|
273
|
+
const enrich = opts.enrichPages ?? (() => {
|
|
274
|
+
});
|
|
275
|
+
enrich(pages, pageByKey);
|
|
276
|
+
const resolveLabel = opts.resolveDirectoryLabel ?? defaultResolveDirectoryLabel;
|
|
277
|
+
const pageIndex = buildPageIndex(pages);
|
|
278
|
+
const metas = await preloadMetas(opts.docsDir, metaFile, pageIndex.subDirNames);
|
|
279
|
+
const ctx = {
|
|
280
|
+
pageIndex,
|
|
281
|
+
metas,
|
|
282
|
+
urlPrefix: opts.urlPrefix,
|
|
283
|
+
acronyms,
|
|
284
|
+
resolveDirectoryLabel: resolveLabel
|
|
285
|
+
};
|
|
286
|
+
const topChildren = buildChildren("", ctx);
|
|
287
|
+
let tree;
|
|
288
|
+
const rootPage = pageByKey.get("");
|
|
289
|
+
if (opts.root) {
|
|
290
|
+
const rootDirectory = {
|
|
291
|
+
type: "directory",
|
|
292
|
+
name: opts.root.name,
|
|
293
|
+
id: opts.root.id,
|
|
294
|
+
children: [],
|
|
295
|
+
order: DEFAULT_ORDER
|
|
296
|
+
};
|
|
297
|
+
if (rootPage) {
|
|
298
|
+
rootDirectory.index = {
|
|
299
|
+
type: "page",
|
|
300
|
+
name: rootPage.nav ?? rootPage.title,
|
|
301
|
+
url: opts.urlPrefix,
|
|
302
|
+
id: `${opts.root.id}${INDEX_SUFFIX}`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const treeChildren = [stripOrder(rootDirectory), ...topChildren.map(stripOrder)];
|
|
306
|
+
tree = { type: "directory", name: opts.treeName, id: opts.treeName, children: treeChildren };
|
|
307
|
+
} else {
|
|
308
|
+
tree = {
|
|
309
|
+
type: "directory",
|
|
310
|
+
name: opts.treeName,
|
|
311
|
+
id: opts.treeName,
|
|
312
|
+
children: topChildren.map(stripOrder)
|
|
313
|
+
};
|
|
314
|
+
if (rootPage) {
|
|
315
|
+
tree.index = {
|
|
316
|
+
type: "page",
|
|
317
|
+
name: rootPage.nav ?? rootPage.title,
|
|
318
|
+
url: opts.urlPrefix,
|
|
319
|
+
id: `${opts.treeName}${INDEX_SUFFIX}`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { pages, tree };
|
|
324
|
+
}
|
|
325
|
+
async function writeOutput(opts, data) {
|
|
326
|
+
const { treeType, pageType } = opts;
|
|
327
|
+
const pagesExport = opts.pagesExportName ?? "pages";
|
|
328
|
+
const treeExport = opts.treeExportName ?? "docsTree";
|
|
329
|
+
const mapEntry = opts.mapPageEntry ?? defaultMapPageEntry;
|
|
330
|
+
if (!SAFE_IDENTIFIER.test(pagesExport)) {
|
|
331
|
+
throw new Error(`Invalid pagesExportName: "${pagesExport}" \u2014 must be a valid JS identifier`);
|
|
332
|
+
}
|
|
333
|
+
if (!SAFE_IDENTIFIER.test(treeExport)) {
|
|
334
|
+
throw new Error(`Invalid treeExportName: "${treeExport}" \u2014 must be a valid JS identifier`);
|
|
335
|
+
}
|
|
336
|
+
if (treeType) {
|
|
337
|
+
if (!SAFE_IDENTIFIER.test(treeType.name)) {
|
|
338
|
+
throw new Error(`Invalid treeType.name: "${treeType.name}" \u2014 must be a valid JS identifier`);
|
|
339
|
+
}
|
|
340
|
+
if (!SAFE_IMPORT_PATH.test(treeType.from)) {
|
|
341
|
+
throw new Error(`Invalid treeType.from: "${treeType.from}" \u2014 must be a valid import path`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (pageType) {
|
|
345
|
+
if (!SAFE_IDENTIFIER.test(pageType.name)) {
|
|
346
|
+
throw new Error(`Invalid pageType.name: "${pageType.name}" \u2014 must be a valid JS identifier`);
|
|
347
|
+
}
|
|
348
|
+
if (!SAFE_IMPORT_PATH.test(pageType.from)) {
|
|
349
|
+
throw new Error(`Invalid pageType.from: "${pageType.from}" \u2014 must be a valid import path`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const pagesEntries = data.pages.map((p) => [
|
|
353
|
+
p.key,
|
|
354
|
+
mapEntry(p)
|
|
355
|
+
]);
|
|
356
|
+
const pagesJson = JSON.stringify(pagesEntries, null, " ");
|
|
357
|
+
const treeJson = JSON.stringify(data.tree, null, " ");
|
|
358
|
+
const lines = [
|
|
359
|
+
"// This file is auto-generated by the content-tree plugin. Do not edit."
|
|
360
|
+
];
|
|
361
|
+
if (treeType && pageType && treeType.from === pageType.from) {
|
|
362
|
+
lines.push(`import type { ${pageType.name}, ${treeType.name} } from "${treeType.from}";`);
|
|
363
|
+
} else {
|
|
364
|
+
if (pageType) {
|
|
365
|
+
lines.push(`import type { ${pageType.name} } from "${pageType.from}";`);
|
|
366
|
+
}
|
|
367
|
+
if (treeType) {
|
|
368
|
+
lines.push(`import type { ${treeType.name} } from "${treeType.from}";`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const pagesValueType = pageType ? pageType.name : "Record<string, unknown>";
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push(`export const ${pagesExport} = new Map<string, ${pagesValueType}>(${pagesJson});`);
|
|
374
|
+
lines.push("");
|
|
375
|
+
if (treeType) {
|
|
376
|
+
lines.push(`export const ${treeExport}: ${treeType.name} = ${treeJson};`);
|
|
377
|
+
} else {
|
|
378
|
+
lines.push(`export const ${treeExport} = ${treeJson};`);
|
|
379
|
+
}
|
|
380
|
+
const outDir = dirname2(opts.outFile);
|
|
381
|
+
await mkdir(outDir, { recursive: true });
|
|
382
|
+
const tmpFile = join2(outDir, `.${randomUUID()}.content-tree.tmp`);
|
|
383
|
+
try {
|
|
384
|
+
await writeFile(tmpFile, `${lines.join("\n")}
|
|
385
|
+
`);
|
|
386
|
+
await rename(tmpFile, opts.outFile);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
try {
|
|
389
|
+
await unlink(tmpFile);
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function generate(opts) {
|
|
396
|
+
const data = await generateData(opts);
|
|
397
|
+
await writeOutput(opts, data);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/plugin.ts
|
|
401
|
+
import { resolve as resolve2 } from "path";
|
|
402
|
+
import { normalizePath } from "vite";
|
|
403
|
+
function contentTreeGeneratorPlugin(opts) {
|
|
404
|
+
let resolved;
|
|
405
|
+
const metaFile = opts.metaFile ?? DEFAULT_META_FILE;
|
|
406
|
+
const pageFile = opts.pageFile ?? DEFAULT_PAGE_FILE;
|
|
407
|
+
return {
|
|
408
|
+
name: "content-tree",
|
|
409
|
+
enforce: "pre",
|
|
410
|
+
configResolved(config) {
|
|
411
|
+
resolved = {
|
|
412
|
+
...opts,
|
|
413
|
+
docsDir: normalizePath(resolve2(config.root, opts.docsDir)),
|
|
414
|
+
outFile: normalizePath(resolve2(config.root, opts.outFile))
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
async buildStart() {
|
|
418
|
+
await generate(resolved);
|
|
419
|
+
},
|
|
420
|
+
configureServer(server) {
|
|
421
|
+
let debounceTimer;
|
|
422
|
+
let generating = false;
|
|
423
|
+
let pendingRegeneration = false;
|
|
424
|
+
async function regenerate() {
|
|
425
|
+
generating = true;
|
|
426
|
+
try {
|
|
427
|
+
await generate(resolved);
|
|
428
|
+
const mod = server.moduleGraph.getModuleById(resolved.outFile);
|
|
429
|
+
if (mod) {
|
|
430
|
+
server.moduleGraph.invalidateModule(mod);
|
|
431
|
+
}
|
|
432
|
+
server.hot.send({ type: "full-reload" });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
server.config.logger.error(
|
|
435
|
+
`[content-tree] Generation failed: ${err instanceof Error ? err.message : err}`
|
|
436
|
+
);
|
|
437
|
+
} finally {
|
|
438
|
+
generating = false;
|
|
439
|
+
if (pendingRegeneration) {
|
|
440
|
+
pendingRegeneration = false;
|
|
441
|
+
debounceTimer = setTimeout(regenerate, 150);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const handler = (file) => {
|
|
446
|
+
const normalized = normalizePath(file);
|
|
447
|
+
if (normalized === resolved.outFile) return;
|
|
448
|
+
if (normalized.startsWith(`${resolved.docsDir}/`) && (normalized.endsWith(`/${pageFile}`) || normalized.endsWith(`/${metaFile}`))) {
|
|
449
|
+
if (generating) {
|
|
450
|
+
pendingRegeneration = true;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
clearTimeout(debounceTimer);
|
|
454
|
+
debounceTimer = setTimeout(regenerate, 150);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
server.watcher.on("change", handler);
|
|
458
|
+
server.watcher.on("add", handler);
|
|
459
|
+
server.watcher.on("unlink", handler);
|
|
460
|
+
server.httpServer?.on("close", () => {
|
|
461
|
+
clearTimeout(debounceTimer);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
export {
|
|
467
|
+
DEFAULT_META_FILE,
|
|
468
|
+
DEFAULT_ORDER,
|
|
469
|
+
DEFAULT_PAGE_FILE,
|
|
470
|
+
INDEX_SUFFIX,
|
|
471
|
+
buildChildren,
|
|
472
|
+
buildPageIndex,
|
|
473
|
+
contentTreeGeneratorPlugin,
|
|
474
|
+
defaultMapFrontmatter,
|
|
475
|
+
defaultResolveDirectoryLabel,
|
|
476
|
+
generate,
|
|
477
|
+
generateData,
|
|
478
|
+
humanize,
|
|
479
|
+
readDirectoryMeta,
|
|
480
|
+
resolveModules,
|
|
481
|
+
scanPages,
|
|
482
|
+
stripOrder,
|
|
483
|
+
writeOutput
|
|
484
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rodavel/vite-plugin-content-tree",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite plugin that scans content directories, extracts frontmatter, and generates a typed navigation tree",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vite",
|
|
8
|
+
"vite-plugin",
|
|
9
|
+
"content-tree",
|
|
10
|
+
"frontmatter",
|
|
11
|
+
"navigation"
|
|
12
|
+
],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"default": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"lint": "biome check .",
|
|
42
|
+
"lint:fix": "biome check --write .",
|
|
43
|
+
"prepublishOnly": "bun run build && bun run test",
|
|
44
|
+
"changeset": "changeset",
|
|
45
|
+
"release": "changeset publish"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": ">=5"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"gray-matter": "^4.0.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.4.8",
|
|
55
|
+
"@changesets/cli": "^2.30.0",
|
|
56
|
+
"@types/node": "^25.5.0",
|
|
57
|
+
"tsup": "^8.4.0",
|
|
58
|
+
"typescript": "^5.7.0",
|
|
59
|
+
"vite": "^6.0.0",
|
|
60
|
+
"vitest": "^3.0.0"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "https://github.com/rodavel/vite-plugin-content-tree.git"
|
|
65
|
+
},
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/rodavel/vite-plugin-content-tree/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/rodavel/vite-plugin-content-tree#readme",
|
|
70
|
+
"license": "MIT"
|
|
71
|
+
}
|