From 894b6e91d722834409b28f59d5cf5aa2b0a34129 Mon Sep 17 00:00:00 2001 From: Thomas Vuillaume Date: Thu, 21 May 2026 14:41:48 +0200 Subject: [PATCH 1/2] build online JSON API during doc build --- .github/workflows/build-tools-api.yml | 36 ++++++++++++++++++ web/README.md | 11 ++++++ web/package.json | 3 +- web/scripts/generate-tools-api.mjs | 55 +++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-tools-api.yml create mode 100644 web/scripts/generate-tools-api.mjs diff --git a/.github/workflows/build-tools-api.yml b/.github/workflows/build-tools-api.yml new file mode 100644 index 00000000..f3d4f1b3 --- /dev/null +++ b/.github/workflows/build-tools-api.yml @@ -0,0 +1,36 @@ +name: Build Aggregated Tools API + +on: + push: + branches: + - main + +jobs: + build_tools_api: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "24" + cache: "npm" + cache-dependency-path: web/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: web + + - name: Generate aggregated API + run: npm run build:api + working-directory: web + + - name: Upload tools API artifact + uses: actions/upload-artifact@v4 + with: + name: tools-api-json + path: web/public/api/tools.json + if-no-files-found: error diff --git a/web/README.md b/web/README.md index 85d188fd..9a37ce5e 100644 --- a/web/README.md +++ b/web/README.md @@ -24,6 +24,7 @@ The app starts with Vite (typically at `http://localhost:5173`). - `npm run dev`: Start local dev server with HMR - `npm run build`: Build production assets into `dist/` +- `npm run build:api`: Generate aggregated tools API JSON at `public/api/tools.json` - `npm run preview`: Preview the production build locally - `npm run lint`: Run ESLint on JS/JSX files - `npm run format-json:check`: Check formatting for `../quality-tools/*.json` @@ -65,6 +66,16 @@ This means: - New/updated JSON files in `quality-tools/` are available to the UI at build time. - Tool IDs in URLs are based on filenames (stored as `_filename` in loader output). +### Aggregated JSON API + +The build process also creates a static aggregated API payload from all files in `../quality-tools`: + +- Build artifact path: `dist/api/tools.json` +- URL on deployed docs site: `.../api/tools.json` +- Payload keys: `generatedAt`, `source`, `count`, `files`, `tools` + +Because this file is generated during `npm run build`, the deployed docs always include an up-to-date machine-readable API of the catalogue. + ## Styling System - Tailwind CSS v4 is enabled via `@import "tailwindcss"` in `src/index.css`. diff --git a/web/package.json b/web/package.json index beb52a46..78cbe8f8 100644 --- a/web/package.json +++ b/web/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "npm run build:api && vite build", + "build:api": "node ./scripts/generate-tools-api.mjs", "lint": "eslint .", "format-json:check": "prettier --check ../quality-tools", "format-json:fix": "prettier --write ../quality-tools", diff --git a/web/scripts/generate-tools-api.mjs b/web/scripts/generate-tools-api.mjs new file mode 100644 index 00000000..2b6cea86 --- /dev/null +++ b/web/scripts/generate-tools-api.mjs @@ -0,0 +1,55 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '..', '..'); +const sourceDir = path.join(repoRoot, 'quality-tools'); +const targetDir = path.join(repoRoot, 'web', 'public', 'api'); +const targetFile = path.join(targetDir, 'tools.json'); + +const readToolFiles = async () => { + const entries = await fs.readdir(sourceDir, { withFileTypes: true }); + const jsonFiles = entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => entry.name) + .sort((a, b) => a.localeCompare(b)); + + const tools = await Promise.all( + jsonFiles.map(async (filename) => { + const filePath = path.join(sourceDir, filename); + const fileContent = await fs.readFile(filePath, 'utf-8'); + const parsed = JSON.parse(fileContent); + + return { + ...parsed, + _filename: filename.replace(/\.json$/i, ''), + }; + }), + ); + + return { tools, files: jsonFiles }; +}; + +const writeApiFile = async () => { + const { tools, files } = await readToolFiles(); + + const payload = { + generatedAt: new Date().toISOString(), + source: 'quality-tools', + count: tools.length, + files, + tools, + }; + + await fs.mkdir(targetDir, { recursive: true }); + await fs.writeFile(targetFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8'); + + console.log(`Generated ${targetFile} with ${tools.length} tools.`); +}; + +writeApiFile().catch((error) => { + console.error('Failed to generate aggregated tools API JSON.'); + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file From 8bd17d91eb9cf7455682721b539301e0c933860b Mon Sep 17 00:00:00 2001 From: Thomas Vuillaume Date: Mon, 1 Jun 2026 10:35:50 +0200 Subject: [PATCH 2/2] formatting --- web/scripts/generate-tools-api.mjs | 68 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/web/scripts/generate-tools-api.mjs b/web/scripts/generate-tools-api.mjs index 2b6cea86..0f807895 100644 --- a/web/scripts/generate-tools-api.mjs +++ b/web/scripts/generate-tools-api.mjs @@ -9,47 +9,47 @@ const targetDir = path.join(repoRoot, 'web', 'public', 'api'); const targetFile = path.join(targetDir, 'tools.json'); const readToolFiles = async () => { - const entries = await fs.readdir(sourceDir, { withFileTypes: true }); - const jsonFiles = entries - .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) - .map((entry) => entry.name) - .sort((a, b) => a.localeCompare(b)); - - const tools = await Promise.all( - jsonFiles.map(async (filename) => { - const filePath = path.join(sourceDir, filename); - const fileContent = await fs.readFile(filePath, 'utf-8'); - const parsed = JSON.parse(fileContent); - - return { - ...parsed, - _filename: filename.replace(/\.json$/i, ''), - }; - }), - ); - - return { tools, files: jsonFiles }; + const entries = await fs.readdir(sourceDir, { withFileTypes: true }); + const jsonFiles = entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => entry.name) + .sort((a, b) => a.localeCompare(b)); + + const tools = await Promise.all( + jsonFiles.map(async (filename) => { + const filePath = path.join(sourceDir, filename); + const fileContent = await fs.readFile(filePath, 'utf-8'); + const parsed = JSON.parse(fileContent); + + return { + ...parsed, + _filename: filename.replace(/\.json$/i, ''), + }; + }), + ); + + return { tools, files: jsonFiles }; }; const writeApiFile = async () => { - const { tools, files } = await readToolFiles(); + const { tools, files } = await readToolFiles(); - const payload = { - generatedAt: new Date().toISOString(), - source: 'quality-tools', - count: tools.length, - files, - tools, - }; + const payload = { + generatedAt: new Date().toISOString(), + source: 'quality-tools', + count: tools.length, + files, + tools, + }; - await fs.mkdir(targetDir, { recursive: true }); - await fs.writeFile(targetFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8'); + await fs.mkdir(targetDir, { recursive: true }); + await fs.writeFile(targetFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8'); - console.log(`Generated ${targetFile} with ${tools.length} tools.`); + console.log(`Generated ${targetFile} with ${tools.length} tools.`); }; writeApiFile().catch((error) => { - console.error('Failed to generate aggregated tools API JSON.'); - console.error(error); - process.exitCode = 1; + console.error('Failed to generate aggregated tools API JSON.'); + console.error(error); + process.exitCode = 1; }); \ No newline at end of file