diff --git a/.gitignore b/.gitignore index fba1a8c..99173fd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ Thumbs.db # Playwright MCP session artifacts (console logs + page snapshots) .playwright-mcp/ + +# Generated at build time by scripts/fetch-*.py — never committed. +# Schema-only examples (*.example.json) are committed alongside as documentation. +src/data/plugins_metadata.json +src/data/verdi-cli.json +src/data/pypi-stats.json diff --git a/package-lock.json b/package-lock.json index 73f17e3..efa6d21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "astro": "^5.17.1", + "fuse.js": "^7.3.0", "js-yaml": "^4.1.1", "react": "^19.2.4", "react-dom": "^19.2.4" @@ -2936,6 +2937,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fuse.js": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 6f93ffc..db2679c 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,16 @@ "type": "module", "version": "0.0.1", "scripts": { + "fetch-data": "python3 scripts/fetch-verdi-cli.py && python3 scripts/fetch-pypi-stats.py && python3 scripts/fetch-registry.py", + "predev": "test -f src/data/verdi-cli.json && test -f src/data/pypi-stats.json && test -f src/data/plugins_metadata.json || npm run fetch-data", "dev": "astro dev", - "prebuild": "python3 scripts/fetch-verdi-cli.py", + "prebuild": "npm run fetch-data", "build": "astro build", "preview": "astro preview", "astro": "astro", "update-cli": "python3 scripts/fetch-verdi-cli.py", - "update-stats": "python3 scripts/fetch-pypi-stats.py" + "update-stats": "python3 scripts/fetch-pypi-stats.py", + "update-registry": "python3 scripts/fetch-registry.py" }, "dependencies": { "@astrojs/react": "^4.4.2", @@ -17,6 +20,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "astro": "^5.17.1", + "fuse.js": "^7.3.0", "js-yaml": "^4.1.1", "react": "^19.2.4", "react-dom": "^19.2.4" diff --git a/scripts/fetch-pypi-stats.py b/scripts/fetch-pypi-stats.py index a576df7..b1b88d6 100644 --- a/scripts/fetch-pypi-stats.py +++ b/scripts/fetch-pypi-stats.py @@ -2,12 +2,20 @@ """Fetch monthly PyPI download stats for aiida-core. Run: python scripts/fetch-pypi-stats.py -Output: src/data/pypi-stats.json +Output: src/data/pypi-stats.json (gitignored — generated fresh on every build) -Intended to run at build time or periodically. -The generated JSON is committed to the repo and imported statically. +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +Build/dev fails loudly if pypistats.org is unreachable or returns zero +downloads — there is deliberately no graceful fallback to stale data. +Expected output schema: src/data/pypi-stats.example.json. """ import json +import sys import urllib.request from datetime import datetime, timedelta, timezone from pathlib import Path @@ -52,10 +60,17 @@ def fetch_stats(): "package": "aiida-core", } + if monthly_total <= 0: + raise RuntimeError("pypistats returned zero downloads — refusing to overwrite with empty data.") + out_path = Path(__file__).resolve().parent.parent / "src" / "data" / "pypi-stats.json" out_path.write_text(json.dumps(result, indent=2) + "\n") print(f"PyPI stats: {display} downloads/month ({result['period_start']} to {result['period_end']})") return result if __name__ == "__main__": - fetch_stats() + try: + fetch_stats() + except Exception as e: + print(f"fetch-pypi-stats: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/fetch-registry.py b/scripts/fetch-registry.py new file mode 100644 index 0000000..ccc7a4a --- /dev/null +++ b/scripts/fetch-registry.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Fetch the AiiDA plugin registry metadata. + +Run: python3 scripts/fetch-registry.py +Output: src/data/plugins_metadata.json (gitignored — generated fresh on every build) + +Pulls the pre-built metadata from the upstream aiida-registry, which +runs its own daily cron to scrape PyPI, parse pyproject.toml/setup.cfg, +test installation, and emit JSON. We just consume that artefact so this +site stays a thin presentation layer over the canonical registry. + +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +For nightly freshness in production, a Cloudflare Pages deploy hook re-triggers +the build, which re-runs prebuild. Build/dev fails loudly if upstream is +unreachable or the schema drifts — there is deliberately no graceful fallback +to stale data. Expected output schema: src/data/plugins_metadata.example.json. +""" +import json +import sys +import urllib.request +from pathlib import Path + +UPSTREAM = "https://aiidateam.github.io/aiida-registry/plugins_metadata.json" +TIMEOUT_SEC = 30 + + +def fetch(): + req = urllib.request.Request(UPSTREAM, headers={"User-Agent": "aiida-website-build"}) + with urllib.request.urlopen(req, timeout=TIMEOUT_SEC) as resp: + raw = resp.read() + data = json.loads(raw.decode()) + + expected = {"plugins", "globalsummary", "status_dict", "entrypointtypes"} + missing = expected - set(data.keys()) + if missing: + raise RuntimeError(f"upstream JSON missing keys: {missing}") + if not data["plugins"]: + raise RuntimeError("upstream JSON contains zero plugins — refusing to overwrite with empty data.") + + out_path = Path(__file__).resolve().parent.parent / "src" / "data" / "plugins_metadata.json" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(data, indent=2) + "\n") + + n_plugins = len(data["plugins"]) + size_kb = len(raw) // 1024 + print(f"Plugin registry: {n_plugins} plugins, {size_kb} KB") + + +if __name__ == "__main__": + try: + fetch() + except Exception as e: + print(f"fetch-registry: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/fetch-verdi-cli.py b/scripts/fetch-verdi-cli.py index 6c02801..8d0252e 100644 --- a/scripts/fetch-verdi-cli.py +++ b/scripts/fetch-verdi-cli.py @@ -2,13 +2,20 @@ """Generate verdi CLI tree from aiida-core for the website. Run: python scripts/fetch-verdi-cli.py -Output: src/data/verdi-cli.json +Output: src/data/verdi-cli.json (gitignored — generated fresh on every build) -- Developer mode: uses locally installed aiida-core -- CI / deploy: fetches the latest stable release from PyPI automatically +- Developer mode: uses locally installed aiida-core (fast, no install). +- CI / fresh clone without aiida-core: creates a temp venv, installs aiida-core + from PyPI, and re-runs this script inside it (slow first time, ~30 seconds). -This only needs to be re-run when aiida-core updates its CLI. -The generated JSON is committed to the repo and imported statically. +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +Build/dev fails loudly if generation fails — there is deliberately no graceful +fallback to stale data. Expected output schema: src/data/verdi-cli.example.json. """ import json import os @@ -60,6 +67,9 @@ def walk_group(group: click.MultiCommand, prefix: str = "verdi") -> dict: "help": tree["help"], } + if not output["commands"]: + raise RuntimeError("verdi command tree came back empty — aborting rather than write an empty CLI reference.") + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUT_PATH.write_text(json.dumps(output, indent=2, sort_keys=True) + "\n") print(f"Generated {OUT_PATH} with {len(output['commands'])} commands, {len(output['subcommands'])} groups") @@ -93,6 +103,10 @@ def fetch_and_generate(): if __name__ == "__main__": try: - generate() - except ImportError: - fetch_and_generate() + try: + generate() + except ImportError: + fetch_and_generate() + except Exception as e: + print(f"fetch-verdi-cli: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/components/Docs.tsx b/src/components/Docs.tsx index 8c76183..a5a59ae 100644 --- a/src/components/Docs.tsx +++ b/src/components/Docs.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from 'react'; +const base = import.meta.env.BASE_URL?.replace(/\/$/, '') || ''; + const ExtIcon = () => ( @@ -168,7 +170,7 @@ export default function Docs(): ReactNode {

Source code, issues, and contributing

- +
diff --git a/src/components/Ecosystem.tsx b/src/components/Ecosystem.tsx index 46d89d1..8d885d0 100644 --- a/src/components/Ecosystem.tsx +++ b/src/components/Ecosystem.tsx @@ -35,7 +35,7 @@ const NODES: EcoNode[] = [ name: 'Plugin registry', tagline: '100+ community plugins', desc: 'A curated directory of all AiiDA plugins — from simulation codes (Quantum ESPRESSO, VASP, CP2K, FLEUR, Siesta, …) to data types, schedulers, and transports. Plugins extend AiiDA via Python entry points.', - url: 'https://aiidateam.github.io/aiida-registry/', + url: `${base}/plugin-registry/`, color: '#0096de', category: 'core', }, @@ -193,7 +193,7 @@ export default function Ecosystem(): ReactNode { {/* Bottom row: extensions and tools */}
-
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index eac5052..06b5293 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -652,7 +652,7 @@ function PluginShowcase(): ReactNode {
))} - + Browse all 100+ plugins in the registry →
@@ -3112,7 +3112,7 @@ function Numbers(): ReactNode { 1000+ Publications - + 100+ Plugins diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 9422195..43ccfc6 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -56,7 +56,7 @@ export default function Navbar() {
  • Ecosystem
  • Docs
  • News
  • -
  • Plugins
  • +
  • Plugins
  • Community