Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
76aa567
docs: add graph command design spec
kanoru3101 Jun 11, 2026
4901e9d
docs: add graph command implementation plan
kanoru3101 Jun 11, 2026
159d367
feat: add dependency graph builder for graph command
kanoru3101 Jun 11, 2026
ec61bfc
docs: sync graph plan with review fixes (slash, ResolveError, typeche…
kanoru3101 Jun 11, 2026
66e168e
feat: add affected-files filter for graph command
kanoru3101 Jun 11, 2026
f0f6022
feat: add graph command output renderers
kanoru3101 Jun 11, 2026
32421fc
docs: reconcile mermaid label escaping in graph spec with implementation
kanoru3101 Jun 11, 2026
f5d3fe3
feat: register graph command in CLI
kanoru3101 Jun 11, 2026
0db7f34
docs: document repeated --affected-by flag syntax in graph spec and plan
kanoru3101 Jun 11, 2026
0bd9972
test: add graph command e2e tests
kanoru3101 Jun 11, 2026
fd71521
docs: document graph command and add changeset
kanoru3101 Jun 11, 2026
fa6d868
docs: align spec warning wording with implementation
kanoru3101 Jun 11, 2026
cb940b7
docs: add tree command rework spec and plan
kanoru3101 Jun 12, 2026
873b7b7
refactor: rename graph command to tree
kanoru3101 Jun 12, 2026
759df76
feat: add pointer-to-node mapping for the tree structure view
kanoru3101 Jun 12, 2026
38a12d4
feat: add internal-structure builder for the tree command
kanoru3101 Jun 12, 2026
9e7c47d
refactor: make stylish summary and empty message caller-provided
kanoru3101 Jun 12, 2026
5013774
feat: match affected-by inputs against tree nodes
kanoru3101 Jun 12, 2026
b20163b
feat: make document structure the default tree view behind --files fa…
kanoru3101 Jun 12, 2026
bcdebfa
test: cover tree structure mode end to end
kanoru3101 Jun 12, 2026
0fb8560
docs: document the tree command and update the changeset
kanoru3101 Jun 12, 2026
8f2e055
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 12, 2026
dcf6a67
fix: hoist entity test imports to avoid per-test transform timeout
kanoru3101 Jun 12, 2026
ff3ad50
docs: unbreak documentation tests for the tree command
kanoru3101 Jun 15, 2026
2b0b60d
refactor(tree): share toNodeId/byString/OPERATION_METHODS via node-id
kanoru3101 Jun 15, 2026
e12ebba
fix: improvments
kanoru3101 Jun 15, 2026
a71529c
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 17, 2026
d925da9
refactor: tidy tree renderer sort and trim a restating comment
kanoru3101 Jun 17, 2026
6607ffc
test: drop redundant tree tests (runtime round-trip, path.resolve var…
kanoru3101 Jun 17, 2026
755baa3
docs: clarify --affected-by per mode and JSON field scope in tree docs
kanoru3101 Jun 17, 2026
f87eb4d
test: cover multi-API rejection in default view and multi-API --files…
kanoru3101 Jun 17, 2026
6348a8e
fix: tidy tree command — share mode context, dedupe location mapping,…
kanoru3101 Jun 17, 2026
b46857f
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 17, 2026
7efffd9
fix: remove comments
kanoru3101 Jun 17, 2026
655dc5e
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 22, 2026
3e690f6
fix: refactoring
kanoru3101 Jun 22, 2026
e411931
fix: resolve bug with splitted files
kanoru3101 Jun 22, 2026
3273609
fix: update tests
kanoru3101 Jun 22, 2026
d6e78f8
Apply suggestions from code review
JLekawa Jun 23, 2026
fb2f569
Update tree.md
JLekawa Jun 23, 2026
7aa745b
Update docs/@v2/commands/tree.md
JLekawa Jun 23, 2026
94256c8
docs(cli): fix issues in file
JLekawa Jun 23, 2026
9c02500
Apply suggestion from @JLekawa
JLekawa Jun 23, 2026
c460414
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 23, 2026
f8f3f83
fix: add spaces in the options-table separator
kanoru3101 Jun 23, 2026
1df3366
fix: refactoring and update snapshots
kanoru3101 Jun 24, 2026
3738f2a
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 24, 2026
820d75e
fix: update docs
kanoru3101 Jun 24, 2026
8917cf0
Merge branches 'feat/graph-command' and 'feat/graph-command' of githu…
kanoru3101 Jun 24, 2026
1f161b5
fix: resolve cursor bugs
kanoru3101 Jun 24, 2026
e111d75
fix: update docs
kanoru3101 Jun 24, 2026
8bef8af
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 25, 2026
dce5f2f
chore: small fixes
kanoru3101 Jun 26, 2026
4cdd016
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 1, 2026
64fb2dd
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 2, 2026
dda77cf
feat: add level flag
kanoru3101 Jul 13, 2026
96a6cbf
test: add snapshot
kanoru3101 Jul 13, 2026
a5bfe1f
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 13, 2026
40ea0d7
feat(tree): show operationId on operation nodes
kanoru3101 Jul 16, 2026
d7c39f7
docs(tree): fix options table having a stray fourth column
kanoru3101 Jul 17, 2026
2bfcd8c
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 17, 2026
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
5 changes: 5 additions & 0 deletions .changeset/graph-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/cli': minor
---

Added the `tree` command that displays the structure of an API description — its paths, operations, and component dependency chains.
1 change: 1 addition & 0 deletions docs/@v2/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ API management commands:
- [`score`](score.md) Score an API for integration simplicity and AI agent readiness.
- [`split`](split.md) Split API description into a multi-file structure.
- [`stats`](stats.md) Gather statistics for a document.
- [`tree`](tree.md) Display the structure of an API description as a tree.

Linting commands:

Expand Down
493 changes: 493 additions & 0 deletions docs/@v2/commands/tree.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
page: commands/stats.md
- label: translate
page: commands/translate.md
- label: tree
page: commands/tree.md
- group: Guides
page: guides/index.md
items:
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ export async function handleLintConfig(argv: Exact<CommandArgv>, version: string
return;
}

if (argv.format === 'json' || argv.format === 'junit' || argv.format === 'checkstyle') {
if (
argv.format === 'json' ||
argv.format === 'junit' ||
argv.format === 'checkstyle' ||
argv.format === 'mermaid' ||
argv.format === 'dot'
) {
// these are single-document formats, so a separate config-lint document would break the output
return;
}
Expand Down
163 changes: 163 additions & 0 deletions packages/cli/src/commands/tree/__tests__/build-graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ResolveError, Source, type Document, type ResolvedRefMap } from '@redocly/openapi-core';
import * as path from 'node:path';

import { buildGraph } from '../build-graph.js';

const CWD = '/project';

function makeDocument(absoluteRef: string): Document {
return { source: new Source(absoluteRef, ''), parsed: {} };
}

function resolvedEntry(targetAbsoluteRef: string, isRemote = true) {
return {
resolved: true as const,
isRemote,
node: {},
nodePointer: '#/',
document: makeDocument(targetAbsoluteRef),
};
}

const resolveRef = (base: string, uri: string) => path.resolve(path.dirname(base), uri);

describe('buildGraph', () => {
it('builds nodes and edges from cross-file refs, transitively', () => {
const refMap: ResolvedRefMap = new Map([
['/project/openapi.yaml::paths/users.yaml', resolvedEntry('/project/paths/users.yaml')],
[
'/project/paths/users.yaml::../components/User.yaml',
resolvedEntry('/project/components/User.yaml'),
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph).toEqual({
roots: ['openapi.yaml'],
nodes: [
{ id: 'components/User.yaml', resolved: true },
{ id: 'openapi.yaml', root: true, resolved: true },
{ id: 'paths/users.yaml', resolved: true },
],
edges: [
{ from: 'openapi.yaml', to: 'paths/users.yaml', refs: ['paths/users.yaml'] },
{
from: 'paths/users.yaml',
to: 'components/User.yaml',
refs: ['../components/User.yaml'],
},
],
});
});

it('skips same-file refs', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::#/components/schemas/Pet',
{ ...resolvedEntry('/project/openapi.yaml'), isRemote: false },
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([{ id: 'openapi.yaml', root: true, resolved: true }]);
expect(graph.edges).toEqual([]);
});

it('dedupes edges across refs and across roots, collecting distinct sorted refs', () => {
const entryY = resolvedEntry('/project/b.yaml');
const entryX = resolvedEntry('/project/b.yaml');
const refMapA: ResolvedRefMap = new Map([
['/project/a.yaml::b.yaml#/Y', entryY],
['/project/a.yaml::b.yaml#/X', entryX],
]);
const refMapB: ResolvedRefMap = new Map([['/project/a.yaml::b.yaml#/X', entryX]]);

const graph = buildGraph(
[
{ rootDocument: makeDocument('/project/a.yaml'), refMap: refMapA },
{ rootDocument: makeDocument('/project/b.yaml'), refMap: refMapB },
],
{ base: CWD, resolveRef }
);

expect(graph.roots).toEqual(['a.yaml', 'b.yaml']);
expect(graph.edges).toEqual([
{ from: 'a.yaml', to: 'b.yaml', refs: ['b.yaml#/X', 'b.yaml#/Y'] },
]);
expect(graph.nodes).toEqual([
{ id: 'a.yaml', root: true, resolved: true },
{ id: 'b.yaml', root: true, resolved: true },
]);
});

it('represents unresolved refs as resolved:false nodes with an edge', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::./missing.yaml#/Pet',
{
resolved: false as const,
isRemote: true,
document: undefined,
error: new ResolveError(new Error('ENOENT')),
},
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([
{ id: 'missing.yaml', resolved: false },
{ id: 'openapi.yaml', root: true, resolved: true },
]);
expect(graph.edges).toEqual([
{ from: 'openapi.yaml', to: 'missing.yaml', refs: ['./missing.yaml#/Pet'] },
]);
});

it('keeps http(s) targets as external URL nodes', () => {
const refMap: ResolvedRefMap = new Map([
[
'/project/openapi.yaml::https://example.com/shared.yaml#/S',
resolvedEntry('https://example.com/shared.yaml'),
],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/openapi.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.nodes).toEqual([
{ id: 'https://example.com/shared.yaml', external: true, resolved: true },
{ id: 'openapi.yaml', root: true, resolved: true },
]);
});

it('handles cyclic file references', () => {
const refMap: ResolvedRefMap = new Map([
['/project/a.yaml::b.yaml', resolvedEntry('/project/b.yaml')],
['/project/b.yaml::a.yaml', resolvedEntry('/project/a.yaml')],
]);

const graph = buildGraph([{ rootDocument: makeDocument('/project/a.yaml'), refMap }], {
base: CWD,
resolveRef,
});

expect(graph.edges).toEqual([
{ from: 'a.yaml', to: 'b.yaml', refs: ['b.yaml'] },
{ from: 'b.yaml', to: 'a.yaml', refs: ['a.yaml'] },
]);
});
});
Loading
Loading