Skip to content

feat(cli): cdk explore, a web explorer for your CDK app - #2058

Open
rix0rrr wants to merge 11 commits into
explorer_3_explorer_refactorfrom
explorer_4_web_explorer
Open

rix0rrr wants to merge 11 commits into
explorer_3_explorer_refactorfrom
explorer_4_web_explorer

Conversation

@rix0rrr

@rix0rrr rix0rrr commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Serve a local web UI that shows the construct tree of a synthesized app, and maps between construct tree node, source view, and template view. The nodes and source lines are annotated with any diagnostics that the app may have produced.

cdk explore [--port] starts a local-only server (default port 3411).

The application is a React + Cloudscape SPA, hitting a local backend server that services reads from the CloudAssembly. The user interface is organized into tabs, which can be extended in the future with different types of views (diagrams, ops actions, etc).

image

New repository components:

cdk-explorer/
├── frontend/          #  React SPA
└── lib/
     ├── expressy/     #  HTTP framework with an 'express'-like API
     ├── web-shared/   #  Shared between 'frontend' and 'web'
     └── web/          #  Local HTTP server code

Adds a bunch of tsconfig.json files so that the frontend and web-shared components can be typechecked with different settings than the regular code. At build time, the frontend code is bundled and then written to a web-assets.generated.ts file, that gets read by the web server (and subsequently bundled into the CLI at CLI build time). There is also a CDK_LIVE=1 mode that can be used to iterate on the static assets as the server is running.

Includes prismjs for syntax highlighting.

Closes #1748.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@rix0rrr
rix0rrr added this pull request to stack #2056 October 19, 2026 14:51
@rix0rrr
rix0rrr deployed to no-approval October 19, 2026 14:51 — with GitHub Actions Active
@aws-cdk-automation
aws-cdk-automation requested a review from a team October 19, 2026 14:51
@github-actions github-actions Bot added the p2 label Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses.
  • ⚠️ 14 packages with OpenSSF Scorecard issues.

View full job summary

@codecov-commenter

codecov-commenter commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.07%. Comparing base (120c1be) to head (635a7dc).

Files with missing lines Patch % Lines
packages/aws-cdk/lib/cli/cli.ts 22.22% 7 Missing ⚠️
packages/aws-cdk/lib/commands/explore.ts 97.22% 1 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           explorer_3_explorer_refactor    #2058      +/-   ##
================================================================
- Coverage                         92.10%   92.07%   -0.03%     
================================================================
  Files                                82       83       +1     
  Lines                             13189    13242      +53     
  Branches                           1879     1886       +7     
================================================================
+ Hits                              12148    12193      +45     
- Misses                             1003     1011       +8     
  Partials                             38       38              
Flag Coverage Δ
suite.unit 92.07% <85.71%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread packages/@aws-cdk/cdk-explorer/CONTRIBUTING.md Outdated
Comment thread packages/@aws-cdk/cdk-explorer/build-tools/watch-explore.sh Outdated
Comment thread packages/@aws-cdk/cdk-explorer/build-tools/watch-explore.sh Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svg?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PNG is what it is.

Comment thread packages/@aws-cdk/cdk-explorer/lib/expressy/http-server.ts
}),
)
.command('doctor', 'Check your set-up for potential problems')
.command('explore', 'Opens an interactive web explorer for your CDK app', (yargs: Argv) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explore or explorer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mm. To my mind it's (mostly) cdk <verb>. So explore. But we can make an alias as well.

@mrgrain mrgrain left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just tiny stuff

@maintainer-for-aws

maintainer-for-aws Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Automated review

A maintainer will still review this — treat the notes below as a starting point.

📌 Commit 635a7dc

This PR adds the cdk explore command and the web layer backing it in the @aws-cdk/cdk-explorer package: a local-only HTTP server that reads an already-synthesized cloud assembly and serves a React single-page explorer (construct tree, source and template views, policy violations) with live refresh. The change is large but almost entirely additive, with the only cross-package edits being the CLI wiring and one exported toolkit-lib lock-file constant.

The security-sensitive surfaces — path confinement, the loopback-only and session-token admission middlewares, the strict CSP, and the hand-rolled HTTP stack — hold up well, and the feature is heavily tested. The remaining items are a build-integrity concern around keeping the generated and source CLI argument definitions in sync, one library logging path that bypasses the IoHost, and a few smaller consistency and robustness nits on the web layer.

🔴 1 blocking · 🟡 2 recommended · ⚪ 3 optional

Files with findings (6)
File Findings
packages/aws-cdk/lib/cli/parse-command-line-arguments.ts 🔴 1
packages/@aws-cdk/cdk-explorer/lib/expressy/index.ts 🟡 1
packages/aws-cdk/lib/cli/cli-config.ts 🟡 1
packages/@aws-cdk/cdk-explorer/lib/expressy/http-server.ts ⚪ 1
packages/@aws-cdk/cdk-explorer/lib/web/server.ts ⚪ 1
packages/@aws-cdk/cdk-explorer/lib/web/web-assets.ts ⚪ 1

Generated automatically. React 👍 or 👎 to tell us whether this review helped, so we can improve these reviews.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +24 to +25
const onSignal = () => {
// eslint-disable-next-line no-console

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — The // eslint-disable-next-line no-console directive sits above void options.ioHelper.defaults.info('Stopping.'), which contains no console.* call — output correctly goes through the IoHost. The directive therefore suppresses nothing and is a stray artifact; if the lint config reports unused disable directives it is also a latent build break.

Suggested change: Delete the // eslint-disable-next-line no-console line above the options.ioHelper.defaults.info('Stopping.') call.

Comment on lines +2 to +3
* Serves the web explorer SPA from memory. The bytes come from
* web-assets.generated.json (written by build-tools/bundle-frontend.ts); the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — The file header describes the embedded-assets artifact as web-assets.generated.json, but the code imports ./web-assets.generated, which resolves to a TypeScript file (web-assets.generated.ts) — the build writes and requires a .ts module, not JSON. The comment misdescribes the mechanism it documents, so a reader looking for a JSON artifact will not find one.

Suggested change: Change "web-assets.generated.json" to "web-assets.generated.ts" in the header comment.

Comment on lines +3 to +4
* Builds the web explorer SPA into lib/web/static, then writes the same assets
* to lib/web/web-assets.generated.json so they ride the require() graph into

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — The file header says it writes the assets to lib/web/web-assets.generated.json, but embeddedAssetsFile is set to ...web-assets.generated.ts and writeEmbeddedAssets emits a TypeScript module (export const EMBEDDED_ASSETS = {...}), not JSON. The comment misdescribes the output file it documents, which can mislead the next reader.

Suggested change: Change "web-assets.generated.json" to "web-assets.generated.ts" in the header comment.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +555 to +560
'explore': {
description: 'Opens an interactive web explorer for your CDK app',
options: {
port: { type: 'number', desc: 'Port to bind the web server on', default: 3411 },
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Recommended — The explore command declares port with default: 3411, so yargs always populates args.port even when the user omits --port, and that concrete value is forwarded to startWebServer. In http-server.ts, a defined port sets startPort = endPort = port (no search, hard fail on conflict), while an undefined port searches DEFAULT_PORT..DEFAULT_PORT+100. Because the CLI always supplies 3411, the auto-increment fallback is unreachable from cdk explore: a user whose 3411 is already busy — e.g. a second cdk explore — gets a hard Port 3411 is already in use error instead of binding 3412.

Suggested change: Drop default: 3411 from the explore port option (keep only desc) so an omitted --port reaches startWebServer as undefined and the port search runs, while an explicit --port 3411 still fails hard. DEFAULT_PORT in http-server.ts already supplies 3411 as the search base, preserving the user-visible default.

Comment on lines +34 to +42

try {
const diskLocation = BUNDLED_FILES.includes(name)
? path.resolve(__dirname, 'static', `${name}`)
: path.resolve(__dirname, '..', '..', 'frontend', 'src', `${name}`)
;

return {
contentType: CONTENT_TYPES[name],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — When CDK_LIVE is set, webAsset(name) resolves path.resolve(__dirname, '..', '..', 'frontend', 'src', name) with name taken straight from the /:asset route param, which the expressy matcher captures as ([^/]+) and then decodeURIComponents. A request like GET /..%2F..%2Fsecret yields name = '../../secret' and escapes the intended frontend/src directory. This is the one asset path that bypasses resolveWithinRoot, unlike the bundled-file allow-list and the /api/file and /api/template handlers that enforce containment. Reachable only in the developer-only CDK_LIVE mode, behind sessionAuth, and frontend/src is excluded from the published package, so there is no end-user exposure — but it is worth hardening for defense-in-depth and consistency.

Suggested change: Route the CDK_LIVE path through resolveWithinRoot(path.resolve(__dirname, '..', '..', 'frontend', 'src'), name) (and the static dir for bundled files) and return 404 when it returns undefined, mirroring /api/file and /api/template.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +1175 to +1181
.command('explore', 'Opens an interactive web explorer for your CDK app', (yargs: Argv) =>
yargs.option('port', {
default: 3411,
type: 'number',
desc: 'Port to bind the web server on',
}),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking — This generated parser declares the explore port option with default: 3411, but its source of truth cli-config.ts declares port: { type: 'number', desc: ... } with no default — so this file and user-input.ts (which carries @default - 3411) are out of sync with the generator and appear hand-edited. Generated arg files must never be edited by hand because the next regeneration silently overwrites them (AGENTS.md#projen). The next yarn projen run will rewrite default: 3411 to default: undefined, flipping observable behavior: cdk explore with no --port goes from hard-failing when 3411 is busy to auto-searching the next 100 ports.

Suggested change: Decide the intended default and encode it in cli-config.ts only, then regenerate so all three files are the output of a single generator run. For auto-search (as startWebServer's doc comment states), leave port with no default in cli-config.ts and regenerate so the generated files carry default: undefined.

Comment on lines +214 to +237
*/
export function createApp(): Express {
const router = createRouter();

const app = ((raw: IncomingMessage, rawRes: ServerResponse): void => {
const res = makeResponse(rawRes);

// Parsing the request can fail, handle it gracefully
let req: Request;
try {
req = makeRequest(raw);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`${raw.method} ${raw.url}:`, e);
res.status(400).end();
return;
}

try {
router.handle(req, res, req.path, () => {
// Nothing matched: mirror express's default 404.
if (!res.headersSent) {
res.status(404);
res.set('Content-Type', 'text/plain; charset=utf-8');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Recommended — The request-parse and handler-dispatch failure paths in createApp write diagnostics with console.error behind // eslint-disable-next-line no-console directives, in library code bundled into the CLI. The guidelines say library output must go through the IoHost pattern rather than console, and eslint-disable directives must not be left in place (AGENTS.md#anti-patterns). The framework already threads an injected RequestLogger at the router level and server.ts wires a logger that routes to the IoHost, but this app-level error sink does not use it, so these diagnostics escape to raw stderr and are invisible to the IoHost/telemetry.

Suggested change: Let createApp accept an optional error-logging callback (defaulting to a no-op or to the same RequestLogger the server already owns) and call it instead of console.error, removing the eslint-disable directives.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +32 to +39
let startPort: number;
let endPort: number;
if (this.options.port) {
startPort = this.options.port;
endPort = startPort;
} else {
startPort = DEFAULT_PORT;
endPort = startPort + 100;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — start() selects the port with if (this.options.port), a truthiness check. --port 0 is a legitimate value meaning 'let the OS assign a free ephemeral port', but 0 is falsy, so the code ignores it and instead auto-increments from DEFAULT_PORT (3411). A user who explicitly asks for an ephemeral port silently gets a 3411-range port instead.

Suggested change: Test for presence rather than truthiness, e.g. if (this.options.port !== undefined).

Comment on lines 94 to 96
* `synth.lock` appearing is instead reported as `onSynthActivity`, the only
* signal we get that a synth has started (the explorer never starts one itself).
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — This doc comment says synth.lock appearing is reported as onSynthActivity, but the option added in the same diff is named onSynthStart (and it calls through to the synth-start staleness path). A comment the diff itself makes inaccurate misleads the next reader; the server.ts call-site comment similarly refers to onSynthActivity.

Suggested change: Update the comment here and the matching server.ts call-site comment to name onSynthStart.

@github-actions

Copy link
Copy Markdown
Contributor

Total lines changed 9267 is greater than 1000. Please consider breaking this PR down.

@maintainer-for-aws maintainer-for-aws Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review summary comment for the overview; the notes below are inline.

Comment on lines +142 to +152

// Serve the SPA from the embedded bundle (survives CLI bundling). Named assets
// by path; any other GET falls back to index.html for client-side routing.
// (securityHeaders already sets Cache-Control: no-store, which is what makes a
// rebuilt bundle show up on reload despite its unversioned filename.)
app.get('/:asset', async (req, res) => {
const asset = await webAsset(req.params.asset);
if (asset) {
return res.type(asset.contentType).send(asset.body);
}
return res.status(404).send('Not Found');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Optional — The comment above the SPA asset route states "any other GET falls back to index.html for client-side routing," but the /:asset handler returns res.status(404).send('Not Found') for any unrecognized asset — there is no fallback to index.html. The /:asset matcher is also single-segment ([^/]+), so a multi-segment client-side route such as /tree/foo never reaches this handler at all. The comment describes a client-side-routing fallback the code does not implement, which will mislead the next reader who tries to rely on or extend it (AGENTS.md#anti-patterns).

Suggested change: Correct the comment to describe the actual behavior (an unknown asset name returns 404, with no SPA deep-link fallback), or, if deep-link client-side routing is intended, add the real fallback that serves index.html for unmatched GETs.

rix0rrr and others added 11 commits October 23, 2026 14:44
Serve a local web UI that shows a synthesized app three ways -- construct
tree, authored source, and CloudFormation template -- with navigation
between them and policy-validation findings in context.

- `cdk explore [--port]` starts a local-only server (default 3411)
- `lib/web/`: routes for tree/template/file/policy-validation, SSE live
  refresh on re-synth, source staleness tracking, and local-only,
  security-header and session-token middleware
- `lib/expressy/`: a small HTTP layer replacing Express, so the CLI
  bundle does not carry a web framework
- `lib/web-shared/`: the wire protocol and severity model shared with
  the SPA
- `frontend/`: React + Cloudscape single-page app, type-checked
  separately and bundled with esbuild into the published package
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

This branch had an error being deployed

1 failed (outdated), 1 active, and 1 inactive deployments
run-tests — 635a7dc8 Deployed Oct 23, 2026 by rix0rrr via integ_init-templates (init-go, 24.19) #7441
no-approval — 635a7dc8 Deployed Oct 23, 2026 by rix0rrr via prepare #7441
automation — 575ffae6 Deployed Oct 19, 2026 by rix0rrr via Set AutoQueue on PR #2058 #3766
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants