diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index 35b995b5..c839f45f 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.4.0", + "version": "0.5.0", "description": "The execution engine of the unified Prisma CLI.", "type": "module", "exports": { diff --git a/packages/cli-engine/src/commands.ts b/packages/cli-engine/src/commands.ts index a5fc412e..5fc54b77 100644 --- a/packages/cli-engine/src/commands.ts +++ b/packages/cli-engine/src/commands.ts @@ -401,6 +401,8 @@ export interface ServerCommandDefinition< readonly cwd: string; readonly env: Readonly>; readonly config: TConfig; + /** The config file the run read, absolute; see CommandContext. */ + readonly configFile: string | null; }, ) => Promise; } diff --git a/packages/cli-engine/src/context.ts b/packages/cli-engine/src/context.ts index 554dfdd6..ba4926f2 100644 --- a/packages/cli-engine/src/context.ts +++ b/packages/cli-engine/src/context.ts @@ -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. diff --git a/packages/cli-engine/src/execution/command-context.ts b/packages/cli-engine/src/execution/command-context.ts index bac5f6df..704c1d60 100644 --- a/packages/cli-engine/src/execution/command-context.ts +++ b/packages/cli-engine/src/execution/command-context.ts @@ -123,6 +123,7 @@ export function makeContext( invocation: Invocation, def: AnyCommand, config: unknown, + configFile: string | null, capabilities: CommandCapabilities, ): CommandContext { const state = invocation.state; @@ -164,6 +165,7 @@ export function makeContext( let api: ManagementApiClient | undefined; const context: CommandContext = { config, + configFile, present: present as CommandContext["present"], activeCredential: (): Promise => invocation.runtime.credentialManager?.activeCredential() ?? diff --git a/packages/cli-engine/src/execution/engine.ts b/packages/cli-engine/src/execution/engine.ts index 2bed1cc6..cdf1e12a 100644 --- a/packages/cli-engine/src/execution/engine.ts +++ b/packages/cli-engine/src/execution/engine.ts @@ -275,6 +275,7 @@ type ErasedServerHandler = ( readonly cwd: string; readonly env: Readonly>; readonly config: unknown; + readonly configFile: string | null; }, ) => Promise; @@ -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") { @@ -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( diff --git a/packages/cli-engine/src/execution/needs.ts b/packages/cli-engine/src/execution/needs.ts index b45b5825..7b00318f 100644 --- a/packages/cli-engine/src/execution/needs.ts +++ b/packages/cli-engine/src/execution/needs.ts @@ -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; @@ -90,6 +93,7 @@ export async function checkNeeds( return { kind: "ok", config: undefined, + configFile: null, spawnCredential: credentials.spawnCredential, }; } @@ -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, loaded: LoadedConfig, invocation: Invocation, + namedFile: string, configFile: string, ): NeedsOutcome { const raw = loaded.sections[section.name]; @@ -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: [ { @@ -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 diff --git a/packages/cli-engine/tests/config.test.ts b/packages/cli-engine/tests/config.test.ts index 65bfad5b..d8868fe9 100644 --- a/packages/cli-engine/tests/config.test.ts +++ b/packages/cli-engine/tests/config.test.ts @@ -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) { + 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([]); + }); +}); diff --git a/packages/cli-engine/tests/fixtures/config/discovered/prisma.config.ts b/packages/cli-engine/tests/fixtures/config/discovered/prisma.config.ts new file mode 100644 index 00000000..8d911cff --- /dev/null +++ b/packages/cli-engine/tests/fixtures/config/discovered/prisma.config.ts @@ -0,0 +1,5 @@ +import { definePrismaConfig } from "@prisma/cli-engine"; + +export default definePrismaConfig({ + toy: { greeting: "found in cwd" }, +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index 20c240fe..84976467 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.4.0", + "@prisma/cli-engine": "workspace:0.5.0", "@prisma/composer-cli": "0.20.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 2f69d6a1..1a6804c6 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.4.0", + "@prisma/cli-engine": "workspace:0.5.0", "@prisma/composer-cli": "0.20.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aadcbc8..82a7295a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.4.0 + specifier: workspace:0.5.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.20.0 @@ -207,7 +207,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.4.0 + specifier: workspace:0.5.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.20.0