diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 12fdba0..50b2856 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -26,7 +26,7 @@ jobs: - sync-jira steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true - name: Log in to the Container registry @@ -44,7 +44,7 @@ jobs: type=semver,pattern={{version}} type=edge,branch=main - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: ./gropius-backend build-args: | diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 51e538c..984f160 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -6,68 +6,62 @@ on: - main pull_request: -env: - NODE_OPTIONS: '--max_old_space_size=6144' - jobs: deploy: name: Deploy to GitHub Pages runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: submodules: true - - uses: actions/setup-java@v3 + + # Rendering the ~5,500 generated pages peaks at roughly 11 GB resident. That fits a standard + # runner's 16 GB, but not with much to spare, and the default 4 GB swapfile is thin cover. + - name: Enlarge the swapfile + run: | + sudo swapoff -a + sudo fallocate -l 16G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h + + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - - name: Execute Gradle build - run: ./gradlew build dokkaHtmlMultiModule - working-directory: gropius-backend - - name: Save graphql schemas - run: ./website/scripts/generate-graphql-schema.sh - - uses: actions/setup-node@v3 + uses: gradle/actions/setup-gradle@v4 + + - uses: actions/setup-node@v4 with: - node-version: 21.x + node-version: 22.x cache: npm cache-dependency-path: "**/package-lock.json" + + - name: Install website dependencies + run: npm ci + working-directory: website - name: Install login-service dependencies run: npm ci working-directory: gropius-login-service/backend - - name: Save openapi schemas - run: ./website/scripts/generate-openapi-schema.sh - - name: Modify Dokka output - uses: graphglue/dokka-html-mdx-transform@v1.7.20 - with: - src: "gropius-backend/build/dokka/htmlMultiModule" - modules: | - core - api-common - api-public - api-internal - sync - sync-github - sync-jira - dest: "website/docs" - folder: "api" - - name: Install dependencies - run: npm ci + + # Runs Dokka, boots both API modules and the login service to dump their schemas, and runs + # TypeDoc - the same four stages `npm run generate` runs locally. + - name: Generate the API references + run: npm run generate working-directory: website - - name: Update graphql documentation - run: ./website/scripts/update-graphql-doc.sh - - name: Update rest documentation - run: ./website/scripts/update-rest-doc.sh + - name: Build website - run: npm run build + run: npm run site working-directory: website + - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 if: github.ref == 'refs/heads/main' with: deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} - publish_dir: ./website/build + publish_dir: ./website/.vitepress/dist force_orphan: true external_repository: ccims/gropius-docs - publish_branch: main \ No newline at end of file + publish_branch: main diff --git a/docker-compose-testing.yaml b/docker-compose-testing.yaml index 2365f6a..abf19f8 100644 --- a/docker-compose-testing.yaml +++ b/docker-compose-testing.yaml @@ -124,7 +124,6 @@ services: login-service: condition: service_started environment: - - LOGIN_OAUTH_CLIENT_ID=8ee1287d-71ff-4c85-becd-cba829f390a0 - API_PUBLIC_ENDPOINT=http://api-public:8080/graphql - LOGIN_SERVICE_ENDPOINT=http://login-service:3000 mongo: @@ -149,11 +148,11 @@ services: - login-service environment: - SPRING_NEO4J_URI=bolt://neo4j:7687 - - SPRING_DATA_MONGODB_HOST=mongo - - SPRING_DATA_MONGODB_PORT=27017 - - SPRING_DATA_MONGODB_DATABASE=gropius - - SPRING_DATA_MONGODB_USERNAME=root - - SPRING_DATA_MONGODB_PASSWORD=root + - SPRING_MONGODB_HOST=mongo + - SPRING_MONGODB_PORT=27017 + - SPRING_MONGODB_DATABASE=gropius + - SPRING_MONGODB_USERNAME=root + - SPRING_MONGODB_PASSWORD=root - GROPIUS_SYNC_LOGIN_SERVICE_BASE=http://login-service:3000/ - GROPIUS_SYNC_API_SECRET=TODO_loginSecret healthcheck: @@ -173,11 +172,11 @@ services: - login-service environment: - SPRING_NEO4J_URI=bolt://neo4j:7687 - - SPRING_DATA_MONGODB_HOST=mongo - - SPRING_DATA_MONGODB_PORT=27017 - - SPRING_DATA_MONGODB_DATABASE=gropius - - SPRING_DATA_MONGODB_USERNAME=root - - SPRING_DATA_MONGODB_PASSWORD=root + - SPRING_MONGODB_HOST=mongo + - SPRING_MONGODB_PORT=27017 + - SPRING_MONGODB_DATABASE=gropius + - SPRING_MONGODB_USERNAME=root + - SPRING_MONGODB_PASSWORD=root - GROPIUS_SYNC_LOGIN_SERVICE_BASE=http://login-service:3000/ - GROPIUS_SYNC_API_SECRET=TODO_loginSecret healthcheck: diff --git a/docker-compose.yaml b/docker-compose.yaml index 6ab34ba..01d9f8d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -139,11 +139,11 @@ services: - SPRING_NEO4J_URI=bolt://neo4j:7687 - SPRING_NEO4J_AUTHENTICATION_USERNAME=neo4j - SPRING_NEO4J_AUTHENTICATION_PASSWORD=$GROPIUS_NEO4J_PASSWORD - - SPRING_DATA_MONGODB_HOST=mongo - - SPRING_DATA_MONGODB_PORT=27017 - - SPRING_DATA_MONGODB_DATABASE=gropius - - SPRING_DATA_MONGODB_USERNAME=root - - SPRING_DATA_MONGODB_PASSWORD=$GROPIUS_MONGO_PASSWORD + - SPRING_MONGODB_HOST=mongo + - SPRING_MONGODB_PORT=27017 + - SPRING_MONGODB_DATABASE=gropius + - SPRING_MONGODB_USERNAME=root + - SPRING_MONGODB_PASSWORD=$GROPIUS_MONGO_PASSWORD - GROPIUS_SYNC_LOGIN_SERVICE_BASE=http://login-service:3000/ - GROPIUS_SYNC_API_SECRET=$GROPIUS_SYNC_API_SECRET healthcheck: @@ -165,11 +165,11 @@ services: - SPRING_NEO4J_URI=bolt://neo4j:7687 - SPRING_NEO4J_AUTHENTICATION_USERNAME=neo4j - SPRING_NEO4J_AUTHENTICATION_PASSWORD=$GROPIUS_NEO4J_PASSWORD - - SPRING_DATA_MONGODB_HOST=mongo - - SPRING_DATA_MONGODB_PORT=27017 - - SPRING_DATA_MONGODB_DATABASE=gropius - - SPRING_DATA_MONGODB_USERNAME=root - - SPRING_DATA_MONGODB_PASSWORD=$GROPIUS_MONGO_PASSWORD + - SPRING_MONGODB_HOST=mongo + - SPRING_MONGODB_PORT=27017 + - SPRING_MONGODB_DATABASE=gropius + - SPRING_MONGODB_USERNAME=root + - SPRING_MONGODB_PASSWORD=$GROPIUS_MONGO_PASSWORD - GROPIUS_SYNC_LOGIN_SERVICE_BASE=http://login-service:3000/ - GROPIUS_SYNC_API_SECRET=$GROPIUS_SYNC_API_SECRET healthcheck: diff --git a/gropius-backend b/gropius-backend index dbf6c88..9f3dd70 160000 --- a/gropius-backend +++ b/gropius-backend @@ -1 +1 @@ -Subproject commit dbf6c884b2c0d301d406efef9f2014bea37931b3 +Subproject commit 9f3dd70ba807807aab519b2449198a6d5882d41c diff --git a/gropius-frontend b/gropius-frontend index 63f167b..928382c 160000 --- a/gropius-frontend +++ b/gropius-frontend @@ -1 +1 @@ -Subproject commit 63f167be96839f70665e28170b50155c045362e4 +Subproject commit 928382c584325d45e305ef2f05e4144b638baaa8 diff --git a/gropius-login-service b/gropius-login-service index 5aaa9c9..f4a1929 160000 --- a/gropius-login-service +++ b/gropius-login-service @@ -1 +1 @@ -Subproject commit 5aaa9c97da956a8b56dda75fbeec7ffa8ebec3d3 +Subproject commit f4a1929422cec358f2f710a06054c0e192bcff4e diff --git a/website/.gitignore b/website/.gitignore index 1ab92b8..e7db80b 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -1,12 +1,20 @@ # Dependencies /node_modules -# Production -/build +# VitePress +/.vitepress/cache +/.vitepress/dist -# Generated files -.docusaurus -.cache-loader +# Generated by `npm run generate`; see scripts/docs.mjs +/.vitepress/dokka +/.vitepress/generated +/docs/api +/docs/graphql/public +/docs/graphql/internal +/docs/login-service + +# Schemas dumped from the running services +/schemas # Misc .DS_Store @@ -18,17 +26,3 @@ npm-debug.log* yarn-debug.log* yarn-error.log* - -# generated api documentation -docs/api/ -docs/login-service-api/ - -# generated schema -schemas/ - -# generated graphql documentation -docs/graphql/api-public/ -docs/graphql/api-internal/ - -# generated rest documentation -rest-docs/login-service/ \ No newline at end of file diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts new file mode 100644 index 0000000..79d2072 --- /dev/null +++ b/website/.vitepress/config.ts @@ -0,0 +1,288 @@ +import { existsSync, readFileSync } from 'node:fs' +import { readdir, readFile, stat, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig, type DefaultTheme } from 'vitepress' +import { withMermaid } from 'vitepress-plugin-mermaid' +import { useSidebar } from 'vitepress-openapi' + +/** + * Every API sidebar is generated by `scripts/docs.mjs`. Loading them lazily keeps the site + * buildable in a fresh checkout, where nothing has been generated yet. + */ +function generatedSidebar(file: string): DefaultTheme.SidebarItem[] { + const resolved = fileURLToPath(new URL(file, import.meta.url)) + if (!existsSync(resolved)) return [] + try { + return JSON.parse(readFileSync(resolved, 'utf8')) as DefaultTheme.SidebarItem[] + } catch { + return [] + } +} + +const kotlinApi = generatedSidebar('./dokka/sidebar.json') +const loginServiceApi = generatedSidebar('../docs/login-service/typedoc-sidebar.json') +const graphqlPublic = generatedSidebar('./generated/graphql-public.sidebar.json') +const graphqlInternal = generatedSidebar('./generated/graphql-internal.sidebar.json') + +/** + * The REST sidebar is derived from the OpenAPI document itself rather than from files on disk - + * the operation pages are dynamic routes, so there is nothing to walk. + */ +function restSidebar(): DefaultTheme.SidebarItem[] { + const file = fileURLToPath(new URL('./generated/login-openapi.json', import.meta.url)) + if (!existsSync(file)) return [] + try { + const spec = JSON.parse(readFileSync(file, 'utf8')) + if (!spec.paths || Object.keys(spec.paths).length === 0) return [] + return useSidebar({ spec, linkPrefix: '/rest/operations/' }).generateSidebarGroups() + } catch { + return [] + } +} + +const rest = restSidebar() + +/** The only chunks a page needs before it can render: the framework, the theme, and its own. */ +const EAGER_CHUNK = /\/(?:framework|theme)\.[^/]+\.js$|\.lean\.js$/ + +/** + * Drops the `` tags for chunks that are not needed to render the page. + * + * VitePress preloads every dynamic import of the app chunk from every page. Registering mermaid as + * a global component puts its ~40 diagram renderers in that list, along with katex - 44 tags and + * 3.9 KB of head on each of 5,500 pages, telling every visitor to fetch 1.4 MB of diagram code + * whether or not the page contains a diagram. mermaid loads the renderer it needs when it meets a + * diagram, so none of it belongs in the head. + * + * This runs after the build because no hook can remove a head tag - `transformHead` only merges + * into the existing tags, and `shouldPreload` turns a preload into a prefetch, which is the same + * bytes and still fetches. + */ +async function dropEagerPreloads(outDir: string, collapsed: Set): Promise { + const entries = await readdir(outDir, { recursive: true, withFileTypes: true }) + let saved = 0 + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.html')) continue + + const file = path.join(entry.parentPath, entry.name) + const html = await readFile(file, 'utf8') + const stripped = html.replace(//g, (tag, href: string) => { + if (!EAGER_CHUNK.test(href)) { + saved += tag.length + return '' + } + + // A collapsed lean chunk is one line that re-exports the full one, so preload that too + // rather than let the browser discover it a round trip later. + const full = href.replace(/\.lean\.js$/, '.js') + if (full !== href && collapsed.has(path.basename(full))) { + saved -= tag.length + return `${tag}` + } + return tag + }) + + if (stripped !== html) await writeFile(file, stripped) + } + + console.log(`dropped ${(saved / 1048576).toFixed(1)} MB of modulepreload tags`) +} + +/** + * Collapses the `.lean.js` chunks that are not actually lean into a re-export of the full chunk. + * + * VitePress emits two chunks per page: the full one for client-side navigation to it, and a lean + * one - the page without its content, since on the initial load the content is already in the HTML + * - that the router imports instead. For the TypeScript reference the lean chunk is 15% of the full + * one, as intended. For the Kotlin reference it is 95%, because `@graphglue/dokka-vitepress` hands + * the Dokka HTML to Vue as a `?raw` import, and a raw import is a JS constant rather than markdown + * content, so there is nothing for the lean transform to strip. Those pages ship their content + * three times: in the HTML, in the full chunk, and again in the chunk that exists to avoid it. + * + * Where the lean chunk saves nothing, pointing it at the full one costs the initial load a redirect + * to a chunk it would otherwise have fetched verbatim, and saves the copy on disk. Where it does + * its job, it is left alone. + */ +async function dedupeLeanChunks(outDir: string): Promise> { + const assetsDir = path.join(outDir, 'assets') + const entries = await readdir(assetsDir, { withFileTypes: true }) + const collapsed = new Set() + let saved = 0 + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.lean.js')) continue + + const fullName = entry.name.replace(/\.lean\.js$/, '.js') + const [lean, full] = await Promise.all([ + stat(path.join(assetsDir, entry.name)), + stat(path.join(assetsDir, fullName)).catch(() => null) + ]) + if (!full || lean.size < full.size * 0.8) continue + + // Both chunks export exactly these two names. + const reexport = `export { __pageData, default } from './${fullName}'\n` + await writeFile(path.join(assetsDir, entry.name), reexport) + saved += lean.size - reexport.length + collapsed.add(fullName) + } + + console.log(`collapsed ${collapsed.size} lean chunks, ${(saved / 1048576).toFixed(1)} MB`) + return collapsed +} + +/** Falls back to the guide while a section has not been generated, so the nav never dead-ends. */ +const linkOr = (items: DefaultTheme.SidebarItem[], link: string) => (items.length > 0 ? link : '/docs/') + +export default withMermaid( + defineConfig({ + title: 'Gropius', + description: + 'A cross-component issue management system for component-based architectures - backend, login service and GraphQL API documentation.', + lang: 'en-US', + + srcDir: 'docs', + cleanUrls: true, + lastUpdated: true, + appearance: 'dark', + + // VitePress inlines a hash map of every page into every page. Across the roughly 5,500 + // pages the generated references add up to, that map is ~850 KB - on its own it accounted + // for 4.6 GB of a 6.3 GB build. Moving it into a chunk of its own also lets browsers cache + // it once instead of re-downloading it with every page. + metaChunk: true, + + // Rendering 64 of those pages at a time is a lot of large strings in flight at once. + buildConcurrency: 8, + + // The site is published to ccims.github.io/gropius-docs. + base: '/gropius-docs/', + + // The generated API references are large and link to each other by construction, so a + // broken link there should fail the build rather than ship. The guide does point at a + // handful of dev-server URLs, which are not reachable from a build and never will be. + ignoreDeadLinks: [/^https?:\/\/localhost(:\d+)?/], + + head: [ + ['link', { rel: 'icon', href: '/gropius-docs/img/logo.svg', type: 'image/svg+xml' }], + ['meta', { name: 'theme-color', content: '#00639b' }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'Gropius' }] + ], + + sitemap: { + hostname: 'https://ccims.github.io/gropius-docs/' + }, + + buildEnd: async ({ outDir }) => { + await dropEagerPreloads(outDir, await dedupeLeanChunks(outDir)) + }, + + themeConfig: { + logo: '/img/logo.svg', + + nav: [ + { text: 'Guide', link: '/docs/', activeMatch: '^/docs/' }, + { text: 'Kotlin API', link: linkOr(kotlinApi, '/api/'), activeMatch: '^/api/' }, + { + text: 'GraphQL', + activeMatch: '^/graphql/', + items: [ + { text: 'Public API', link: linkOr(graphqlPublic, '/graphql/public/') }, + { text: 'Internal API', link: linkOr(graphqlInternal, '/graphql/internal/') } + ] + }, + { + text: 'Login service', + activeMatch: '^/(rest|login-service)/', + items: [ + { text: 'REST API', link: '/rest/' }, + { text: 'TypeScript API', link: linkOr(loginServiceApi, '/login-service/') } + ] + }, + { text: 'GitHub', link: 'https://github.com/ccims' } + ], + + sidebar: { + '/docs/': [ + { + text: 'Guide', + items: [ + { text: 'Gropius Backend', link: '/docs/' }, + { text: 'Modules', link: '/docs/modules' }, + { text: 'Login Service', link: '/docs/login' }, + { text: 'How to login', link: '/docs/how-to-login' }, + { text: 'GitHub sync', link: '/docs/github' }, + { text: 'Jira sync', link: '/docs/jira' } + ] + } + ], + // Each reference gets its generated tree as-is. DeferredSidebar renders it on the + // client, so the size of a tree no longer has anything to do with the size of the + // pages it appears on. + '/api/': kotlinApi, + '/graphql/public/': graphqlPublic, + '/graphql/internal/': graphqlInternal, + '/login-service/': loginServiceApi, + + '/rest/': [{ text: 'REST API', link: '/rest/', items: rest }] + }, + + search: { + provider: 'local' + }, + + socialLinks: [{ icon: 'github', link: 'https://github.com/ccims/gropius' }], + + editLink: { + pattern: 'https://github.com/ccims/gropius/edit/main/website/docs/:path', + text: 'Edit this page on GitHub' + }, + + outline: [2, 3], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Software Quality and Architecture - University of Stuttgart' + } + }, + + vite: { + resolve: { + alias: [ + { + // Renders the sidebar entries on the client instead of into every page's + // HTML. See the comment at the top of the component - it is the difference + // between shipping the sidebar once and shipping it 5,500 times. + // + // The pattern has to match the whole specifier, because Vite replaces only + // the part that matched. It deliberately does not match VPSidebarGroup.vue, + // which the replacement imports, nor the replacement's own filename. + find: /^.*\/VPSidebar\.vue$/, + replacement: fileURLToPath(new URL('./theme/components/DeferredSidebar.vue', import.meta.url)) + }, + { + // The same, for the contents of the nav bar's dropdown menus. + find: /^.*\/VPFlyout\.vue$/, + replacement: fileURLToPath(new URL('./theme/components/DeferredFlyout.vue', import.meta.url)) + } + ] + } + }, + + // VitePress bundles the grammars for every language the docs use - kotlin, graphql, + // typescript, sh - so only the themes need naming. + markdown: { + theme: { + light: 'github-light', + dark: 'github-dark' + } + }, + + mermaid: { + // C4 diagrams in the guide need the layout config the pages set themselves. + c4: { diagramMarginY: 20 } + } + }) +) diff --git a/website/.vitepress/theme/components/DeferredFlyout.vue b/website/.vitepress/theme/components/DeferredFlyout.vue new file mode 100644 index 0000000..8e5f182 --- /dev/null +++ b/website/.vitepress/theme/components/DeferredFlyout.vue @@ -0,0 +1,154 @@ + + + + + + + diff --git a/website/.vitepress/theme/components/DeferredSidebar.vue b/website/.vitepress/theme/components/DeferredSidebar.vue new file mode 100644 index 0000000..39686b3 --- /dev/null +++ b/website/.vitepress/theme/components/DeferredSidebar.vue @@ -0,0 +1,162 @@ + + + + + + diff --git a/website/.vitepress/theme/custom.css b/website/.vitepress/theme/custom.css new file mode 100644 index 0000000..6c177bf --- /dev/null +++ b/website/.vitepress/theme/custom.css @@ -0,0 +1,86 @@ +/** + * Gropius branding. + * + * Everything about how the Kotlin API reference looks lives in `@graphglue/dokka-vitepress`, which + * derives its colours from the VitePress tokens set here - so re-branding the site re-brands the + * generated Kotlin docs with it. + */ + +:root { + --vp-c-brand-1: #00639b; + --vp-c-brand-2: #00527f; + --vp-c-brand-3: #0083cc; + --vp-c-brand-soft: rgba(0, 131, 204, 0.14); + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: linear-gradient(120deg, #0083cc 30%, #46b3ff); + + /* + * VitePress puts a large blurred blob behind the hero image. The logo is itself blue, so a blue + * blob directly behind it is what made it hard to make out - it reads best against the plain + * page background. + */ + --vp-home-hero-image-background-image: none; + --vp-home-hero-image-filter: none; +} + +.dark { + --vp-c-brand-1: #7cd0ff; + --vp-c-brand-2: #9bdbff; + --vp-c-brand-3: #0083cc; + --vp-c-brand-soft: rgba(0, 131, 204, 0.24); +} + +/* + * The logo is a 64px SVG and VitePress only bounds the hero image with `max-width`, which does not + * scale anything up - so it rendered at 64px in a slot sized for 320. Sizing it explicitly fills it. + */ +.VPHero .image-src { + width: 192px; + height: 192px; +} + +@media (min-width: 640px) { + .VPHero .image-src { + width: 256px; + height: 256px; + } +} + +@media (min-width: 960px) { + .VPHero .image-src { + width: 320px; + height: 320px; + } +} + +/** + * The generated GraphQL pages separate a field from its type with a bullet, and close a + * collapsible block with a "show less" label. Both come from `scripts/graphql-vitepress-formatter.mjs`. + */ +.graphql-markdown-bullet { + color: var(--vp-c-text-3); + font-size: 0.75em; + vertical-align: middle; +} + +.graphql-markdown-entity { + font-weight: normal; +} + +.graphql-markdown-details-close { + color: var(--vp-c-text-2); + font-size: 0.9em; +} + +/* Mermaid renders into an SVG that has no business being as wide as its viewBox claims. */ +.mermaid { + display: flex; + justify-content: center; + overflow-x: auto; +} + +.mermaid svg { + max-width: 100%; + height: auto; +} diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts new file mode 100644 index 0000000..4b62deb --- /dev/null +++ b/website/.vitepress/theme/index.ts @@ -0,0 +1,31 @@ +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import { useDokka } from '@graphglue/dokka-vitepress/client' +import { theme as openapiTheme, useOpenapi } from 'vitepress-openapi/client' + +// Order matters for these three. `base.css` builds the page frame and is meant to lose against +// Dokka's own rules; `dokka.css` is Dokka's stylesheet, scoped to the embedded content; and +// `theme.css` re-skins it in VitePress' design tokens, so it has to come last. +import '@graphglue/dokka-vitepress/base.css' +// Generated by `npm run kotlin` from the Dokka output. `scripts/docs.mjs` writes an empty +// placeholder when the Kotlin docs have not been built, so this import always resolves. +import '../dokka/dokka.css' +import '@graphglue/dokka-vitepress/theme.css' +// Order-independent: styles the generated entries in VitePress' own sidebar. +import '@graphglue/dokka-vitepress/sidebar.css' + +import 'vitepress-openapi/dist/style.css' +import './custom.css' + +// Written by `npm run rest`; `scripts/docs.mjs` leaves an empty spec behind otherwise. +import spec from '../generated/login-openapi.json' + +export default { + extends: DefaultTheme, + enhanceApp(ctx) { + useDokka(ctx.app) + + useOpenapi({ spec }) + openapiTheme.enhanceApp(ctx) + } +} satisfies Theme diff --git a/website/LICENSES b/website/LICENSES index 48f7d56..e0320e9 100644 --- a/website/LICENSES +++ b/website/LICENSES @@ -1,7 +1,16 @@ +============================== Kotlin/dokka ============================== +https://github.com/Kotlin/dokka +files (generated at build time by @graphglue/dokka-vitepress, not checked in): +- .vitepress/dokka/dokka.css derived from Dokka's stylesheets +- .vitepress/dokka/images/* Dokka's declaration icons +- .vitepress/dokka/ui-kit/fonts/* the Inter and JetBrains Mono subsets Dokka ships + +Licensed under the Apache License, Version 2.0, reproduced below. + ============================== JetBrains/intellij-community ============================== https://github.com/JetBrains/intellij-community files: -- static/img/package.svg +- docs/public/img/package.svg Apache License Version 2.0, January 2004 diff --git a/website/README.md b/website/README.md index ae42bf3..17f7644 100644 --- a/website/README.md +++ b/website/README.md @@ -1,27 +1,170 @@ -# Gropius Backend Website +# Gropius website -## Installation +The documentation site at [ccims.github.io/gropius-docs](https://ccims.github.io/gropius-docs), +built with [VitePress](https://vitepress.dev). The guide lives in `docs/docs`; everything else is +generated from the sources of the three submodules and is not checked in: -``` -$ npm install -``` +| Section | Generated from | With | +| --- | --- | --- | +| `/api/` | the Kotlin backend | Dokka, then [`@graphglue/dokka-vitepress`](https://github.com/graphglue/dokka-vitepress) | +| `/graphql/public/`, `/graphql/internal/` | the schema each API serves on `/sdl` | [GraphQL-Markdown](https://graphql-markdown.dev) with the VitePress formatter in `scripts/` | +| `/rest/` | the login-service OpenAPI document | [`vitepress-openapi`](https://github.com/enzonotario/vitepress-openapi) | +| `/login-service/` | the login-service TypeScript sources | TypeDoc with [`typedoc-vitepress-theme`](https://typedoc-plugin-markdown.org/plugins/vitepress) | + +## Prerequisites + +- Node.js 22 or newer +- A JDK 21, for the Gradle runs that produce the Dokka output and the GraphQL schemas -## Local Development +Everything the backend build needs, including `dokka-graphql-description-plugin` - which makes Dokka +fall back to the contents of `@GraphQLDescription` where a declaration has no KDoc - and graph-glue, +resolves from Maven Central. Nothing has to be installed by hand. +## Getting started + +```sh +cd gropius/website + +npm install +npm run dev # generate what is missing, then serve on localhost:5173 ``` -$ npm start + +`npm run dev` generates the Kotlin and TypeScript references if they are absent, then starts the dev +server. It deliberately does *not* run the two stages that have to boot a server. Run those once by +hand and they are reused from then on: + +```sh +npm run graphql # boots api-public and api-internal to read their schemas, then renders them +npm run rest # boots the login-service against sqlite to read its OpenAPI document ``` -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. +Editing the guide only needs `npm run dev`; nothing has to be regenerated. + +## Commands + +Each of these is `node scripts/docs.mjs ` behind the scenes. + +| Command | What it does | +| --- | --- | +| `npm run dev` | Fills in missing references, then starts the dev server | +| `npm run build` | Regenerates everything and builds the static site into `.vitepress/dist` | +| `npm run generate` | Runs all four generators without building the site | +| `npm run site` | Builds the static site from what is already generated | +| `npm run kotlin` | `./gradlew :dokkaGenerate` in the backend, transformed into `docs/api` | +| `npm run graphql` | Dumps both schemas into `schemas/`, renders them into `docs/graphql` | +| `npm run rest` | Dumps the OpenAPI document into `schemas/login.json` | +| `npm run typedoc` | TypeDoc over the login-service sources into `docs/login-service` | +| `npm run preview` | Serves the last build | +| `npm run clean` | Deletes every generated file | + +Each stage compares its output against its sources and skips the work when it is already up to +date. The two stages that boot a server never re-run implicitly - once `schemas/` is populated it is +reused. `--force` overrides both, and `--skip`/`--only` narrow what runs: + +```sh +npm run graphql -- --force +npm run generate -- --force +npm run generate -- --skip=kotlin,rest +``` -## Build +## Layout ``` -$ npm run build +website/ +├── docs/ VitePress srcDir +│ ├── index.md landing page +│ ├── docs/ the guide +│ ├── rest/ REST pages - dynamic routes over the OpenAPI document +│ ├── api/ generated - the Kotlin reference +│ ├── graphql/ generated - both GraphQL schemas +│ ├── login-service/ generated - the TypeScript reference +│ └── public/ static assets +├── .vitepress/ +│ ├── config.ts site configuration +│ ├── theme/ theme extension and brand styles +│ │ └── components/ replace their VitePress originals, see below +│ │ ├── DeferredSidebar.vue +│ │ └── DeferredFlyout.vue +│ ├── dokka/ generated - Dokka stylesheet, icons, fonts, sidebar +│ └── generated/ generated - sidebars and the OpenAPI document +├── schemas/ generated - the schemas dumped from the running services +└── scripts/ + ├── docs.mjs the script behind every npm script above + └── graphql-vitepress-formatter.mjs renders GraphQL-Markdown output as VitePress markdown ``` -This command generates static content into the `build` directory and can be served using any static contents hosting service. +## Notes on the setup + +- **VitePress 1.x, not 2.x.** Both `vitepress-openapi` and `vitepress-plugin-mermaid` require + VitePress 1, and the guide relies on mermaid for its C4 and sequence diagrams. +- **`graphql` is pinned through `overrides`.** GraphQL-Markdown and the graphql-tools loader check + schema objects with `instanceof`, which fails silently across two copies of `graphql`. +- **Each GraphQL schema renders in its own process.** GraphQL-Markdown keeps its printer in static + state and its `init` is a no-op once that state is set, so a second schema rendered in the same + process would inherit the first one's base path and every link would point into the wrong section. +- **The build needs a large heap.** The generated references come to roughly 5,500 pages and + VitePress holds the whole build in memory; `scripts/docs.mjs` passes `--max-old-space-size` for + you unless `NODE_OPTIONS` already sets one. `npm run dev` compiles pages on demand and is + unaffected - only a full `build` is heavy. +- **Five things keep the output the size it should be.** Almost 5,500 of these pages are generated, + so anything VitePress writes into *every* page is multiplied by 5,500. Left alone the Kotlin page + that was 33 KB under Docusaurus came to 242 KB, and the site to 6.3 GB against the 1 GB GitHub + Pages accepts. + + What was being written into every page: + + 1. `metaChunk: true` moves the map of every page into one cacheable chunk. 4.6 GB of the 6.3 GB. + 2. `theme/components/DeferredSidebar.vue`, aliased over VitePress' `VPSidebar`, renders the + sidebar entries on the client. Docusaurus rendered a collapsed category as an empty stub and + filled it in on the client; VitePress renders all of it, which was 91% of that Kotlin page. + The `