diff --git a/.vitepress/data/modules.ts b/.vitepress/data/modules.ts
index 74d687cd..d41cf325 100644
--- a/.vitepress/data/modules.ts
+++ b/.vitepress/data/modules.ts
@@ -43,7 +43,7 @@ export const publicModules = [
guide: '/en/guide/auth',
api: '/en/api/@connectum/auth/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/auth',
- example: 'https://github.com/Connectum-Framework/examples/tree/main/auth',
+ example: 'https://github.com/Connectum-Framework/examples/tree/main/car-sharing',
entryPoints: ['createJwtAuthInterceptor', 'createAuthzInterceptor', 'requireAuthContext'],
},
{
@@ -62,10 +62,10 @@ export const publicModules = [
name: '@connectum/events-nats',
category: 'Communication',
summary: 'NATS JetStream adapter for the Connectum EventBus.',
- guide: '/en/guide/events/adapters',
+ guide: '/en/guide/events/adapters#nats-jetstream-adapter',
api: '/en/api/@connectum/events-nats/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/events-nats',
- example: 'https://github.com/Connectum-Framework/examples/tree/main/with-events-nats',
+ example: 'https://github.com/Connectum-Framework/examples/tree/main/with-events-dlq',
entryPoints: ['NatsAdapter', 'NatsAdapterOptions'],
},
{
@@ -73,7 +73,7 @@ export const publicModules = [
name: '@connectum/events-kafka',
category: 'Communication',
summary: 'Kafka and Redpanda adapter for the Connectum EventBus.',
- guide: '/en/guide/events/adapters',
+ guide: '/en/guide/events/adapters#kafka-adapter',
api: '/en/api/@connectum/events-kafka/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/events-kafka',
example: 'https://github.com/Connectum-Framework/examples/tree/main/with-events-kafka',
@@ -84,7 +84,7 @@ export const publicModules = [
name: '@connectum/events-redis',
category: 'Communication',
summary: 'Redis Streams and Valkey adapter for the Connectum EventBus.',
- guide: '/en/guide/events/adapters',
+ guide: '/en/guide/events/adapters#redis-streams-adapter',
api: '/en/api/@connectum/events-redis/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/events-redis',
example: 'https://github.com/Connectum-Framework/examples/tree/main/with-events-valkey',
@@ -95,7 +95,7 @@ export const publicModules = [
name: '@connectum/events-amqp',
category: 'Communication',
summary: 'AMQP and RabbitMQ adapter with topology and reliable-publishing controls.',
- guide: '/en/guide/events/adapters',
+ guide: '/en/guide/events/adapters#amqp--rabbitmq-adapter',
api: '/en/api/@connectum/events-amqp/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/events-amqp',
example: 'https://github.com/Connectum-Framework/examples/tree/main/with-events-amqp',
@@ -126,7 +126,7 @@ export const publicModules = [
slug: 'reflection',
name: '@connectum/reflection',
category: 'Operations',
- summary: 'gRPC Server Reflection for discovery, inspection, and tooling.',
+ summary: 'gRPC Server Reflection for schema inspection and client tooling.',
guide: '/en/guide/protocols/reflection',
api: '/en/api/@connectum/reflection/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/reflection',
@@ -150,7 +150,7 @@ export const publicModules = [
guide: '/en/guide/service-communication/service-catalog',
api: '/en/api/@connectum/protoc-gen-catalog/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/protoc-gen-catalog',
- entryPoints: ['protoc-gen-catalog'],
+ entryPoints: ['protoc-gen-connectum-catalog'],
},
{
slug: 'testing',
@@ -160,7 +160,6 @@ export const publicModules = [
guide: '/en/guide/testing/integration',
api: '/en/api/@connectum/testing/',
source: 'https://github.com/Connectum-Framework/connectum/tree/main/packages/testing',
- example: 'https://github.com/Connectum-Framework/examples/tree/main/runn',
entryPoints: ['createTestServer', 'withTestServer', 'createLocalClient'],
},
{
diff --git a/.vitepress/homepage/deployment-markup.mjs b/.vitepress/homepage/deployment-markup.mjs
index 8e072ab3..c6bcda51 100644
--- a/.vitepress/homepage/deployment-markup.mjs
+++ b/.vitepress/homepage/deployment-markup.mjs
@@ -15,10 +15,11 @@ export function deploymentMarkup(svg = '') {
Service calls
Same code.
Different deployments.
Keep your Protobuf contracts and service handlers. Run services together as a monolith, separately as microservices, or in a hybrid deployment.
- Call other services with ctx.call(). The same typed call runs in-process or over RPC, depending on your server configuration.
+ A generated catalog supplies the types for ctx.call(). Local registration and configured remote resolvers choose where each call runs.
+ These diagrams illustrate routing; endpoints, credentials and process deployment remain your configuration.
`;
}
diff --git a/.vitepress/theme/components/HomePage.vue b/.vitepress/theme/components/HomePage.vue
index e6ac4a4b..0ebcfde6 100644
--- a/.vitepress/theme/components/HomePage.vue
+++ b/.vitepress/theme/components/HomePage.vue
@@ -39,32 +39,32 @@ onBeforeUnmount(() => revealObserver?.disconnect());
// biome-ignore lint/correctness/noUnusedVariables: the Vue template iterates these goal links.
const goals = [
{
- label: 'New to Connectum',
- title: 'Build a first service',
- description: 'Go from an empty directory to a verified gRPC call with a focused, linear tutorial.',
+ label: 'Your first service',
+ title: 'Build a protected RPC',
+ description: 'Scaffold a project, add a Protobuf method and check calls with and without a JWT.',
link: '/en/guide/quickstart',
- action: 'Start the tutorial',
+ action: 'Follow the quickstart',
},
{
- label: 'Building a capability',
- title: 'Solve a service task',
- description: 'Add validation, service calls, events, authentication, or testing with a guide for the task at hand.',
- link: '/en/guide/about#choose-your-path',
- action: 'Browse guides',
+ label: 'Connecting services',
+ title: 'Choose how services communicate',
+ description: 'Compare typed RPC calls and events, then choose local or remote routing.',
+ link: '/en/guide/communication',
+ action: 'Communication guides',
},
{
- label: 'Operating services',
- title: 'Deploy and operate services',
- description: 'Configure health checks, observability, shutdown, containers, Kubernetes, or a gateway.',
- link: '/en/guide/production/kubernetes',
- action: 'Explore operations',
+ label: 'Deploying a service',
+ title: 'Choose a deployment setup',
+ description: 'Package the service for containers and find guidance for Kubernetes, gateways and service meshes.',
+ link: '/en/guide/deploy',
+ action: 'Deployment guides',
},
{
- label: 'Know what you need',
+ label: 'Exact API',
title: 'Find an exact option',
- description: 'Jump by module or symbol into the generated API, compatibility matrix, or migration guidance.',
+ description: 'Look up exported functions, TypeScript interfaces and configuration fields in the generated API.',
link: '/en/api/',
- action: 'Open reference',
+ action: 'Open the API',
},
] as const;
@@ -72,7 +72,7 @@ const goals = [
const lifecycle = [
['Define', 'Proto contracts and generated types'],
['Run', 'Server lifecycle and transport'],
- ['Protect', 'Validation, auth, and resilience'],
+ ['Protect', 'Configured validation, auth and resilience'],
['Observe', 'Traces, metrics, and logs'],
['Operate', 'Health, shutdown, and deployment'],
] as const;
@@ -97,7 +97,7 @@ const lifecycle = [
@@ -105,10 +105,47 @@ const lifecycle = [
+
+
+
+
+
From contract to handler
+
Implement your contract in TypeScript.
+
+ Define the API in Protobuf, generate its types and implement the handlers as ordinary functions. Register services, middleware and protocols with
+ createServer().
+
+
+
+
+
+
+
+
+
+
Start by goal
-
Choose a path for the task at hand.
+
Choose your next step.
@@ -128,7 +165,8 @@ const lifecycle = [
Add capabilities through explicit modules.
- The framework keeps lifecycle and registration predictable while modules remain explicit. Begin with the server; add production behavior as the service grows.
+ Use the same startup and shutdown model across services, with only the modules each service needs. Your application keeps its database, business logic and
+ dependency wiring.
@@ -138,49 +176,22 @@ const lifecycle = [
{{ detail }}
+
+ For a small RPC endpoint, or when your existing setup already covers these needs, ConnectRPC alone may be enough.
+ Decide whether Connectum fits your service.
+
-
-
-
-
Fits your stack
-
Choose a runtime for each service.
-
-
Use ConnectRPC or gRPC, connect to supported brokers, and choose Node.js or Bun for each service.
-
-
-
-
-
-
-
The central primitive
-
Compose a service with an explicit lifecycle.
-
- Define a Protobuf contract, generate TypeScript types, implement a handler, and start a server with createServer(). Then call the same service with
- Connect and gRPC.
-
-
-
-
-
-
-
-
-
-
diff --git a/scripts/check-landing-browser.mjs b/scripts/check-landing-browser.mjs
new file mode 100644
index 00000000..9330b297
--- /dev/null
+++ b/scripts/check-landing-browser.mjs
@@ -0,0 +1,186 @@
+import assert from 'node:assert/strict';
+import { mkdir, writeFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { parseArgs } from 'node:util';
+import { publicModules } from '../.vitepress/data/modules.ts';
+import { findRedirect } from '../.vitepress/data/redirects.ts';
+import { checkHttpLink } from './lib/package-readme-links.mjs';
+
+const { values } = parseArgs({ options: { url: { type: 'string', default: 'http://127.0.0.1:4185' }, output: { type: 'string', default: '.tmp/landing-browser' } } });
+assert.ok(process.env.PLAYWRIGHT_MODULE, 'Set PLAYWRIGHT_MODULE to an installed Playwright index.mjs. This check starts no server.');
+const { chromium } = await import(pathToFileURL(resolve(process.env.PLAYWRIGHT_MODULE)).href);
+const base = new URL(values.url);
+const output = resolve(values.output);
+await mkdir(output, { recursive: true });
+const browser = await chromium.launch({ headless: true, channel: 'chrome' });
+const report = { completed: false, site: base.href, layouts: [], clicks: [], keyboard: [], static: [], links: [], errors: [] };
+const deadline = setTimeout(() => void browser.close(), 300_000);
+const sectionOrder = ['home-hero', 'home-ecosystem', 'home-example', 'home-deployments', 'home-intents', 'home-lifecycle', 'home-modules', 'home-resources'];
+const pages = new Map();
+const links = new Set();
+
+async function fetchPage(url) {
+ const key = new URL(url);
+ key.hash = '';
+ if (!pages.has(key.href)) {
+ pages.set(
+ key.href,
+ (async () => {
+ const response = await fetch(key, { signal: AbortSignal.timeout(20_000), headers: { 'user-agent': 'Connectum-documentation-link-check' } });
+ return { status: response.status, html: await response.text(), finalUrl: response.url };
+ })(),
+ );
+ }
+ return pages.get(key.href);
+}
+
+async function visit(page) {
+ await page.goto(new URL('/en/', base).href, { waitUntil: 'domcontentloaded' });
+ await page.waitForFunction(() => document.querySelector('#app')?.__vue_app__);
+ await page.locator('.home-code[data-phase="ready"]').waitFor();
+ await page.locator('.home-deployments[data-phase="ready"]').waitFor();
+ assert.ok((await page.locator('.home-code button').count()) >= 5);
+ assert.ok((await page.locator('.deployment-controls button').count()) >= 3);
+}
+
+async function checkOrder(page) {
+ const sections = await page.locator('.connectum-home > section').evaluateAll((nodes) =>
+ nodes.map((node) => ({
+ name: [...node.classList].find((name) => name !== 'home-section' && name.startsWith('home-')),
+ top: node.getBoundingClientRect().top + scrollY,
+ bottom: node.getBoundingClientRect().bottom + scrollY,
+ labelled: Boolean(document.getElementById(node.getAttribute('aria-labelledby'))?.textContent.trim()),
+ })),
+ );
+ assert.deepEqual(
+ sections.map((section) => section.name),
+ sectionOrder,
+ );
+ for (const [index, section] of sections.entries()) {
+ assert.ok(section.labelled, `${section.name} has no accessible heading`);
+ if (index) assert.ok(section.top >= sections[index - 1].bottom - 1, `${section.name} overlaps the previous section`);
+ }
+ return sections;
+}
+
+try {
+ for (const theme of ['light', 'dark']) {
+ const context = await browser.newContext({ colorScheme: theme, reducedMotion: 'reduce', viewport: { width: 1440, height: 1000 } });
+ context.setDefaultTimeout(20_000);
+ const page = await context.newPage();
+ page.on('pageerror', (error) => report.errors.push(error.message));
+ page.on('console', (message) => {
+ if (message.type() === 'error') report.errors.push(message.text());
+ });
+ try {
+ await visit(page);
+ assert.equal(await page.locator('html').evaluate((node) => node.classList.contains('dark')), theme === 'dark');
+ for (const href of await page.locator('.connectum-home a[href], .VPNav a[href], footer a[href]').evaluateAll((nodes) => nodes.map((node) => node.href)))
+ links.add(href);
+ assert.equal(await page.locator('.module-card').count(), 15);
+ const ecosystem = await page.locator('.home-ecosystem').innerText();
+ assert.ok(ecosystem.includes('ConnectRPC supplies the RPC layer.'));
+ assert.ok(ecosystem.includes('optional modules'));
+ assert.ok((await page.locator('.home-lifecycle').innerText()).includes('ConnectRPC alone may be enough.'));
+ for (const width of [1440, 768, 390, 320]) {
+ await page.setViewportSize({ width, height: 1000 });
+ const sections = await checkOrder(page);
+ const documentWidth = await page.evaluate(() => document.documentElement.scrollWidth);
+ assert.ok(documentWidth <= width + 1, `${theme}/${width}: horizontal overflow ${documentWidth}`);
+ await writeFile(resolve(output, `${theme}-${width}.yml`), await page.locator('.connectum-home').ariaSnapshot());
+ for (const name of ['home-ecosystem', 'home-example', 'home-deployments', 'home-intents', 'home-lifecycle']) {
+ await page.locator(`.${name}`).screenshot({ path: resolve(output, `${theme}-${width}-${name}.png`) });
+ }
+ report.layouts.push({ theme, width, documentWidth, sections });
+ }
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ const goalLinks = page.locator('.home-goal-grid a');
+ await goalLinks.first().focus();
+ for (let index = 0; index < 4; index++) {
+ assert.ok(await goalLinks.nth(index).evaluate((node) => document.activeElement === node));
+ const focus = await goalLinks.nth(index).evaluate((node) => ({
+ visible: node.matches(':focus-visible'),
+ outline: getComputedStyle(node).outlineStyle,
+ outlineWidth: getComputedStyle(node).outlineWidth,
+ width: node.getBoundingClientRect().width,
+ height: node.getBoundingClientRect().height,
+ }));
+ assert.ok(focus.visible && focus.outline !== 'none' && Number.parseFloat(focus.outlineWidth) > 0, JSON.stringify(focus));
+ assert.ok(focus.width >= 24 && focus.height >= 24);
+ report.keyboard.push({ theme, index, ...focus });
+ await page.keyboard.press('Tab');
+ }
+ assert.ok(await page.locator('.home-lifecycle a').evaluate((node) => document.activeElement === node));
+ for (const [selector, destination] of [
+ ['.home-goal-card:nth-child(1)', '/en/guide/quickstart'],
+ ['.home-goal-card:nth-child(2)', '/en/guide/communication'],
+ ['.home-goal-card:nth-child(3)', '/en/guide/deploy'],
+ ['.home-goal-card:nth-child(4)', '/en/api/'],
+ ['.home-example a:first-child', '/en/guide/quickstart-manual'],
+ ['.home-support a', '/en/guide/runtime-compatibility-details#compatibility-matrix'],
+ ['.deployment-copy a:last-child', '/en/guide/service-communication/resolver-configuration'],
+ ['.home-modules > .home-section__more a', '/en/guide/production/architecture#extension-seams'],
+ ['.module-card a[href$="#nats-jetstream-adapter"]', '/en/guide/events/adapters#nats-jetstream-adapter'],
+ ['.module-card a[href$="#kafka-adapter"]', '/en/guide/events/adapters#kafka-adapter'],
+ ['.module-card a[href$="#redis-streams-adapter"]', '/en/guide/events/adapters#redis-streams-adapter'],
+ ['.module-card a[href$="#amqp--rabbitmq-adapter"]', '/en/guide/events/adapters#amqp--rabbitmq-adapter'],
+ ]) {
+ await visit(page);
+ await page.locator(selector).first().click();
+ const expected = new URL(destination, base);
+ await page.waitForURL((url) => url.pathname === expected.pathname && url.hash === expected.hash);
+ await page.locator('.connectum-home').waitFor({ state: 'detached' });
+ await page.locator('main h1').waitFor();
+ if (expected.hash) await page.locator(`[id="${expected.hash.slice(1)}"]`).waitFor({ state: 'visible' });
+ report.clicks.push({ theme, destination, title: await page.locator('main h1').innerText() });
+ }
+ } catch (error) {
+ await page.screenshot({ path: resolve(output, `${theme}-failure.png`) });
+ throw error;
+ } finally {
+ await context.close();
+ }
+ const staticContext = await browser.newContext({ javaScriptEnabled: false, colorScheme: theme, viewport: { width: 390, height: 950 } });
+ try {
+ const staticPage = await staticContext.newPage();
+ await staticPage.goto(new URL('/en/', base).href);
+ const renderedTheme = await staticPage.locator('html').evaluate((node) => (node.classList.contains('dark') ? 'dark' : 'light'));
+ await checkOrder(staticPage);
+ assert.equal(await staticPage.locator('.module-card').count(), 15);
+ assert.ok(await staticPage.locator('.home-code pre').first().isVisible());
+ assert.ok(await staticPage.locator('.deployment-static--compact').isVisible());
+ await staticPage.locator('.home-goal-card').first().click();
+ await staticPage.waitForURL(new URL('/en/guide/quickstart', base).href);
+ assert.ok((await staticPage.locator('h1').innerText()).includes('First Connectum Service'));
+ report.static.push({ colorScheme: theme, renderedTheme, sections: sectionOrder, cards: 15, tutorialLinkWorks: true });
+ } finally {
+ await staticContext.close();
+ }
+ }
+ for (const module of publicModules) {
+ for (const href of [module.guide, module.api, module.source, module.example].filter(Boolean)) links.add(new URL(href, base).href);
+ }
+ for (const href of links) {
+ const url = new URL(href);
+ if (url.origin === base.origin) assert.equal(findRedirect(url.pathname), undefined, `Landing links through a compatibility route: ${href}`);
+ const result = await checkHttpLink(url, fetchPage);
+ report.links.push({ href, ...result });
+ if (!result.ok) report.errors.push(`${href}: ${result.reason}`);
+ }
+ assert.deepEqual(report.errors, []);
+ report.completed = true;
+} catch (error) {
+ report.errors.push(error.stack ?? String(error));
+ throw error;
+} finally {
+ clearTimeout(deadline);
+ try {
+ await writeFile(resolve(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`);
+ } finally {
+ await browser.close();
+ }
+}
+console.log(
+ `Landing: ${report.layouts.length} layouts, ${report.clicks.length} intentional transitions, keyboard in both themes, ${report.static.length} no-JS fallbacks (actual themes recorded); ${report.links.length} rendered/metadata links with HTTP and fragments checked.`,
+);
diff --git a/scripts/docs-validate.mjs b/scripts/docs-validate.mjs
index 25e87b6e..3e23f90e 100644
--- a/scripts/docs-validate.mjs
+++ b/scripts/docs-validate.mjs
@@ -248,7 +248,10 @@ for (const module of publicModules) {
if (seenSlugs.has(module.slug)) errors.push(`duplicate module slug: ${module.slug}`);
seenSlugs.add(module.slug);
if (!resolveRoute(module.api)) errors.push(`${module.name}: missing generated API destination ${module.api}`);
- if (!resolveRoute(module.guide)) errors.push(`${module.name}: missing guide destination ${module.guide}`);
+ const [guidePath, guideFragment] = module.guide.split('#');
+ const guideRoute = resolveRoute(guidePath);
+ if (!guideRoute) errors.push(`${module.name}: missing guide destination ${module.guide}`);
+ else if (guideFragment && !pages.get(guideRoute).anchors.has(guideFragment)) errors.push(`${module.name}: missing guide fragment ${module.guide}`);
}
const navigationSource = readFileSync(resolve(projectRoot, '.vitepress/config/en.ts'), 'utf8');
diff --git a/tests/homepage/landing.test.mjs b/tests/homepage/landing.test.mjs
new file mode 100644
index 00000000..67cf61ad
--- /dev/null
+++ b/tests/homepage/landing.test.mjs
@@ -0,0 +1,103 @@
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+import { test } from 'node:test';
+import { publicModules } from '../../.vitepress/data/modules.ts';
+
+const root = new URL('../../', import.meta.url);
+const read = (path) => readFileSync(new URL(path, root), 'utf8');
+const home = read('.vitepress/theme/components/HomePage.vue');
+
+test('the landing introduces the stack, code and deployment before goals, lifecycle and the unchanged module grid', () => {
+ const template = home.slice(home.indexOf(''));
+ const markers = ['home-hero', 'home-ecosystem', 'home-example', ' previous, `${marker} is missing or out of order`);
+ previous = index;
+ }
+ for (const component of ['EcosystemRail', 'HomeCodeWalkthrough', 'HomeDeployments', 'ModuleGrid']) {
+ assert.equal((template.match(new RegExp(`<${component}(?:\\s|>)`, 'g')) ?? []).length, 1, component);
+ }
+ assert.match(template, //);
+});
+
+test('landing copy explains the added lifecycle and optional policies without changing the approved hero', () => {
+ assert.match(home, />Microservices framework);
+ assert.match(home, />Proto is the single source of truth);
+ assert.match(home, /Connectum provides a TypeScript service runtime for proto contracts, request middleware, security, observability, and operations\./);
+ assert.match(home, /ConnectRPC supplies the RPC layer/);
+ assert.match(home, /shared server lifecycle and optional modules/);
+ assert.match(home, /handlers as ordinary functions/);
+ assert.match(home, /Configured validation, auth and resilience/);
+ assert.match(home, /ConnectRPC alone may be enough/);
+ assert.match(home, /Your application keeps its database, business logic and\s+dependency wiring/);
+ const deployment = read('.vitepress/homepage/deployment-markup.mjs');
+ assert.match(deployment, /generated catalog supplies the types/);
+ assert.match(deployment, /Local registration and configured remote resolvers/);
+ assert.match(deployment, /illustrate routing; endpoints, credentials and process deployment remain your configuration/);
+});
+
+test('landing transitions match the task promised by their labels', () => {
+ const goals = [...home.matchAll(/title: '([^']+)',\s+description: '[^']+',\s+link: '([^']+)'/g)].map((match) => match.slice(1));
+ assert.deepEqual(goals, [
+ ['Build a protected RPC', '/en/guide/quickstart'],
+ ['Choose how services communicate', '/en/guide/communication'],
+ ['Choose a deployment setup', '/en/guide/deploy'],
+ ['Find an exact option', '/en/api/'],
+ ]);
+ assert.match(home, /href="\/en\/guide\/quickstart-manual">Build a service step by step/);
+ assert.match(home, /href="\/en\/guide\/runtime-compatibility-details#compatibility-matrix">Runtime matrix/);
+ assert.match(home, /href="\/en\/guide\/production\/architecture#extension-seams">Understand module boundaries/);
+ assert.match(read('.vitepress/homepage/deployment-markup.mjs'), /href="\/en\/guide\/service-communication\/resolver-configuration">Configure remote routes/);
+});
+
+test('module metadata uses precise adapter sections, the published catalog binary and current example paths', () => {
+ for (const [slug, fragment] of [
+ ['events-nats', 'nats-jetstream-adapter'],
+ ['events-kafka', 'kafka-adapter'],
+ ['events-redis', 'redis-streams-adapter'],
+ ['events-amqp', 'amqp--rabbitmq-adapter'],
+ ])
+ assert.equal(publicModules.find((module) => module.slug === slug).guide, `/en/guide/events/adapters#${fragment}`);
+ assert.deepEqual(publicModules.find((module) => module.slug === 'protoc-gen-catalog').entryPoints, ['protoc-gen-connectum-catalog']);
+ assert.equal(publicModules.find((module) => module.slug === 'reflection').summary, 'gRPC Server Reflection for schema inspection and client tooling.');
+ for (const [slug, example] of [
+ ['auth', 'car-sharing'],
+ ['events-nats', 'with-events-dlq'],
+ ]) {
+ assert.equal(publicModules.find((module) => module.slug === slug).example, `https://github.com/Connectum-Framework/examples/tree/main/${example}`);
+ }
+ assert.equal(
+ Object.hasOwn(
+ publicModules.find((module) => module.slug === 'testing'),
+ 'example',
+ ),
+ false,
+ );
+});
+
+test('documentation validation accepts real module fragments and rejects missing pages and fragments', () => {
+ for (const [guide, expected] of [
+ ['/en/guide/events/adapters#nats-jetstream-adapter', null],
+ ['/en/guide/events/adapters#missing-adapter-section', 'missing guide fragment'],
+ ['/en/guide/missing-adapter-page#nats-jetstream-adapter', 'missing guide destination'],
+ ]) {
+ const result = spawnSync(
+ process.execPath,
+ [
+ '--input-type=module',
+ '-e',
+ `
+ import { publicModules } from './.vitepress/data/modules.ts';
+ publicModules.find((module) => module.slug === 'events-nats').guide = ${JSON.stringify(guide)};
+ await import('./scripts/docs-validate.mjs');
+ `,
+ ],
+ { cwd: root, encoding: 'utf8' },
+ );
+ assert.equal(result.status, expected ? 1 : 0, result.stderr);
+ if (expected) assert.ok(result.stderr.includes(`@connectum/events-nats: ${expected} ${guide}`), result.stderr);
+ }
+});