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
24 changes: 24 additions & 0 deletions packages/1-framework/3-tooling/cli/src/orm/cli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fstatSync } from 'node:fs';
import { ifDefined } from '@internal/utils/defined';
import type { Cli, HostProcess, MountedTree, Runtime } from '@prisma/cli-engine';
import { createCli, telemetryCommandGroup } from '@prisma/cli-engine';
Expand Down Expand Up @@ -117,11 +118,33 @@ export function createOrmCli(): Cli {
});
}

/**
* The file identity behind a stream's fd, or undefined where the host exposes
* no fd (a harness stream) or the fd cannot be stat-ed.
*/
function streamFileIdentity(
stream: HostProcess['stdout'] | HostProcess['stderr'],
): string | undefined {
if (!('fd' in stream) || typeof stream.fd !== 'number') {
return undefined;
}
try {
const stat = fstatSync(stream.fd);
return `${stat.dev}:${stat.ino}`;
} catch {
return undefined;
}
}

/**
* Everything environmental the engine is given, adapted from the host process
* once. The engine owns signal policy; the bin is dumb wiring.
*/
export function runtimeFromProcess(proc: HostProcess): Runtime {
const stdoutFile = streamFileIdentity(proc.stdout);
const stderrFile = streamFileIdentity(proc.stderr);
const outputStreamsShareDevice =
stdoutFile !== undefined && stderrFile !== undefined ? stdoutFile === stderrFile : undefined;
return {
stdout: { write: (text) => void proc.stdout.write(text) },
// The terminal width the drawings get to use, read once with everything
Expand All @@ -139,6 +162,7 @@ export function runtimeFromProcess(proc: HostProcess): Runtime {
stdout: proc.stdout.isTTY === true,
stderr: proc.stderr.isTTY === true,
},
...ifDefined('outputStreamsShareDevice', outputStreamsShareDevice),
host: {
runtime: { name: 'node', version: proc.version },
platform: proc.platform,
Expand Down
81 changes: 80 additions & 1 deletion packages/1-framework/3-tooling/cli/test/orm/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { closeSync, openSync } from 'node:fs';
import type { HostProcess, LoadedConfig } from '@prisma/cli-engine';
import { createTestCli } from '@prisma/cli-engine/testing';
import { join } from 'pathe';
import { describe, expect, it } from 'vitest';
import { BIN_COMMANDS, BIN_GROUPS, createOrmCli, runOrmCli } from '../../src/orm/cli';
import {
BIN_COMMANDS,
BIN_GROUPS,
createOrmCli,
runOrmCli,
runtimeFromProcess,
} from '../../src/orm/cli';
import { ormCommandFamily } from '../../src/orm/family';
import { createTestProjectDir } from '../utils/test-project-dir';

Expand Down Expand Up @@ -205,6 +212,78 @@ function processWithUnreadableCwd(stderr: string[]): HostProcess {
};
}

function processWithOutputStreams(
stdout: HostProcess['stdout'] & { readonly fd?: number },
stderr: HostProcess['stderr'] & { readonly fd?: number },
): HostProcess {
return {
argv: ['node', 'prisma-next'],
env: {},
version: process.version,
versions: process.versions,
platform: process.platform,
arch: process.arch,
cwd: () => '/',
stdout,
stderr,
stdin: { [Symbol.asyncIterator]: () => [][Symbol.iterator]() as never },
on: () => undefined,
off: () => undefined,
exit: () => {
throw new Error('exit must not be called');
},
};
}

describe('runtimeFromProcess', () => {
it('reports that stdout and stderr share a device when their fds name the same file', () => {
const dir = createTestProjectDir('orm-share-device-same');
const first = openSync(join(dir, 'screen.log'), 'w');
const second = openSync(join(dir, 'screen.log'), 'w');
try {
const runtime = runtimeFromProcess(
processWithOutputStreams(
{ write: () => true, fd: first },
{ write: () => true, fd: second },
),
);
expect(runtime.outputStreamsShareDevice).toBe(true);
} finally {
closeSync(first);
closeSync(second);
}
});

it('reports separate devices when the fds name different files', () => {
const dir = createTestProjectDir('orm-share-device-split');
const out = openSync(join(dir, 'out.log'), 'w');
const err = openSync(join(dir, 'err.log'), 'w');
try {
const runtime = runtimeFromProcess(
processWithOutputStreams({ write: () => true, fd: out }, { write: () => true, fd: err }),
);
expect(runtime.outputStreamsShareDevice).toBe(false);
} finally {
closeSync(out);
closeSync(err);
}
});

it('leaves the answer absent when a stream exposes no fd', () => {
const runtime = runtimeFromProcess(
processWithOutputStreams({ write: () => true }, { write: () => true }),
);
expect(runtime.outputStreamsShareDevice).toBeUndefined();
});

it('leaves the answer absent when an fd cannot be stat-ed', () => {
const runtime = runtimeFromProcess(
processWithOutputStreams({ write: () => true, fd: -1 }, { write: () => true, fd: -1 }),
);
expect(runtime.outputStreamsShareDevice).toBeUndefined();
});
});

describe('runOrmCli', () => {
it('reports a startup failure as a structured line instead of a raw stack trace', async () => {
const stderr: string[] = [];
Expand Down
18 changes: 17 additions & 1 deletion packages/1-framework/3-tooling/config-loader/src/load.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { realpathSync } from 'node:fs';
import { access } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import {
hasCurrentConfigFormatVersion,
type PrismaNextConfig,
Expand Down Expand Up @@ -190,6 +193,19 @@ function toConfigLoadFailure(error: unknown, configPath?: string): CliStructured
* fail the load: they are returned as section-tagged diagnostics so commands
* fail only on the sections they read (via {@link requireConfigSections}).
*/
/**
* Imports c12 by the realpath of its entry file. Under pnpm, resolving the
* bare specifier can pin c12 at its symlinked node_modules path — Node's
* synchronous ESM linker and some resolver states skip the realpath step —
* and from that path c12's own dependencies (`dotenv`) do not resolve, which
* fails every config load with CONFIG.EVALUATION_FAILED. Anchoring the import
* at the real on-disk location keeps every transitive resolution working.
*/
async function importC12(): Promise<typeof import('c12')> {
const entry = realpathSync(createRequire(import.meta.url).resolve('c12'));
return await import(pathToFileURL(entry).href);
}

export async function loadConfig(
configPath?: string,
options?: { readonly cwd?: string },
Expand All @@ -214,7 +230,7 @@ export async function loadConfig(

let result: Awaited<ReturnType<typeof import('c12').loadConfig<Record<string, unknown>>>>;
try {
const c12 = await import('c12');
const c12 = await importC12();
result = await c12.loadConfig<Record<string, unknown>>({
name: discoveryName,
...ifDefined('configFile', resolvedConfigPath),
Expand Down
Loading