-
Notifications
You must be signed in to change notification settings - Fork 100
feat: synth triggering #1634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
megha-narayanan
wants to merge
21
commits into
aws:feat/cdk-lsp
Choose a base branch
from
megha-narayanan:feat/explorer-manual-synth
base: feat/cdk-lsp
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: synth triggering #1634
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
1f865fd
feat(cdk-explorer): add LspIoHost adapter for the Toolkit
megha-narayanan 332ddb9
feat(cdk-explorer): add cdk-config reader
megha-narayanan 8aa4813
feat(cdk-explorer): add synth-runner with typed outcome
megha-narayanan 0c48235
feat(cdk-explorer): command dispatcher for synthNow and refresh
megha-narayanan 3dc68d2
feat(cdk-explorer): wire executeCommandProvider and onExecuteCommand …
megha-narayanan 2efedca
feat(cdk-explorer): prepend Synth now and Refresh header lenses on CD…
megha-narayanan fa30f8a
refactor(cdk-explorer): drop Refresh command, add auto-synth-on-save
megha-narayanan 2b5a7f2
feat(cdk-explorer): wire Toolkit, LspIoHost, and synth runner in main.ts
megha-narayanan ee40944
feat(cdk-explorer): add auto-synth toggle with Enable/Disable CodeLens
megha-narayanan 9ab7df6
fix(cdk-explorer): gate toggle refresh on codeLensRefreshSupport, add…
megha-narayanan 91f1405
docs(cdk-explorer): clarify NonInteractiveIoHost exclusion and lock-c…
megha-narayanan 020a03c
fix(cdk-explorer): catch unexpected guardedSynth rejection; document …
megha-narayanan 8a26f7e
docs(cdk-explorer): fix stale comments and JSDoc after code review
megha-narayanan bd8a9c0
refactor(cdk-explorer): simplify synth option wiring
megha-narayanan 418a373
refactor(cdk-explorer): name the synth runner factory; clarify suppre…
megha-narayanan b23b1b4
refactor(cdk-explorer): trim over-explained dispose-failure comment
megha-narayanan 6fae0f5
feat(toolkit-lib): add LockError subclass and ToolkitError.isLockError
megha-narayanan c56f121
feat(cdk-explorer): log the auto-answered prompt response in LspIoHost
megha-narayanan 8ba9593
feat(toolkit-lib): add ContextLookupsDisabledError; LSP synth runs wi…
megha-narayanan cdcae1a
feat(cdk-explorer): surface synth failures as LSP diagnostics
megha-narayanan 316aa98
fix(cdk-explorer): read cdk.json app per synth, not once at startup
megha-narayanan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
|
|
||
| /** | ||
| * The subset of `cdk.json` the explorer cares about. | ||
| * | ||
| * `app` is the command CDK runs to produce a cloud assembly (e.g. | ||
| * `npx ts-node bin/app.ts`). We need it to invoke `Toolkit.synth()` | ||
| * via `fromCdkApp`. | ||
| */ | ||
| export interface CdkConfig { | ||
| /** The `app` command, or `undefined` if missing/malformed. */ | ||
| readonly app: string | undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Reads `<projectDir>/cdk.json` and returns the parts the explorer uses. | ||
| * Never throws. Treats missing files, malformed JSON, or wrong-typed | ||
| * fields as "not configured" so callers can fall back gracefully | ||
| */ | ||
| export function readCdkConfig(projectDir: string): CdkConfig { | ||
| const configPath = path.join(projectDir, 'cdk.json'); | ||
| if (!fs.existsSync(configPath)) return { app: undefined }; | ||
|
|
||
| let parsed: unknown; | ||
| try { | ||
| parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8')); | ||
| } catch { | ||
| return { app: undefined }; | ||
| } | ||
|
|
||
| if (parsed === null || typeof parsed !== 'object') return { app: undefined }; | ||
| const app = (parsed as { app?: unknown }).app; | ||
| return { app: typeof app === 'string' ? app : undefined }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { ToolkitError, type Toolkit } from '@aws-cdk/toolkit-lib'; | ||
| import { readCdkConfig } from './cdk-config'; | ||
|
|
||
| /** | ||
| * The outcome of a single synth attempt. | ||
| * | ||
| * `success` means the assembly was written to disk (the watcher will see it). | ||
| * `app-failure` means the user's CDK app threw, did not compile, or needs uncached context lookups. | ||
| * `lock-conflict` means another process holds `<projectDir>/cdk.out` (a `cdk | ||
| * synth` running in a terminal, a `cdk watch` loop, or our own previous synth | ||
| * not yet released). Callers should not surface this as a hard error. | ||
| * `unavailable` means `cdk.json` is missing or has no `app` key, so there is | ||
| * nothing to synth. Read fresh on every call, so adding an `app` later is | ||
| * picked up without restarting the LSP. | ||
| * `error` is reserved for anything we did not classify, including failures | ||
| * during dispose. | ||
| */ | ||
| export type SynthRunResult = | ||
| | { status: 'success' } | ||
| | { status: 'app-failure'; message: string; details?: string } | ||
| | { status: 'lock-conflict' } | ||
| | { status: 'unavailable' } | ||
| | { status: 'error'; message: string }; | ||
|
|
||
| export interface SynthRunnerOptions { | ||
| /** A configured Toolkit instance (its IoHost decides where messages go). */ | ||
| readonly toolkit: Toolkit; | ||
| /** Directory containing the user's `cdk.json`; also the synth working dir. */ | ||
| readonly projectDir: string; | ||
| } | ||
|
|
||
| /** | ||
| * Run a one-shot synth of the user's CDK app. Writes `<projectDir>/cdk.out` | ||
| * via `Toolkit.synth(fromCdkApp(...))`, then immediately disposes the cached | ||
| * assembly so the read lock is released before the next call. Holding the | ||
| * cached assembly between calls would cause the next acquireWrite to throw | ||
| * `ConcurrentReadLock` against ourselves. | ||
| * | ||
| * The `app` command is read from `cdk.json` on every call, not cached, so an | ||
| * edited command or a newly added `app` takes effect on the next synth. | ||
| */ | ||
| export async function runSynth(options: SynthRunnerOptions): Promise<SynthRunResult> { | ||
| const app = readCdkConfig(options.projectDir).app; | ||
| if (app === undefined) return { status: 'unavailable' }; | ||
|
|
||
| let cached; | ||
| try { | ||
| const cx = await options.toolkit.fromCdkApp(app, { | ||
| workingDirectory: options.projectDir, | ||
| lookups: false, | ||
| }); | ||
| cached = await options.toolkit.synth(cx); | ||
| } catch (err) { | ||
| return classify(err); | ||
| } | ||
|
|
||
| try { | ||
| await cached.dispose(); | ||
| } catch (err) { | ||
| // Releases the read lock synth() left on the assembly. Failure is rare (an | ||
| // fs error deleting the lock file); report it as `error` so the next synth | ||
| // does not silently self-conflict on the stale reader. | ||
| return { status: 'error', message: (err as Error).message }; | ||
| } | ||
|
|
||
| return { status: 'success' }; | ||
| } | ||
|
|
||
| function classify(err: unknown): SynthRunResult { | ||
| if (ToolkitError.isLockError(err)) { | ||
| return { status: 'lock-conflict' }; | ||
| } | ||
| if (ToolkitError.isContextLookupsDisabledError(err)) { | ||
| return { | ||
| status: 'app-failure', | ||
| message: 'This app needs context lookups that are not in cdk.context.json. ' | ||
| + 'Run `cdk synth` in a terminal (with AWS credentials) to populate it, then retry.', | ||
| }; | ||
| } | ||
| if (ToolkitError.isAssemblyError(err)) { | ||
| // details = captured subprocess stderr (file:line:col), used for diagnostics. | ||
| return { status: 'app-failure', message: err.message, details: (err.cause as Error | undefined)?.message }; | ||
| } | ||
| return { status: 'error', message: (err as Error).message }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import type { SynthRunResult } from '../core/synth-runner'; | ||
|
|
||
| /** | ||
| * Trigger a one-shot synth of the user's CDK app. Only shown when | ||
| * auto-synth is disabled (saves handle synth when it is enabled). | ||
| */ | ||
| export const COMMAND_SYNTH_NOW = 'cdk.explorer.synthNow'; | ||
| /** Enable auto-synth-on-save. Replaces "Synth now" in the header lens. */ | ||
| export const COMMAND_ENABLE_AUTO_SYNTH = 'cdk.explorer.enableAutoSynth'; | ||
| /** Disable auto-synth-on-save. Restores the "Synth now" header lens. */ | ||
| export const COMMAND_DISABLE_AUTO_SYNTH = 'cdk.explorer.disableAutoSynth'; | ||
|
|
||
| /** All commands this LSP advertises via `executeCommandProvider`. */ | ||
| export const SUPPORTED_COMMANDS = [COMMAND_SYNTH_NOW, COMMAND_ENABLE_AUTO_SYNTH, COMMAND_DISABLE_AUTO_SYNTH] as const; | ||
|
|
||
| /** | ||
| * UI sinks the dispatcher uses to communicate with the user. | ||
| * Implementations bridge to `connection.console` in the LSP layer. | ||
| */ | ||
| export interface NotifySink { | ||
| /** Write a non-error informational message to the Output panel. */ | ||
| info(message: string): void; | ||
| /** Write an error message to the Output panel. */ | ||
| error(message: string): void; | ||
| /** | ||
| * Run a long operation with a visible progress indicator. The implementation | ||
| * is responsible for ending the indicator regardless of success or failure. | ||
| */ | ||
| withProgress<T>(message: string, fn: () => Promise<T>): Promise<T>; | ||
| } | ||
|
|
||
| export interface CommandHandlerOptions { | ||
| /** Invokes a single synth. Resolves with the typed outcome; never rejects. */ | ||
| readonly synth: () => Promise<SynthRunResult>; | ||
| /** Called with the new desired state when the user toggles auto-synth. */ | ||
| readonly toggleAutoSynth: (enabled: boolean) => void; | ||
| /** UI sinks for messages and progress. */ | ||
| readonly notify: NotifySink; | ||
| } | ||
|
|
||
| const SYNTH_UNAVAILABLE_MESSAGE = "CDK synth unavailable: 'cdk.json' missing or has no 'app' key."; | ||
| const LOCK_CONFLICT_MESSAGE = 'Another synth is in progress. Results will refresh shortly.'; | ||
| const PROGRESS_MESSAGE = 'Synthesizing CDK app...'; | ||
|
|
||
| /** | ||
| * Handle a `workspace/executeCommand` request. The synth command runs under a | ||
| * progress indicator and reports outcomes through the notify sinks. Unknown | ||
| * commands are silently ignored. | ||
| */ | ||
| export async function executeCommand( | ||
| command: string, | ||
| _args: unknown[], | ||
| options: CommandHandlerOptions, | ||
| ): Promise<void> { | ||
| switch (command) { | ||
| case COMMAND_ENABLE_AUTO_SYNTH: | ||
| options.toggleAutoSynth(true); | ||
| return; | ||
|
|
||
| case COMMAND_DISABLE_AUTO_SYNTH: | ||
| options.toggleAutoSynth(false); | ||
| return; | ||
|
|
||
| case COMMAND_SYNTH_NOW: | ||
| { | ||
| const result = await options.notify.withProgress(PROGRESS_MESSAGE, () => options.synth()); | ||
| handleSynthResult(result, options.notify); | ||
| } | ||
| return; | ||
|
|
||
| default: | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| function handleSynthResult(result: SynthRunResult, notify: NotifySink): void { | ||
| switch (result.status) { | ||
| case 'success': | ||
| // Silent. The watcher refreshes the editor when `cdk.out` changes. | ||
| return; | ||
| case 'app-failure': | ||
| notify.error(`CDK synth failed: ${result.message}`); | ||
| return; | ||
| case 'lock-conflict': | ||
| notify.info(LOCK_CONFLICT_MESSAGE); | ||
| return; | ||
| case 'unavailable': | ||
| notify.info(SYNTH_UNAVAILABLE_MESSAGE); | ||
| return; | ||
| case 'error': | ||
| notify.error(`CDK synth failed unexpectedly: ${result.message}`); | ||
| return; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import type { IIoHost, IoMessage, IoRequest } from '@aws-cdk/toolkit-lib'; | ||
| import type { RemoteConsole } from 'vscode-languageserver/node'; | ||
|
|
||
| /** | ||
| * IoHost for the LSP. Routes Toolkit messages into the editor's Output | ||
| * channel via the LSP connection's console. | ||
| * | ||
| * We do NOT reuse `NonInteractiveIoHost` from toolkit-lib even though its | ||
| * `requestResponse` is identical: that class writes to `process.stdout` / | ||
| * `process.stderr`, which are the JSON-RPC transport for this process. | ||
| * Writing Toolkit output there would corrupt the protocol stream. | ||
| * | ||
| * The LSP cannot prompt the user synchronously through `connection.console`, | ||
| * so `requestResponse` returns each message's `defaultResponse`. For synth, | ||
| * the only reachable interactive prompt is an MFA token (when the app has | ||
| * context lookups, no cached `cdk.context.json`, and an MFA-protected profile). | ||
| * Returning the default causes an auth failure, surfaced as `app-failure` with | ||
| * a clear message. All other prompts are on deploy/destroy paths we don't call. | ||
| */ | ||
| export class LspIoHost implements IIoHost { | ||
| public constructor(private readonly console: RemoteConsole) { | ||
| } | ||
|
|
||
| public async notify(msg: IoMessage<unknown>): Promise<void> { | ||
| switch (msg.level) { | ||
| case 'error': | ||
| this.console.error(msg.message); | ||
| break; | ||
| case 'warn': | ||
| this.console.warn(msg.message); | ||
| break; | ||
| case 'debug': | ||
| case 'trace': | ||
| // Suppress noisy levels; keeps the Output panel readable. | ||
| break; | ||
| default: | ||
| this.console.info(msg.message); | ||
| } | ||
| } | ||
|
|
||
| public async requestResponse<T>(msg: IoRequest<unknown, T>): Promise<T> { | ||
| await this.notify(msg); | ||
| this.console.info(`Auto-answered with default response: ${JSON.stringify(msg.defaultResponse)}`); | ||
| return msg.defaultResponse; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't that panel have its own verbosity level filtering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some clients do, some don't, so I didn't want to rely on it. connection.console emits window/logMessage with a MessageType. New VS Code versions route that into a channel with user-selectable levels, but older versions and several non-VS Code clients write everything to a plain channel with no level filter. trace is also a separate channel, not a logMessage level. So I kept the suppression to keep the Output panel readable regardless of client.
If you'd rather defer to the client, lmk. Happy to go either way.