@stritti/vitepress-plugin-openspec 0.7.0 → 0.8.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/README.md +12 -2
- package/dist/index.cjs +537 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -217
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +537 -136
- package/dist/index.js.map +1 -1
- package/dist/lib/detect.d.ts +43 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/dev-server.d.ts +28 -0
- package/dist/lib/dev-server.d.ts.map +1 -0
- package/dist/lib/generator.d.ts +12 -0
- package/dist/lib/generator.d.ts.map +1 -0
- package/dist/lib/links.d.ts +10 -0
- package/dist/lib/links.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +5 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/markdown.d.ts +6 -0
- package/dist/lib/markdown.d.ts.map +1 -0
- package/dist/lib/navigation.d.ts +16 -0
- package/dist/lib/navigation.d.ts.map +1 -0
- package/dist/lib/options.d.ts +28 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/pages.d.ts +22 -0
- package/dist/lib/pages.d.ts.map +1 -0
- package/dist/lib/reader.d.ts +10 -0
- package/dist/lib/reader.d.ts.map +1 -0
- package/dist/lib/watch.d.ts +36 -0
- package/dist/lib/watch.d.ts.map +1 -0
- package/dist/plugin.d.ts +50 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +5 -5
- package/dist/index.d.cts +0 -217
package/dist/index.js
CHANGED
|
@@ -1,14 +1,212 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import
|
|
4
|
+
import { load } from 'js-yaml';
|
|
5
5
|
|
|
6
6
|
// src/plugin.ts
|
|
7
|
+
var PLUGIN_NAME = "vitepress-plugin-openspec";
|
|
8
|
+
function prefix(color) {
|
|
9
|
+
return pc.bold(color(`[${PLUGIN_NAME}]`));
|
|
10
|
+
}
|
|
11
|
+
function logWarn(message) {
|
|
12
|
+
console.warn(`${prefix(pc.yellow)} ${message}`);
|
|
13
|
+
}
|
|
14
|
+
function logInfo(message) {
|
|
15
|
+
console.log(`${prefix(pc.cyan)} ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function logError(message) {
|
|
18
|
+
console.error(`${prefix(pc.red)} ${message}`);
|
|
19
|
+
}
|
|
20
|
+
var GENERATED_MARKER = "# Generated by vitepress-plugin-openspec";
|
|
21
|
+
function looksLikeOpenSpecDir(dir) {
|
|
22
|
+
const gitignore = path.join(dir, ".gitignore");
|
|
23
|
+
if (fs.existsSync(gitignore)) {
|
|
24
|
+
try {
|
|
25
|
+
if (fs.readFileSync(gitignore, "utf-8").startsWith(GENERATED_MARKER)) return false;
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (fs.existsSync(path.join(dir, "project.md"))) return "strong";
|
|
30
|
+
if (fs.existsSync(path.join(dir, "config.yaml")) || fs.existsSync(path.join(dir, "config.yml"))) return "strong";
|
|
31
|
+
const changesDir = path.join(dir, "changes");
|
|
32
|
+
if (fs.existsSync(changesDir)) {
|
|
33
|
+
try {
|
|
34
|
+
for (const entry of fs.readdirSync(changesDir, { withFileTypes: true })) {
|
|
35
|
+
if (!entry.isDirectory() && !entry.name.endsWith(".yaml")) continue;
|
|
36
|
+
const marker = path.join(changesDir, entry.name, ".openspec.yaml");
|
|
37
|
+
const yaml = path.join(changesDir, `${entry.name}.yaml`);
|
|
38
|
+
if (fs.existsSync(marker) || fs.existsSync(yaml)) return "strong";
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const specsDir = path.join(dir, "specs");
|
|
44
|
+
if (fs.existsSync(specsDir)) {
|
|
45
|
+
try {
|
|
46
|
+
for (const entry of fs.readdirSync(specsDir, { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isDirectory()) continue;
|
|
48
|
+
if (fs.existsSync(path.join(specsDir, entry.name, "spec.md"))) return "strong";
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const archiveDir = path.join(changesDir, "archive");
|
|
54
|
+
if (fs.existsSync(archiveDir)) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.readdirSync(archiveDir, { withFileTypes: true }).some((e) => e.isDirectory())) return "weak";
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function findOpenSpecDir(start, generatedOutputDir) {
|
|
63
|
+
let current = path.resolve(start);
|
|
64
|
+
for (; ; ) {
|
|
65
|
+
const candidate = path.join(current, "openspec");
|
|
66
|
+
if (fs.existsSync(candidate)) {
|
|
67
|
+
const match = looksLikeOpenSpecDir(candidate);
|
|
68
|
+
if (match === "weak" && generatedOutputDir && realPath(candidate) === realPath(generatedOutputDir)) {
|
|
69
|
+
const parent2 = path.dirname(current);
|
|
70
|
+
if (parent2 === current) return void 0;
|
|
71
|
+
current = parent2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (match === "strong" || match === "weak") return candidate;
|
|
75
|
+
}
|
|
76
|
+
const parent = path.dirname(current);
|
|
77
|
+
if (parent === current) return void 0;
|
|
78
|
+
current = parent;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function realPath(target) {
|
|
82
|
+
let current = path.resolve(target);
|
|
83
|
+
const suffix = [];
|
|
84
|
+
for (; ; ) {
|
|
85
|
+
if (fs.existsSync(current)) break;
|
|
86
|
+
const parent = path.dirname(current);
|
|
87
|
+
if (parent === current) return path.resolve(target);
|
|
88
|
+
suffix.unshift(path.basename(current));
|
|
89
|
+
current = parent;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
current = fs.realpathSync(current);
|
|
93
|
+
} catch {
|
|
94
|
+
return path.resolve(target);
|
|
95
|
+
}
|
|
96
|
+
return suffix.length ? path.join(current, ...suffix) : current;
|
|
97
|
+
}
|
|
98
|
+
var srcDirCache = /* @__PURE__ */ new Map();
|
|
99
|
+
function detectSrcDir(cwd) {
|
|
100
|
+
const key = path.resolve(cwd);
|
|
101
|
+
if (srcDirCache.has(key)) return srcDirCache.get(key);
|
|
102
|
+
const result = resolveSrcDir(key);
|
|
103
|
+
srcDirCache.set(key, result);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function resolveSrcDir(cwd) {
|
|
107
|
+
if (path.basename(cwd) === ".vitepress") return path.dirname(cwd);
|
|
108
|
+
if (fs.existsSync(path.join(cwd, ".vitepress"))) return cwd;
|
|
109
|
+
const matches = [];
|
|
110
|
+
try {
|
|
111
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
112
|
+
if (!entry.isDirectory()) continue;
|
|
113
|
+
const subDir = path.join(cwd, entry.name);
|
|
114
|
+
if (fs.existsSync(path.join(subDir, ".vitepress"))) matches.push(subDir);
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
if (matches.length === 1) return matches[0];
|
|
119
|
+
if (matches.length > 1) {
|
|
120
|
+
const docsMatch = matches.find((m) => path.basename(m) === "docs");
|
|
121
|
+
if (docsMatch) return docsMatch;
|
|
122
|
+
logWarn(
|
|
123
|
+
`Multiple VitePress source directories found (${matches.map((m) => path.relative(cwd, m)).join(", ")}) \u2014 pass an explicit \`srcDir\` option. Skipping page generation.`
|
|
124
|
+
);
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return cwd;
|
|
128
|
+
}
|
|
129
|
+
function detectVitePressRoot(cwd) {
|
|
130
|
+
const srcDir = detectSrcDir(cwd);
|
|
131
|
+
return srcDir && fs.existsSync(path.join(srcDir, ".vitepress")) ? srcDir : cwd;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/lib/options.ts
|
|
135
|
+
function resolveOptions(userOptions = {}, fallbackSrcDir) {
|
|
136
|
+
const outDir = (userOptions.outDir ?? "openspec").replace(/\\/g, "/");
|
|
137
|
+
if (path.isAbsolute(outDir) || outDir === "" || outDir === "." || outDir === "..") {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[vitepress-plugin-openspec] Invalid outDir "${outDir}": it must be a relative path inside the VitePress srcDir.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const srcDir = userOptions.srcDir ?? fallbackSrcDir ?? detectSrcDir(process.cwd()) ?? process.cwd();
|
|
143
|
+
const absoluteSrcDir = path.resolve(srcDir);
|
|
144
|
+
const absoluteOutDir = path.resolve(absoluteSrcDir, outDir);
|
|
145
|
+
const rel = path.relative(absoluteSrcDir, absoluteOutDir);
|
|
146
|
+
if (rel.startsWith("..") || path.isAbsolute(rel) || rel === "") {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[vitepress-plugin-openspec] Invalid outDir "${outDir}": it resolves outside the VitePress srcDir.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const specDir = userOptions.specDir ?? findOpenSpecDir(process.cwd(), absoluteOutDir) ?? "./openspec";
|
|
152
|
+
return {
|
|
153
|
+
specDir: path.resolve(specDir),
|
|
154
|
+
outDir,
|
|
155
|
+
srcDir: absoluteSrcDir,
|
|
156
|
+
absoluteOutDir
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var ALWAYS_GENERATED_BASENAMES = /* @__PURE__ */ new Set(["index.md", ".gitignore", ".openspec-manifest.json"]);
|
|
160
|
+
var ARTIFACT_BASENAMES = /* @__PURE__ */ new Set(["proposal.md", "design.md", "tasks.md"]);
|
|
161
|
+
var generatedDestCache = /* @__PURE__ */ new Map();
|
|
162
|
+
function generatedDestinations(absoluteOutDir) {
|
|
163
|
+
const cached = generatedDestCache.get(absoluteOutDir);
|
|
164
|
+
if (cached) return cached;
|
|
165
|
+
const set = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const entry of readManifest(absoluteOutDir)) {
|
|
167
|
+
set.add(path.resolve(absoluteOutDir, entry));
|
|
168
|
+
}
|
|
169
|
+
generatedDestCache.set(absoluteOutDir, set);
|
|
170
|
+
return set;
|
|
171
|
+
}
|
|
172
|
+
function invalidateGeneratedDestinations(absoluteOutDir) {
|
|
173
|
+
generatedDestCache.delete(absoluteOutDir);
|
|
174
|
+
}
|
|
175
|
+
function isInside(parent, child) {
|
|
176
|
+
const rel = path.relative(parent, child);
|
|
177
|
+
return !(rel.startsWith("..") || path.isAbsolute(rel));
|
|
178
|
+
}
|
|
179
|
+
function classifyLayout(options) {
|
|
180
|
+
if (options.specDir === options.absoluteOutDir) return "equal";
|
|
181
|
+
if (isInside(options.specDir, options.absoluteOutDir)) return "out-inside-spec";
|
|
182
|
+
if (isInside(options.absoluteOutDir, options.specDir)) return "spec-inside-out";
|
|
183
|
+
return "disjoint";
|
|
184
|
+
}
|
|
185
|
+
function isPluginGeneratedFile(options, absFile) {
|
|
186
|
+
if (!isInside(options.absoluteOutDir, absFile)) return false;
|
|
187
|
+
if (generatedDestinations(options.absoluteOutDir).has(path.resolve(absFile))) return true;
|
|
188
|
+
const base = path.basename(absFile);
|
|
189
|
+
switch (classifyLayout(options)) {
|
|
190
|
+
case "equal":
|
|
191
|
+
return ALWAYS_GENERATED_BASENAMES.has(base);
|
|
192
|
+
case "out-inside-spec":
|
|
193
|
+
return ALWAYS_GENERATED_BASENAMES.has(base) && !isInside(options.specDir, absFile);
|
|
194
|
+
case "disjoint":
|
|
195
|
+
return ALWAYS_GENERATED_BASENAMES.has(base) || ARTIFACT_BASENAMES.has(base);
|
|
196
|
+
case "spec-inside-out":
|
|
197
|
+
return !isInside(options.specDir, absFile) && (ALWAYS_GENERATED_BASENAMES.has(base) || ARTIFACT_BASENAMES.has(base));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function isSourceArtifactEvent(options, absFile) {
|
|
201
|
+
if (!isInside(options.specDir, absFile)) return false;
|
|
202
|
+
return !isPluginGeneratedFile(options, absFile);
|
|
203
|
+
}
|
|
204
|
+
var ARTIFACT_NAMES = ["proposal", "design", "tasks"];
|
|
7
205
|
function readOpenSpecYaml(dir) {
|
|
8
206
|
const yamlPath = path.join(dir, ".openspec.yaml");
|
|
9
207
|
if (!fs.existsSync(yamlPath)) return {};
|
|
10
208
|
try {
|
|
11
|
-
return
|
|
209
|
+
return load(fs.readFileSync(yamlPath, "utf-8")) ?? {};
|
|
12
210
|
} catch {
|
|
13
211
|
return {};
|
|
14
212
|
}
|
|
@@ -57,11 +255,10 @@ function formatDate(val) {
|
|
|
57
255
|
return String(val);
|
|
58
256
|
}
|
|
59
257
|
function readArtifacts(dir) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return artifacts;
|
|
258
|
+
return ARTIFACT_NAMES.filter((name) => fs.existsSync(path.join(dir, `${name}.md`)));
|
|
259
|
+
}
|
|
260
|
+
function isDirectory(dir, name) {
|
|
261
|
+
return fs.existsSync(path.join(dir, name, ".openspec.yaml"));
|
|
65
262
|
}
|
|
66
263
|
function readOpenSpecFolder(dir) {
|
|
67
264
|
const resolved = path.resolve(dir);
|
|
@@ -90,7 +287,7 @@ function readOpenSpecFolder(dir) {
|
|
|
90
287
|
for (const entry of fs.readdirSync(changesDir, { withFileTypes: true })) {
|
|
91
288
|
if (!entry.isDirectory() || entry.name === "archive") continue;
|
|
92
289
|
const changeDir = path.join(changesDir, entry.name);
|
|
93
|
-
if (!
|
|
290
|
+
if (!isDirectory(changeDir, ".")) continue;
|
|
94
291
|
const meta = readOpenSpecYaml(changeDir);
|
|
95
292
|
changes.push({
|
|
96
293
|
name: entry.name,
|
|
@@ -107,23 +304,52 @@ function readOpenSpecFolder(dir) {
|
|
|
107
304
|
for (const entry of fs.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
108
305
|
if (!entry.isDirectory()) continue;
|
|
109
306
|
const changeDir = path.join(archiveDir, entry.name);
|
|
110
|
-
const match = entry.name.match(/^(\d{4}-\d{2}-\d{2})-(.+)$/);
|
|
111
|
-
const archivedDate = match?.[1];
|
|
112
|
-
const name = match?.[2] ?? entry.name;
|
|
113
307
|
const meta = readOpenSpecYaml(changeDir);
|
|
308
|
+
const match = entry.name.match(/^(\d{4}-\d{2}-\d{2})-(.+)$/);
|
|
114
309
|
archivedChanges.push({
|
|
115
|
-
name,
|
|
310
|
+
name: match?.[2] ?? entry.name,
|
|
116
311
|
title: meta.title ? String(meta.title) : void 0,
|
|
117
312
|
dir: changeDir,
|
|
118
313
|
artifacts: readArtifacts(changeDir),
|
|
119
314
|
createdDate: formatDate(meta.created),
|
|
120
|
-
archivedDate,
|
|
315
|
+
archivedDate: match?.[1],
|
|
121
316
|
archiveFolderName: entry.name
|
|
122
317
|
});
|
|
123
318
|
}
|
|
124
319
|
}
|
|
125
320
|
return { dir: resolved, specs, changes, archivedChanges };
|
|
126
321
|
}
|
|
322
|
+
function rewriteRelativeLinks(content, srcFilePath, openspecRootDir, outDir) {
|
|
323
|
+
const srcDir = path.dirname(srcFilePath);
|
|
324
|
+
return content.replace(
|
|
325
|
+
/(\[[^\]]*\])\(([^)]+)\)/g,
|
|
326
|
+
(_match, linkText, urlPart) => {
|
|
327
|
+
const titleMatch = urlPart.match(/^(.+?)(\s+["'][^"']*["'])?\s*$/);
|
|
328
|
+
if (!titleMatch) return _match;
|
|
329
|
+
const rawUrl = titleMatch[1].trim();
|
|
330
|
+
const title = titleMatch[2] ?? "";
|
|
331
|
+
if (!rawUrl || rawUrl.startsWith("http://") || rawUrl.startsWith("https://") || rawUrl.startsWith("//") || rawUrl.startsWith("/") || rawUrl.startsWith("#") || rawUrl.startsWith("mailto:") || rawUrl.startsWith("tel:")) {
|
|
332
|
+
return _match;
|
|
333
|
+
}
|
|
334
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
335
|
+
const urlWithoutFragment = hashIdx >= 0 ? rawUrl.slice(0, hashIdx) : rawUrl;
|
|
336
|
+
const fragment = hashIdx >= 0 ? rawUrl.slice(hashIdx) : "";
|
|
337
|
+
if (!urlWithoutFragment) {
|
|
338
|
+
return _match;
|
|
339
|
+
}
|
|
340
|
+
const resolvedPath = path.resolve(srcDir, urlWithoutFragment);
|
|
341
|
+
const relToOpenspec = path.relative(openspecRootDir, resolvedPath);
|
|
342
|
+
if (relToOpenspec.startsWith("..") || path.isAbsolute(relToOpenspec)) {
|
|
343
|
+
return _match;
|
|
344
|
+
}
|
|
345
|
+
const vitePath = relToOpenspec.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
346
|
+
const absoluteLink = `/${outDir}/${vitePath}${fragment}`;
|
|
347
|
+
return `${linkText}(${absoluteLink}${title})`;
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/lib/markdown.ts
|
|
127
353
|
function extractSpecDescription(content) {
|
|
128
354
|
const reqMatch = content.match(/^### Requirement:[^\n]*\n+([\s\S]*?)(?=\n#{1,4} |\n*$)/m);
|
|
129
355
|
if (!reqMatch) return void 0;
|
|
@@ -136,7 +362,7 @@ function extractSpecDescription(content) {
|
|
|
136
362
|
const cut = sentence.lastIndexOf(" ", 160);
|
|
137
363
|
sentence = (cut > 0 ? sentence.slice(0, cut) : sentence.slice(0, 160)) + "\u2026";
|
|
138
364
|
}
|
|
139
|
-
return sentence.replace(/"/g,
|
|
365
|
+
return sentence.replace(/([\\"])/g, "\\$1");
|
|
140
366
|
}
|
|
141
367
|
function stripDeltaMarkers(content) {
|
|
142
368
|
const stripped = content.split("\n").filter((line) => !/^## (ADDED|MODIFIED|REMOVED) Requirements\s*$/.test(line)).join("\n");
|
|
@@ -165,6 +391,11 @@ function transformScenarios(content) {
|
|
|
165
391
|
if (inScenario) result.push(":::");
|
|
166
392
|
return result.join("\n");
|
|
167
393
|
}
|
|
394
|
+
function specTitle(spec) {
|
|
395
|
+
return spec.title ?? humanizeLabel(spec.name);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/lib/pages.ts
|
|
168
399
|
function generateSpecPage(spec) {
|
|
169
400
|
const description = extractSpecDescription(spec.content);
|
|
170
401
|
const transformed = transformScenarios(stripDeltaMarkers(spec.content));
|
|
@@ -175,7 +406,7 @@ function generateSpecPage(spec) {
|
|
|
175
406
|
lines.push("---");
|
|
176
407
|
lines.push("");
|
|
177
408
|
}
|
|
178
|
-
lines.push(`# ${
|
|
409
|
+
lines.push(`# ${specTitle(spec)}`);
|
|
179
410
|
lines.push("");
|
|
180
411
|
lines.push(transformed.trimEnd());
|
|
181
412
|
lines.push("");
|
|
@@ -191,7 +422,7 @@ function generateSpecsIndexPage(specs, outDir) {
|
|
|
191
422
|
lines.push("*No specifications defined yet.*");
|
|
192
423
|
} else {
|
|
193
424
|
for (const spec of specs) {
|
|
194
|
-
lines.push(`- [${
|
|
425
|
+
lines.push(`- [${specTitle(spec)}](/${outDir}/specs/${spec.name}/)`);
|
|
195
426
|
}
|
|
196
427
|
}
|
|
197
428
|
lines.push("");
|
|
@@ -211,10 +442,10 @@ function generateChangeIndexPage(change, outDir) {
|
|
|
211
442
|
}
|
|
212
443
|
lines.push("## Artifacts");
|
|
213
444
|
lines.push("");
|
|
214
|
-
const
|
|
445
|
+
const prefix2 = change.archiveFolderName ? `/${outDir}/changes/archive/${change.archiveFolderName}` : `/${outDir}/changes/${change.name}`;
|
|
215
446
|
for (const artifact of change.artifacts) {
|
|
216
447
|
const label = artifact.charAt(0).toUpperCase() + artifact.slice(1);
|
|
217
|
-
lines.push(`- [${label}](${
|
|
448
|
+
lines.push(`- [${label}](${prefix2}/${artifact})`);
|
|
218
449
|
}
|
|
219
450
|
lines.push("");
|
|
220
451
|
return lines.join("\n");
|
|
@@ -247,46 +478,176 @@ function generateChangesIndexPage(folder, outDir) {
|
|
|
247
478
|
lines.push("");
|
|
248
479
|
return lines.join("\n");
|
|
249
480
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
481
|
+
function generateRootIndexPage(outDir) {
|
|
482
|
+
return [
|
|
483
|
+
"# Project Documentation",
|
|
484
|
+
"",
|
|
485
|
+
"This section is generated from the project's [OpenSpec](https://openspec.dev/) folder.",
|
|
486
|
+
"OpenSpec is a lightweight, file-based workflow for spec-driven development \u2014",
|
|
487
|
+
"it structures your project's capability specifications and change proposals as plain Markdown files.",
|
|
488
|
+
"",
|
|
489
|
+
`- [Specifications](/${outDir}/specs/) \u2014 canonical capability specs`,
|
|
490
|
+
`- [Changes](/${outDir}/changes/) \u2014 active and archived change proposals`,
|
|
491
|
+
""
|
|
492
|
+
].join("\n");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/lib/generator.ts
|
|
496
|
+
var MANIFEST_FILE = ".openspec-manifest.json";
|
|
497
|
+
var GITIGNORE_CONTENT = "# Generated by vitepress-plugin-openspec \u2014 do not commit generated files.\n*\n!.gitignore\n";
|
|
498
|
+
function writeFile(filePath, content) {
|
|
499
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
500
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
501
|
+
}
|
|
502
|
+
function toRel(absoluteOutDir, filePath) {
|
|
503
|
+
return path.relative(absoluteOutDir, filePath).split(path.sep).join("/");
|
|
504
|
+
}
|
|
505
|
+
function pruneEmptyDirs(rootDir, startDir) {
|
|
506
|
+
let dir = startDir;
|
|
507
|
+
while (dir.startsWith(rootDir) && dir !== rootDir) {
|
|
508
|
+
try {
|
|
509
|
+
if (fs.readdirSync(dir).length > 0) break;
|
|
510
|
+
fs.rmdirSync(dir);
|
|
511
|
+
} catch {
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
dir = path.dirname(dir);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function cleanupPreviousManifest(rootDir) {
|
|
518
|
+
const manifestPath = path.join(rootDir, MANIFEST_FILE);
|
|
519
|
+
if (!fs.existsSync(manifestPath)) return [];
|
|
520
|
+
let previous = [];
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
523
|
+
if (Array.isArray(parsed)) previous = parsed.filter((f) => typeof f === "string");
|
|
524
|
+
} catch {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
const retained = [];
|
|
528
|
+
for (const entry of previous) {
|
|
529
|
+
const entryPath = path.resolve(rootDir, entry);
|
|
530
|
+
const rel = path.relative(rootDir, entryPath);
|
|
531
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
532
|
+
try {
|
|
533
|
+
if (fs.existsSync(entryPath)) {
|
|
534
|
+
fs.rmSync(entryPath);
|
|
535
|
+
pruneEmptyDirs(rootDir, path.dirname(entryPath));
|
|
272
536
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return `${linkText}(${absoluteLink}${title})`;
|
|
537
|
+
} catch {
|
|
538
|
+
retained.push(entry);
|
|
276
539
|
}
|
|
277
|
-
|
|
540
|
+
}
|
|
541
|
+
return retained;
|
|
542
|
+
}
|
|
543
|
+
function readManifest(rootDir) {
|
|
544
|
+
try {
|
|
545
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(rootDir, MANIFEST_FILE), "utf-8"));
|
|
546
|
+
if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string");
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
function writeManifest(files, rootDir) {
|
|
552
|
+
const manifestPath = path.join(rootDir, MANIFEST_FILE);
|
|
553
|
+
try {
|
|
554
|
+
writeFile(manifestPath, JSON.stringify(files, null, 2));
|
|
555
|
+
invalidateGeneratedDestinations(rootDir);
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function renderChangePages(change, options, isArchived) {
|
|
560
|
+
const subPath = isArchived ? path.join("changes", "archive", change.archiveFolderName ?? change.name) : path.join("changes", change.name);
|
|
561
|
+
const changeOutDir = path.join(options.absoluteOutDir, subPath);
|
|
562
|
+
const files = [];
|
|
563
|
+
files.push({
|
|
564
|
+
rel: toRel(options.absoluteOutDir, path.join(changeOutDir, "index.md")),
|
|
565
|
+
content: generateChangeIndexPage(change, options.outDir)
|
|
566
|
+
});
|
|
567
|
+
for (const artifact of change.artifacts) {
|
|
568
|
+
const srcFile = path.join(change.dir, `${artifact}.md`);
|
|
569
|
+
const destFile = path.join(changeOutDir, `${artifact}.md`);
|
|
570
|
+
if (path.resolve(srcFile) === path.resolve(destFile)) continue;
|
|
571
|
+
const content = fs.readFileSync(srcFile, "utf-8");
|
|
572
|
+
files.push({
|
|
573
|
+
rel: toRel(options.absoluteOutDir, destFile),
|
|
574
|
+
content: rewriteRelativeLinks(content, srcFile, options.specDir, options.outDir)
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return files;
|
|
578
|
+
}
|
|
579
|
+
function renderAllPages(options) {
|
|
580
|
+
const folder = readOpenSpecFolder(options.specDir);
|
|
581
|
+
const files = [];
|
|
582
|
+
for (const spec of folder.specs) {
|
|
583
|
+
const dest = path.join(options.absoluteOutDir, "specs", spec.name, "index.md");
|
|
584
|
+
files.push({ rel: toRel(options.absoluteOutDir, dest), content: generateSpecPage(spec) });
|
|
585
|
+
}
|
|
586
|
+
files.push({
|
|
587
|
+
rel: "specs/index.md",
|
|
588
|
+
content: generateSpecsIndexPage(folder.specs, options.outDir)
|
|
589
|
+
});
|
|
590
|
+
for (const change of folder.changes) {
|
|
591
|
+
files.push(...renderChangePages(change, options, false));
|
|
592
|
+
}
|
|
593
|
+
for (const change of folder.archivedChanges) {
|
|
594
|
+
files.push(...renderChangePages(change, options, true));
|
|
595
|
+
}
|
|
596
|
+
files.push({
|
|
597
|
+
rel: "changes/index.md",
|
|
598
|
+
content: generateChangesIndexPage(folder, options.outDir)
|
|
599
|
+
});
|
|
600
|
+
files.push({
|
|
601
|
+
rel: "index.md",
|
|
602
|
+
content: generateRootIndexPage(options.outDir)
|
|
603
|
+
});
|
|
604
|
+
return files;
|
|
605
|
+
}
|
|
606
|
+
function generatePages(options) {
|
|
607
|
+
if (!fs.existsSync(options.specDir)) {
|
|
608
|
+
logWarn(
|
|
609
|
+
`openspec directory not found: ${path.relative(options.srcDir, options.specDir)} \u2014 skipping page generation`
|
|
610
|
+
);
|
|
611
|
+
const retained2 = cleanupPreviousManifest(options.absoluteOutDir);
|
|
612
|
+
if (retained2.length > 0 || fs.existsSync(options.absoluteOutDir)) {
|
|
613
|
+
writeManifest(retained2, options.absoluteOutDir);
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
let files;
|
|
618
|
+
try {
|
|
619
|
+
files = renderAllPages(options);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
logError(`Failed to process openspec directory "${options.specDir}": ${String(err)}`);
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
const retained = cleanupPreviousManifest(options.absoluteOutDir);
|
|
625
|
+
try {
|
|
626
|
+
writeFile(path.join(options.absoluteOutDir, ".gitignore"), GITIGNORE_CONTENT);
|
|
627
|
+
for (const file of files) {
|
|
628
|
+
writeFile(path.join(options.absoluteOutDir, file.rel), file.content);
|
|
629
|
+
}
|
|
630
|
+
writeManifest([...files.map((f) => f.rel), ...retained], options.absoluteOutDir);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
logError(`Failed to write openspec pages: ${String(err)}`);
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
logInfo(`Generated docs from ${options.specDir}: ${files.length} file(s)`);
|
|
636
|
+
return true;
|
|
278
637
|
}
|
|
279
638
|
function changeItems(change, outDir, isArchived = false) {
|
|
280
|
-
const
|
|
639
|
+
const prefix2 = isArchived ? `/${outDir}/changes/archive/${change.archiveFolderName}` : `/${outDir}/changes/${change.name}`;
|
|
281
640
|
return change.artifacts.map((a) => ({
|
|
282
641
|
text: a.charAt(0).toUpperCase() + a.slice(1),
|
|
283
|
-
link: `${
|
|
642
|
+
link: `${prefix2}/${a}`
|
|
284
643
|
}));
|
|
285
644
|
}
|
|
286
645
|
function generateOpenSpecSidebar(specDir, options = {}) {
|
|
287
646
|
const outDir = options.outDir ?? "openspec";
|
|
288
647
|
if (!fs.existsSync(path.resolve(specDir))) {
|
|
289
|
-
|
|
648
|
+
logWarn(
|
|
649
|
+
`openspec directory not found: ${path.relative(process.cwd(), path.resolve(specDir))} \u2014 skipping sidebar generation`
|
|
650
|
+
);
|
|
290
651
|
return [];
|
|
291
652
|
}
|
|
292
653
|
const folder = readOpenSpecFolder(specDir);
|
|
@@ -296,7 +657,7 @@ function generateOpenSpecSidebar(specDir, options = {}) {
|
|
|
296
657
|
collapsed: false,
|
|
297
658
|
items: [
|
|
298
659
|
{ text: "Overview", link: `/${outDir}/specs/` },
|
|
299
|
-
...folder.specs.map((s) => ({ text:
|
|
660
|
+
...folder.specs.map((s) => ({ text: specTitle(s), link: `/${outDir}/specs/${s.name}/` }))
|
|
300
661
|
]
|
|
301
662
|
});
|
|
302
663
|
groups.push({
|
|
@@ -327,7 +688,9 @@ function generateOpenSpecSidebar(specDir, options = {}) {
|
|
|
327
688
|
function openspecNav(specDir, options = {}) {
|
|
328
689
|
const outDir = options.outDir ?? "openspec";
|
|
329
690
|
if (!fs.existsSync(path.resolve(specDir))) {
|
|
330
|
-
|
|
691
|
+
logWarn(
|
|
692
|
+
`openspec directory not found: ${path.relative(process.cwd(), path.resolve(specDir))} \u2014 skipping nav generation`
|
|
693
|
+
);
|
|
331
694
|
return null;
|
|
332
695
|
}
|
|
333
696
|
return {
|
|
@@ -336,113 +699,151 @@ function openspecNav(specDir, options = {}) {
|
|
|
336
699
|
};
|
|
337
700
|
}
|
|
338
701
|
|
|
339
|
-
// src/
|
|
340
|
-
|
|
341
|
-
function writeFile(filePath, content) {
|
|
342
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
343
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
344
|
-
}
|
|
345
|
-
function generateOpenSpecPages(userOptions = {}) {
|
|
346
|
-
const specDir = userOptions.specDir ?? "./openspec";
|
|
347
|
-
const outDir = userOptions.outDir ?? "openspec";
|
|
348
|
-
const srcDir = userOptions.srcDir ?? process.cwd();
|
|
349
|
-
const absoluteOutDir = path.resolve(srcDir, outDir);
|
|
350
|
-
if (!fs.existsSync(path.resolve(specDir))) {
|
|
351
|
-
console.warn(
|
|
352
|
-
`${pc.bold(pc.yellow(`[${PLUGIN_NAME}]`))} openspec directory not found: ${path.relative(srcDir, path.resolve(specDir))} \u2014 skipping page generation`
|
|
353
|
-
);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
702
|
+
// src/lib/dev-server.ts
|
|
703
|
+
function readNavigationSignature(options) {
|
|
356
704
|
try {
|
|
357
|
-
const folder = readOpenSpecFolder(specDir);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
366
|
-
for (const change of folder.changes) {
|
|
367
|
-
writeChangePage(change, absoluteOutDir, outDir, false, folder.dir);
|
|
368
|
-
}
|
|
369
|
-
for (const change of folder.archivedChanges) {
|
|
370
|
-
writeChangePage(change, absoluteOutDir, outDir, true, folder.dir);
|
|
371
|
-
}
|
|
372
|
-
writeFile(
|
|
373
|
-
path.join(absoluteOutDir, "changes", "index.md"),
|
|
374
|
-
generateChangesIndexPage(folder, outDir)
|
|
375
|
-
);
|
|
376
|
-
const rootIndex = [
|
|
377
|
-
"# Project Documentation",
|
|
378
|
-
"",
|
|
379
|
-
"This section is generated from the project's [OpenSpec](https://openspec.dev/) folder.",
|
|
380
|
-
"OpenSpec is a lightweight, file-based workflow for spec-driven development \u2014",
|
|
381
|
-
"it structures your project's capability specifications and change proposals as plain Markdown files.",
|
|
382
|
-
"",
|
|
383
|
-
`- [Specifications](/${outDir}/specs/) \u2014 canonical capability specs`,
|
|
384
|
-
`- [Changes](/${outDir}/changes/) \u2014 active and archived change proposals`,
|
|
385
|
-
""
|
|
386
|
-
].join("\n");
|
|
387
|
-
writeFile(path.join(absoluteOutDir, "index.md"), rootIndex);
|
|
388
|
-
writeFile(
|
|
389
|
-
path.join(absoluteOutDir, ".gitignore"),
|
|
390
|
-
"# Generated by vitepress-plugin-openspec \u2014 do not commit generated files.\n*\n!.gitignore\n"
|
|
391
|
-
);
|
|
392
|
-
console.log(
|
|
393
|
-
`${pc.bold(pc.cyan(`[${PLUGIN_NAME}]`))} Generated docs from ${pc.cyan(specDir)}: ${pc.green(String(folder.specs.length))} spec(s), ${pc.green(String(folder.changes.length))} change(s), ${pc.green(String(folder.archivedChanges.length))} archived`
|
|
394
|
-
);
|
|
395
|
-
} catch (err) {
|
|
396
|
-
console.error(
|
|
397
|
-
`${pc.bold(pc.red(`[${PLUGIN_NAME}]`))} Failed to process openspec directory "${specDir}": ${String(err)}`
|
|
398
|
-
);
|
|
705
|
+
const folder = readOpenSpecFolder(options.specDir);
|
|
706
|
+
return JSON.stringify({
|
|
707
|
+
specs: folder.specs.map((s) => [s.name, s.title ?? null]),
|
|
708
|
+
changes: folder.changes.map((c) => [c.name, c.title ?? null, c.createdDate ?? null, c.artifacts]),
|
|
709
|
+
archived: folder.archivedChanges.map((c) => [c.name, c.title ?? null, c.archivedDate ?? null, c.artifacts])
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
return "unreadable";
|
|
399
713
|
}
|
|
400
714
|
}
|
|
715
|
+
function createScheduler() {
|
|
716
|
+
let timer;
|
|
717
|
+
return {
|
|
718
|
+
schedule(options, server) {
|
|
719
|
+
if (timer) clearTimeout(timer);
|
|
720
|
+
timer = setTimeout(() => {
|
|
721
|
+
timer = void 0;
|
|
722
|
+
generatePages(options);
|
|
723
|
+
invalidateGeneratedDestinations(options.absoluteOutDir);
|
|
724
|
+
server.ws.send({ type: "full-reload" });
|
|
725
|
+
}, 100);
|
|
726
|
+
},
|
|
727
|
+
cancel() {
|
|
728
|
+
if (timer) {
|
|
729
|
+
clearTimeout(timer);
|
|
730
|
+
timer = void 0;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/plugin.ts
|
|
737
|
+
function generateOpenSpecPages(userOptions = {}) {
|
|
738
|
+
const detectedSrcDir = userOptions.srcDir ?? detectSrcDir(process.cwd());
|
|
739
|
+
if (!detectedSrcDir) return false;
|
|
740
|
+
const options = resolveOptions(userOptions, detectedSrcDir);
|
|
741
|
+
return generatePages(options);
|
|
742
|
+
}
|
|
401
743
|
function openspec(userOptions = {}) {
|
|
744
|
+
let resolvedOptions;
|
|
745
|
+
let detachListeners;
|
|
402
746
|
return {
|
|
403
747
|
name: PLUGIN_NAME,
|
|
404
748
|
enforce: "pre",
|
|
405
749
|
configResolved(resolvedConfig) {
|
|
750
|
+
detachListeners?.();
|
|
406
751
|
const vpConfig = resolvedConfig.vitepress;
|
|
407
752
|
const srcDir = userOptions.srcDir ?? vpConfig?.srcDir ?? resolvedConfig.root ?? process.cwd();
|
|
408
|
-
|
|
753
|
+
try {
|
|
754
|
+
resolvedOptions = resolveOptions(userOptions, srcDir);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
logWarn(String(err));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (!fs.existsSync(resolvedOptions.specDir)) {
|
|
760
|
+
logWarn(
|
|
761
|
+
`openspec directory not found: ${resolvedOptions.specDir} \u2014 skipping page generation`
|
|
762
|
+
);
|
|
763
|
+
generatePages(resolvedOptions);
|
|
764
|
+
resolvedOptions = void 0;
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
generatePages(resolvedOptions);
|
|
768
|
+
},
|
|
769
|
+
configureServer(server) {
|
|
770
|
+
if (!resolvedOptions || !fs.existsSync(resolvedOptions.specDir)) return;
|
|
771
|
+
const options = resolvedOptions;
|
|
772
|
+
server.watcher.add(options.specDir);
|
|
773
|
+
const scheduler = createScheduler();
|
|
774
|
+
let navigationSignature = readNavigationSignature(options);
|
|
775
|
+
const onWatchEvent = (event, file) => {
|
|
776
|
+
const absFile = path.resolve(file);
|
|
777
|
+
if (!isSourceArtifactEvent(options, absFile)) return;
|
|
778
|
+
const signature = readNavigationSignature(options);
|
|
779
|
+
const affectsNavigation = event !== "change" || signature !== navigationSignature;
|
|
780
|
+
if (affectsNavigation) {
|
|
781
|
+
scheduler.cancel();
|
|
782
|
+
navigationSignature = signature;
|
|
783
|
+
server.restart(true);
|
|
784
|
+
} else {
|
|
785
|
+
scheduler.schedule(options, server);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const onChange = (f) => onWatchEvent("change", f);
|
|
789
|
+
const onAdd = (f) => onWatchEvent("add", f);
|
|
790
|
+
const onUnlink = (f) => onWatchEvent("unlink", f);
|
|
791
|
+
server.watcher.on("change", onChange);
|
|
792
|
+
server.watcher.on("add", onAdd);
|
|
793
|
+
server.watcher.on("unlink", onUnlink);
|
|
794
|
+
detachListeners = () => {
|
|
795
|
+
scheduler.cancel();
|
|
796
|
+
server.watcher.off("change", onChange);
|
|
797
|
+
server.watcher.off("add", onAdd);
|
|
798
|
+
server.watcher.off("unlink", onUnlink);
|
|
799
|
+
server.httpServer?.removeListener("close", detachListeners);
|
|
800
|
+
detachListeners = void 0;
|
|
801
|
+
};
|
|
802
|
+
server.httpServer?.once("close", detachListeners);
|
|
803
|
+
},
|
|
804
|
+
closeBundle() {
|
|
805
|
+
detachListeners?.();
|
|
806
|
+
detachListeners = void 0;
|
|
409
807
|
}
|
|
410
808
|
};
|
|
411
809
|
}
|
|
412
|
-
function writeChangePage(change, absoluteOutDir, outDir, isArchived, openspecRootDir) {
|
|
413
|
-
const subPath = isArchived ? path.join("changes", "archive", `${change.archivedDate}-${change.name}`) : path.join("changes", change.name);
|
|
414
|
-
const changeOutDir = path.join(absoluteOutDir, subPath);
|
|
415
|
-
writeFile(path.join(changeOutDir, "index.md"), generateChangeIndexPage(change, outDir));
|
|
416
|
-
for (const artifact of change.artifacts) {
|
|
417
|
-
const srcFile = path.join(change.dir, `${artifact}.md`);
|
|
418
|
-
const destFile = path.join(changeOutDir, `${artifact}.md`);
|
|
419
|
-
const content = fs.readFileSync(srcFile, "utf-8");
|
|
420
|
-
writeFile(destFile, rewriteRelativeLinks(content, srcFile, openspecRootDir, outDir));
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
810
|
function withOpenSpec(config, options = {}) {
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
generateOpenSpecPages({ specDir, outDir, srcDir });
|
|
811
|
+
const cwd = process.cwd();
|
|
812
|
+
const configSrcDir = typeof config.srcDir === "string" && config.srcDir ? path.resolve(detectVitePressRoot(cwd), config.srcDir) : void 0;
|
|
813
|
+
const detectedSrcDir = options.srcDir ?? configSrcDir ?? detectSrcDir(cwd);
|
|
428
814
|
const result = { ...config };
|
|
815
|
+
const themeConfig = result.themeConfig ?? {};
|
|
816
|
+
if (Array.isArray(themeConfig.sidebar)) {
|
|
817
|
+
themeConfig.sidebar = { "/": themeConfig.sidebar };
|
|
818
|
+
}
|
|
819
|
+
result.themeConfig = themeConfig;
|
|
820
|
+
if (detectedSrcDir === void 0) return result;
|
|
821
|
+
let resolved;
|
|
822
|
+
try {
|
|
823
|
+
resolved = resolveOptions(options, detectedSrcDir);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
logWarn(String(err));
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
const generated = generatePages(resolved);
|
|
429
829
|
const vite = result.vite ?? {};
|
|
430
830
|
const existingPlugins = vite.plugins ?? [];
|
|
431
|
-
result.vite = { ...vite, plugins: [...existingPlugins, openspec({
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
831
|
+
result.vite = { ...vite, plugins: [...existingPlugins, openspec({ ...options })] };
|
|
832
|
+
if (generated && options.nav !== false) {
|
|
833
|
+
const navEntry = openspecNav(resolved.specDir, {
|
|
834
|
+
outDir: resolved.outDir,
|
|
835
|
+
text: options.navText
|
|
836
|
+
});
|
|
435
837
|
const existingNav = themeConfig.nav ?? [];
|
|
436
838
|
themeConfig.nav = navEntry ? [navEntry, ...existingNav] : existingNav;
|
|
437
839
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
};
|
|
840
|
+
const sidebarKey = `/${resolved.outDir}/`;
|
|
841
|
+
const existingSidebar = themeConfig.sidebar ?? {};
|
|
842
|
+
const pluginOwnsSidebarEntry = generated && options.sidebar !== false && !existingSidebar[sidebarKey];
|
|
843
|
+
if (pluginOwnsSidebarEntry) {
|
|
844
|
+
const items = generateOpenSpecSidebar(resolved.specDir, { outDir: resolved.outDir });
|
|
845
|
+
if (items.length > 0) {
|
|
846
|
+
themeConfig.sidebar = { ...existingSidebar, [sidebarKey]: items };
|
|
446
847
|
}
|
|
447
848
|
}
|
|
448
849
|
result.themeConfig = themeConfig;
|