Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/cli-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prisma/cli-engine",
"version": "0.6.1",
"version": "0.6.2",
"description": "The execution engine of the unified Prisma CLI.",
"type": "module",
"exports": {
Expand Down
118 changes: 118 additions & 0 deletions packages/cli-engine/src/execution/bin-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
type Block,
PRESENTED,
type PresentedResult,
type Span,
type Text,
} from "../presentation";
import type { Diagnostic, NextAction } from "../protocol";
import { substituteBinName } from "./stricli-adapter";

function isRecord(value: unknown): boolean {
return typeof value === "object" && value !== null;
}

function inText(text: Text, cliName: string): Text {
if (!Array.isArray(text)) {
return substituteBinName(text, cliName);
}
return text.map((span: Span) =>
isRecord(span)
? { ...span, text: substituteBinName(span.text, cliName) }
: span,
);
}

function inBlock(block: Block, cliName: string): Block {
switch (block.kind) {
case "summary":
return { ...block, text: inText(block.text, cliName) };
case "list":
return {
...block,
items: block.items.map((item) => inText(item, cliName)),
};
case "fields":
case "table":
case "tree":
case "drawing":
return block;
}
}

function nextActionWithBinName(
action: NextAction,
cliName: string,
): NextAction {
if (!isRecord(action)) {
return action;
}
return {
...action,
label: substituteBinName(action.label, cliName),
...(action.reason === undefined
? {}
: { reason: substituteBinName(action.reason, cliName) }),
...(action.command === undefined
? {}
: { command: substituteBinName(action.command, cliName) }),
...(Array.isArray(action.commands)
? {
commands: action.commands.map((command: string) =>
substituteBinName(command, cliName),
),
}
: {}),
};
}

export function nextActionsWithBinName(
actions: readonly NextAction[],
cliName: string,
): readonly NextAction[] {
return Array.isArray(actions)
? actions.map((action: NextAction) =>
nextActionWithBinName(action, cliName),
)
: actions;
}

export function diagnosticWithBinName(
diagnostic: Diagnostic,
cliName: string,
): Diagnostic {
if (!isRecord(diagnostic)) {
return diagnostic;
}
return {
...diagnostic,
summary: substituteBinName(diagnostic.summary, cliName),
...(diagnostic.why === undefined
? {}
: { why: substituteBinName(diagnostic.why, cliName) }),
nextActions: nextActionsWithBinName(diagnostic.nextActions, cliName),
};
}

/** Only summary and list blocks are rewritten. Every other block, the
* data, the json result and the stdout lines can hold user data and
* pass through unchanged. */
export function presentedWithBinName<T>(
presented: PresentedResult<T>,
cliName: string,
): PresentedResult<T> {
return {
...presented,
[PRESENTED]: true,
diagnostics: presented.diagnostics.map((diagnostic) =>
diagnosticWithBinName(diagnostic, cliName),
),
presentation: {
...presented.presentation,
human: presented.presentation.human.map((block) =>
inBlock(block, cliName),
),
next: nextActionsWithBinName(presented.presentation.next, cliName),
},
};
}
3 changes: 3 additions & 0 deletions packages/cli-engine/src/execution/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ export interface RunState {
}

export interface Invocation {
/** The name of the binary the user ran, substituted for `{bin}`. */
readonly cliName: string;
readonly runtime: Runtime;
readonly hooks: RunHooks;
readonly now: () => Date;
Expand Down Expand Up @@ -376,6 +378,7 @@ export class EngineImpl implements Engine {
};
const unsubscribe = runtime.onSignal(deliverSignal);
const invocation: Invocation = {
cliName: this.spec.name,
runtime,
hooks: { ...hooks },
now: this.now,
Expand Down
8 changes: 5 additions & 3 deletions packages/cli-engine/src/execution/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
commentaryLine,
MASK,
PLACEHOLDER,
renderableNextActions,
sentenceCase,
withDocsUrl,
} from "./rendering";
Expand All @@ -28,7 +29,8 @@ function longestBacktickRun(text: string): number {

/** An inline code span whose delimiter is one backtick longer than any
* run inside it, padded when the content starts or ends with one. */
export function codeSpan(text: string): string {
export function codeSpan(value: string): string {
const text = String(value);
const delimiter = "`".repeat(longestBacktickRun(text) + 1);
const padded =
text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text;
Expand Down Expand Up @@ -116,7 +118,7 @@ function renderTreeNode(node: TreeNode, depth: number): string[] {

export function renderNextActionMarkdown(action: NextAction): string[] {
const target = action.command ?? action.url;
if (target === undefined && action.commands !== undefined) {
if (target === undefined && Array.isArray(action.commands)) {
return [
`- ${action.label}`,
...action.commands.map((command) => ` - ${codeSpan(command)}`),
Expand Down Expand Up @@ -163,7 +165,7 @@ export function renderDiagnosticMarkdown(diagnostic: Diagnostic): string[] {
if (where !== undefined) {
lines.push(where);
}
for (const action of diagnostic.nextActions) {
for (const action of renderableNextActions(diagnostic.nextActions)) {
lines.push(...renderNextActionMarkdown(action));
}
if (diagnostic.docsUrl !== undefined) {
Expand Down
5 changes: 4 additions & 1 deletion packages/cli-engine/src/execution/needs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../package-manager";
import { CliStructuredError, type Diagnostic } from "../protocol";
import type { LoadedConfig, LoadedConfigFile } from "../runtime";
import { diagnosticWithBinName } from "./bin-name";
import type { Invocation } from "./engine";
import { renderWarningsMarkdown } from "./markdown";
import { makePaint } from "./palette";
Expand Down Expand Up @@ -413,7 +414,9 @@ function writeSectionWarnings(
(diagnostic) =>
SEVERITY_RANK[diagnostic.severity] <= SEVERITY_RANK[state.logLevel],
)
.map((diagnostic) => withDocsUrl(state, diagnostic));
.map((diagnostic) =>
withDocsUrl(state, diagnosticWithBinName(diagnostic, invocation.cliName)),
);
if (state.format === "markdown") {
renderWarningsMarkdown(invocation, shown);
return;
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-engine/src/execution/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export function writeDiagnostic(
if (diagnostic.why !== undefined) {
stream.write(` ${paint("muted", `why: ${diagnostic.why}`)}\n`);
}
for (const action of diagnostic.nextActions) {
for (const action of renderableNextActions(diagnostic.nextActions)) {
stream.write(`${renderNextAction(action, paint)}\n`);
}
if (diagnostic.docsUrl !== undefined) {
Expand All @@ -320,6 +320,15 @@ export function writeDiagnostic(
}
}

/** A diagnostic reaches the renderers unvalidated: a list that is
* missing renders as no next actions, and an entry that is not an
* object is skipped. */
export function renderableNextActions(actions: unknown): NextAction[] {
return Array.isArray(actions)
? actions.filter((action) => typeof action === "object" && action !== null)
: [];
}

/** `label` is required, so a mapper building an action out of a bare
* command string — a legacy error's follow-up step, with no prose
* beside it — has nothing to put in the label but the command itself.
Expand Down
36 changes: 24 additions & 12 deletions packages/cli-engine/src/execution/settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import {
type NextAction,
} from "../protocol";
import { type ChildStatusSettlement, childExitCode } from "../spawn";
import {
diagnosticWithBinName,
nextActionsWithBinName,
presentedWithBinName,
} from "./bin-name";
import type { EngineSpec, Invocation } from "./engine";
import {
renderChildNextActionsMarkdown,
Expand Down Expand Up @@ -50,12 +55,12 @@ function undocumentedExitCode(
export function settleCompleted(
invocation: Invocation,
def: AnyCommand,
presented: PresentedResult<unknown>,
returned: PresentedResult<unknown>,
): void {
if (
typeof presented !== "object" ||
presented === null ||
(presented as unknown as Record<symbol, unknown>)[PRESENTED] !== true
typeof returned !== "object" ||
returned === null ||
(returned as unknown as Record<symbol, unknown>)[PRESENTED] !== true
) {
settleBug(
invocation,
Expand All @@ -65,11 +70,12 @@ export function settleCompleted(
);
return;
}
const violation = undocumentedExitCode(def, presented.exitCode);
const violation = undocumentedExitCode(def, returned.exitCode);
if (violation !== undefined) {
settleBug(invocation, violation);
return;
}
const presented = presentedWithBinName(returned, invocation.cliName);
const state = invocation.state;
invocation.hooks.onPresented?.(presented);
const exitCode = runExitCode(invocation, presented.exitCode);
Expand Down Expand Up @@ -127,9 +133,11 @@ export function settleErrored(
emitErrored(invocation, {
ok: false,
commandId: state.commandId,
error: diagnosticOf(error),
diagnostics: accompanyingFindings(diagnostics),
nextActions: error.nextActions,
error: diagnosticWithBinName(diagnosticOf(error), invocation.cliName),
diagnostics: accompanyingFindings(diagnostics).map((diagnostic) =>
diagnosticWithBinName(diagnostic, invocation.cliName),
),
nextActions: nextActionsWithBinName(error.nextActions, invocation.cliName),
});
}

Expand Down Expand Up @@ -244,12 +252,16 @@ export function settleChildStatus(
);
}
const exitCode = childExitCode(child);
const nextActions = nextActionsWithBinName(
settlement.nextActions,
invocation.cliName,
);
if (invocation.state.format === "json") {
settleStructuredChildStatus(invocation, settlement, child, exitCode);
settleStructuredChildStatus(invocation, nextActions, child, exitCode);
return;
}
if (child.signal === null) {
renderChildNextActions(invocation, settlement.nextActions);
renderChildNextActions(invocation, nextActions);
}
settleVerbatimExitCode(invocation, exitCode);
}
Expand All @@ -270,12 +282,12 @@ function renderChildNextActions(

function settleStructuredChildStatus(
invocation: Invocation,
settlement: ChildStatusSettlement,
actions: readonly NextAction[],
child: { readonly exitCode: number | null; readonly signal: string | null },
exitCode: number,
): void {
settleVerbatimExitCode(invocation, exitCode);
const nextActions = child.signal === null ? settlement.nextActions : [];
const nextActions = child.signal === null ? actions : [];
if (exitCode === 0) {
const envelope: CompletedEnvelope = {
ok: true,
Expand Down
10 changes: 9 additions & 1 deletion packages/cli-engine/src/execution/stricli-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,18 @@ function commandParameters(def: AnyCommand): Record<string, unknown> {
* written the same way and rendered by the same rule. */
export function resolveExample(example: string, cliName: string): string {
return example.includes("{bin}")
? example.replaceAll("{bin}", cliName)
? substituteBinName(example, cliName)
: `${cliName} ${example}`;
}

/** Settlement hands this values nothing has validated, so anything that
* is not a string is returned as it came. */
export function substituteBinName<T>(text: T, cliName: string): T {
return typeof text === "string"
? (text.replaceAll("{bin}", cliName) as T)
: text;
}

function commandDocs(
def: AnyCommand,
cliName: string,
Expand Down
Loading
Loading