@intentius/chant 0.44.13 → 0.45.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/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +11 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +57 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +144 -5
- package/src/cli/commands/build.ts +66 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +19 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +1 -1
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +22 -2
- package/src/cli/handlers/lifecycle.test.ts +117 -0
- package/src/cli/handlers/lifecycle.ts +16 -0
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +55 -6
- package/src/cli/main.ts +5 -3
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +3 -1
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +16 -3
- package/src/codegen/validate.ts +17 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +56 -1
- package/src/config.ts +152 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/policy.ts +14 -3
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders-exports.test.ts +60 -0
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { readAuthoredPages } from "./docs-pages";
|
|
7
|
+
import { docsPipeline, writeDocsSite, GENERATED_MARKER_TAG } from "./docs";
|
|
8
|
+
import { buildSidebar, quadrantItems } from "./docs-sidebar";
|
|
9
|
+
import type { DocsConfig, DocsResult, SidebarPage } from "./docs-types";
|
|
10
|
+
|
|
11
|
+
let root: string;
|
|
12
|
+
|
|
13
|
+
function config(overrides: Partial<DocsConfig> = {}): DocsConfig {
|
|
14
|
+
return {
|
|
15
|
+
name: "fixture",
|
|
16
|
+
displayName: "Fixture",
|
|
17
|
+
description: "fixture lexicon",
|
|
18
|
+
distDir: join(root, "dist"),
|
|
19
|
+
outDir: join(root, "docs"),
|
|
20
|
+
srcDir: join(root, "src"),
|
|
21
|
+
examplesDir: join(root, "examples"),
|
|
22
|
+
basePath: "/chant/lexicons/fixture/",
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function page(name: string, frontmatter: string, body = "Body.\n"): void {
|
|
28
|
+
mkdirSync(join(root, "docs", "pages"), { recursive: true });
|
|
29
|
+
writeFileSync(join(root, "docs", "pages", name), `---\n${frontmatter}\n---\n\n${body}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
root = mkdtempSync(join(tmpdir(), "chant-docs-pages-"));
|
|
34
|
+
mkdirSync(join(root, "dist"), { recursive: true });
|
|
35
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
36
|
+
mkdirSync(join(root, "examples"), { recursive: true });
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(root, "dist", "manifest.json"),
|
|
39
|
+
JSON.stringify({ name: "fixture", version: "0.0.0", intrinsics: [{ name: "ref", description: "a ref" }] }),
|
|
40
|
+
);
|
|
41
|
+
writeFileSync(join(root, "dist", "meta.json"), JSON.stringify({}));
|
|
42
|
+
writeFileSync(join(root, "examples", "snippet.ts"), "export const x = 1;\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("readAuthoredPages", () => {
|
|
50
|
+
test("returns nothing when docs/pages/ does not exist", () => {
|
|
51
|
+
expect(readAuthoredPages(config())).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("reads frontmatter, expands file markers, strips sidebar-only keys", () => {
|
|
55
|
+
page(
|
|
56
|
+
"getting-started.mdx",
|
|
57
|
+
'title: "Getting Started"\ndescription: "Start here"\ndiataxis: tutorial\norder: 1\ngroup: "Intro"\nlabel: "Start"\nhidden: false',
|
|
58
|
+
"Before\n\n{{file:snippet.ts}}\n\nAfter\n",
|
|
59
|
+
);
|
|
60
|
+
const [p] = readAuthoredPages(config());
|
|
61
|
+
expect(p.slug).toBe("getting-started");
|
|
62
|
+
expect(p.quadrant).toBe("tutorial");
|
|
63
|
+
expect(p.order).toBe(1);
|
|
64
|
+
expect(p.group).toBe("Intro");
|
|
65
|
+
expect(p.label).toBe("Start");
|
|
66
|
+
expect(p.hidden).toBe(false);
|
|
67
|
+
expect(p.content).toContain('```typescript title="snippet.ts"\nexport const x = 1;\n```');
|
|
68
|
+
expect(p.content).toContain("diataxis: tutorial");
|
|
69
|
+
expect(p.content).not.toMatch(/^(order|group|label|hidden):/m);
|
|
70
|
+
expect(p.content).toContain('title: "Getting Started"');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("label defaults to title", () => {
|
|
74
|
+
page("x.mdx", "title: Plain Title\ndiataxis: reference");
|
|
75
|
+
expect(readAuthoredPages(config())[0].label).toBe("Plain Title");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("missing diataxis throws with the file path", () => {
|
|
79
|
+
page("untagged.mdx", "title: Untagged");
|
|
80
|
+
expect(() => readAuthoredPages(config())).toThrow(/untagged\.mdx.*diataxis/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("unknown diataxis value throws", () => {
|
|
84
|
+
page("bad.mdx", "title: Bad\ndiataxis: guide");
|
|
85
|
+
expect(() => readAuthoredPages(config())).toThrow(/"guide" is not one of/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("docsPipeline with authored pages", () => {
|
|
90
|
+
test("authored page wins a slug collision with a generated page", () => {
|
|
91
|
+
page("intrinsics.mdx", "title: My Intrinsics\ndiataxis: reference", "Hand-written.\n");
|
|
92
|
+
const result = docsPipeline(config());
|
|
93
|
+
expect(result.pages.get("intrinsics.mdx")).toContain("Hand-written.");
|
|
94
|
+
expect(result.sidebarPages.filter((p) => p.slug === "intrinsics")).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("generated reference pages land in the reference quadrant after authored ones", () => {
|
|
98
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
99
|
+
const result = docsPipeline(config());
|
|
100
|
+
const ref = result.sidebarPages.filter((p) => p.quadrant === "reference").map((p) => p.slug);
|
|
101
|
+
expect(ref).toEqual(["intrinsics", "serialization"]);
|
|
102
|
+
expect(result.sidebarPages.find((p) => p.slug === "howto")?.quadrant).toBe("how-to");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("hidden pages are written but kept out of the sidebar", () => {
|
|
106
|
+
page("secret.mdx", "title: Secret\ndiataxis: reference\nhidden: true");
|
|
107
|
+
const result = docsPipeline(config());
|
|
108
|
+
expect(result.pages.has("secret.mdx")).toBe(true);
|
|
109
|
+
expect(result.sidebarPages.some((p) => p.slug === "secret")).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("authored pages carry a provenance marker naming the source file", () => {
|
|
113
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
114
|
+
const result = docsPipeline(config());
|
|
115
|
+
const out = result.pages.get("howto.mdx") as string;
|
|
116
|
+
expect(out).toContain(GENERATED_MARKER_TAG);
|
|
117
|
+
expect(out).toContain("docs/pages/howto.mdx");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("writeDocsSite reaps the written copy once the source is removed", () => {
|
|
121
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
122
|
+
const cfg = config();
|
|
123
|
+
writeDocsSite(cfg, docsPipeline(cfg));
|
|
124
|
+
const written = join(root, "docs", "src", "content", "docs", "howto.mdx");
|
|
125
|
+
expect(existsSync(written)).toBe(true);
|
|
126
|
+
rmSync(join(root, "docs", "pages", "howto.mdx"));
|
|
127
|
+
writeDocsSite(cfg, docsPipeline(cfg));
|
|
128
|
+
expect(existsSync(written)).toBe(false);
|
|
129
|
+
const astro = readFileSync(join(root, "docs", "astro.config.mjs"), "utf-8");
|
|
130
|
+
expect(astro).not.toContain("howto");
|
|
131
|
+
const schema = readFileSync(join(root, "docs", "src", "content.config.ts"), "utf-8");
|
|
132
|
+
expect(schema).toContain("diataxis");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("buildSidebar", () => {
|
|
137
|
+
const sb = (pages: SidebarPage[]) =>
|
|
138
|
+
buildSidebar(config(), { pages: new Map(), sidebarPages: pages, stats: { resources: 0, properties: 0, services: 0, rules: 0, intrinsics: 0 } } satisfies DocsResult);
|
|
139
|
+
|
|
140
|
+
test("groups in fixed quadrant order and omits empty quadrants", () => {
|
|
141
|
+
const items = sb([
|
|
142
|
+
{ slug: "why", label: "Why", quadrant: "explanation" },
|
|
143
|
+
{ slug: "start", label: "Start", quadrant: "tutorial" },
|
|
144
|
+
{ slug: "rules", label: "All Rules", quadrant: "reference", order: 1000 },
|
|
145
|
+
]);
|
|
146
|
+
expect(items.map((i) => i.label)).toEqual(["← chant docs", "Overview", "Tutorials", "Reference", "Explanation"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("orders by order then label and nests groups after loose pages", () => {
|
|
150
|
+
const items = quadrantItems([
|
|
151
|
+
{ slug: "b", label: "Bravo", quadrant: "reference" },
|
|
152
|
+
{ slug: "a", label: "Alpha", quadrant: "reference" },
|
|
153
|
+
{ slug: "z", label: "Zulu", quadrant: "reference", order: 0 },
|
|
154
|
+
{ slug: "eks", label: "EKS", quadrant: "reference", group: "Vendor" },
|
|
155
|
+
{ slug: "aks", label: "AKS", quadrant: "reference", group: "Vendor" },
|
|
156
|
+
]);
|
|
157
|
+
expect(items).toEqual([
|
|
158
|
+
{ label: "Zulu", slug: "z" },
|
|
159
|
+
{ label: "Alpha", slug: "a" },
|
|
160
|
+
{ label: "Bravo", slug: "b" },
|
|
161
|
+
{ label: "Vendor", items: [{ label: "AKS", slug: "aks" }, { label: "EKS", slug: "eks" }] },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* chant #1377 — `dist/manifest.json` is written by `npm run bundle` (prepack
|
|
168
|
+
* only), so its version is whatever was last bundled on the machine; on one
|
|
169
|
+
* checkout it put the fountain docs two minor versions behind what was
|
|
170
|
+
* already committed. `package.json` is the source of truth, so the rendered
|
|
171
|
+
* version comes from there and the manifest is only a fallback.
|
|
172
|
+
*/
|
|
173
|
+
describe("lexicon version in generated docs comes from package.json (#1377)", () => {
|
|
174
|
+
test("package.json beside dist/ wins over a stale manifest", () => {
|
|
175
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture", version: "0.37.2" }));
|
|
176
|
+
const index = docsPipeline(config()).pages.get("index.mdx");
|
|
177
|
+
expect(index).toContain("**Lexicon version:** 0.37.2");
|
|
178
|
+
expect(index).not.toContain("0.0.0");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("packageJsonPath overrides the default location", () => {
|
|
182
|
+
mkdirSync(join(root, "elsewhere"), { recursive: true });
|
|
183
|
+
writeFileSync(join(root, "elsewhere", "package.json"), JSON.stringify({ version: "1.2.3" }));
|
|
184
|
+
const index = docsPipeline(config({ packageJsonPath: join(root, "elsewhere", "package.json") })).pages.get(
|
|
185
|
+
"index.mdx",
|
|
186
|
+
);
|
|
187
|
+
expect(index).toContain("**Lexicon version:** 1.2.3");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("falls back to the manifest when no package.json is present", () => {
|
|
191
|
+
const index = docsPipeline(config()).pages.get("index.mdx");
|
|
192
|
+
expect(index).toContain("**Lexicon version:** 0.0.0");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authored lexicon doc pages (chant #1731 / #1733).
|
|
3
|
+
*
|
|
4
|
+
* A lexicon's prose lives as `.mdx` files under `lexicons/<name>/docs/pages/`.
|
|
5
|
+
* Each carries a `diataxis` frontmatter field naming its Diátaxis quadrant
|
|
6
|
+
* (https://diataxis.fr). The pipeline expands `{{file:...}}` markers, stamps a
|
|
7
|
+
* provenance marker and writes the result into `src/content/docs/`, and the
|
|
8
|
+
* sidebar is grouped from the field. This replaced two older modes — prose
|
|
9
|
+
* inside `docs.ts` template literals (`extraPages`) and hand-written content
|
|
10
|
+
* pages wired through `sidebarExtra` — removed in chant #1757.
|
|
11
|
+
*
|
|
12
|
+
* Frontmatter recognised here, on top of Starlight's `title` / `description`:
|
|
13
|
+
*
|
|
14
|
+
* - `diataxis`: `tutorial | how-to | reference | explanation`. Required.
|
|
15
|
+
* - `label`: sidebar label; defaults to `title`.
|
|
16
|
+
* - `group`: a nested subgroup label inside the quadrant (e.g. "Vendor Composites").
|
|
17
|
+
* - `order`: number; lower sorts first within its group. Unordered pages follow, by label.
|
|
18
|
+
* - `hidden`: `true` keeps the page out of the sidebar (reachable by URL only).
|
|
19
|
+
*
|
|
20
|
+
* `group` / `order` / `hidden` / `label` are stripped from the written copy so
|
|
21
|
+
* Starlight's schema never sees them; `diataxis` stays, and the generated
|
|
22
|
+
* `content.config.ts` declares it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
|
|
28
|
+
import { expandFileMarkers } from "./docs-file-markers";
|
|
29
|
+
import type { DocsConfig, Quadrant, SidebarPage } from "./docs-types";
|
|
30
|
+
|
|
31
|
+
export const QUADRANTS: readonly Quadrant[] = ["tutorial", "how-to", "reference", "explanation"];
|
|
32
|
+
|
|
33
|
+
/** Sidebar group label for each quadrant, in display order. */
|
|
34
|
+
export const QUADRANT_LABELS: Record<Quadrant, string> = {
|
|
35
|
+
tutorial: "Tutorials",
|
|
36
|
+
"how-to": "How-to guides",
|
|
37
|
+
reference: "Reference",
|
|
38
|
+
explanation: "Explanation",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface AuthoredPage extends SidebarPage {
|
|
42
|
+
/** Source file name under `pagesDir`, e.g. `getting-started.mdx`. */
|
|
43
|
+
file: string;
|
|
44
|
+
/** Page body with the rewritten frontmatter, markers expanded. */
|
|
45
|
+
content: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Frontmatter {
|
|
49
|
+
fields: Map<string, string>;
|
|
50
|
+
/** Raw frontmatter lines, in order, so unknown keys survive the rewrite. */
|
|
51
|
+
lines: string[];
|
|
52
|
+
body: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseFrontmatter(text: string, file: string): Frontmatter {
|
|
56
|
+
if (!text.startsWith("---\n")) {
|
|
57
|
+
throw new Error(`${file}: authored doc page must start with a frontmatter block`);
|
|
58
|
+
}
|
|
59
|
+
const end = text.indexOf("\n---", 4);
|
|
60
|
+
if (end === -1) throw new Error(`${file}: unterminated frontmatter`);
|
|
61
|
+
const lines = text.slice(4, end).split("\n");
|
|
62
|
+
const fields = new Map<string, string>();
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
65
|
+
if (m) fields.set(m[1], m[2].trim());
|
|
66
|
+
}
|
|
67
|
+
// Skip the closing `---` and the newline after it.
|
|
68
|
+
const afterClose = text.indexOf("\n", end + 1);
|
|
69
|
+
const body = afterClose === -1 ? "" : text.slice(afterClose + 1);
|
|
70
|
+
return { fields, lines, body };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unquote(v: string): string {
|
|
74
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
75
|
+
try {
|
|
76
|
+
return v.startsWith('"') ? (JSON.parse(v) as string) : v.slice(1, -1);
|
|
77
|
+
} catch {
|
|
78
|
+
return v.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return v;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Keys this module consumes and removes from the written page. */
|
|
85
|
+
const SIDEBAR_KEYS = new Set(["label", "group", "order", "hidden"]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read every authored page under `pagesDir`. Returns an empty list when the
|
|
89
|
+
* directory does not exist, which is every lexicon before it migrates.
|
|
90
|
+
*/
|
|
91
|
+
export function readAuthoredPages(config: DocsConfig): AuthoredPage[] {
|
|
92
|
+
const pagesDir = config.pagesDir ?? join(config.outDir, "pages");
|
|
93
|
+
if (!existsSync(pagesDir)) return [];
|
|
94
|
+
|
|
95
|
+
const pages: AuthoredPage[] = [];
|
|
96
|
+
for (const file of readdirSync(pagesDir).sort()) {
|
|
97
|
+
if (!file.endsWith(".mdx") && !file.endsWith(".md")) continue;
|
|
98
|
+
const path = join(pagesDir, file);
|
|
99
|
+
const { fields, lines, body } = parseFrontmatter(readFileSync(path, "utf-8"), path);
|
|
100
|
+
|
|
101
|
+
const title = fields.get("title");
|
|
102
|
+
if (!title) throw new Error(`${path}: frontmatter needs a title`);
|
|
103
|
+
const quadrant = fields.get("diataxis");
|
|
104
|
+
if (!quadrant) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${path}: frontmatter needs \`diataxis: ${QUADRANTS.join(" | ")}\` (https://diataxis.fr)`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!QUADRANTS.includes(quadrant as Quadrant)) {
|
|
110
|
+
throw new Error(`${path}: diataxis "${quadrant}" is not one of ${QUADRANTS.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const orderRaw = fields.get("order");
|
|
114
|
+
const order = orderRaw === undefined ? undefined : Number(orderRaw);
|
|
115
|
+
if (order !== undefined && Number.isNaN(order)) {
|
|
116
|
+
throw new Error(`${path}: order must be a number, got "${orderRaw}"`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const kept = lines.filter((line) => {
|
|
120
|
+
const key = line.match(/^([A-Za-z_][\w-]*):/)?.[1];
|
|
121
|
+
return !(key && SIDEBAR_KEYS.has(key));
|
|
122
|
+
});
|
|
123
|
+
let content = body;
|
|
124
|
+
if (config.examplesDir) content = expandFileMarkers(content, config.examplesDir);
|
|
125
|
+
|
|
126
|
+
pages.push({
|
|
127
|
+
file,
|
|
128
|
+
slug: file.replace(/\.mdx?$/, ""),
|
|
129
|
+
label: unquote(fields.get("label") ?? title),
|
|
130
|
+
quadrant: quadrant as Quadrant,
|
|
131
|
+
group: fields.has("group") ? unquote(fields.get("group") as string) : undefined,
|
|
132
|
+
order,
|
|
133
|
+
hidden: fields.get("hidden") === "true",
|
|
134
|
+
content: ["---", ...kept, "---", "", content].join("\n"),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return pages;
|
|
138
|
+
}
|
|
@@ -42,14 +42,6 @@ export function generateOverview(
|
|
|
42
42
|
|
|
43
43
|
const suppress = new Set(config.suppressPages ?? []);
|
|
44
44
|
|
|
45
|
-
// Extra pages listed first in reference links
|
|
46
|
-
if (config.extraPages && config.extraPages.length > 0) {
|
|
47
|
-
for (const page of config.extraPages) {
|
|
48
|
-
if (page.sidebar === false) continue;
|
|
49
|
-
lines.push(`- [${page.title}](./${page.slug})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
45
|
if (!suppress.has("intrinsics") && manifest.intrinsics && manifest.intrinsics.length > 0) {
|
|
54
46
|
lines.push(
|
|
55
47
|
`- [Intrinsic Functions](./intrinsics) — ${manifest.intrinsics.length} built-in functions`,
|
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidebar generation for Starlight docs sites.
|
|
3
|
+
*
|
|
4
|
+
* Grouped by Diátaxis quadrant (https://diataxis.fr), chant #1731. Every page
|
|
5
|
+
* the pipeline knows about — authored under docs/pages/ and generated reference
|
|
6
|
+
* tables — arrives as a {@link SidebarPage} with a quadrant; this module only sorts and nests. Empty quadrants are omitted.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import
|
|
9
|
+
import { QUADRANTS, QUADRANT_LABELS } from "./docs-pages";
|
|
10
|
+
import type { DocsConfig, DocsResult, Quadrant, SidebarPage } from "./docs-types";
|
|
11
|
+
|
|
12
|
+
type Entry = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function byOrderThenLabel(a: SidebarPage, b: SidebarPage): number {
|
|
15
|
+
const ao = a.order ?? Number.POSITIVE_INFINITY;
|
|
16
|
+
const bo = b.order ?? Number.POSITIVE_INFINITY;
|
|
17
|
+
if (ao !== bo) return ao - bo;
|
|
18
|
+
return a.label.localeCompare(b.label);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One quadrant's entries: ungrouped pages first (by order, then label), then
|
|
23
|
+
* each nested `group` in order of its lowest-ordered member.
|
|
24
|
+
*/
|
|
25
|
+
export function quadrantItems(pages: SidebarPage[]): Entry[] {
|
|
26
|
+
const loose = pages.filter((p) => !p.group).sort(byOrderThenLabel);
|
|
27
|
+
const groups = new Map<string, SidebarPage[]>();
|
|
28
|
+
for (const p of pages) {
|
|
29
|
+
if (!p.group) continue;
|
|
30
|
+
const list = groups.get(p.group) ?? [];
|
|
31
|
+
list.push(p);
|
|
32
|
+
groups.set(p.group, list);
|
|
33
|
+
}
|
|
34
|
+
const items: Entry[] = loose.map((p) => ({ label: p.label, slug: p.slug }));
|
|
35
|
+
const grouped = [...groups.entries()]
|
|
36
|
+
.map(([label, members]) => ({ label, members: members.sort(byOrderThenLabel) }))
|
|
37
|
+
.sort((a, b) => byOrderThenLabel(a.members[0], b.members[0]));
|
|
38
|
+
for (const g of grouped) {
|
|
39
|
+
items.push({ label: g.label, items: g.members.map((p) => ({ label: p.label, slug: p.slug })) });
|
|
40
|
+
}
|
|
41
|
+
return items;
|
|
42
|
+
}
|
|
6
43
|
|
|
7
44
|
export function buildSidebar(
|
|
8
45
|
config: DocsConfig,
|
|
9
46
|
result: DocsResult,
|
|
10
|
-
):
|
|
47
|
+
): Entry[] {
|
|
11
48
|
// Starlight prepends basePath to every sidebar `link`, so a site-root-relative
|
|
12
49
|
// path like "/chant/" becomes "/chant/lexicons/aws/chant/" — a 404. Instead
|
|
13
50
|
// we use relative traversal: "../../" is prepended to become
|
|
@@ -15,47 +52,23 @@ export function buildSidebar(
|
|
|
15
52
|
const segments = (config.basePath ?? "/").replace(/^\/|\/$/g, "").split("/");
|
|
16
53
|
const backLink = segments.length > 1 ? "../".repeat(segments.length - 1) : "/";
|
|
17
54
|
|
|
18
|
-
const items:
|
|
55
|
+
const items: Entry[] = [
|
|
19
56
|
{ label: "← chant docs", link: backLink },
|
|
20
57
|
{ label: "Overview", slug: "index" },
|
|
21
58
|
];
|
|
22
59
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (page.sidebar === false) continue;
|
|
30
|
-
items.push({ label: page.title, slug: page.slug });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!suppress.has("intrinsics") && !extraSlugs.has("intrinsics") && result.pages.has("intrinsics.mdx")) {
|
|
35
|
-
items.push({ label: "Intrinsics", slug: "intrinsics" });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!suppress.has("pseudo-parameters") && !extraSlugs.has("pseudo-parameters") && result.pages.has("pseudo-parameters.mdx")) {
|
|
39
|
-
items.push({ label: "Pseudo-Parameters", slug: "pseudo-parameters" });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Every lexicon links its generated rules table, whether or not it also
|
|
43
|
-
// ships a prose `lint-rules` page. Skipping it when one existed was how gcp
|
|
44
|
-
// ended up emitting a page nothing pointed at (#1312), and it left readers
|
|
45
|
-
// with no complete list on the lexicons whose prose covers only part of the
|
|
46
|
-
// set — aws documented 26 of 50 that way. The label distinguishes the
|
|
47
|
-
// generated table from a prose page rather than competing with it.
|
|
48
|
-
if (!suppress.has("rules") && !extraSlugs.has("rules") && result.pages.has("rules.mdx")) {
|
|
49
|
-
items.push({ label: "All Rules", slug: "rules" });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!suppress.has("serialization") && !extraSlugs.has("serialization") && result.pages.has("serialization.mdx")) {
|
|
53
|
-
items.push({ label: "Serialization", slug: "serialization" });
|
|
60
|
+
const byQuadrant = new Map<Quadrant, SidebarPage[]>();
|
|
61
|
+
for (const page of result.sidebarPages) {
|
|
62
|
+
if (page.hidden) continue;
|
|
63
|
+
const list = byQuadrant.get(page.quadrant) ?? [];
|
|
64
|
+
list.push(page);
|
|
65
|
+
byQuadrant.set(page.quadrant, list);
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
for (const q of QUADRANTS) {
|
|
69
|
+
const pages = byQuadrant.get(q);
|
|
70
|
+
if (!pages || pages.length === 0) continue;
|
|
71
|
+
items.push({ label: QUADRANT_LABELS[q], items: quadrantItems(pages) });
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
return items;
|
|
@@ -13,6 +13,12 @@ export interface DocsConfig {
|
|
|
13
13
|
distDir: string;
|
|
14
14
|
/** Output directory for generated .mdx files */
|
|
15
15
|
outDir: string;
|
|
16
|
+
/**
|
|
17
|
+
* The lexicon's package.json, read for the version rendered into the docs
|
|
18
|
+
* (chant #1377). Defaults to `<distDir>/../package.json`. The manifest's
|
|
19
|
+
* version is only a fallback; it goes stale between bundles.
|
|
20
|
+
*/
|
|
21
|
+
packageJsonPath?: string;
|
|
16
22
|
/** Lexicon-specific overview content (markdown) */
|
|
17
23
|
overview?: string;
|
|
18
24
|
/** Output format description (e.g. "CloudFormation JSON template") */
|
|
@@ -21,8 +27,6 @@ export interface DocsConfig {
|
|
|
21
27
|
serviceFromType?: (resourceType: string) => string;
|
|
22
28
|
/** Custom sections to append to overview page */
|
|
23
29
|
extraSections?: Array<{ title: string; content: string }>;
|
|
24
|
-
/** Standalone pages added to the sidebar after Overview */
|
|
25
|
-
extraPages?: Array<{ slug: string; title: string; description?: string; content: string; sidebar?: boolean }>;
|
|
26
30
|
/** Slugs of auto-generated pages to suppress (e.g. "pseudo-parameters") */
|
|
27
31
|
suppressPages?: string[];
|
|
28
32
|
/** Source directory for scanning rule files (defaults to srcDir sibling of distDir) */
|
|
@@ -31,12 +35,36 @@ export interface DocsConfig {
|
|
|
31
35
|
basePath?: string;
|
|
32
36
|
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
33
37
|
examplesDir?: string;
|
|
34
|
-
/**
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Directory of authored `.mdx` pages, each tagged with a Diátaxis quadrant
|
|
40
|
+
* (chant #1733). Defaults to `<outDir>/pages`. See docs-pages.ts. This is
|
|
41
|
+
* the only way a lexicon adds prose pages; `extraPages` (prose in docs.ts
|
|
42
|
+
* template literals) and `sidebarExtra` (hand-listed content files) were
|
|
43
|
+
* removed in chant #1757.
|
|
44
|
+
*/
|
|
45
|
+
pagesDir?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Diátaxis quadrant (https://diataxis.fr). */
|
|
49
|
+
export type Quadrant = "tutorial" | "how-to" | "reference" | "explanation";
|
|
50
|
+
|
|
51
|
+
/** One sidebar entry, as the quadrant-grouped sidebar builder sees it. */
|
|
52
|
+
export interface SidebarPage {
|
|
53
|
+
slug: string;
|
|
54
|
+
label: string;
|
|
55
|
+
quadrant: Quadrant;
|
|
56
|
+
/** Nested subgroup label inside the quadrant. */
|
|
57
|
+
group?: string;
|
|
58
|
+
/** Lower sorts first within its group; unordered pages follow, by label. */
|
|
59
|
+
order?: number;
|
|
60
|
+
/** Keep out of the sidebar. */
|
|
61
|
+
hidden?: boolean;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
export interface DocsResult {
|
|
39
65
|
pages: Map<string, string>;
|
|
66
|
+
/** Every page the sidebar should list, authored and generated, by quadrant. */
|
|
67
|
+
sidebarPages: SidebarPage[];
|
|
40
68
|
stats: {
|
|
41
69
|
resources: number;
|
|
42
70
|
properties: number;
|