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: 2 additions & 0 deletions packages/cli-engine/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ export interface ServerCommandDefinition<
readonly cwd: string;
readonly env: Readonly<Record<string, string | undefined>>;
readonly config: TConfig;
/** The config file the run read, absolute; see CommandContext. */
readonly configFile: string | null;
},
) => Promise<number>;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-engine/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ export interface CommandContext<
/** Where the user invoked the CLI. Handlers never read process.cwd(). */
readonly cwd: string;

/**
* The config file this run read, absolute: the one `--config` named,
* resolved against cwd, otherwise the prisma.config.ts discovered in
* cwd. A relative path inside the file is relative to the file, so a
* handler anchors such paths on this file's directory, never on cwd —
* the two differ whenever `--config` points into another directory.
* Null for a command with no config need, which never reads the file.
*/
readonly configFile: string | null;

/**
* The invocation's environment, from Runtime.env. Handlers read env
* via ctx.env, never process.env.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-engine/src/execution/command-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export function makeContext(
invocation: Invocation,
def: AnyCommand,
config: unknown,
configFile: string | null,
capabilities: CommandCapabilities,
): CommandContext<unknown, number> {
const state = invocation.state;
Expand Down Expand Up @@ -164,6 +165,7 @@ export function makeContext(
let api: ManagementApiClient | undefined;
const context: CommandContext<unknown, number> = {
config,
configFile,
present: present as CommandContext<unknown, number>["present"],
activeCredential: (): Promise<ActiveCredential | null> =>
invocation.runtime.credentialManager?.activeCredential() ??
Expand Down
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 @@ -275,6 +275,7 @@ type ErasedServerHandler = (
readonly cwd: string;
readonly env: Readonly<Record<string, string | undefined>>;
readonly config: unknown;
readonly configFile: string | null;
},
) => Promise<number>;

Expand Down Expand Up @@ -660,6 +661,7 @@ export class EngineImpl implements Engine {
invocation,
entry.def,
needsOutcome.config,
needsOutcome.configFile,
declaredCapabilities(entry.def),
);
if (entry.def.kind === "session-command") {
Expand Down Expand Up @@ -744,6 +746,7 @@ export class EngineImpl implements Engine {
cwd: runtime.cwd,
env: runtime.env,
config: needsOutcome.config,
configFile: needsOutcome.configFile,
});
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
settleBug(
Expand Down
22 changes: 18 additions & 4 deletions packages/cli-engine/src/execution/needs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export type NeedsOutcome =
| {
readonly kind: "ok";
readonly config: unknown;
/** The file the config came from, absolute; null when the command
* has no config need and the file was never read. */
readonly configFile: string | null;
/** The credential resolved for a `credentials: "child"` command,
* carried forward so the spawn path never re-resolves it. */
readonly spawnCredential: ActiveCredential | undefined;
Expand Down Expand Up @@ -90,6 +93,7 @@ export async function checkNeeds(
return {
kind: "ok",
config: undefined,
configFile: null,
spawnCredential: credentials.spawnCredential,
};
}
Expand Down Expand Up @@ -270,18 +274,23 @@ async function checkConfiguration(
loaded,
invocation,
configPath ?? CONFIG_FILE_NAME,
resolve(invocation.runtime.cwd, loaded.path),
);
}

/** Validates the command's needed config section. The validator
* owns absence (it receives undefined when the section is missing) and
* never throws — a throw is an engine-boundary bug, settled as one.
* `configFile` is named in the error so a run under --config points at
* the file it actually read. */
* `namedFile` is named in the error so a run under --config points at
* the file it actually read; `configFile` is the same file made
* absolute against cwd, which is what the handler anchors on — the
* loader's own report is resolved because a host loader may hand back
* the path exactly as `--config` gave it. */
function validateConfigSection(
section: ConfigSection<unknown>,
loaded: LoadedConfig,
invocation: Invocation,
namedFile: string,
configFile: string,
): NeedsOutcome {
const raw = loaded.sections[section.name];
Expand All @@ -301,7 +310,7 @@ function validateConfigSection(
return needsErrored(
new CliStructuredError(
"CLI.CONFIG_SECTION_INVALID",
`The '${section.name}' section of ${configFile} is invalid.`,
`The '${section.name}' section of ${namedFile} is invalid.`,
{
nextActions: [
{
Expand All @@ -316,7 +325,12 @@ function validateConfigSection(
);
}
writeSectionWarnings(invocation, validation.diagnostics);
return { kind: "ok", config: validation.value, spawnCredential: undefined };
return {
kind: "ok",
config: validation.value,
configFile,
spawnCredential: undefined,
};
}

/** Diagnostics on an OK validation are warnings: written to stderr as
Expand Down
102 changes: 102 additions & 0 deletions packages/cli-engine/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,3 +1298,105 @@ describe("warnings on a successful section validation", {
expect(run.stderr).toBe("✔ hi\n");
});
});

/**
* A relative path inside a config file means "relative to this file",
* and a handler can only honour that if it knows which file was read.
* The path is absolute whatever the host's loader reported, because a
* handler anchoring on its directory must not depend on the loader.
*/
describe("ctx.configFile", { timeout: 60_000 }, () => {
function probeCommand(section: ConfigSection<ToyConfig>) {
return defineCommand({
help: { summary: "Reports the config file the run read" },
needs: { config: section },
handler: async (_args, ctx) =>
ok(
ctx.present(
{ data: { configFile: ctx.configFile } },
{
human: () => [],
stdout: () => [],
json: () => ({ configFile: ctx.configFile }),
next: () => [],
},
),
),
});
}

function probingCli(loader: Runtime["loadConfig"]) {
return createTestCli({
commands: { probe: probeCommand(toySection()) },
loadConfig: loader,
});
}

test("a --config path given relative to cwd arrives absolute", async () => {
const cli = probingCli((request) => loadConfig(FIXTURES, request));
const run = await cli.run(
["probe", "--config", join("named", "elsewhere.config.ts")],
{ cwd: FIXTURES },
);
expect(run.exitCode).toBe(0);
expect(run.presented?.data).toEqual({
configFile: join(FIXTURES, "named", "elsewhere.config.ts"),
});
});

test("without the flag it is the discovered prisma.config.ts in cwd", async () => {
const cwd = join(FIXTURES, "discovered");
const cli = probingCli((request) => loadConfig(cwd, request));
const run = await cli.run(["probe"], { cwd });
expect(run.exitCode).toBe(0);
expect(run.presented?.data).toEqual({
configFile: join(cwd, "prisma.config.ts"),
});
});

test("a host loader that reports a relative path is resolved against cwd", async () => {
const cli = probingCli(async (configPath) => ({
path: configPath ?? "prisma.config.ts",
sections: { toy: { greeting: "hi" } },
diagnostics: [],
}));
const run = await cli.run(["probe", "--config", "other.config.ts"], {
cwd: "/somewhere/project",
});
expect(run.exitCode).toBe(0);
expect(run.presented?.data).toEqual({
configFile: "/somewhere/project/other.config.ts",
});
});

test("a command with no config need gets null", async () => {
const asked: (string | undefined)[] = [];
const cli = createTestCli({
commands: {
probe: defineCommand({
help: { summary: "Reports the config file the run read" },
handler: async (_args, ctx) =>
ok(
ctx.present(
{ data: { configFile: ctx.configFile }, exitCode: 0 },
{
human: () => [],
stdout: () => [],
json: () => ({ configFile: ctx.configFile }),
next: () => [],
},
),
),
}),
},
loadConfig: async (configPath) => {
asked.push(configPath);
return { path: "prisma.config.ts", sections: {}, diagnostics: [] };
},
});
const run = await cli.run(["probe", "--config", "other.config.ts"]);
expect(run.exitCode).toBe(0);
expect(run.presented?.data).toEqual({ configFile: null });
expect(asked).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { definePrismaConfig } from "@prisma/cli-engine";

export default definePrismaConfig({
toy: { greeting: "found in cwd" },
});
Loading