@lepid-labs/weft-core 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/dist/anchors/__tests__/markdown.test.d.ts +2 -0
- package/dist/anchors/__tests__/markdown.test.d.ts.map +1 -0
- package/dist/anchors/__tests__/markdown.test.js +130 -0
- package/dist/anchors/__tests__/markdown.test.js.map +1 -0
- package/dist/anchors/__tests__/openapi.test.d.ts +2 -0
- package/dist/anchors/__tests__/openapi.test.d.ts.map +1 -0
- package/dist/anchors/__tests__/openapi.test.js +115 -0
- package/dist/anchors/__tests__/openapi.test.js.map +1 -0
- package/dist/anchors/index.d.ts +35 -0
- package/dist/anchors/index.d.ts.map +1 -0
- package/dist/anchors/index.js +68 -0
- package/dist/anchors/index.js.map +1 -0
- package/dist/anchors/markdown.d.ts +15 -0
- package/dist/anchors/markdown.d.ts.map +1 -0
- package/dist/anchors/markdown.js +71 -0
- package/dist/anchors/markdown.js.map +1 -0
- package/dist/anchors/openapi.d.ts +24 -0
- package/dist/anchors/openapi.d.ts.map +1 -0
- package/dist/anchors/openapi.js +75 -0
- package/dist/anchors/openapi.js.map +1 -0
- package/dist/browser.d.ts +10 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +8 -0
- package/dist/browser.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +393 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +454 -0
- package/dist/config.test.js.map +1 -0
- package/dist/content.d.ts +37 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +57 -0
- package/dist/content.js.map +1 -0
- package/dist/content.test.d.ts +2 -0
- package/dist/content.test.d.ts.map +1 -0
- package/dist/content.test.js +100 -0
- package/dist/content.test.js.map +1 -0
- package/dist/contributions.d.ts +64 -0
- package/dist/contributions.d.ts.map +1 -0
- package/dist/contributions.js +184 -0
- package/dist/contributions.js.map +1 -0
- package/dist/contributions.test.d.ts +2 -0
- package/dist/contributions.test.d.ts.map +1 -0
- package/dist/contributions.test.js +202 -0
- package/dist/contributions.test.js.map +1 -0
- package/dist/fetch/auth.d.ts +15 -0
- package/dist/fetch/auth.d.ts.map +1 -0
- package/dist/fetch/auth.js +38 -0
- package/dist/fetch/auth.js.map +1 -0
- package/dist/fetch/cache.d.ts +25 -0
- package/dist/fetch/cache.d.ts.map +1 -0
- package/dist/fetch/cache.js +68 -0
- package/dist/fetch/cache.js.map +1 -0
- package/dist/fetch/cache.test.d.ts +2 -0
- package/dist/fetch/cache.test.d.ts.map +1 -0
- package/dist/fetch/cache.test.js +73 -0
- package/dist/fetch/cache.test.js.map +1 -0
- package/dist/fetch/clone.d.ts +26 -0
- package/dist/fetch/clone.d.ts.map +1 -0
- package/dist/fetch/clone.js +105 -0
- package/dist/fetch/clone.js.map +1 -0
- package/dist/fetch/fetch.test.d.ts +2 -0
- package/dist/fetch/fetch.test.d.ts.map +1 -0
- package/dist/fetch/fetch.test.js +167 -0
- package/dist/fetch/fetch.test.js.map +1 -0
- package/dist/fetch/resolve.d.ts +34 -0
- package/dist/fetch/resolve.d.ts.map +1 -0
- package/dist/fetch/resolve.js +59 -0
- package/dist/fetch/resolve.js.map +1 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +170 -0
- package/dist/freshness.js.map +1 -0
- package/dist/freshness.test.d.ts +2 -0
- package/dist/freshness.test.d.ts.map +1 -0
- package/dist/freshness.test.js +209 -0
- package/dist/freshness.test.js.map +1 -0
- package/dist/frontmatter.d.ts +15 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +24 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/frontmatter.test.d.ts +2 -0
- package/dist/frontmatter.test.d.ts.map +1 -0
- package/dist/frontmatter.test.js +54 -0
- package/dist/frontmatter.test.js.map +1 -0
- package/dist/git.d.ts +55 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +127 -0
- package/dist/git.js.map +1 -0
- package/dist/git.test.d.ts +2 -0
- package/dist/git.test.d.ts.map +1 -0
- package/dist/git.test.js +198 -0
- package/dist/git.test.js.map +1 -0
- package/dist/includes.d.ts +53 -0
- package/dist/includes.d.ts.map +1 -0
- package/dist/includes.js +73 -0
- package/dist/includes.js.map +1 -0
- package/dist/includes.test.d.ts +2 -0
- package/dist/includes.test.d.ts.map +1 -0
- package/dist/includes.test.js +106 -0
- package/dist/includes.test.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/links/__tests__/markdown.test.d.ts +2 -0
- package/dist/links/__tests__/markdown.test.d.ts.map +1 -0
- package/dist/links/__tests__/markdown.test.js +173 -0
- package/dist/links/__tests__/markdown.test.js.map +1 -0
- package/dist/links/__tests__/sidecar.test.d.ts +2 -0
- package/dist/links/__tests__/sidecar.test.d.ts.map +1 -0
- package/dist/links/__tests__/sidecar.test.js +238 -0
- package/dist/links/__tests__/sidecar.test.js.map +1 -0
- package/dist/links/index.d.ts +3 -0
- package/dist/links/index.d.ts.map +1 -0
- package/dist/links/index.js +3 -0
- package/dist/links/index.js.map +1 -0
- package/dist/links/markdown.d.ts +13 -0
- package/dist/links/markdown.d.ts.map +1 -0
- package/dist/links/markdown.js +93 -0
- package/dist/links/markdown.js.map +1 -0
- package/dist/links/sidecar.d.ts +12 -0
- package/dist/links/sidecar.d.ts.map +1 -0
- package/dist/links/sidecar.js +88 -0
- package/dist/links/sidecar.js.map +1 -0
- package/dist/manifest.d.ts +45 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +269 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.test.d.ts +2 -0
- package/dist/manifest.test.d.ts.map +1 -0
- package/dist/manifest.test.js +625 -0
- package/dist/manifest.test.js.map +1 -0
- package/dist/multi-repo.test.d.ts +2 -0
- package/dist/multi-repo.test.d.ts.map +1 -0
- package/dist/multi-repo.test.js +179 -0
- package/dist/multi-repo.test.js.map +1 -0
- package/dist/node-path.d.ts +13 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/node-path.js +21 -0
- package/dist/node-path.js.map +1 -0
- package/dist/node-path.test.d.ts +2 -0
- package/dist/node-path.test.d.ts.map +1 -0
- package/dist/node-path.test.js +29 -0
- package/dist/node-path.test.js.map +1 -0
- package/dist/published-links.d.ts +16 -0
- package/dist/published-links.d.ts.map +1 -0
- package/dist/published-links.js +59 -0
- package/dist/published-links.js.map +1 -0
- package/dist/published-links.test.d.ts +2 -0
- package/dist/published-links.test.d.ts.map +1 -0
- package/dist/published-links.test.js +82 -0
- package/dist/published-links.test.js.map +1 -0
- package/dist/repos.d.ts +32 -0
- package/dist/repos.d.ts.map +1 -0
- package/dist/repos.js +46 -0
- package/dist/repos.js.map +1 -0
- package/dist/repos.test.d.ts +2 -0
- package/dist/repos.test.d.ts.map +1 -0
- package/dist/repos.test.js +61 -0
- package/dist/repos.test.js.map +1 -0
- package/dist/scalar-source.d.ts +15 -0
- package/dist/scalar-source.d.ts.map +1 -0
- package/dist/scalar-source.js +21 -0
- package/dist/scalar-source.js.map +1 -0
- package/dist/search.d.ts +14 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +59 -0
- package/dist/search.js.map +1 -0
- package/dist/service-concurrency.test.d.ts +2 -0
- package/dist/service-concurrency.test.d.ts.map +1 -0
- package/dist/service-concurrency.test.js +89 -0
- package/dist/service-concurrency.test.js.map +1 -0
- package/dist/service-history.test.d.ts +2 -0
- package/dist/service-history.test.d.ts.map +1 -0
- package/dist/service-history.test.js +76 -0
- package/dist/service-history.test.js.map +1 -0
- package/dist/service.d.ts +118 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +347 -0
- package/dist/service.js.map +1 -0
- package/dist/service.test.d.ts +2 -0
- package/dist/service.test.d.ts.map +1 -0
- package/dist/service.test.js +313 -0
- package/dist/service.test.js.map +1 -0
- package/dist/types.d.ts +403 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate/__tests__/artifacts.test.d.ts +2 -0
- package/dist/validate/__tests__/artifacts.test.d.ts.map +1 -0
- package/dist/validate/__tests__/artifacts.test.js +167 -0
- package/dist/validate/__tests__/artifacts.test.js.map +1 -0
- package/dist/validate/__tests__/assertions.test.d.ts +2 -0
- package/dist/validate/__tests__/assertions.test.d.ts.map +1 -0
- package/dist/validate/__tests__/assertions.test.js +254 -0
- package/dist/validate/__tests__/assertions.test.js.map +1 -0
- package/dist/validate/__tests__/duplicates.test.d.ts +2 -0
- package/dist/validate/__tests__/duplicates.test.d.ts.map +1 -0
- package/dist/validate/__tests__/duplicates.test.js +130 -0
- package/dist/validate/__tests__/duplicates.test.js.map +1 -0
- package/dist/validate/__tests__/edge-resolution.test.d.ts +2 -0
- package/dist/validate/__tests__/edge-resolution.test.d.ts.map +1 -0
- package/dist/validate/__tests__/edge-resolution.test.js +279 -0
- package/dist/validate/__tests__/edge-resolution.test.js.map +1 -0
- package/dist/validate/__tests__/history.test.d.ts +2 -0
- package/dist/validate/__tests__/history.test.d.ts.map +1 -0
- package/dist/validate/__tests__/history.test.js +93 -0
- package/dist/validate/__tests__/history.test.js.map +1 -0
- package/dist/validate/__tests__/includes.test.d.ts +2 -0
- package/dist/validate/__tests__/includes.test.d.ts.map +1 -0
- package/dist/validate/__tests__/includes.test.js +93 -0
- package/dist/validate/__tests__/includes.test.js.map +1 -0
- package/dist/validate/__tests__/registry.test.d.ts +2 -0
- package/dist/validate/__tests__/registry.test.d.ts.map +1 -0
- package/dist/validate/__tests__/registry.test.js +78 -0
- package/dist/validate/__tests__/registry.test.js.map +1 -0
- package/dist/validate/__tests__/run.test.d.ts +2 -0
- package/dist/validate/__tests__/run.test.d.ts.map +1 -0
- package/dist/validate/__tests__/run.test.js +215 -0
- package/dist/validate/__tests__/run.test.js.map +1 -0
- package/dist/validate/history.d.ts +23 -0
- package/dist/validate/history.d.ts.map +1 -0
- package/dist/validate/history.js +45 -0
- package/dist/validate/history.js.map +1 -0
- package/dist/validate/index.d.ts +10 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +9 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/registry.d.ts +27 -0
- package/dist/validate/registry.d.ts.map +1 -0
- package/dist/validate/registry.js +67 -0
- package/dist/validate/registry.js.map +1 -0
- package/dist/validate/rules/artifacts.d.ts +25 -0
- package/dist/validate/rules/artifacts.d.ts.map +1 -0
- package/dist/validate/rules/artifacts.js +87 -0
- package/dist/validate/rules/artifacts.js.map +1 -0
- package/dist/validate/rules/assertions.d.ts +19 -0
- package/dist/validate/rules/assertions.d.ts.map +1 -0
- package/dist/validate/rules/assertions.js +149 -0
- package/dist/validate/rules/assertions.js.map +1 -0
- package/dist/validate/rules/duplicates.d.ts +20 -0
- package/dist/validate/rules/duplicates.d.ts.map +1 -0
- package/dist/validate/rules/duplicates.js +0 -0
- package/dist/validate/rules/duplicates.js.map +1 -0
- package/dist/validate/rules/edge-resolution.d.ts +15 -0
- package/dist/validate/rules/edge-resolution.d.ts.map +1 -0
- package/dist/validate/rules/edge-resolution.js +185 -0
- package/dist/validate/rules/edge-resolution.js.map +1 -0
- package/dist/validate/rules/includes.d.ts +21 -0
- package/dist/validate/rules/includes.d.ts.map +1 -0
- package/dist/validate/rules/includes.js +112 -0
- package/dist/validate/rules/includes.js.map +1 -0
- package/dist/validate/run.d.ts +20 -0
- package/dist/validate/run.d.ts.map +1 -0
- package/dist/validate/run.js +90 -0
- package/dist/validate/run.js.map +1 -0
- package/dist/validate/types.d.ts +101 -0
- package/dist/validate/types.d.ts.map +1 -0
- package/dist/validate/types.js +2 -0
- package/dist/validate/types.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { resolveDocsRoots } from "./config.js";
|
|
7
|
+
import { countLines, hashBytes, hashContent } from "./content.js";
|
|
8
|
+
import { MANIFEST_VERSION, buildManifest, buildRootGraph, mergeGraphs, splitManifest, } from "./manifest.js";
|
|
9
|
+
const __dirname = resolve(fileURLToPath(import.meta.url), "..");
|
|
10
|
+
const FIXTURES_DIR = resolve(__dirname, "__fixtures__");
|
|
11
|
+
const MONOREPO_DIR = resolve(FIXTURES_DIR, "monorepo");
|
|
12
|
+
function fixtureConfig(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
rootDir: FIXTURES_DIR,
|
|
15
|
+
docsDir: "docs",
|
|
16
|
+
entryPoint: "docs/README.md",
|
|
17
|
+
ignore: [],
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function monorepoConfig(overrides = {}) {
|
|
22
|
+
return {
|
|
23
|
+
rootDir: MONOREPO_DIR,
|
|
24
|
+
docsDir: "docs",
|
|
25
|
+
entryPoint: "docs/README.md",
|
|
26
|
+
ignore: [],
|
|
27
|
+
projects: [
|
|
28
|
+
{ name: "Alpha", docsDir: "products/alpha/docs" },
|
|
29
|
+
{ name: "Beta", docsDir: "products/beta/docs" },
|
|
30
|
+
],
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
describe("buildRootGraph (shared history)", () => {
|
|
35
|
+
it("stamps modified dates from a caller-provided walk instead of its own", async () => {
|
|
36
|
+
const config = fixtureConfig();
|
|
37
|
+
const roots = resolveDocsRoots(config);
|
|
38
|
+
const date = "2026-02-03T04:05:06+00:00";
|
|
39
|
+
const graph = await buildRootGraph(config, roots[0], roots, new Map([["README.md", date]]));
|
|
40
|
+
const readme = graph.nodes.find((node) => node.id === "README.md");
|
|
41
|
+
expect(readme?.modified).toBe(date);
|
|
42
|
+
// A path the provided walk did not date stays undated — the scan must not
|
|
43
|
+
// fall back to a walk of its own once one has been handed in.
|
|
44
|
+
const arch = graph.nodes.find((node) => node.id === "architecture.md");
|
|
45
|
+
expect(arch?.modified).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("buildManifest", () => {
|
|
49
|
+
const tempDirs = [];
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
for (const dir of tempDirs.splice(0))
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
it("discovers all doc nodes", async () => {
|
|
55
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
56
|
+
const ids = manifest.nodes.map((n) => n.id).sort();
|
|
57
|
+
expect(ids).toEqual(["README.md", "api.yaml", "architecture.md"]);
|
|
58
|
+
});
|
|
59
|
+
it("extracts markdown anchors", async () => {
|
|
60
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
61
|
+
const arch = manifest.nodes.find((n) => n.id === "architecture.md");
|
|
62
|
+
const slugs = arch?.anchors.map((a) => a.slug);
|
|
63
|
+
expect(slugs).toContain("#overview");
|
|
64
|
+
expect(slugs).toContain("#data-flow");
|
|
65
|
+
expect(slugs).toContain("#database-schema");
|
|
66
|
+
});
|
|
67
|
+
it("carries line and level on markdown anchors", async () => {
|
|
68
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
69
|
+
const overview = manifest.nodes
|
|
70
|
+
.find((n) => n.id === "architecture.md")
|
|
71
|
+
?.anchors.find((a) => a.slug === "#overview");
|
|
72
|
+
expect(overview?.text).toBe("Overview");
|
|
73
|
+
expect(overview?.level).toBe(2);
|
|
74
|
+
expect(overview?.line).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
it("extracts openapi anchors", async () => {
|
|
77
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
78
|
+
const api = manifest.nodes.find((n) => n.id === "api.yaml");
|
|
79
|
+
const slugs = api?.anchors.map((a) => a.slug);
|
|
80
|
+
expect(slugs).toContain("#listUsers");
|
|
81
|
+
expect(slugs).toContain("#/components/schemas/User");
|
|
82
|
+
});
|
|
83
|
+
it("extracts edges from markdown links", async () => {
|
|
84
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
85
|
+
const readmeToArch = manifest.edges.filter((e) => e.from.node === "README.md" && e.to.node === "architecture.md");
|
|
86
|
+
expect(readmeToArch.length).toBeGreaterThanOrEqual(1);
|
|
87
|
+
const withAnchor = readmeToArch.find((e) => e.to.anchor === "#overview");
|
|
88
|
+
expect(withAnchor).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
it("sets correct node types", async () => {
|
|
91
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
92
|
+
expect(manifest.nodes.find((n) => n.id === "README.md")?.type).toBe("markdown");
|
|
93
|
+
expect(manifest.nodes.find((n) => n.id === "api.yaml")?.type).toBe("openapi");
|
|
94
|
+
});
|
|
95
|
+
it("records a content hash and line count for every document", async () => {
|
|
96
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
97
|
+
for (const node of manifest.nodes) {
|
|
98
|
+
expect(node.contentHash).toMatch(/^[0-9a-f]{16}$/);
|
|
99
|
+
expect(node.lineCount).toBeGreaterThan(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it("hashes openapi documents as well as markdown ones", async () => {
|
|
103
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
104
|
+
const api = manifest.nodes.find((n) => n.id === "api.yaml");
|
|
105
|
+
expect(api?.contentHash).toMatch(/^[0-9a-f]{16}$/);
|
|
106
|
+
expect(api?.lineCount).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
it("gives different documents different hashes", async () => {
|
|
109
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
110
|
+
const hashes = manifest.nodes.map((n) => n.contentHash);
|
|
111
|
+
expect(new Set(hashes).size).toBe(hashes.length);
|
|
112
|
+
});
|
|
113
|
+
it("matches hashing the file's content directly", async () => {
|
|
114
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
115
|
+
const arch = manifest.nodes.find((n) => n.id === "architecture.md");
|
|
116
|
+
const raw = readFileSync(resolve(FIXTURES_DIR, "docs", "architecture.md"), "utf-8");
|
|
117
|
+
expect(arch?.contentHash).toBe(hashContent(raw));
|
|
118
|
+
expect(arch?.lineCount).toBe(countLines(raw));
|
|
119
|
+
});
|
|
120
|
+
it("records the version a document declares in its frontmatter", async () => {
|
|
121
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: resolve(FIXTURES_DIR, "assertions") }));
|
|
122
|
+
expect(manifest.nodes.find((n) => n.id === "spec.md")?.version).toBe("2.42");
|
|
123
|
+
});
|
|
124
|
+
it("leaves a document that declares no version without one", async () => {
|
|
125
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: resolve(FIXTURES_DIR, "assertions") }));
|
|
126
|
+
// Absence is normal — an append-only registry has no version to give —
|
|
127
|
+
// so nothing downstream may treat it as an error.
|
|
128
|
+
expect(manifest.nodes.find((n) => n.id === "registry.md")?.version).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
it("dates every document from git history", async () => {
|
|
131
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
132
|
+
for (const node of manifest.nodes) {
|
|
133
|
+
// The fixtures are committed, so every one of them has a date. It comes
|
|
134
|
+
// from the last commit touching the file, never from the filesystem: a
|
|
135
|
+
// clone would give every document the checkout's mtime.
|
|
136
|
+
expect(node.modified).toBeDefined();
|
|
137
|
+
expect(Number.isNaN(Date.parse(node.modified))).toBe(false);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
it("indexes a docs tree outside a repository, just without dates", async () => {
|
|
141
|
+
const loose = mkdtempSync(resolve(tmpdir(), "weft-loose-"));
|
|
142
|
+
tempDirs.push(loose);
|
|
143
|
+
cpSync(resolve(FIXTURES_DIR, "docs"), resolve(loose, "docs"), { recursive: true });
|
|
144
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: loose }));
|
|
145
|
+
expect(manifest.nodes.length).toBeGreaterThan(0);
|
|
146
|
+
expect(manifest.nodes.every((n) => n.modified === undefined)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
it("stamps the current manifest schema version", async () => {
|
|
149
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
150
|
+
expect(manifest.version).toBe(MANIFEST_VERSION);
|
|
151
|
+
expect(MANIFEST_VERSION).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
it("carries presentation config in the site block", async () => {
|
|
154
|
+
const manifest = await buildManifest(fixtureConfig({
|
|
155
|
+
defaultTheme: "dark",
|
|
156
|
+
style: { dark: "luminous-precision", light: "summer-cloud" },
|
|
157
|
+
styleUrl: "https://cdn.example.com/styles",
|
|
158
|
+
layout: "reader",
|
|
159
|
+
siteTitle: "Weft Docs",
|
|
160
|
+
siteUrl: "https://docs.example.com",
|
|
161
|
+
ogImage: "og.png",
|
|
162
|
+
}));
|
|
163
|
+
expect(manifest.site).toEqual({
|
|
164
|
+
defaultTheme: "dark",
|
|
165
|
+
style: { dark: "luminous-precision", light: "summer-cloud" },
|
|
166
|
+
styleUrl: "https://cdn.example.com/styles",
|
|
167
|
+
layout: "reader",
|
|
168
|
+
siteTitle: "Weft Docs",
|
|
169
|
+
siteUrl: "https://docs.example.com",
|
|
170
|
+
ogImage: "og.png",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it("omits the site block when nothing is configured", async () => {
|
|
174
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
175
|
+
expect(manifest.site).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
it("omits projects in single-project mode", async () => {
|
|
178
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
179
|
+
expect(manifest.projects).toBeUndefined();
|
|
180
|
+
expect(manifest.nodes.every((n) => n.project === undefined)).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
it("strips a docsDir prefix from docOrder entries", async () => {
|
|
183
|
+
const manifest = await buildManifest(fixtureConfig({ docOrder: ["docs/architecture.md", "README.md"] }));
|
|
184
|
+
expect(manifest.nodes.slice(0, 2).map((n) => n.id)).toEqual(["architecture.md", "README.md"]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("buildManifest (freshness)", () => {
|
|
188
|
+
it("stamps a build block with an ISO 8601 builtAt and a hex inputsHash", async () => {
|
|
189
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
190
|
+
expect(manifest.build?.builtAt).toBeDefined();
|
|
191
|
+
expect(Number.isNaN(Date.parse(manifest.build?.builtAt))).toBe(false);
|
|
192
|
+
expect(manifest.build?.inputsHash).toMatch(/^[0-9a-f]{16}$/);
|
|
193
|
+
});
|
|
194
|
+
it("round-trips through JSON with the build block intact", async () => {
|
|
195
|
+
const manifest = await buildManifest(fixtureConfig());
|
|
196
|
+
const roundTripped = JSON.parse(JSON.stringify(manifest));
|
|
197
|
+
expect(roundTripped.build).toEqual(manifest.build);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe("mergeGraphs (freshness)", () => {
|
|
201
|
+
it("stamps the build block it is passed", () => {
|
|
202
|
+
const roots = resolveDocsRoots(fixtureConfig());
|
|
203
|
+
const build = { builtAt: "2026-08-01T00:00:00.000Z", inputsHash: "abc123" };
|
|
204
|
+
const manifest = mergeGraphs(fixtureConfig(), roots, new Map(), [], build);
|
|
205
|
+
expect(manifest.build).toEqual(build);
|
|
206
|
+
});
|
|
207
|
+
it("omits the build block when none is passed", () => {
|
|
208
|
+
const roots = resolveDocsRoots(fixtureConfig());
|
|
209
|
+
const manifest = mergeGraphs(fixtureConfig(), roots, new Map());
|
|
210
|
+
expect(manifest.build).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe("buildManifest (includes)", () => {
|
|
214
|
+
const tempDirs = [];
|
|
215
|
+
afterEach(() => {
|
|
216
|
+
for (const dir of tempDirs.splice(0))
|
|
217
|
+
rmSync(dir, { recursive: true, force: true });
|
|
218
|
+
});
|
|
219
|
+
function includeFixture() {
|
|
220
|
+
const dir = mkdtempSync(resolve(tmpdir(), "weft-includes-"));
|
|
221
|
+
tempDirs.push(dir);
|
|
222
|
+
cpSync(resolve(FIXTURES_DIR, "docs"), resolve(dir, "docs"), { recursive: true });
|
|
223
|
+
writeFileSync(resolve(dir, "docs/README.md.weft"), ["links:", " - target: architecture.md#overview", " type: includes"].join("\n"));
|
|
224
|
+
return dir;
|
|
225
|
+
}
|
|
226
|
+
it("stamps resolved defaults onto an includes edge", async () => {
|
|
227
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: includeFixture() }));
|
|
228
|
+
const edge = manifest.edges.find((e) => e.type === "includes");
|
|
229
|
+
expect(edge?.headingShift).toBe("auto");
|
|
230
|
+
expect(edge?.contributes).toBe("source");
|
|
231
|
+
});
|
|
232
|
+
it("stamps configured defaults over built-in ones", async () => {
|
|
233
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: includeFixture(), includes: { headingShift: "none" } }));
|
|
234
|
+
const edge = manifest.edges.find((e) => e.type === "includes");
|
|
235
|
+
expect(edge?.headingShift).toBe("none");
|
|
236
|
+
expect(edge?.contributes).toBe("source");
|
|
237
|
+
});
|
|
238
|
+
it("leaves other edge types unstamped", async () => {
|
|
239
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: includeFixture() }));
|
|
240
|
+
for (const edge of manifest.edges.filter((e) => e.type !== "includes")) {
|
|
241
|
+
expect("headingShift" in edge).toBe(false);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe("buildManifest (artifacts)", () => {
|
|
246
|
+
const ARTIFACTS_DIR = resolve(FIXTURES_DIR, "artifacts");
|
|
247
|
+
function artifactConfig(overrides = {}) {
|
|
248
|
+
return {
|
|
249
|
+
rootDir: ARTIFACTS_DIR,
|
|
250
|
+
docsDir: "docs",
|
|
251
|
+
entryPoint: "docs/README.md",
|
|
252
|
+
ignore: [],
|
|
253
|
+
artifacts: ["**/*.pdf"],
|
|
254
|
+
...overrides,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
it("registers a configured output as a node", async () => {
|
|
258
|
+
const manifest = await buildManifest(artifactConfig());
|
|
259
|
+
expect(manifest.nodes.map((n) => n.id).sort()).toContain("handbook.pdf");
|
|
260
|
+
});
|
|
261
|
+
it("indexes nothing extra when no artifacts are configured", async () => {
|
|
262
|
+
const manifest = await buildManifest(artifactConfig({ artifacts: undefined }));
|
|
263
|
+
// A PDF is not an indexable extension, so without the config key it was
|
|
264
|
+
// never going to become a node.
|
|
265
|
+
expect(manifest.nodes.every((n) => !n.id.endsWith(".pdf"))).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
it("marks an artifact so it never reaches the nav", async () => {
|
|
268
|
+
const manifest = await buildManifest(artifactConfig());
|
|
269
|
+
const pdf = manifest.nodes.find((n) => n.id === "handbook.pdf");
|
|
270
|
+
expect(pdf?.type).toBe("artifact");
|
|
271
|
+
expect(pdf?.hiddenFromNav).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
it("gives an artifact a hash but no anchors and no line count", async () => {
|
|
274
|
+
const manifest = await buildManifest(artifactConfig());
|
|
275
|
+
const pdf = manifest.nodes.find((n) => n.id === "handbook.pdf");
|
|
276
|
+
expect(pdf?.contentHash).toMatch(/^[0-9a-f]{16}$/);
|
|
277
|
+
expect(pdf?.anchors).toEqual([]);
|
|
278
|
+
expect(pdf?.lineCount).toBeUndefined();
|
|
279
|
+
});
|
|
280
|
+
it("hashes the artifact's bytes rather than a text reading of them", async () => {
|
|
281
|
+
const manifest = await buildManifest(artifactConfig());
|
|
282
|
+
const pdf = manifest.nodes.find((n) => n.id === "handbook.pdf");
|
|
283
|
+
const raw = readFileSync(resolve(ARTIFACTS_DIR, "docs", "handbook.pdf"));
|
|
284
|
+
expect(pdf?.contentHash).toBe(hashBytes(raw));
|
|
285
|
+
// Reading a PDF as UTF-8 does not throw, it silently mangles — so the two
|
|
286
|
+
// recipes genuinely disagree here rather than coinciding.
|
|
287
|
+
expect(pdf?.contentHash).not.toBe(hashContent(raw.toString("utf-8")));
|
|
288
|
+
});
|
|
289
|
+
it("titles an artifact with its file name", async () => {
|
|
290
|
+
const manifest = await buildManifest(artifactConfig());
|
|
291
|
+
expect(manifest.nodes.find((n) => n.id === "handbook.pdf")?.title).toBe("handbook.pdf");
|
|
292
|
+
});
|
|
293
|
+
it("respects ignore globs", async () => {
|
|
294
|
+
const manifest = await buildManifest(artifactConfig({ ignore: ["**/appendix.pdf"] }));
|
|
295
|
+
const ids = manifest.nodes.map((n) => n.id);
|
|
296
|
+
expect(ids).toContain("handbook.pdf");
|
|
297
|
+
expect(ids).not.toContain("appendix.pdf");
|
|
298
|
+
});
|
|
299
|
+
it("does not let a wide artifact glob replace an indexed document", async () => {
|
|
300
|
+
const manifest = await buildManifest(artifactConfig({ artifacts: ["**/*"] }));
|
|
301
|
+
const handbook = manifest.nodes.find((n) => n.id === "handbook.md");
|
|
302
|
+
// The document has anchors and content; replacing it with a hash and a
|
|
303
|
+
// file name would be a downgrade.
|
|
304
|
+
expect(handbook?.type).toBe("markdown");
|
|
305
|
+
expect(manifest.nodes.filter((n) => n.id === "handbook.md")).toHaveLength(1);
|
|
306
|
+
});
|
|
307
|
+
it("leaves a link to a registered artifact pointing at the artifact", async () => {
|
|
308
|
+
const manifest = await buildManifest(artifactConfig());
|
|
309
|
+
const link = manifest.edges.find((e) => e.from.node === "README.md" && e.to.node === "handbook.pdf");
|
|
310
|
+
// Published-form resolution rewrites `handbook.pdf` to `handbook.md` when
|
|
311
|
+
// the PDF is not a node. Registering it means the real target exists, so
|
|
312
|
+
// the link must be left alone.
|
|
313
|
+
expect(link).toBeDefined();
|
|
314
|
+
expect(link?.resolvedFrom).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
it("namespaces an artifact id by project", async () => {
|
|
317
|
+
const manifest = await buildManifest({
|
|
318
|
+
rootDir: ARTIFACTS_DIR,
|
|
319
|
+
docsDir: "docs",
|
|
320
|
+
entryPoint: "docs/README.md",
|
|
321
|
+
ignore: [],
|
|
322
|
+
artifacts: ["**/*.pdf"],
|
|
323
|
+
projects: [{ name: "Guides", docsDir: "docs" }],
|
|
324
|
+
});
|
|
325
|
+
const pdf = manifest.nodes.find((n) => n.id === "guides/handbook.pdf");
|
|
326
|
+
expect(pdf).toBeDefined();
|
|
327
|
+
expect(pdf?.project).toBe("guides");
|
|
328
|
+
});
|
|
329
|
+
it("reads derives-from edges from a sidecar beside the artifact", async () => {
|
|
330
|
+
const manifest = await buildManifest(artifactConfig());
|
|
331
|
+
const derived = manifest.edges.filter((e) => e.type === "derives-from");
|
|
332
|
+
expect(derived.map((e) => `${e.from.node} -> ${e.to.node}`).sort()).toEqual([
|
|
333
|
+
"appendix.pdf -> appendix.md",
|
|
334
|
+
"appendix.pdf -> handbook.md",
|
|
335
|
+
"handbook.pdf -> handbook.md",
|
|
336
|
+
]);
|
|
337
|
+
});
|
|
338
|
+
it("carries the recorded source hash onto the edge", async () => {
|
|
339
|
+
const manifest = await buildManifest(artifactConfig());
|
|
340
|
+
const edge = manifest.edges.find((e) => e.from.node === "handbook.pdf");
|
|
341
|
+
const source = manifest.nodes.find((n) => n.id === "handbook.md");
|
|
342
|
+
expect(edge?.sourceHash).toBe(source?.contentHash);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
describe("buildManifest (published-form links)", () => {
|
|
346
|
+
const publishedConfig = () => ({
|
|
347
|
+
rootDir: resolve(FIXTURES_DIR, "published"),
|
|
348
|
+
docsDir: "docs",
|
|
349
|
+
entryPoint: "docs/index.md",
|
|
350
|
+
ignore: [],
|
|
351
|
+
});
|
|
352
|
+
const edgesFrom = async () => (await buildManifest(publishedConfig())).edges.filter((e) => e.from.node === "index.md");
|
|
353
|
+
it("resolves a link to a rendered sibling back to its source", async () => {
|
|
354
|
+
const edges = await edgesFrom();
|
|
355
|
+
const resolved = edges.filter((e) => e.resolvedFrom === "guide.html");
|
|
356
|
+
expect(resolved.map((e) => e.to.node)).toEqual(["guide.md", "guide.md"]);
|
|
357
|
+
});
|
|
358
|
+
it("keeps the anchor when rewriting a published link", async () => {
|
|
359
|
+
const edges = await edgesFrom();
|
|
360
|
+
const withAnchor = edges.find((e) => e.resolvedFrom === "guide.html" && e.to.anchor);
|
|
361
|
+
expect(withAnchor?.to).toEqual({ node: "guide.md", anchor: "#setup" });
|
|
362
|
+
});
|
|
363
|
+
it("resolves a rendered sibling of an openapi document", async () => {
|
|
364
|
+
const edges = await edgesFrom();
|
|
365
|
+
expect(edges.find((e) => e.resolvedFrom === "api.html")?.to.node).toBe("api.yaml");
|
|
366
|
+
});
|
|
367
|
+
it("leaves a published link with no source sibling unresolved", async () => {
|
|
368
|
+
const edges = await edgesFrom();
|
|
369
|
+
const orphan = edges.find((e) => e.to.node === "reports/q4.html");
|
|
370
|
+
expect(orphan).toBeDefined();
|
|
371
|
+
expect(orphan?.resolvedFrom).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
it("does not mark a link that already named a node", async () => {
|
|
374
|
+
const edges = await edgesFrom();
|
|
375
|
+
const direct = edges.filter((e) => e.to.node === "guide.md" && !e.resolvedFrom);
|
|
376
|
+
expect(direct).toHaveLength(1);
|
|
377
|
+
});
|
|
378
|
+
it("makes those edges resolve, where before they pointed at nothing", async () => {
|
|
379
|
+
const manifest = await buildManifest(publishedConfig());
|
|
380
|
+
const ids = new Set(manifest.nodes.map((n) => n.id));
|
|
381
|
+
const dead = manifest.edges.filter((e) => !ids.has(e.to.node));
|
|
382
|
+
// Only the genuinely orphaned one is left.
|
|
383
|
+
expect(dead.map((e) => e.to.node)).toEqual(["reports/q4.html"]);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
describe("buildManifest (contributions)", () => {
|
|
387
|
+
const dirs = [];
|
|
388
|
+
/** A copy of the docs fixture plus a contribution file, somewhere writable. */
|
|
389
|
+
function projectWith(contribution) {
|
|
390
|
+
const dir = mkdtempSync(resolve(tmpdir(), "weft-contrib-build-"));
|
|
391
|
+
dirs.push(dir);
|
|
392
|
+
cpSync(resolve(FIXTURES_DIR, "docs"), resolve(dir, "docs"), { recursive: true });
|
|
393
|
+
writeFileSync(resolve(dir, "build.json"), JSON.stringify({ version: 1, ...contribution }));
|
|
394
|
+
return dir;
|
|
395
|
+
}
|
|
396
|
+
function contributedConfig(dir, overrides = {}) {
|
|
397
|
+
return {
|
|
398
|
+
rootDir: dir,
|
|
399
|
+
docsDir: "docs",
|
|
400
|
+
entryPoint: "docs/README.md",
|
|
401
|
+
ignore: [],
|
|
402
|
+
contributions: ["build.json"],
|
|
403
|
+
...overrides,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
afterEach(() => {
|
|
407
|
+
for (const dir of dirs.splice(0))
|
|
408
|
+
rmSync(dir, { recursive: true, force: true });
|
|
409
|
+
});
|
|
410
|
+
it("merges a contributed node into the indexed graph", async () => {
|
|
411
|
+
const dir = projectWith({ nodes: [{ id: "generated.md", type: "markdown", title: "Gen" }] });
|
|
412
|
+
const manifest = await buildManifest(contributedConfig(dir));
|
|
413
|
+
expect(manifest.nodes.map((n) => n.id)).toContain("generated.md");
|
|
414
|
+
expect(manifest.nodes.map((n) => n.id)).toContain("architecture.md");
|
|
415
|
+
});
|
|
416
|
+
it("sorts a contributed node with the indexed ones rather than appending it", async () => {
|
|
417
|
+
const dir = projectWith({ nodes: [{ id: "AAA.md", type: "markdown" }] });
|
|
418
|
+
const manifest = await buildManifest(contributedConfig(dir));
|
|
419
|
+
expect(manifest.nodes[0].id).toBe("AAA.md");
|
|
420
|
+
});
|
|
421
|
+
it("honours docOrder for a contributed node", async () => {
|
|
422
|
+
const dir = projectWith({ nodes: [{ id: "generated.md", type: "markdown" }] });
|
|
423
|
+
const manifest = await buildManifest(contributedConfig(dir, { docOrder: ["generated.md", "README.md"] }));
|
|
424
|
+
expect(manifest.nodes.slice(0, 2).map((n) => n.id)).toEqual(["generated.md", "README.md"]);
|
|
425
|
+
});
|
|
426
|
+
it("applies a metadata patch to a document that was indexed", async () => {
|
|
427
|
+
const dir = projectWith({ metadata: { "architecture.md": { title: "Architecture v2.41" } } });
|
|
428
|
+
const manifest = await buildManifest(contributedConfig(dir));
|
|
429
|
+
expect(manifest.nodes.find((n) => n.id === "architecture.md")?.title).toBe("Architecture v2.41");
|
|
430
|
+
});
|
|
431
|
+
it("keeps the indexed hash and anchors a patch did not touch", async () => {
|
|
432
|
+
const dir = projectWith({ metadata: { "architecture.md": { title: "Renamed" } } });
|
|
433
|
+
const arch = (await buildManifest(contributedConfig(dir))).nodes.find((n) => n.id === "architecture.md");
|
|
434
|
+
expect(arch?.contentHash).toMatch(/^[0-9a-f]{16}$/);
|
|
435
|
+
expect(arch?.anchors.length).toBeGreaterThan(0);
|
|
436
|
+
});
|
|
437
|
+
it("adds contributed edges to the graph", async () => {
|
|
438
|
+
const dir = projectWith({
|
|
439
|
+
edges: [
|
|
440
|
+
{ from: { node: "README.md" }, to: { node: "architecture.md" }, type: "derives-from" },
|
|
441
|
+
],
|
|
442
|
+
});
|
|
443
|
+
const manifest = await buildManifest(contributedConfig(dir));
|
|
444
|
+
expect(manifest.edges.some((e) => e.type === "derives-from")).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
it("indexes source unchanged when no contributions are configured", async () => {
|
|
447
|
+
const dir = projectWith({ nodes: [{ id: "generated.md", type: "markdown" }] });
|
|
448
|
+
const manifest = await buildManifest(contributedConfig(dir, { contributions: undefined }));
|
|
449
|
+
expect(manifest.nodes.map((n) => n.id)).not.toContain("generated.md");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
describe("buildManifest (docOrderStrict)", () => {
|
|
453
|
+
const strict = () => buildManifest(fixtureConfig({ docOrder: ["architecture.md"], docOrderStrict: true }));
|
|
454
|
+
it("keeps every document in the graph", async () => {
|
|
455
|
+
const manifest = await strict();
|
|
456
|
+
expect(manifest.nodes.map((n) => n.id).sort()).toEqual([
|
|
457
|
+
"README.md",
|
|
458
|
+
"api.yaml",
|
|
459
|
+
"architecture.md",
|
|
460
|
+
]);
|
|
461
|
+
});
|
|
462
|
+
it("marks the unlisted documents as hidden from the nav", async () => {
|
|
463
|
+
const manifest = await strict();
|
|
464
|
+
const hidden = manifest.nodes.filter((n) => n.hiddenFromNav).map((n) => n.id);
|
|
465
|
+
expect(hidden.sort()).toEqual(["README.md", "api.yaml"]);
|
|
466
|
+
expect(manifest.nodes.find((n) => n.id === "architecture.md")?.hiddenFromNav).toBeUndefined();
|
|
467
|
+
});
|
|
468
|
+
it("still orders the listed documents first", async () => {
|
|
469
|
+
const manifest = await strict();
|
|
470
|
+
expect(manifest.nodes[0].id).toBe("architecture.md");
|
|
471
|
+
});
|
|
472
|
+
it("leaves no edge pointing at a document outside the graph", async () => {
|
|
473
|
+
const manifest = await strict();
|
|
474
|
+
const ids = new Set(manifest.nodes.map((n) => n.id));
|
|
475
|
+
// The invariant strict mode used to break: an edge whose source or target
|
|
476
|
+
// was filtered out survived in the manifest with nothing to resolve to.
|
|
477
|
+
for (const edge of manifest.edges) {
|
|
478
|
+
expect(ids.has(edge.from.node)).toBe(true);
|
|
479
|
+
expect(ids.has(edge.to.node)).toBe(true);
|
|
480
|
+
}
|
|
481
|
+
expect(manifest.edges.length).toBeGreaterThan(0);
|
|
482
|
+
});
|
|
483
|
+
it("marks nothing when docOrderStrict is off", async () => {
|
|
484
|
+
const manifest = await buildManifest(fixtureConfig({ docOrder: ["architecture.md"] }));
|
|
485
|
+
expect(manifest.nodes.every((n) => n.hiddenFromNav === undefined)).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
describe("buildManifest (multi-project)", () => {
|
|
489
|
+
it("namespaces node ids by project slug", async () => {
|
|
490
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
491
|
+
expect(manifest.nodes.map((n) => n.id).sort()).toEqual([
|
|
492
|
+
"alpha/README.md",
|
|
493
|
+
"alpha/features.md",
|
|
494
|
+
"beta/README.md",
|
|
495
|
+
"beta/api.yaml",
|
|
496
|
+
]);
|
|
497
|
+
});
|
|
498
|
+
it("tags each node with its owning project", async () => {
|
|
499
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
500
|
+
expect(manifest.nodes.find((n) => n.id === "alpha/features.md")?.project).toBe("alpha");
|
|
501
|
+
expect(manifest.nodes.find((n) => n.id === "beta/api.yaml")?.project).toBe("beta");
|
|
502
|
+
});
|
|
503
|
+
it("records the configured projects", async () => {
|
|
504
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
505
|
+
expect(manifest.projects).toEqual([
|
|
506
|
+
{ name: "Alpha", slug: "alpha", docsDir: "products/alpha/docs" },
|
|
507
|
+
{ name: "Beta", slug: "beta", docsDir: "products/beta/docs" },
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
it("resolves a markdown link that crosses products", async () => {
|
|
511
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
512
|
+
const crossing = manifest.edges.find((e) => e.from.node === "alpha/features.md" && e.to.node === "beta/api.yaml");
|
|
513
|
+
expect(crossing).toBeDefined();
|
|
514
|
+
expect(crossing?.to.anchor).toBe("#listUsers");
|
|
515
|
+
});
|
|
516
|
+
it("resolves bare and slug-qualified sidecar targets", async () => {
|
|
517
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
518
|
+
const fromFeatures = manifest.edges.filter((e) => e.from.node === "alpha/features.md");
|
|
519
|
+
const crossProduct = fromFeatures.find((e) => e.type === "implements");
|
|
520
|
+
expect(crossProduct?.to).toEqual({
|
|
521
|
+
node: "beta/api.yaml",
|
|
522
|
+
anchor: "#/components/schemas/User",
|
|
523
|
+
});
|
|
524
|
+
expect(crossProduct?.from.anchor).toBe("#sync");
|
|
525
|
+
const ownProject = fromFeatures.find((e) => e.type === "see-also");
|
|
526
|
+
expect(ownProject?.to.node).toBe("alpha/README.md");
|
|
527
|
+
});
|
|
528
|
+
it("keeps intra-project links inside their own project", async () => {
|
|
529
|
+
const manifest = await buildManifest(monorepoConfig());
|
|
530
|
+
const edge = manifest.edges.find((e) => e.from.node === "alpha/README.md" && e.type === "references");
|
|
531
|
+
expect(edge?.to.node).toBe("alpha/features.md");
|
|
532
|
+
});
|
|
533
|
+
it("accepts docOrder as a repo-relative path or a qualified id", async () => {
|
|
534
|
+
const manifest = await buildManifest(monorepoConfig({
|
|
535
|
+
docOrder: ["products/beta/docs/api.yaml", "alpha/features.md"],
|
|
536
|
+
docOrderStrict: true,
|
|
537
|
+
}));
|
|
538
|
+
expect(manifest.nodes.slice(0, 2).map((n) => n.id)).toEqual([
|
|
539
|
+
"beta/api.yaml",
|
|
540
|
+
"alpha/features.md",
|
|
541
|
+
]);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
describe("splitManifest", () => {
|
|
545
|
+
it("returns nothing in single-project mode", async () => {
|
|
546
|
+
const config = fixtureConfig();
|
|
547
|
+
const manifest = await buildManifest(config);
|
|
548
|
+
expect(splitManifest(manifest, resolveDocsRoots(config))).toEqual([]);
|
|
549
|
+
});
|
|
550
|
+
it("partitions nodes and edges by project, losing nothing", async () => {
|
|
551
|
+
const config = monorepoConfig();
|
|
552
|
+
const roots = resolveDocsRoots(config);
|
|
553
|
+
const manifest = await buildManifest(config);
|
|
554
|
+
const split = splitManifest(manifest, roots);
|
|
555
|
+
expect(split.map((p) => p.project.slug)).toEqual(["alpha", "beta"]);
|
|
556
|
+
const ids = (list) => list.map((n) => n.id).sort();
|
|
557
|
+
expect(ids(split.flatMap((p) => p.nodes))).toEqual(ids(manifest.nodes));
|
|
558
|
+
const edgeKey = (e) => `${e.from.node}->${e.to.node}:${e.type}`;
|
|
559
|
+
expect(split.flatMap((p) => p.edges.map(edgeKey)).sort()).toEqual(manifest.edges.map(edgeKey).sort());
|
|
560
|
+
});
|
|
561
|
+
it("assigns every edge to the project of its source node", async () => {
|
|
562
|
+
const config = monorepoConfig();
|
|
563
|
+
const split = splitManifest(await buildManifest(config), resolveDocsRoots(config));
|
|
564
|
+
for (const project of split) {
|
|
565
|
+
expect(project.edges.every((e) => e.from.node.startsWith(`${project.project.slug}/`))).toBe(true);
|
|
566
|
+
expect(project.nodes.every((n) => n.project === project.project.slug)).toBe(true);
|
|
567
|
+
}
|
|
568
|
+
// The cross-product edge lives with its source, not its target.
|
|
569
|
+
const alpha = split.find((p) => p.project.slug === "alpha");
|
|
570
|
+
expect(alpha?.edges.some((e) => e.to.node.startsWith("beta/"))).toBe(true);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
describe("buildManifest (extensions)", () => {
|
|
574
|
+
const dirs = [];
|
|
575
|
+
/** A copy of the docs fixture in a writable temp dir, plus extra files. */
|
|
576
|
+
function projectWith(files) {
|
|
577
|
+
const dir = mkdtempSync(resolve(tmpdir(), "weft-extensions-"));
|
|
578
|
+
dirs.push(dir);
|
|
579
|
+
cpSync(resolve(FIXTURES_DIR, "docs"), resolve(dir, "docs"), { recursive: true });
|
|
580
|
+
for (const [name, content] of Object.entries(files)) {
|
|
581
|
+
writeFileSync(resolve(dir, "docs", name), content);
|
|
582
|
+
}
|
|
583
|
+
return dir;
|
|
584
|
+
}
|
|
585
|
+
afterEach(() => {
|
|
586
|
+
for (const dir of dirs.splice(0))
|
|
587
|
+
rmSync(dir, { recursive: true, force: true });
|
|
588
|
+
});
|
|
589
|
+
it("indexes a configured extension exactly as a built-in one", async () => {
|
|
590
|
+
const dir = projectWith({
|
|
591
|
+
"notes.qmd": "---\ntitle: Field Notes\n---\n\n# Field Notes\n\nBody.\n",
|
|
592
|
+
});
|
|
593
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: dir, extensions: { ".qmd": "markdown" } }));
|
|
594
|
+
const node = manifest.nodes.find((n) => n.id === "notes.qmd");
|
|
595
|
+
expect(node?.type).toBe("markdown");
|
|
596
|
+
expect(node?.title).toBe("Field Notes");
|
|
597
|
+
expect(node?.anchors.map((a) => a.slug)).toContain("#field-notes");
|
|
598
|
+
});
|
|
599
|
+
it("leaves an extension neither built in nor configured un-indexed", async () => {
|
|
600
|
+
const dir = projectWith({ "notes.txt": "plain text, not a doc type Weft knows" });
|
|
601
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: dir }));
|
|
602
|
+
expect(manifest.nodes.map((n) => n.id)).not.toContain("notes.txt");
|
|
603
|
+
});
|
|
604
|
+
it("does not index .json by default", async () => {
|
|
605
|
+
const dir = projectWith({
|
|
606
|
+
"spec.json": JSON.stringify({ openapi: "3.0.0", info: { title: "Spec" }, paths: {} }),
|
|
607
|
+
});
|
|
608
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: dir }));
|
|
609
|
+
expect(manifest.nodes.map((n) => n.id)).not.toContain("spec.json");
|
|
610
|
+
});
|
|
611
|
+
it("indexes .json as openapi when explicitly configured", async () => {
|
|
612
|
+
const dir = projectWith({
|
|
613
|
+
"spec.json": JSON.stringify({
|
|
614
|
+
openapi: "3.0.0",
|
|
615
|
+
info: { title: "Spec" },
|
|
616
|
+
paths: { "/users": { get: { operationId: "listUsers" } } },
|
|
617
|
+
}),
|
|
618
|
+
});
|
|
619
|
+
const manifest = await buildManifest(fixtureConfig({ rootDir: dir, extensions: { ".json": "openapi" } }));
|
|
620
|
+
const node = manifest.nodes.find((n) => n.id === "spec.json");
|
|
621
|
+
expect(node?.type).toBe("openapi");
|
|
622
|
+
expect(node?.anchors.map((a) => a.slug)).toContain("#listUsers");
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
//# sourceMappingURL=manifest.test.js.map
|