Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/defaults/development.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const config = {
swagger: {
enable: true,
},
docs: {
// Grouping primitive for guide sections. See `public.docs.tree.js` JSDoc for full schema details.
guideSections: [
{ title: 'Get Started', prefixMin: 0, prefixMax: 9 },
],
},
api: {
protocol: 'http',
port: 3000,
Expand Down
20 changes: 9 additions & 11 deletions lib/helpers/guides.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/**
* Markdown guide loader for the Redoc API reference.
* Markdown guide loader for the OpenAPI reference.
*
* Per-module markdown guides live under `modules/{name}/doc/guides/*.md`
* and are discovered by the same globbing mechanism as OpenAPI YAML files
* (see `config/assets.js` → `allGuides`).
*
* Guides are merged into the OpenAPI spec via `info.description`, which
* Redoc renders as a top-level "Introduction" section in the sidebar and
* splits on markdown H1/H2 headings.
* Guides are merged into the OpenAPI spec via `info.description`, which an
* OpenAPI viewer renders as a top-level "Introduction" section, split on
* markdown H1/H2 headings.
*
* When `mergeGuidesIntoSpec` is called with a `{ sections }` option, guides
* are grouped under H1 section dividers (with each guide rendered as H2).
* Redoc auto-nests H2 entries under their parent H1 in the sidebar, giving
* the 5-section IA structure instead of a flat list.
* are grouped under H1 section dividers (with each guide rendered as H2),
* giving a sectioned IA structure instead of a flat list.
*/
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -105,17 +104,16 @@ const prefixFromPath = (filePath) => {
* Merge loaded guides into an OpenAPI spec's `info.description`.
*
* **Flat mode (default)** — each guide becomes a top-level H1 section.
* Redoc renders each H1 as a sidebar entry.
* An OpenAPI viewer renders each H1 as a sidebar entry.
*
* **Sectioned mode** — when `options.sections` is provided, guides are
* grouped under H1 dividers (one per section) with each guide rendered as
* H2. Redoc auto-nests H2 entries under their parent H1 in the sidebar,
* giving a compact 5-section IA instead of a flat list of 18 guides.
* H2, giving a compact sectioned IA instead of a flat list.
* Guides whose filename prefix does not fall in any section range are
* appended at the end as H2 (never silently dropped).
*
* The original spec is mutated (and returned) to match the merge style used
* by `initSwagger` in `lib/services/express.js`.
* by `initApiSpec` in `lib/services/express.js`.
*
* @param {object} spec - OpenAPI spec object (will be mutated).
* @param {{ title: string, body: string, path?: string }[]} guides - Loaded guide entries.
Expand Down
74 changes: 10 additions & 64 deletions lib/services/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import cors from 'cors';
import morgan from 'morgan';
import fs from 'fs';
import YAML from 'js-yaml';
import redoc from 'redoc-express';

import config from '../../config/index.js';
import configHelper from '../helpers/config.js';
Expand All @@ -41,44 +40,14 @@ export const computeOpenApiServerUrl = (domain) => {
};

/**
* Default Redoc theme — Inter + JetBrains Mono, tighter sidebar, refined right panel.
* No hardcoded brand color. Downstream projects override via config.docs.redocTheme
* (deep-merged, zero devkit edits required).
*/
const defaultRedocTheme = {
typography: {
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
headings: { fontFamily: '"Inter", -apple-system, sans-serif', fontWeight: '600' },
code: { fontFamily: '"JetBrains Mono", Menlo, Consolas, monospace' },
},
sidebar: {
width: '260px',
textTransform: 'none',
},
rightPanel: { backgroundColor: '#1a1a1a' },
spacing: { unit: 4 },
};

/**
* Custom CSS injected into the Redoc UI to cover what the theme schema cannot.
* ≤ 30 lines.
*/
const redocCustomCss = `
.menu-content { letter-spacing: 0; }
.menu-content label, .menu-content .operation-type { text-transform: none !important; }
pre, code { font-feature-settings: "calt" 0, "liga" 0; }
.api-content blockquote { border-left: 3px solid #888; padding-left: 12px; color: #555; }
`.trim();

/**
* Initialize API documentation (Redoc UI + JSON spec endpoint)
* Initialize API documentation (OpenAPI JSON spec endpoint)
* @param {object} app - express application instance
* @returns {void}
*/
const initSwagger = (app) => {
// Secure-by-default: the API docs surface (/api/spec.json + /api/docs) is
// UNAUTHENTICATED, so it is only mounted in dev-grade envs. Any production-grade
// env (the literal `production` OR a deployment env label) skips it even when
const initApiSpec = (app) => {
// Secure-by-default: the API spec surface (/api/spec.json) is UNAUTHENTICATED,
// so it is only mounted in dev-grade envs. Any production-grade env (the
// literal `production` OR a deployment env label) skips it even when
// config.swagger.enable is still truthy — opt-OUT by default in non-dev.
if (config.swagger.enable && !configHelper.isProd()) {
if (!config.files.swagger || config.files.swagger.length === 0) {
Expand Down Expand Up @@ -136,8 +105,8 @@ const initSwagger = (app) => {
};
spec.servers = [{ url: computeOpenApiServerUrl(config.domain) }];

// Merge per-module markdown guides into info.description so Redoc
// renders them in its sidebar alongside the OpenAPI reference.
// Merge per-module markdown guides into info.description so an OpenAPI
// viewer renders them alongside the reference.
const guides = guidesHelper.loadGuides(config.files.guides || []);
guidesHelper.mergeGuidesIntoSpec(spec, guides);
if (guides.length > 0) {
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Expand All @@ -156,29 +125,6 @@ const initSwagger = (app) => {

// Serve the merged spec as JSON
app.get('/api/spec.json', serveSpec);

// Deep-merge devkit default theme with optional per-project override from
// config.docs.redocTheme — downstream projects need zero devkit edits.
const theme = _.merge({}, defaultRedocTheme, (config.docs && config.docs.redocTheme) || {});

// Mount Redoc API reference UI — consumes the spec via URL (not inline).
// Equivalents for the previous Scalar `hideModels` behavior: hide the
// download button and schema titles, and expand common success responses
// so the reference feels compact and consumer-focused.
app.get(
'/api/docs',
redoc({
title: config.app.title,
specUrl: '/api/spec.json',
redocOptions: {
hideDownloadButton: true,
hideSchemaTitles: true,
expandResponses: '200,201',
theme,
customCss: redocCustomCss,
},
}),
);
}
};

Expand Down Expand Up @@ -371,8 +317,8 @@ const initErrorRoutes = (app) => {
const init = async () => {
// Initialize express app
const app = express();
// Initialize modules swagger doc
initSwagger(app);
// Initialize the OpenAPI JSON spec endpoint
initApiSpec(app);
// Initialize local variables
initLocalVariables(app);
// Assign a unique request ID before any route registration
Expand Down Expand Up @@ -415,7 +361,7 @@ const init = async () => {
};

export default {
initSwagger,
initApiSpec,
initLocalVariables,
initPreParserRoutes,
initMiddleware,
Expand Down
Loading
Loading