diff --git a/.gitignore b/.gitignore index 59708e97..fe81d902 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules/ .idea/ .vscode/ .indexing-lock.json +.tmp/ diff --git a/.vitepress/config/shared.ts b/.vitepress/config/shared.ts index 302ed9cb..91f4c72a 100644 --- a/.vitepress/config/shared.ts +++ b/.vitepress/config/shared.ts @@ -1,5 +1,24 @@ import { defineConfig } from 'vitepress'; import llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms'; +import { runtimeContainerPlugin } from '../plugins/runtimeContainer.js'; +import { DEFAULT_RUNTIME, RUNTIME_STORAGE_KEY } from '../theme/runtime.js'; + +/** + * Applies the stored runtime before the first paint, the same way VitePress restores the + * dark-mode class -- otherwise a reader who picked Bun would see a flash of Node content. + * `?runtime=bun` wins over the stored value so a link can carry the choice. + */ +const restoreRuntimeScript = `;(() => { + try { + const fromUrl = new URLSearchParams(location.search).get('runtime') + const shared = fromUrl === 'node' || fromUrl === 'bun' ? fromUrl : null + const stored = shared ?? localStorage.getItem('${RUNTIME_STORAGE_KEY}') + document.documentElement.dataset.runtime = stored === 'bun' ? 'bun' : '${DEFAULT_RUNTIME}' + if (shared) localStorage.setItem('${RUNTIME_STORAGE_KEY}', shared) + } catch { + document.documentElement.dataset.runtime = '${DEFAULT_RUNTIME}' + } +})()`; export const sharedConfig = defineConfig({ title: 'Connectum', @@ -14,6 +33,7 @@ export const sharedConfig = defineConfig({ ['meta', { property: 'og:site_name', content: 'Connectum' }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:image', content: 'https://connectum.dev/assets/splash.png' }], + ['script', { id: 'restore-runtime' }, restoreRuntimeScript], ], themeConfig: { logo: '/assets/name.png', @@ -37,6 +57,7 @@ export const sharedConfig = defineConfig({ lineNumbers: true, config(md) { md.use(copyOrDownloadAsMarkdownButtons); + md.use(runtimeContainerPlugin); }, }, srcExclude: ['**/api/_media/**'], diff --git a/.vitepress/plugins/runtimeContainer.ts b/.vitepress/plugins/runtimeContainer.ts new file mode 100644 index 00000000..9855dc8f --- /dev/null +++ b/.vitepress/plugins/runtimeContainer.ts @@ -0,0 +1,277 @@ +/** + * markdown-it plugin for the site-wide runtime switcher. + * + * Renders runtime-specific documentation as plain server-rendered markdown: every + * variant ends up in the static HTML (so crawlers, the local search index and + * `llms.txt` see all of them), and only CSS -- driven by `data-runtime` on `` -- + * decides which one is visible. + * + * Two forms: + * + * ::: runtime <- grouped, one section per runtime + * == node + * ```bash + * node src/index.ts + * ``` + * == bun + * ```bash + * bun src/index.ts + * ``` + * ::: + * + * ::: runtime bun <- single-runtime block, hidden unless Bun is selected + * Bun does not support X, use Y instead. + * ::: + * + * Nesting follows the usual VitePress rule: an outer container needs more colons than + * the inner one (`:::: runtime` wrapping a `::: tip`). + */ + +import { DEFAULT_RUNTIME, RUNTIME_LABELS, RUNTIMES, type Runtime } from '../theme/runtime.ts'; + +const MARKER = 0x3a; /* ':' */ +const MIN_MARKER_LEN = 3; +const NAME = 'runtime'; + +/** Minimal structural view of markdown-it's block state -- avoids a @types/markdown-it dependency. */ +interface StateBlock { + src: string; + env?: { relativePath?: string }; + bMarks: number[]; + eMarks: number[]; + tShift: number[]; + sCount: number[]; + blkIndent: number; + line: number; + lineMax: number; + parentType: string; + md: { block: { tokenize(state: StateBlock, start: number, end: number): void } }; + push(type: string, tag: string, nesting: number): Token; +} + +interface Token { + content: string; + markup: string; + block: boolean; + map: [number, number] | null; + hidden: boolean; +} + +interface MarkdownItLike { + block: { + ruler: { + before( + beforeName: string, + ruleName: string, + rule: (state: StateBlock, startLine: number, endLine: number, silent: boolean) => boolean, + options?: { alt: string[] }, + ): void; + }; + }; + renderer: { + rules: Record string) | undefined>; + }; +} + +interface Section { + runtime: Runtime; + start: number; + end: number; +} + +function lineText(state: StateBlock, line: number): string { + return state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]); +} + +/** Number of leading `:` characters, or 0 when the line does not start a container. */ +function markerLength(text: string): number { + let len = 0; + while (text.charCodeAt(len) === MARKER) len++; + return len; +} + +function isRuntimeName(value: string): value is Runtime { + return (RUNTIMES as readonly string[]).includes(value); +} + +/** + * Splits the container body into per-runtime sections at `== ` lines, + * ignoring markers inside fenced code blocks and nested containers. + */ +function splitSections(state: StateBlock, bodyStart: number, bodyEnd: number, logPrefix: string): Section[] { + const sections: Section[] = []; + let fence: string | null = null; + let depth = 0; + + for (let line = bodyStart; line < bodyEnd; line++) { + const text = lineText(state, line).trim(); + + if (fence) { + if (text.startsWith(fence)) fence = null; + continue; + } + const fenceMatch = /^(`{3,}|~{3,})/.exec(text); + if (fenceMatch) { + fence = fenceMatch[1]; + continue; + } + + const colons = markerLength(text); + if (colons >= MIN_MARKER_LEN) { + // An opening marker carries a name, a closing one does not. + if (text.slice(colons).trim()) depth++; + else if (depth > 0) depth--; + continue; + } + if (depth > 0) continue; + + const sectionMatch = /^==\s*(\S+)\s*$/.exec(text); + if (!sectionMatch) continue; + + const name = sectionMatch[1]; + if (!isRuntimeName(name)) { + throw new Error(`${logPrefix} unknown runtime "${name}" -- expected one of ${RUNTIMES.join(', ')}.`); + } + if (sections.some((section) => section.runtime === name)) { + throw new Error(`${logPrefix} runtime "${name}" appears twice in the same block.`); + } + if (sections.length) sections[sections.length - 1].end = line; + sections.push({ runtime: name, start: line + 1, end: bodyEnd }); + } + + // Keep the tab order canonical (Node.js first) regardless of authoring order. + return sections.sort((a, b) => RUNTIMES.indexOf(a.runtime) - RUNTIMES.indexOf(b.runtime)); +} + +/** + * Headings inside a runtime block would land in the page outline twice, produce + * suffixed anchors (`#install-1`) for the second variant and break scroll-spy on the + * hidden panel. Keep headings outside the block and put only the bodies inside. + */ +function assertNoHeadings(state: StateBlock, bodyStart: number, bodyEnd: number, logPrefix: string): void { + let fence: string | null = null; + + for (let line = bodyStart; line < bodyEnd; line++) { + const text = lineText(state, line).trim(); + + if (fence) { + if (text.startsWith(fence)) fence = null; + continue; + } + const fenceMatch = /^(`{3,}|~{3,})/.exec(text); + if (fenceMatch) { + fence = fenceMatch[1]; + continue; + } + if (/^#{1,6}\s/.test(text)) { + throw new Error( + `${logPrefix} headings are not allowed inside "::: ${NAME}" (found "${text}").\n` + + 'Keep the heading above the block and put only the runtime-specific body inside it.', + ); + } + } +} + +function openTag(state: StateBlock, html: string): void { + const token = state.push('runtime_html', '', 0); + token.content = html; + token.block = true; + token.markup = ''; +} + +function renderSections(state: StateBlock, sections: Section[], grouped: boolean): void { + if (grouped) { + const tabs = sections + .map( + ({ runtime }) => + ``, + ) + .join(''); + openTag(state, `
${tabs}
`); + } + + for (const section of sections) { + const { runtime, start, end } = section; + const label = grouped ? '' : `
${RUNTIME_LABELS[runtime]}
`; + const className = grouped ? 'runtime-panel' : 'runtime-panel runtime-only'; + openTag(state, `
${label}`); + state.md.block.tokenize(state, start, end); + openTag(state, '
'); + } + + if (grouped) openTag(state, '
'); +} + +export function runtimeContainerPlugin(md: MarkdownItLike): void { + md.block.ruler.before( + 'fence', + `container_${NAME}`, + (state, startLine, endLine, silent) => { + // Four-space indent means "code block", not a container. + if (state.sCount[startLine] - state.blkIndent >= 4) return false; + + const openText = lineText(state, startLine); + const openLen = markerLength(openText); + if (openLen < MIN_MARKER_LEN) return false; + + const params = openText.slice(openLen).trim(); + const [name, ...rest] = params.split(/\s+/); + if (name !== NAME) return false; + + if (silent) return true; + + const explicit = rest[0]; + if (explicit !== undefined && !isRuntimeName(explicit)) { + throw new Error( + `[runtime-container] ${state.env?.relativePath ?? ''}:${startLine + 1}: ` + + `unknown runtime "${explicit}" -- expected one of ${RUNTIMES.join(', ')}.`, + ); + } + + // Find the matching closing marker, honouring nested containers. + let nextLine = startLine; + let depth = 1; + while (++nextLine < endLine) { + const text = lineText(state, nextLine); + if (state.sCount[nextLine] - state.blkIndent >= 4) continue; + const len = markerLength(text.trimStart()); + if (len < openLen) continue; + if (text.trimStart().slice(len).trim()) depth++; + else if (--depth === 0) break; + } + + const file = state.env?.relativePath ?? ''; + const logPrefix = `[runtime-container] ${file}:${startLine + 1}:`; + const bodyEnd = Math.min(nextLine, endLine); + assertNoHeadings(state, startLine + 1, bodyEnd, logPrefix); + const sections = explicit + ? [{ runtime: explicit, start: startLine + 1, end: bodyEnd }] + : splitSections(state, startLine + 1, bodyEnd, logPrefix); + + if (!sections.length) { + console.warn( + `${logPrefix} "::: ${NAME}" has no "== node" / "== bun" sections -- rendering as ${DEFAULT_RUNTIME}.`, + ); + sections.push({ runtime: DEFAULT_RUNTIME, start: startLine + 1, end: bodyEnd }); + } + + const oldParent = state.parentType; + const oldLineMax = state.lineMax; + state.parentType = `container_${NAME}`; + state.lineMax = bodyEnd; + + renderSections(state, sections, !explicit && sections.length > 1); + + state.parentType = oldParent; + state.lineMax = oldLineMax; + state.line = bodyEnd + (nextLine < endLine ? 1 : 0); + + return true; + }, + { alt: ['paragraph', 'reference', 'blockquote', 'list'] }, + ); + + // The tokens above carry ready-made HTML. + md.renderer.rules.runtime_html = (tokens, idx) => tokens[idx].content; +} diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 9afa5ae7..3e962f2c 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -1,8 +1,10 @@ diff --git a/.vitepress/theme/RuntimeIcon.vue b/.vitepress/theme/RuntimeIcon.vue new file mode 100644 index 00000000..408ca5b9 --- /dev/null +++ b/.vitepress/theme/RuntimeIcon.vue @@ -0,0 +1,23 @@ + + + diff --git a/.vitepress/theme/RuntimeSwitch.vue b/.vitepress/theme/RuntimeSwitch.vue new file mode 100644 index 00000000..668cbaf8 --- /dev/null +++ b/.vitepress/theme/RuntimeSwitch.vue @@ -0,0 +1,101 @@ + + + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 4597c6a0..eac62ba8 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -101,3 +101,259 @@ .medium-zoom-image--opened { z-index: 999; } + +/* ========================================================================== + Runtime switcher (Node.js | Bun) + + Every runtime variant is server-rendered into the page; only CSS decides which + one is visible, driven by `data-runtime` on . Node.js is the fallback, so + crawlers and readers without JavaScript always get a complete page. + ========================================================================== */ + +html:not([data-runtime='bun']) .runtime-panel[data-runtime-value='bun'], +html[data-runtime='bun'] .runtime-panel[data-runtime-value='node'] { + display: none; +} + +/* --- In-page block: tabs + panels --- */ + +.runtime-group { + margin: 16px 0; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + overflow: hidden; +} + +.runtime-tabs { + display: flex; + gap: 4px; + padding: 0 12px; + background-color: var(--vp-c-bg-soft); + border-bottom: 1px solid var(--vp-c-divider); +} + +.runtime-tab { + padding: 8px 12px; + border-bottom: 1px solid transparent; + color: var(--vp-c-text-2); + font-size: 13px; + font-weight: 500; + line-height: 24px; + white-space: nowrap; + transition: color 0.25s, border-color 0.25s; +} + +.runtime-tab:hover { + color: var(--vp-c-text-1); +} + +html:not([data-runtime='bun']) .runtime-tab[data-runtime-value='node'], +html[data-runtime='bun'] .runtime-tab[data-runtime-value='bun'] { + color: var(--vp-c-brand-1); + border-bottom-color: var(--vp-c-brand-1); +} + +.runtime-panel { + padding: 0 16px; +} + +/* Code blocks sit flush inside the panel, matching VitePress code groups. */ +.runtime-panel > div[class*='language-'] { + margin: 16px -16px; + border-radius: 0; +} + +/* --- Single-runtime block (`::: runtime bun`) --- */ + +.runtime-only { + margin: 16px 0; + padding: 0 16px 1px; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + background-color: var(--vp-c-bg-soft); +} + +.runtime-only-label { + margin: 12px 0 -8px; + color: var(--vp-c-text-2); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; +} + +/* --- Print: no interactivity, so show every variant, labelled --- */ + +@media print { + .runtime-panel[data-runtime-value] { + display: block !important; + } + + .runtime-tabs, + .RuntimeSwitch { + display: none !important; + } + + .runtime-panel::before { + content: 'Runtime: ' attr(data-runtime-value); + display: block; + margin-top: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + } +} + +/* --- Navbar control --- */ + +.RuntimeSwitch { + position: relative; + display: none; +} + +/* + * Below 960px the navigation bar has no room left for another control (it already + * overflows at 768px without it), and VitePress hides the hamburger from 768px up. + * In that band the per-block tabs are the way to switch runtimes. + * + * The control lives in the `nav-bar-content-after` slot -- the only injection point the + * default theme offers -- and is reordered next to the search box, which gives up its + * `flex-grow` so the pair stays on the left instead of drifting towards the menu. + */ +@media (min-width: 960px) { + .VPNavBar .content-body > .search { + order: -2; + flex-grow: 0; + } + + .RuntimeSwitch { + order: -1; + flex-grow: 1; + display: flex; + justify-content: flex-start; + margin: 0 16px 0 12px; + } + + .runtime-switch-menu { + left: 0; + right: auto; + } +} + +.runtime-switch-trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 0 10px; + height: 32px; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + color: var(--vp-c-text-1); + font-size: 13px; + font-weight: 500; + transition: border-color 0.25s, color 0.25s; +} + +.runtime-switch-trigger:hover { + border-color: var(--vp-c-brand-1); + color: var(--vp-c-brand-1); +} + +.runtime-switch-current { + display: none; + align-items: center; + gap: 6px; +} + +html:not([data-runtime='bun']) .runtime-switch-current[data-runtime-value='node'], +html[data-runtime='bun'] .runtime-switch-current[data-runtime-value='bun'] { + display: inline-flex; +} + +.runtime-switch-chevron { + display: inline-flex; + color: var(--vp-c-text-3); +} + +.runtime-switch-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; + width: 240px; + padding: 6px; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + background-color: var(--vp-c-bg-elv); + box-shadow: var(--vp-shadow-3); +} + +.runtime-switch-option, +.runtime-switch-segment { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 10px; + border-radius: 8px; + color: var(--vp-c-text-2); + font-size: 14px; + font-weight: 500; + transition: background-color 0.25s, color 0.25s; +} + +.runtime-switch-option:hover, +.runtime-switch-segment:hover { + background-color: var(--vp-c-default-soft); + color: var(--vp-c-text-1); +} + +html:not([data-runtime='bun']) .runtime-switch-option[data-runtime-value='node'], +html[data-runtime='bun'] .runtime-switch-option[data-runtime-value='bun'], +html:not([data-runtime='bun']) .runtime-switch-segment[data-runtime-value='node'], +html[data-runtime='bun'] .runtime-switch-segment[data-runtime-value='bun'] { + background-color: var(--vp-c-default-soft); + color: var(--vp-c-brand-1); +} + +.runtime-switch-note { + margin: 6px 6px 2px; + padding-top: 8px; + /* The navbar sets `white-space: nowrap` on its content. */ + white-space: normal; + border-top: 1px solid var(--vp-c-divider); + color: var(--vp-c-text-3); + font-size: 11px; + line-height: 1.5; +} + +.runtime-switch-note a { + color: var(--vp-c-text-2); + text-decoration: underline; +} + +/* --- Mobile nav screen --- */ + +.runtime-switch-screen { + display: block; + margin: 0; + padding-top: 24px; + border-top: 1px solid var(--vp-c-divider); +} + +.runtime-switch-caption { + margin-bottom: 8px; + color: var(--vp-c-text-1); + font-size: 14px; + font-weight: 500; +} + +.runtime-switch-segments { + display: flex; + gap: 8px; +} + +.runtime-switch-segment { + justify-content: center; + border: 1px solid var(--vp-c-divider); +} diff --git a/.vitepress/theme/runtime.ts b/.vitepress/theme/runtime.ts new file mode 100644 index 00000000..84fddc57 --- /dev/null +++ b/.vitepress/theme/runtime.ts @@ -0,0 +1,45 @@ +/** + * Shared state for the site-wide runtime switcher (Node.js | Bun). + * + * The selected runtime lives in a single place -- the `data-runtime` attribute on + * `` -- and everything visual is derived from it with CSS. Nothing about the + * rendered content depends on Vue state, so server-rendered HTML and client HTML are + * byte-identical and hydration can never mismatch. + * + * The attribute is set before first paint by the inline `` script defined in + * `.vitepress/config/shared.ts` (the same technique VitePress uses to restore the + * dark-mode class), so a reader who picked Bun never sees a flash of Node content. + */ + +export const RUNTIMES = ['node', 'bun'] as const; + +export type Runtime = (typeof RUNTIMES)[number]; + +/** Shown when nothing is stored yet, and to crawlers and readers without JavaScript. */ +export const DEFAULT_RUNTIME: Runtime = 'node'; + +/** localStorage key. Kept in sync with the inline head script in `config/shared.ts`. */ +export const RUNTIME_STORAGE_KEY = 'connectum-runtime'; + +export const RUNTIME_LABELS: Record = { + node: 'Node.js', + bun: 'Bun', +}; + +export function isRuntime(value: unknown): value is Runtime { + return value === 'node' || value === 'bun'; +} + +/** + * Persist and apply a runtime. Every control and every content block reads the + * attribute through CSS, so nothing else has to be notified. + */ +export function setRuntime(runtime: Runtime): void { + if (typeof document === 'undefined') return; + document.documentElement.dataset.runtime = runtime; + try { + localStorage.setItem(RUNTIME_STORAGE_KEY, runtime); + } catch { + // Private mode / storage disabled -- the choice still applies to this page view. + } +} diff --git a/en/contributing/documentation-style.md b/en/contributing/documentation-style.md index 206e9f9b..9845a820 100644 --- a/en/contributing/documentation-style.md +++ b/en/contributing/documentation-style.md @@ -116,12 +116,62 @@ hand-edit it. ```` - **Admonitions** for callouts: `::: tip`, `::: warning`, `::: danger`, `::: info`. +- **Runtime blocks** for content that differs between Node.js and Bun — see + [Runtime-specific content](#runtime-specific-content). - **Heading levels.** One `#` H1 per page (or the `title:` frontmatter); use `##` / `###` for structure. Keep heading text in sync with the sidebar label. - **Package layer** statements must match the [architecture map](/en/guide/about) (Layer 0 / 1 / 2). Do not state a different layer on a package page than the canonical map. +### Runtime-specific content + +The site has a runtime switcher (Node.js | Bun) in the navigation bar. Its state lives in +`data-runtime` on ``, is persisted in `localStorage`, and can be shared through a +`?runtime=bun` query parameter. Use the `::: runtime` container to write content that +differs between the two runtimes: + +````md +::: runtime +== node +```bash +pnpm add @connectum/core +``` +== bun +```bash +bun add @connectum/core +``` +::: +```` + +A block with a single runtime is written as `::: runtime bun` (or `::: runtime node`) and +is shown only when that runtime is selected — use it for a caveat that has no Node.js +counterpart. Wrap a block that contains another container in four colons +(`:::: runtime` … `::::`). + +Rules: + +- **No headings inside a runtime block.** Both variants are rendered into the page, so a + heading would appear twice in the outline and the second one would get a suffixed + anchor (`#install-1`). Keep the heading above the block and put only the body inside. + The build fails if a heading is found inside a block. +- **Keep both variants in step.** Every grouped block must carry both `== node` and + `== bun`; if the runtimes really do the same thing, use a normal code block instead of + a runtime block. +- **Only for consumer-facing pages.** Contributor documentation (`docs/en/contributing/**`) + always uses pnpm and Node.js — the framework itself is developed on that stack, and a + runtime switch there would be a lie. +- **Do not leave a section empty.** A `::: runtime bun` block must not be the only content + under a heading, or Node.js readers see a heading with nothing under it. +- **Bun content must be verified**, exactly like every other statement in these docs — a + command that was never run under Bun does not go into a `== bun` section. See + [Runtime Compatibility](/en/guide/runtime-compatibility) for the support level. + +Implementation: `.vitepress/plugins/runtimeContainer.ts` (markdown container), +`.vitepress/theme/RuntimeSwitch.vue` (navbar control), `.vitepress/theme/custom.css` +(visibility rules). Every variant is server-rendered and only hidden with CSS, so the +local search index, `llms.txt` and crawlers always see both. + ### Where things go | Content | Location | diff --git a/en/guide/auth/context.md b/en/guide/auth/context.md index bd90f011..7ce34048 100644 --- a/en/guide/auth/context.md +++ b/en/guide/auth/context.md @@ -122,6 +122,11 @@ const token = await createTestJwt({ sub: 'user-1', roles: ['admin'] }); ### Full Test Example +::: runtime bun +Import the runner from `bun:test` instead of `node:test` and run the file with `bun test`. +Everything else is identical -- `node:assert` works under Bun. +::: + ```typescript import { describe, it } from 'node:test'; import assert from 'node:assert'; diff --git a/en/guide/health-checks/protocol.md b/en/guide/health-checks/protocol.md index 466bec9d..f3d32935 100644 --- a/en/guide/health-checks/protocol.md +++ b/en/guide/health-checks/protocol.md @@ -198,6 +198,10 @@ curl http://localhost:5000/healthz Use `createHealthcheckManager()` to create isolated instances for tests or multi-server setups: +::: runtime bun +Import the runner from `bun:test` instead of `node:test` and run the file with `bun test`. +::: + ```typescript import { describe, it } from 'node:test'; import assert from 'node:assert'; diff --git a/en/guide/interceptors/custom.md b/en/guide/interceptors/custom.md index 42cbc34d..93781636 100644 --- a/en/guide/interceptors/custom.md +++ b/en/guide/interceptors/custom.md @@ -240,7 +240,13 @@ function createAuditLogInterceptor(options: { ## Testing Custom Interceptors -Use `node:test`. Create a mock `next` function and invoke the interceptor directly: +Create a mock `next` function and invoke the interceptor directly: + +::: runtime bun +Use `bun test` and import the runner from `bun:test` instead of `node:test`. The rest of +the example is unchanged -- `node:assert` works under Bun, and the Connectum mock helpers +do not depend on `node:test`. +::: ```typescript import { describe, it } from 'node:test'; diff --git a/en/guide/production/docker.md b/en/guide/production/docker.md index 094c8283..2d04dce3 100644 --- a/en/guide/production/docker.md +++ b/en/guide/production/docker.md @@ -13,44 +13,62 @@ A production `Dockerfile` is available in the [car-sharing example](https://gith ## Multi-Stage Dockerfile -### Recommended: `node:25-slim` +### Recommended Layout -Two-stage build: install dependencies in an isolated stage, then copy only production `node_modules` into a slim runtime image with a non-root user and health check. +Two-stage build: install dependencies in an isolated stage, then copy only production `node_modules` into a slim runtime image (`node:25-slim` on Node.js, `oven/bun:1-slim` on Bun) with a non-root user and health check. See [Dockerfile](https://github.com/Connectum-Framework/examples/blob/main/car-sharing/Dockerfile) for the full listing. Key highlights: +::: runtime +== node - **Stage 1 (deps)** -- `pnpm install --frozen-lockfile --prod` for reproducible, minimal dependencies - **Stage 2 (runtime)** -- non-root `node` user, `wget`-based HEALTHCHECK against `/healthz`, native TypeScript via `node src/index.ts` - Environment defaults: `NODE_ENV=production`, `PORT=5000`, `LOG_FORMAT=json`, health and graceful shutdown enabled +== bun +- **Stage 1 (deps)** -- `bun install --frozen-lockfile` for reproducible dependencies +- **Stage 2 (runtime)** -- `oven/bun:1-slim`, HEALTHCHECK against `/healthz`, TypeScript executed directly via `bun run src/index.ts` +- Environment defaults: `NODE_ENV=production`, `PORT=5000`, `LOG_FORMAT=json`, health and graceful shutdown enabled + +The reference `Dockerfile` in the examples repository targets Node.js; the Bun variant +above mirrors it stage for stage. +::: +:::: runtime node ::: tip Base image selection If your own application code is compiled to JavaScript (e.g., via tsup or tsx), you can use any Node.js 22+ base image instead of `node:25-slim`. Use `node:25-slim` only when you want to run your own `.ts` files natively via Node.js type stripping. ::: +:::: -### Alternative Runtimes: Bun and tsx - -You can replace the Node.js `CMD` with Bun or tsx: +### Runtime Command +::: runtime +== node ```dockerfile # Node.js 25+ (native TypeScript for your own .ts files) CMD ["node", "src/index.ts"] -# Bun -CMD ["bun", "src/index.ts"] - # tsx (works on Node.js 22+) CMD ["npx", "tsx", "src/index.ts"] ``` When using **tsx**, you can use any Node.js 22+ base image (e.g., `node:22-slim`, `node:24-slim`) and add `tsx` as a dependency. Since `@connectum/*` packages ship compiled JavaScript, no special loader is needed for any runtime. +== bun +```dockerfile +FROM oven/bun:1-slim AS runtime +CMD ["bun", "run", "src/index.ts"] +``` + +Code generation runs the same way inside the image -- `RUN bunx buf generate`. Since +`@connectum/*` packages ship compiled JavaScript, no loader or register hook is needed. +::: -### Alpine Variant (Smaller Image) +### Alpine Variant (Node.js Images) If you need a smaller image and do not depend on native modules requiring glibc, use the Alpine variant: swap both `FROM node:25-slim` lines in the [Dockerfile](https://github.com/Connectum-Framework/examples/blob/main/car-sharing/Dockerfile) for `node:25-alpine`. Verify the HEALTHCHECK command still resolves on Alpine (its BusyBox `wget` differs from the GNU build); adjust the probe command if needed. -### Image Size Comparison +### Image Size Comparison (Node.js Images) | Base Image | Approximate Size | Use Case | |---|---|---| diff --git a/en/guide/production/transport-matrix.md b/en/guide/production/transport-matrix.md index 8d8c7905..ed89c2d2 100644 --- a/en/guide/production/transport-matrix.md +++ b/en/guide/production/transport-matrix.md @@ -91,22 +91,33 @@ a gateway that downgrades to HTTP/1.1 works for them with no extra setup. ## Runtime support for native gRPC Native gRPC depends on **HTTP/2 response trailers** (`grpc-status`). The -fetch-style `Response` used by Bun, Deno, and Cloudflare Workers carries no -trailers, so those `serve()` APIs **cannot serve native gRPC at all** — they -serve Connect and gRPC-Web (which fold trailers into the body) over HTTP/1.1. +fetch-style `Response` used by `Bun.serve`, `Deno.serve`, and Cloudflare Workers +carries no trailers, so those `serve()` APIs **cannot serve native gRPC at all** — +they serve Connect and gRPC-Web (which fold trailers into the body) over HTTP/1.1. +Connectum does not use them: `createServer()` builds on `node:http2`. | Runtime | Native gRPC server | Connect / gRPC-Web | gRPC + HTTP/1.1 on one plaintext port | |---|---|---|---| | **Node** (`node:http2` — what Connectum uses) | ✅ | ✅ | ❌ — use a sidecar proxy or TLS + ALPN | +| **Bun** (`node:http2` — what Connectum uses) | ✅ | ✅ | ❌ | | **Bun** (`Bun.serve`) | ❌ (no HTTP/2 trailers) | ✅ | ❌ | | **Deno** (`Deno.serve`) | ❌ (no HTTP/2 trailers) | ✅ | ❌ | | **Cloudflare Workers** | ❌ (edge-terminated, no raw ports) | ✅ (Connect / gRPC-Web) | ❌ (n/a) | -**Takeaway:** native gRPC is effectively a **Node** story; **Connect + gRPC-Web -over HTTP/1.1 work on every runtime**. If you develop or deploy on Bun / Deno / -Workers and must expose gRPC, terminate it at a **sidecar proxy** (Envoy / Caddy) -and let the runtime serve Connect / HTTP-1.1 — the proxy owns the protocol -multiplexing the runtime cannot do. +**Takeaway:** the fetch-style `serve()` APIs cannot host native gRPC, but that +does not apply to a Connectum server on Bun: `createServer()` builds on +`node:http2`, whose server side delivers trailers on Bun as well — including +plaintext h2c. **Connect + gRPC-Web over HTTP/1.1 work on every runtime.** If you +deploy on Deno / Workers, or write your own `Bun.serve` handler, and must expose +gRPC, terminate it at a **sidecar proxy** (Envoy / Caddy) and let the runtime +serve Connect / HTTP-1.1 — the proxy owns the protocol multiplexing the runtime +cannot do. + +::: tip Bun client versions +Serving is unaffected on every Bun version, but Bun's `node:http2` **client** only +became usable in **Bun 1.2.6** — see +[Runtime Compatibility](/en/guide/runtime-compatibility#http2-client). +::: ## Startup validation diff --git a/en/guide/quickstart.md b/en/guide/quickstart.md index de5ae38b..c76d4bc5 100644 --- a/en/guide/quickstart.md +++ b/en/guide/quickstart.md @@ -10,23 +10,47 @@ Build a fully functional gRPC/ConnectRPC microservice with health checks, server ## Prerequisites +::: runtime +== node - **Node.js >= 25.2.0** -- native TypeScript via [type stripping](https://nodejs.org/api/typescript.html) - **pnpm >= 11** -- `corepack enable && corepack prepare pnpm@latest --activate` - **buf** -- installed automatically via `@bufbuild/buf` npm package +== bun +- **Bun >= 1.3.6** -- TypeScript runs natively, no loader needed +- **buf** -- installed automatically via `@bufbuild/buf` npm package +::: +:::: runtime node ::: tip Node.js version for consumers This guide uses Node.js 25+ for native `.ts` execution of your own source files. However, `@connectum/*` packages ship **compiled JavaScript**, so if you compile your own code (e.g., with tsx or a build tool), you can run on **Node.js >= 22.13.0**. See [Runtime Support](/en/guide/typescript/runtime-support). ::: +:::: + +::: runtime bun +Bun executes your TypeScript sources directly, and `@connectum/*` packages ship compiled +JavaScript, so nothing else is required. See +[Runtime Compatibility](/en/guide/runtime-compatibility) for the current support level. +::: ## 1. Project Setup +::: runtime +== node ```bash mkdir greeter-service && cd greeter-service pnpm init ``` +== bun +```bash +mkdir greeter-service && cd greeter-service +bun init -y -m # -m keeps it to package.json + tsconfig.json +``` +::: Install dependencies: +::: runtime +== node ```bash # Core framework pnpm add @connectum/core @connectum/healthcheck @connectum/reflection @connectum/interceptors @@ -40,9 +64,26 @@ pnpm add @bufbuild/protovalidate @connectrpc/validate # Dev dependencies (buf + code generation) pnpm add -D typescript @types/node @bufbuild/buf @bufbuild/protoc-gen-es ``` +== bun +```bash +# Core framework +bun add @connectum/core @connectum/healthcheck @connectum/reflection @connectum/interceptors + +# ConnectRPC runtime +bun add @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf + +# Validation (recommended: @connectrpc/validate) +bun add @bufbuild/protovalidate @connectrpc/validate + +# Dev dependencies (buf + code generation) +bun add -d typescript @types/node @bufbuild/buf @bufbuild/protoc-gen-es +``` +::: Configure `package.json`: +::: runtime +== node ```json { "name": "greeter-service", @@ -61,6 +102,25 @@ Configure `package.json`: "engines": { "node": ">=22.13.0" } } ``` +== bun +```json +{ + "name": "greeter-service", + "version": "1.0.0", + "type": "module", + "imports": { + "#gen/*": "./gen/*", + "#*": "./src/*" + }, + "scripts": { + "start": "bun src/index.ts", + "dev": "bun --watch src/index.ts", + "typecheck": "bunx tsc --noEmit", + "build:proto": "buf generate proto" + } +} +``` +::: Create `tsconfig.json` (type checking only -- no compilation): @@ -211,16 +271,20 @@ await server.start(); ## 6. Run & Test +::: runtime +== node ```bash # Node.js 25+ (native TypeScript) pnpm dev -# Bun -bun src/index.ts - # tsx (Node.js 22+) npx tsx src/index.ts ``` +== bun +```bash +bun --watch src/index.ts +``` +::: ### gRPC (grpcurl) @@ -302,9 +366,16 @@ See [Security (TLS)](/en/guide/security) for `keyPath`/`certPath`, mTLS, and pro ## 9. Add Authentication & Authorization +::: runtime +== node ```bash pnpm add @connectum/auth ``` +== bun +```bash +bun add @connectum/auth +``` +::: ```typescript import { createJwtAuthInterceptor, createAuthzInterceptor } from '@connectum/auth'; @@ -333,9 +404,20 @@ See [Auth & Authorization](/en/guide/auth) for HMAC secrets, gateway auth, sessi ## 10. Add Observability +::: runtime +== node ```bash pnpm add @connectum/otel ``` +== bun +```bash +bun add @connectum/otel +``` + +Instrument manually, as shown below: OpenTelemetry auto-instrumentation +(`@opentelemetry/auto-instrumentations-node`) does not load under Bun. See +[Runtime Compatibility](/en/guide/runtime-compatibility#otel). +::: ```typescript import { createOtelInterceptor } from '@connectum/otel'; @@ -400,11 +482,24 @@ See [Interceptors](/en/guide/interceptors) for the full options reference and cu When Reflection is enabled, clients can sync proto types without `.proto` files: +::: runtime +== node ```bash pnpm add -D @connectum/cli npx connectum proto sync --from localhost:5000 --out ./gen --dry-run # discover npx connectum proto sync --from localhost:5000 --out ./gen # generate ``` +== bun +```bash +bun add -d @connectum/cli +npx connectum proto sync --from localhost:5000 --out ./gen --dry-run # discover +npx connectum proto sync --from localhost:5000 --out ./gen # generate +``` + +The CLI itself is exercised on Node.js only -- it reaches the server through the +Node.js gRPC transport -- so run it with `npx` even in a Bun project. The generated +code has no such restriction. +::: ## 14. Call Another Service diff --git a/en/guide/runtime-compatibility.md b/en/guide/runtime-compatibility.md index 4a72d523..40014970 100644 --- a/en/guide/runtime-compatibility.md +++ b/en/guide/runtime-compatibility.md @@ -4,12 +4,20 @@ outline: deep # Runtime Compatibility -Connectum targets **Node.js 22+** as the primary runtime. Bun compatibility is a secondary goal -- most packages work, but some features require workarounds or have known limitations. This page documents the current state of runtime compatibility across all `@connectum/*` packages. +Connectum targets **Node.js 22+** as the primary runtime and is exercised on Bun in CI on +every pull request. This page documents the current state of runtime compatibility across +all `@connectum/*` packages. + +::: tip Runtime switcher +Pages that show runtime-specific commands have a **Node.js | Bun** switch in the +navigation bar (and next to each affected block). This page deliberately shows both +runtimes side by side. +::: ## Compatibility Matrix -| Package | Node.js 22 | Node.js 25 | Bun | -|---------|:----------:|:----------:|:---:| +| Package | Node.js 22 | Node.js 25 | Bun >= 1.2.6 | +|---------|:----------:|:----------:|:------------:| | `@connectum/core` | Yes | Yes | Yes | | `@connectum/interceptors` | Yes | Yes | Yes | | `@connectum/healthcheck` | Yes | Yes | Yes | @@ -21,11 +29,15 @@ Connectum targets **Node.js 22+** as the primary runtime. Bun compatibility is a | `@connectum/events-redis` | Yes | Yes | Yes | | `@connectum/events-amqp` | Yes | Yes | Yes | | `@connectum/otel` | Yes | Yes | Partial | -| `@connectum/cli` | Yes | Yes | Yes | +| `@connectum/cli` | Yes | Yes | Partial | | `@connectum/testing` | Yes | Yes | Partial | **Legend:** Yes = fully supported, Partial = works with limitations (see details below). +**Bun floor:** HTTP/2 client transports require **Bun >= 1.2.6** (see +[HTTP/2 Client Transport](#http2-client)); the examples repository pins Bun >= 1.3.6. +`@connectum/cli` is exercised on Node.js only -- run it with `npx` even in a Bun project. + ## HTTP/2 Client Transport {#http2-client} ### Node.js @@ -50,9 +62,18 @@ This uses Node.js native `node:http2` module for full HTTP/2 multiplexing. ### Bun -Bun does not fully support the `node:http2` client API. Calling `createGrpcTransport()` or `createConnectTransport({ httpVersion: '2' })` from `@connectrpc/connect-node` throws a `TypeError` at runtime. +**Bun >= 1.2.6 needs no special client code** -- the snippet above works unchanged. Unary, +server-streaming and bidi-streaming calls over `createGrpcTransport()` and +`createConnectTransport({ httpVersion: '2' })` all complete, and status codes carried in +HTTP/2 trailers arrive intact. -**Workaround:** use `createConnectTransport()` with `httpVersion: '1.1'` (the default): +Bun's `node:http2` **client** was incomplete before 1.2.6: on those versions the transport +is constructed without error and the **first RPC hangs** -- the call never completes and +no error is thrown. Bun 1.2.6 rewrote the `node:http2` client and closed this. + +::: warning Bun <= 1.2.5 +Upgrade Bun. If you cannot, the only working option is `createConnectTransport()` over +HTTP/1.1 (the default `httpVersion`): ```typescript import { createClient } from '@connectrpc/connect'; @@ -68,12 +89,15 @@ const client = createClient(GreeterService, transport); const res = await client.sayHello({ name: 'Alice' }); ``` -::: warning Limitations on Bun -- **No native gRPC protocol** -- only the Connect protocol (JSON/Protobuf over HTTP/1.1) is available -- **No HTTP/2 multiplexing** -- each request opens a separate connection -- **Server must support Connect protocol** -- Connectum servers support it by default alongside gRPC +That path carries unary and server-streaming calls but **not bidi streaming**, gives up +HTTP/2 multiplexing, and requires the server to accept HTTP/1.1 (`allowHTTP1: true`, the +default). Connectum servers speak the Connect protocol alongside gRPC, so no server change +is needed. ::: +**Servers are unaffected on every Bun version.** A Connectum server -- including plaintext +h2c (`allowHTTP1: false`) -- serves HTTP/2 correctly; the limitation was always client-side. + ## Streaming RPC {#streaming} ### Node.js @@ -100,54 +124,21 @@ for await (const event of client.watchEvents({ filter: 'error' })) { ### Bun -In Bun, streaming RPCs over `createConnectTransport()` from `@connectrpc/connect-node` may fail because the Node.js HTTP adapter does not work correctly in Bun's `node:http` compatibility layer. The solution is to build a transport that uses `globalThis.fetch` directly: - -```typescript -import { createClient } from '@connectrpc/connect'; -import { createTransport } from '@connectrpc/connect/protocol-connect'; -import { createFetchClient } from '@connectrpc/connect/protocol'; -import { MonitorService } from '#gen/monitor/v1/monitor_pb.js'; - -// Use Bun's native fetch implementation -const transport = createTransport({ - baseUrl: 'http://localhost:5000', - fetch: createFetchClient(globalThis.fetch), - useBinaryFormat: true, -}); - -const client = createClient(MonitorService, transport); - -// Server streaming -- uses Bun's native fetch with ReadableStream -for await (const event of client.watchEvents({ filter: 'error' })) { - console.log(`Event: ${event.type} -- ${event.message}`); -} -``` - -::: tip Cross-Runtime Helper -You can create a helper function that selects the right transport based on the runtime: - -```typescript -import { createConnectTransport } from '@connectrpc/connect-node'; - -function createClientTransport(baseUrl: string) { - // Bun sets globalThis.Bun - if (typeof globalThis.Bun !== 'undefined') { - // Dynamic import to avoid loading connect/protocol-connect on Node.js - const { createTransport } = await import('@connectrpc/connect/protocol-connect'); - const { createFetchClient } = await import('@connectrpc/connect/protocol'); - return createTransport({ - baseUrl, - fetch: createFetchClient(globalThis.fetch), - useBinaryFormat: true, - }); - } - - return createConnectTransport({ - baseUrl, - httpVersion: '2', - }); -} -``` +The same code runs on Bun -- **no runtime branching is needed**, and Connectum itself +contains none. + +- **Server streaming** works on every Bun version tested, over both HTTP/2 and HTTP/1.1 + transports. +- **Bidi streaming** requires HTTP/2, and therefore Bun >= 1.2.6 for the client. This is a + protocol constraint, not a Bun one: bidi streaming is impossible over HTTP/1.1 on any + runtime, and Connectum refuses to start a server that hosts bidi methods on plaintext + HTTP/1.1 (`CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT`). + +::: warning Do not hand-build a fetch transport +Earlier revisions of this page suggested `createTransport()` from +`@connectrpc/connect/protocol-connect` together with `createFetchClient(globalThis.fetch)`. +Do not use that pattern: `createTransport` is marked internal by ConnectRPC and is not +covered by semantic versioning, and on Bun 1.1.x it silently drops the request body. ::: ## Testing Utilities {#testing} @@ -169,6 +160,10 @@ describe('my interceptor', () => { `createMockFn()` is API-compatible with the subset of `node:test`'s `mock.fn()` that the testing helpers rely on (`.mock.calls`, `.mock.callCount()`), so assertions written against one runtime work on the other. Full API: [@connectum/testing](/en/packages/testing). +The package is marked **Partial** on Bun for one reason: the `@connectum/testing/parity` +subpath registers a `node:test` test (`transportParityTest`) and therefore runs on Node.js +only. The main entry point has no such dependency. + ## OpenTelemetry {#otel} `@connectum/otel` depends on the official `@opentelemetry/*` SDK packages, which use `node:perf_hooks`, `node:diagnostics_channel`, and other Node.js-specific APIs. @@ -188,10 +183,10 @@ If you use `@connectum/otel` on Bun, test your specific instrumentation setup th | Issue | Runtime | Status | Workaround | |-------|---------|--------|------------| -| `createGrpcTransport()` throws TypeError | Bun | Open (Bun) | Use `createConnectTransport()` with HTTP/1.1 | -| `createConnectTransport({ httpVersion: '2' })` throws TypeError | Bun | Open (Bun) | Omit `httpVersion` or set to `'1.1'` | -| Streaming RPC fails with Node.js HTTP adapter | Bun | Open (Bun) | Use `createTransport` + `createFetchClient(globalThis.fetch)` | +| HTTP/2 client transports (`createGrpcTransport()`, `createConnectTransport({ httpVersion: '2' })`) hang on the first RPC | Bun <= 1.2.5 | **Fixed in Bun 1.2.6** | Upgrade Bun; on older Bun use `createConnectTransport()` over HTTP/1.1 (no bidi) | | `node:test` mock API unavailable | Bun | By design | Use `bun:test` mock directly | +| `@connectum/testing/parity` requires `node:test` | Bun | By design | Use the main entry point; run parity tests on Node.js | +| `@connectum/cli` is exercised on Node.js only | Bun | Open | Run the CLI with `npx`; generated code is unaffected | | OpenTelemetry auto-instrumentation | Bun | Open (OTel) | Use manual instrumentation | ## Related diff --git a/en/guide/typescript/patterns.md b/en/guide/typescript/patterns.md index 78543d28..95f54ecd 100644 --- a/en/guide/typescript/patterns.md +++ b/en/guide/typescript/patterns.md @@ -94,6 +94,8 @@ console.log(`Listening on port ${port}`); Run type checking as a separate step (not compilation): +::: runtime +== node ```bash # Check types pnpm typecheck # or: tsc --noEmit @@ -101,16 +103,24 @@ pnpm typecheck # or: tsc --noEmit # Watch mode for development tsc --noEmit --watch ``` +== bun +```bash +# Check types +bun run typecheck # or: bunx tsc --noEmit + +# Watch mode for development +bunx tsc --noEmit --watch +``` +::: ## Development Workflow +::: runtime +== node ```bash # Node.js 25+: start with auto-reload (watches for file changes) node --watch src/index.ts -# Bun: start with auto-reload -bun --watch src/index.ts - # tsx: start with auto-reload (Node.js 22+) tsx --watch src/index.ts @@ -120,20 +130,43 @@ tsc --noEmit --watch # Or run once pnpm typecheck && pnpm start ``` +== bun +```bash +# Start with auto-reload (watches for file changes) +bun --watch src/index.ts + +# Type check in a separate terminal +bunx tsc --noEmit --watch + +# Or run once +bun run typecheck && bun run start +``` +::: ## Checklist Before running your Connectum service, verify: -- [ ] Node.js 25+ installed (`node --version`), Bun installed (`bun --version`), or tsx installed (`npx tsx --version`) - [ ] `"type": "module"` in `package.json` -- [ ] `erasableSyntaxOnly: true` in `tsconfig.json` - [ ] `verbatimModuleSyntax: true` in `tsconfig.json` -- [ ] No `enum` in application code (use `const` objects) - [ ] `import type` for all type-only imports -- [ ] `.ts` extensions in relative imports - [ ] `node:` prefix for built-in modules -- [ ] Proto enums handled via two-step generation (if applicable, Node.js only; not needed for Bun or tsx) + +::: runtime +== node +- [ ] Node.js 25+ installed (`node --version`), or tsx installed (`npx tsx --version`) +- [ ] `erasableSyntaxOnly: true` in `tsconfig.json` +- [ ] No `enum` in application code (use `const` objects) -- type stripping cannot execute it +- [ ] `.ts` extensions in relative imports +- [ ] Proto enums handled via [two-step generation](/en/guide/typescript/proto-enums) (if applicable; not needed with tsx) +== bun +- [ ] Bun installed (`bun --version`) +- [ ] `.ts` extensions in relative imports (optional for Bun, but keeps the code portable to Node.js) + +Bun transpiles TypeScript instead of stripping types, so `enum`, `namespace` and +parameter properties execute as written and proto enums need no extra generation step. +Keep to the erasable subset anyway if the same code has to run on Node.js. +::: ## Related diff --git a/en/packages/auth.md b/en/packages/auth.md index 9b479640..68f4ad7c 100644 --- a/en/packages/auth.md +++ b/en/packages/auth.md @@ -24,9 +24,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/auth/) ## Installation +::: runtime +== node ```bash pnpm add @connectum/auth ``` +== bun +```bash +bun add @connectum/auth +``` +::: **Requires**: Node.js 22+ diff --git a/en/packages/cli.md b/en/packages/cli.md index 8ef72f95..3d4d1bd5 100644 --- a/en/packages/cli.md +++ b/en/packages/cli.md @@ -20,9 +20,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/cli/) ## Installation +::: runtime +== node ```bash pnpm add -D @connectum/cli ``` +== bun +```bash +bun add -d @connectum/cli +``` +::: **Requires**: Node.js 22+, `buf` CLI available on PATH diff --git a/en/packages/core.md b/en/packages/core.md index e4b79860..34cebb70 100644 --- a/en/packages/core.md +++ b/en/packages/core.md @@ -23,9 +23,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/core/) ## Installation +::: runtime +== node ```bash pnpm add @connectum/core ``` +== bun +```bash +bun add @connectum/core +``` +::: **Requires**: Node.js >= 22.13.0 (packages ship compiled `.js` + `.d.ts` + source maps) diff --git a/en/packages/events-amqp.md b/en/packages/events-amqp.md index 2225f0fb..e63db0c3 100644 --- a/en/packages/events-amqp.md +++ b/en/packages/events-amqp.md @@ -21,9 +21,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/events ## Installation +::: runtime +== node ```bash pnpm add @connectum/events-amqp ``` +== bun +```bash +bun add @connectum/events-amqp +``` +::: **Peer dependency**: `@connectum/events` diff --git a/en/packages/events-kafka.md b/en/packages/events-kafka.md index 79482175..8dc02ca8 100644 --- a/en/packages/events-kafka.md +++ b/en/packages/events-kafka.md @@ -21,9 +21,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/events ## Installation +::: runtime +== node ```bash pnpm add @connectum/events-kafka ``` +== bun +```bash +bun add @connectum/events-kafka +``` +::: **Peer dependency**: `@connectum/events` diff --git a/en/packages/events-nats.md b/en/packages/events-nats.md index 1264b9f3..517fdd84 100644 --- a/en/packages/events-nats.md +++ b/en/packages/events-nats.md @@ -21,9 +21,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/events ## Installation +::: runtime +== node ```bash pnpm add @connectum/events-nats ``` +== bun +```bash +bun add @connectum/events-nats +``` +::: **Peer dependency**: `@connectum/events` diff --git a/en/packages/events-redis.md b/en/packages/events-redis.md index dd803098..d472c689 100644 --- a/en/packages/events-redis.md +++ b/en/packages/events-redis.md @@ -21,9 +21,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/events ## Installation +::: runtime +== node ```bash pnpm add @connectum/events-redis ``` +== bun +```bash +bun add @connectum/events-redis +``` +::: **Peer dependency**: `@connectum/events` diff --git a/en/packages/events.md b/en/packages/events.md index a56bfc16..9b5b170b 100644 --- a/en/packages/events.md +++ b/en/packages/events.md @@ -23,14 +23,23 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/events ## Installation +::: runtime +== node ```bash pnpm add @connectum/events ``` +== bun +```bash +bun add @connectum/events +``` +::: **Peer dependency**: `@connectum/core` You also need at least one adapter package for production use: +::: runtime +== node ```bash # Choose one (or more) broker adapters: pnpm add @connectum/events-nats # NATS JetStream @@ -38,6 +47,15 @@ pnpm add @connectum/events-kafka # Kafka / Redpanda pnpm add @connectum/events-redis # Redis Streams / Valkey pnpm add @connectum/events-amqp # AMQP / RabbitMQ ``` +== bun +```bash +# Choose one (or more) broker adapters: +bun add @connectum/events-nats # NATS JetStream +bun add @connectum/events-kafka # Kafka / Redpanda +bun add @connectum/events-redis # Redis Streams / Valkey +bun add @connectum/events-amqp # AMQP / RabbitMQ +``` +::: The built-in `MemoryAdapter` is included in `@connectum/events` for testing. diff --git a/en/packages/healthcheck.md b/en/packages/healthcheck.md index b29c66f7..ac68393d 100644 --- a/en/packages/healthcheck.md +++ b/en/packages/healthcheck.md @@ -21,9 +21,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/health ## Installation +::: runtime +== node ```bash pnpm add @connectum/healthcheck ``` +== bun +```bash +bun add @connectum/healthcheck +``` +::: **Peer dependency**: `@connectum/core` diff --git a/en/packages/interceptors.md b/en/packages/interceptors.md index c3dcd126..03f86f2e 100644 --- a/en/packages/interceptors.md +++ b/en/packages/interceptors.md @@ -22,9 +22,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/interc ## Installation +::: runtime +== node ```bash pnpm add @connectum/interceptors ``` +== bun +```bash +bun add @connectum/interceptors +``` +::: **Requires**: Node.js 22+ diff --git a/en/packages/otel.md b/en/packages/otel.md index 00688e41..0558eac8 100644 --- a/en/packages/otel.md +++ b/en/packages/otel.md @@ -25,9 +25,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/otel/) ## Installation +::: runtime +== node ```bash pnpm add @connectum/otel ``` +== bun +```bash +bun add @connectum/otel +``` +::: **Requires**: Node.js 22+ diff --git a/en/packages/protoc-gen-catalog.md b/en/packages/protoc-gen-catalog.md index 4b9387d8..dfaa2f51 100644 --- a/en/packages/protoc-gen-catalog.md +++ b/en/packages/protoc-gen-catalog.md @@ -18,9 +18,16 @@ proto files. The generated `catalog.gen.ts` is what makes `ctx.call`, ## Installation +::: runtime +== node ```bash pnpm add -D @connectum/protoc-gen-catalog ``` +== bun +```bash +bun add -d @connectum/protoc-gen-catalog +``` +::: **Requires**: Node.js 22.13+ diff --git a/en/packages/reflection.md b/en/packages/reflection.md index 5db06d49..1eb04a79 100644 --- a/en/packages/reflection.md +++ b/en/packages/reflection.md @@ -20,9 +20,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/reflec ## Installation +::: runtime +== node ```bash pnpm add @connectum/reflection ``` +== bun +```bash +bun add @connectum/reflection +``` +::: **Peer dependency**: `@connectum/core` diff --git a/en/packages/test-fixtures.md b/en/packages/test-fixtures.md index 606d82a1..65771c78 100644 --- a/en/packages/test-fixtures.md +++ b/en/packages/test-fixtures.md @@ -26,9 +26,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/test-f ## Installation +::: runtime +== node ```bash pnpm add -D @connectum/test-fixtures ``` +== bun +```bash +bun add -d @connectum/test-fixtures +``` +::: **Requires**: Node.js 22.13+ diff --git a/en/packages/testing.md b/en/packages/testing.md index 658a6f98..f1fc3b50 100644 --- a/en/packages/testing.md +++ b/en/packages/testing.md @@ -20,9 +20,16 @@ Complete TypeScript API documentation: [API Reference](/en/api/@connectum/testin ## Installation +::: runtime +== node ```bash pnpm add -D @connectum/testing ``` +== bun +```bash +bun add -d @connectum/testing +``` +::: **Requires**: Node.js 22+ @@ -32,6 +39,12 @@ pnpm add -D @connectum/testing A typical interceptor unit test using @connectum/testing utilities: +::: runtime bun +Import the runner from `bun:test` instead of `node:test` and run the file with `bun test`. +The helpers themselves are runner-agnostic; only the `@connectum/testing/parity` subpath +requires `node:test`. +::: + ```typescript import assert from 'node:assert'; import { describe, it } from 'node:test'; @@ -126,7 +139,7 @@ const req = createMockRequest({ stream: true, message: createMockStream([{ id: ' #### `createMockNext(options?)` -Creates a mock `next` handler returning a successful response. Returns a `mock.fn()` spy from `node:test`. +Creates a mock `next` handler returning a successful response. Returns a portable spy that mirrors the `node:test` `mock.fn()` surface (`.mock.calls`, `.mock.callCount()`) without importing `node:test`, so the same test code runs on Node.js and Bun. ```typescript function createMockNext(options?: MockNextOptions): MockFunction;