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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ Pre-1.0 releases followed it in spirit; their breaking changes are marked **Brea
3.0.0 and appeared in no guide; the guide now has a Diagnostics section
listing the events and what they are useful for.

- **A preflight failure now reports what it found as data.** `PreflightError`
carried its findings as a list of sentences; `problems` carries them as
`{ code, message, subject, moduleId, path }` — `COMMAND_HANDLER_CONFLICT`,
`SERVICE_CAPTIVE_DEPENDENCY`, `TRUST_REQUIRED` and the rest — so a test, a
CI step or an editor integration can branch on what went wrong instead of
matching prose. The service-graph validator and runtime preflight had these
codes all along and were dropping them at the throw. The sentence list stays
as `issues`, unchanged, and the error's message is the same text as before.
`PreflightError` is exported from the root, so the error can be recognised
with `instanceof` rather than by its name.

## [4.0.1] - 2026-08-29

**A patch to the tree-view adapter, plus four corrections to what the project
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ so treat the floor as a formality rather than a tested target.

- **`ERR_REQUIRE_ESM` or `require() of ES Module`**: the package is ESM only, by design; bundle your extension with esbuild/webpack/rollup, which is what VS Code extensions normally do anyway
- **`Symbol.dispose` or `AbortSignal` is not defined in the types**: add `ESNext.Disposable` and one of `DOM` / `WebWorker` / `@types/node` to `lib` — see [Platform Requirements](#platform-requirements)
- **An error at import time, before anything ran**: that is preflight, and it is working; the message names the duplicate id, the missing service or the cycle
- **An error at import time, before anything ran**: that is preflight, and it is working; the message names the duplicate id, the missing service or the cycle, and `problems` on the error carries each one as a code a script can act on
- **A command is greyed out in the Command Palette**: that is `enablement` / `commandPalette` `when` in your `package.json`, not something this package controls
- **A text editor command's result is `undefined`**: VS Code runs those handlers fire-and-forget and discards what they return; use `module.commands.handle` with `Editors.active` when the caller needs the result
- **`vscode` cannot be resolved in tests**: point Vitest's `resolve.alias` at `@kkdev92/vscode-ext-kit/testing/vitest`, or merge `vscodeExtKitVitestConfig`
Expand Down
53 changes: 53 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,59 @@ log, count or attach to a bug report — not an event bus. Delivery is
best-effort by design: a listener that throws is ignored, and nothing waits for
one.

### When preflight says no

Preflight is the other place the framework tells you something went wrong, and
it does so by throwing: `defineExtension` at import time for a structural
problem, `activate` for a host that does not meet a module's requirements. The
error is a `PreflightError`, and it carries every problem it found rather than
the first — each with a stable `code`, the `subject` it is about and, where a
module declared it, the `moduleId`.

<!-- sample: docs/samples/preflight.ts -->

```ts
import { PreflightError } from '@kkdev92/vscode-ext-kit';

/**
* Turns a preflight failure into lines a person can act on.
*
* `defineExtension` throws before VS Code is touched, with every problem it
* found rather than the first. Each problem carries a stable `code` — for a
* script or a test to branch on — and a `message` that says the same thing to
* a person. Anything else is rethrown untouched.
*/
export function explainPreflight(error: unknown): readonly string[] {
if (!(error instanceof PreflightError)) {
throw error;
}
return error.problems.map((problem) =>
problem.moduleId === undefined
? `${problem.code}: ${problem.message}`
: `${problem.code} in ${problem.moduleId}: ${problem.message}`
);
}

/**
* The one check a CI step usually wants: did the graph change shape?
*
* A captive dependency — a singleton holding a transient — is the kind of
* mistake that only shows up as a stale value weeks later. Preflight reports
* it at import time, and the code makes it a one-line gate.
*/
export function holdsATransientCaptive(error: unknown): boolean {
return (
error instanceof PreflightError &&
error.problems.some((problem) => problem.code === 'SERVICE_CAPTIVE_DEPENDENCY')
);
}
```

The message still lists every problem as a sentence, so nothing changes for a
reader of the console. The codes are for everything else: a test that asserts a
plan is well-formed, a CI step, an editor integration. They are listed on
`compileApplication` and on `RuntimeIssue`.

`application.shutdownTimeout` deserves the special attention above because it is
the one event that reports something the framework could not do. VS Code races
every extension's deactivation against a few seconds and then exits; the
Expand Down
34 changes: 34 additions & 0 deletions docs/samples/preflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PreflightError } from '@kkdev92/vscode-ext-kit';

/**
* Turns a preflight failure into lines a person can act on.
*
* `defineExtension` throws before VS Code is touched, with every problem it
* found rather than the first. Each problem carries a stable `code` — for a
* script or a test to branch on — and a `message` that says the same thing to
* a person. Anything else is rethrown untouched.
*/
export function explainPreflight(error: unknown): readonly string[] {
if (!(error instanceof PreflightError)) {
throw error;
}
return error.problems.map((problem) =>
problem.moduleId === undefined
? `${problem.code}: ${problem.message}`
: `${problem.code} in ${problem.moduleId}: ${problem.message}`
);
}

/**
* The one check a CI step usually wants: did the graph change shape?
*
* A captive dependency — a singleton holding a transient — is the kind of
* mistake that only shows up as a stale value weeks later. Preflight reports
* it at import time, and the code makes it a one-line gate.
*/
export function holdsATransientCaptive(error: unknown): boolean {
return (
error instanceof PreflightError &&
error.problems.some((problem) => problem.code === 'SERVICE_CAPTIVE_DEPENDENCY')
);
}
8 changes: 7 additions & 1 deletion src/foundation/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,13 @@ export function createApplication(options: CreateApplicationOptions): Applicatio

const errors = issues.filter((issue) => issue.severity === PreflightSeverity.Error);
if (errors.length > 0) {
throw new PreflightError(errors.map((issue) => issue.message));
throw new PreflightError(
errors.map((issue) => ({
code: issue.code,
message: issue.message,
moduleId: issue.moduleId,
}))
);
}

// Settings accessors are registered by the framework, not by a module, so
Expand Down
Loading