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