Skip to content

Commit b74b89e

Browse files
committed
Move linter rule page assembly from generate script to Astro components
This change refactors how linter rule pages are generated: **Before:** - Generate script consolidated individual linter rules into markdown files - Rules were grouped by AEP number and wrapped in <details> elements - Output was written to src/content/docs/tooling/*/rules/{aep}.md **After:** - Generate script writes individual rules as JSON to generated/linter-rules/ - New Astro component (LinterRules.astro) handles rendering rules - Dynamic route pages ([aep].astro) read JSON and pass to component - Produces identical HTML output using Starlight's layout **Benefits:** - Cleaner separation of concerns (data extraction vs. rendering) - Easier to modify rule presentation without regenerating content - Leverages Astro's component system and build process - Maintains site structure generation for navigation **Files Changed:** - scripts/generate.ts: Use writeLinterRulesJSON() instead of consolidation - scripts/src/linter.ts: Add writeLinterRulesJSON() and getUniqueAeps() - src/components/LinterRules.astro: Component to render rule list - src/pages/tooling/linter/rules/[aep].astro: Protobuf linter dynamic route - src/pages/tooling/openapi-linter/rules/[aep].astro: OpenAPI linter dynamic route
1 parent 72028fe commit b74b89e

File tree

5 files changed

+251
-34
lines changed

5 files changed

+251
-34
lines changed

scripts/generate.ts

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@ import * as fs from "fs";
22
import * as path from "path";
33

44
import loadConfigFiles from "./src/config";
5-
import {
6-
type AEP,
7-
type ConsolidatedLinterRule,
8-
type GroupFile,
9-
type LinterRule,
10-
} from "./src/types";
5+
import { type AEP, type GroupFile, type LinterRule } from "./src/types";
116
import { buildMarkdown, Markdown } from "./src/markdown";
127
import { load, dump } from "js-yaml";
138
import {
149
assembleLinterRules,
1510
assembleOpenAPILinterRules,
16-
consolidateLinterRule,
17-
writeRule,
11+
writeLinterRulesJSON,
12+
getUniqueAeps,
1813
} from "./src/linter";
1914
import {
2015
logFileRead,
@@ -371,18 +366,14 @@ if (AEP_LINTER_LOC != "") {
371366
"",
372367
);
373368

374-
// Write out linter rules.
369+
// Write out linter rules as JSON for Astro to consume
370+
// The rules are now rendered by Astro components instead of being pre-consolidated
371+
// See: src/pages/tooling/linter/rules/[aep].astro and src/components/LinterRules.astro
375372
let linter_rules = await assembleLinterRules(AEP_LINTER_LOC);
376-
let consolidated_rules = consolidateLinterRule({ linterRules: linter_rules });
377-
for (var rule of consolidated_rules) {
378-
writeRule(rule);
379-
}
373+
writeLinterRulesJSON(linter_rules, "generated/linter-rules/protobuf.json");
380374

381375
// Add to site structure
382-
addLinterRules(
383-
siteStructure,
384-
consolidated_rules.map((r) => r.aep),
385-
);
376+
addLinterRules(siteStructure, getUniqueAeps(linter_rules));
386377
addToolingPage(siteStructure, { label: "Website", link: "tooling/website" });
387378
} else {
388379
console.warn("Proto linter repo is not found.");
@@ -405,25 +396,16 @@ if (AEP_OPENAPI_LINTER_LOC != "") {
405396
"OpenAPI Linter",
406397
);
407398

408-
// Consolidate rules (groups by AEP number)
409-
const consolidatedOpenAPIRules = consolidateLinterRule({
410-
linterRules: openapiLinterRules,
411-
});
412-
413-
// Write rule markdown files
414-
for (const rule of consolidatedOpenAPIRules) {
415-
const outputPath = path.join(
416-
"src/content/docs/tooling/openapi-linter/rules",
417-
`${rule.aep}.md`,
418-
);
419-
writeRule(rule, outputPath);
420-
}
399+
// Write OpenAPI linter rules as JSON for Astro to consume
400+
// The rules are now rendered by Astro components instead of being pre-consolidated
401+
// See: src/pages/tooling/openapi-linter/rules/[aep].astro and src/components/LinterRules.astro
402+
writeLinterRulesJSON(
403+
openapiLinterRules,
404+
"generated/linter-rules/openapi.json",
405+
);
421406

422407
// Add to site structure
423-
addOpenAPILinterRules(
424-
siteStructure,
425-
consolidatedOpenAPIRules.map((r) => r.aep),
426-
);
408+
addOpenAPILinterRules(siteStructure, getUniqueAeps(openapiLinterRules));
427409

428410
console.log("✅ OpenAPI linter integration complete\n");
429411
} else {

scripts/src/linter.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,37 @@ export function writeRule(rule: ConsolidatedLinterRule, outputPath?: string) {
237237
path.join(`src/content/docs/tooling/linter/rules/`, `${rule.aep}.md`);
238238
writeFile(filePath, rule.contents);
239239
}
240+
241+
/**
242+
* Writes linter rules as JSON data file for Astro to consume
243+
* This allows Astro components to render the rules instead of pre-generating markdown
244+
*
245+
* @param rules - Array of linter rules to write
246+
* @param outputPath - Path to write the JSON file
247+
*/
248+
export function writeLinterRulesJSON(
249+
rules: LinterRule[],
250+
outputPath: string,
251+
): void {
252+
// Ensure the directory exists
253+
const dir = path.dirname(outputPath);
254+
if (!fs.existsSync(dir)) {
255+
fs.mkdirSync(dir, { recursive: true });
256+
}
257+
258+
// Write the rules as JSON
259+
writeFile(outputPath, JSON.stringify(rules, null, 2));
260+
console.log(`✓ Wrote ${rules.length} linter rules to ${outputPath}`);
261+
}
262+
263+
/**
264+
* Gets unique AEP numbers from linter rules
265+
* Used for site structure generation
266+
*
267+
* @param rules - Array of linter rules
268+
* @returns Array of unique AEP numbers, sorted
269+
*/
270+
export function getUniqueAeps(rules: LinterRule[]): string[] {
271+
const aeps = new Set(rules.map((r) => r.aep));
272+
return Array.from(aeps).sort();
273+
}

src/components/LinterRules.astro

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
/**
3+
* LinterRules Component
4+
*
5+
* Renders a list of linter rules in collapsible <details> elements.
6+
* This replaces the consolidation logic previously done in the generate script.
7+
*
8+
* Props:
9+
* - rules: Array of LinterRule objects to render
10+
* - preamble: Optional preamble text to display before the rules
11+
*/
12+
13+
export interface LinterRule {
14+
title: string;
15+
aep: string;
16+
contents: string; // Markdown content with frontmatter
17+
filename: string;
18+
slug: string;
19+
preamble?: string;
20+
}
21+
22+
export interface Props {
23+
rules: LinterRule[];
24+
preamble?: string;
25+
}
26+
27+
const { rules, preamble } = Astro.props;
28+
29+
// Extract preamble from first rule if available, otherwise use provided preamble
30+
const displayPreamble = rules[0]?.preamble || preamble || "";
31+
32+
// Helper function to strip frontmatter from content
33+
function stripFrontmatter(content: string): string {
34+
return content.replace(/---[\s\S]*?---/m, "").trim();
35+
}
36+
---
37+
38+
{displayPreamble && (
39+
<div class="preamble" set:html={displayPreamble} />
40+
)}
41+
42+
{
43+
rules.map((rule) => (
44+
<details>
45+
<summary>{rule.title}</summary>
46+
<div set:html={stripFrontmatter(rule.contents)} />
47+
</details>
48+
))
49+
}
50+
51+
<style>
52+
.preamble {
53+
margin-bottom: 1.5rem;
54+
}
55+
56+
details {
57+
margin-bottom: 1rem;
58+
border: 1px solid var(--sl-color-gray-5);
59+
border-radius: 0.5rem;
60+
padding: 0;
61+
}
62+
63+
summary {
64+
padding: 1rem;
65+
cursor: pointer;
66+
font-weight: 600;
67+
user-select: none;
68+
}
69+
70+
summary:hover {
71+
background-color: var(--sl-color-gray-6);
72+
}
73+
74+
details[open] summary {
75+
border-bottom: 1px solid var(--sl-color-gray-5);
76+
}
77+
78+
details > div {
79+
padding: 1rem;
80+
}
81+
</style>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
/**
3+
* Dynamic route for Protobuf Linter rule pages
4+
* Replaces the consolidated markdown files previously generated by the script
5+
*/
6+
7+
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
8+
import LinterRules from "../../../../components/LinterRules.astro";
9+
import * as fs from "fs";
10+
import * as path from "path";
11+
12+
export async function getStaticPaths() {
13+
// Read the linter rules JSON data
14+
const linterRulesPath = path.join(
15+
process.cwd(),
16+
"generated/linter-rules/protobuf.json",
17+
);
18+
19+
// Check if the file exists
20+
if (!fs.existsSync(linterRulesPath)) {
21+
console.warn("⚠️ Protobuf linter rules JSON not found, skipping...");
22+
return [];
23+
}
24+
25+
const allRules = JSON.parse(fs.readFileSync(linterRulesPath, "utf-8"));
26+
27+
// Group rules by AEP number
28+
const rulesByAep = allRules.reduce((acc, rule) => {
29+
if (!acc[rule.aep]) {
30+
acc[rule.aep] = [];
31+
}
32+
acc[rule.aep].push(rule);
33+
return acc;
34+
}, {});
35+
36+
// Generate paths for each AEP
37+
return Object.keys(rulesByAep).map((aep) => ({
38+
params: { aep },
39+
props: {
40+
rules: rulesByAep[aep],
41+
aep,
42+
},
43+
}));
44+
}
45+
46+
const { rules, aep } = Astro.props;
47+
48+
// Extract preamble from first rule if available
49+
const preamble = rules[0]?.preamble || "";
50+
---
51+
52+
<StarlightPage
53+
frontmatter={{
54+
title: `AEP-${aep} Linter Rules`,
55+
tableOfContents: { minHeadingLevel: 1 },
56+
}}
57+
headings={[]}
58+
>
59+
<LinterRules rules={rules} preamble={preamble} />
60+
</StarlightPage>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
/**
3+
* Dynamic route for OpenAPI Linter rule pages
4+
* Replaces the consolidated markdown files previously generated by the script
5+
*/
6+
7+
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
8+
import LinterRules from "../../../../components/LinterRules.astro";
9+
import * as fs from "fs";
10+
import * as path from "path";
11+
12+
export async function getStaticPaths() {
13+
// Read the OpenAPI linter rules JSON data
14+
const linterRulesPath = path.join(
15+
process.cwd(),
16+
"generated/linter-rules/openapi.json",
17+
);
18+
19+
// Check if the file exists
20+
if (!fs.existsSync(linterRulesPath)) {
21+
console.warn("⚠️ OpenAPI linter rules JSON not found, skipping...");
22+
return [];
23+
}
24+
25+
const allRules = JSON.parse(fs.readFileSync(linterRulesPath, "utf-8"));
26+
27+
// Group rules by AEP number
28+
const rulesByAep = allRules.reduce((acc, rule) => {
29+
if (!acc[rule.aep]) {
30+
acc[rule.aep] = [];
31+
}
32+
acc[rule.aep].push(rule);
33+
return acc;
34+
}, {});
35+
36+
// Generate paths for each AEP
37+
return Object.keys(rulesByAep).map((aep) => ({
38+
params: { aep },
39+
props: {
40+
rules: rulesByAep[aep],
41+
aep,
42+
},
43+
}));
44+
}
45+
46+
const { rules, aep } = Astro.props;
47+
48+
// Extract preamble from first rule if available
49+
const preamble = rules[0]?.preamble || "";
50+
---
51+
52+
<StarlightPage
53+
frontmatter={{
54+
title: `AEP-${aep} Linter Rules`,
55+
tableOfContents: { minHeadingLevel: 1 },
56+
}}
57+
headings={[]}
58+
>
59+
<LinterRules rules={rules} preamble={preamble} />
60+
</StarlightPage>

0 commit comments

Comments
 (0)