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
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

import { pruneServerPrismaArtifactsForTarget } from './buildServerBinaryArtifactPayload.js';

const PRISMA_NODE_ENGINE_FILE_NAMES = [
'libquery_engine-debian-openssl-3.0.x.so.node',
'libquery_engine-linux-arm64-openssl-3.0.x.so.node',
'libquery_engine-darwin.dylib.node',
'libquery_engine-darwin-arm64.dylib.node',
'query_engine-windows.dll.node',
];

const PRISMA_RUNTIME_FILE_NAMES = [
// Providers actually reachable via ServerDbProvider ('sqlite' | 'mysql') plus the always-generated
// 'postgres' default client -- these must survive pruning.
'query_engine_bg.postgresql.wasm-base64.js',
'query_engine_bg.postgresql.wasm-base64.mjs',
'query_engine_bg.mysql.wasm-base64.js',
'query_engine_bg.mysql.wasm-base64.mjs',
'query_engine_bg.sqlite.wasm-base64.js',
'query_engine_bg.sqlite.wasm-base64.mjs',
// Providers never reachable through resolveRequestedServerDbProviders/BuildDbProvider -- must be pruned.
'query_engine_bg.cockroachdb.wasm-base64.js',
'query_engine_bg.cockroachdb.wasm-base64.mjs',
'query_engine_bg.sqlserver.wasm-base64.js',
'query_engine_bg.sqlserver.wasm-base64.mjs',
// Sourcemaps -- never needed at runtime, must be pruned regardless of provider.
'binary.js.map',
'binary.mjs.map',
'index-browser.js.map',
];

const tempDirs: string[] = [];

async function createTempPayloadDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'build-server-binary-artifact-payload-prisma-prune-'));
tempDirs.push(dir);
return dir;
}

async function writeFixtureFile(path: string): Promise<void> {
await mkdir(join(path, '..'), { recursive: true });
await writeFile(path, 'fixture', 'utf8');
}

async function buildFakePrismaClientDirTree(payloadDir: string, relativeDir: string): Promise<string> {
const dirPath = join(payloadDir, relativeDir);
for (const fileName of PRISMA_NODE_ENGINE_FILE_NAMES) {
await writeFixtureFile(join(dirPath, fileName));
}
return dirPath;
}

async function buildFakePrismaClientRuntimeDirTree(payloadDir: string): Promise<string> {
const dirPath = join(payloadDir, 'node_modules', '@prisma', 'client', 'runtime');
for (const fileName of PRISMA_RUNTIME_FILE_NAMES) {
await writeFixtureFile(join(dirPath, fileName));
}
// A file that is neither a per-provider engine file nor a sourcemap must survive untouched.
await writeFixtureFile(join(dirPath, 'index.js'));
return dirPath;
}

describe('pruneServerPrismaArtifactsForTarget', () => {
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map(async (dir) => {
await rm(dir, { recursive: true, force: true });
}));
});

it('keeps only the linux-arm64 engine file in each generated provider client directory', async () => {
const payloadDir = await createTempPayloadDir();
const sqliteClientDir = await buildFakePrismaClientDirTree(payloadDir, join('generated', 'sqlite-client'));
const mysqlClientDir = await buildFakePrismaClientDirTree(payloadDir, join('generated', 'mysql-client'));
const dotPrismaClientDir = await buildFakePrismaClientDirTree(payloadDir, join('node_modules', '.prisma', 'client'));

await pruneServerPrismaArtifactsForTarget({
payloadDir,
target: { bunTarget: 'bun-linux-arm64', os: 'linux', arch: 'arm64', exeExt: '' },
});

for (const dir of [sqliteClientDir, mysqlClientDir, dotPrismaClientDir]) {
const remaining = await readdir(dir);
expect(remaining).toEqual(['libquery_engine-linux-arm64-openssl-3.0.x.so.node']);
}
});

it('keeps only the darwin-arm64 engine file for a darwin-arm64 target', async () => {
const payloadDir = await createTempPayloadDir();
const sqliteClientDir = await buildFakePrismaClientDirTree(payloadDir, join('generated', 'sqlite-client'));

await pruneServerPrismaArtifactsForTarget({
payloadDir,
target: { bunTarget: 'bun-darwin-arm64', os: 'darwin', arch: 'arm64', exeExt: '' },
});

const remaining = await readdir(sqliteClientDir);
expect(remaining).toEqual(['libquery_engine-darwin-arm64.dylib.node']);
});

it('prunes cockroachdb/sqlserver runtime WASM engines and all sourcemaps from @prisma/client/runtime, keeping reachable providers and unrelated files', async () => {
const payloadDir = await createTempPayloadDir();
const runtimeDir = await buildFakePrismaClientRuntimeDirTree(payloadDir);

await pruneServerPrismaArtifactsForTarget({
payloadDir,
target: { bunTarget: 'bun-linux-arm64', os: 'linux', arch: 'arm64', exeExt: '' },
});

const remaining = await readdir(runtimeDir);
expect(remaining.sort()).toEqual([
'index.js',
'query_engine_bg.mysql.wasm-base64.js',
'query_engine_bg.mysql.wasm-base64.mjs',
'query_engine_bg.postgresql.wasm-base64.js',
'query_engine_bg.postgresql.wasm-base64.mjs',
'query_engine_bg.sqlite.wasm-base64.js',
'query_engine_bg.sqlite.wasm-base64.mjs',
].sort());
});

it('is a no-op when the expected Prisma directories are absent', async () => {
const payloadDir = await createTempPayloadDir();

await expect(pruneServerPrismaArtifactsForTarget({
payloadDir,
target: { bunTarget: 'bun-linux-arm64', os: 'linux', arch: 'arm64', exeExt: '' },
})).resolves.toBeUndefined();
});

it('rejects instead of silently skipping pruning when an expected Prisma directory path is actually a file', async () => {
const payloadDir = await createTempPayloadDir();
const dotPrismaClientPath = join(payloadDir, 'node_modules', '.prisma', 'client');
await mkdir(join(dotPrismaClientPath, '..'), { recursive: true });
// A file where a directory is expected (e.g. from a corrupted staging step) must surface as an
// error, not be silently treated as "directory has no files to prune".
await writeFile(dotPrismaClientPath, 'not a directory', 'utf8');

await expect(pruneServerPrismaArtifactsForTarget({
payloadDir,
target: { bunTarget: 'bun-linux-arm64', os: 'linux', arch: 'arm64', exeExt: '' },
})).rejects.toMatchObject({ code: 'ENOTDIR' });
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cp, mkdir, rm, stat } from 'node:fs/promises';
import { cp, mkdir, readdir, rm, stat } from 'node:fs/promises';
import type { Dirent } from 'node:fs';
import { join } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';

Expand Down Expand Up @@ -33,6 +34,77 @@ async function ensureFile(path: string, message: string): Promise<void> {
}
}

// Prisma client directories are always staged before pruning runs (resolveServerBinarySidecarEntries
// asserts their existence), so a missing directory here is not itself an error worth failing the
// build over -- but any other readdir failure (permission denied, path is actually a file, ...) must
// not be silently treated as "nothing to prune": that would let foreign-platform engines, unreachable
// WASM engines, or sourcemaps survive into the release artifact while validateServerPrismaEnginesForTarget
// still passes (it only checks that the *kept* engine file exists, not that pruning actually ran).
async function readdirOrEmptyIfMissing(directoryPath: string): Promise<Dirent<string>[]> {
return readdir(directoryPath, { withFileTypes: true, encoding: 'utf8' }).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return [];
throw error;
});
}

// Each generated Prisma client directory (generated/*-client, node_modules/.prisma/client) ships a
// native query-engine file per platform (binaryTargets in schema.prisma lists all 5: linux-x64,
// linux-arm64, darwin-x64, darwin-arm64, windows-x64), but a single-platform release payload only
// ever runs on the one platform it was built for. Keep only that target's engine file.
async function pruneNonTargetPrismaEngineFiles(directoryPath: string, target: BinaryTarget): Promise<void> {
const keepFileName = resolvePrismaEngineFileNameForTarget(target);
const entries = await readdirOrEmptyIfMissing(directoryPath);
for (const entry of entries) {
if (!entry.isFile()) continue;
const isEngineFile = entry.name.startsWith('libquery_engine-') || entry.name.startsWith('query_engine-');
if (isEngineFile && entry.name !== keepFileName) {
await rm(join(directoryPath, entry.name), { force: true });
}
}
}

// @prisma/client's runtime/ directory bundles WASM query engines for every database Prisma
// supports (postgresql, mysql, sqlite, cockroachdb, sqlserver) plus .map sourcemaps for every
// bundled format, regardless of which providers this build actually generated clients for.
// ServerDbProvider (serverSidecars.ts) is only ever 'sqlite' | 'mysql', and a postgres client is
// always generated as the default -- cockroachdb and sqlserver are never reachable, and
// sourcemaps are never needed by a production binary. Delete both classes unconditionally.
const PRISMA_RUNTIME_NEVER_REACHABLE_PROVIDER_MARKERS = ['.cockroachdb.', '.sqlserver.'];

async function pruneUnreachablePrismaRuntimeFiles(runtimeDirectoryPath: string): Promise<void> {
const entries = await readdirOrEmptyIfMissing(runtimeDirectoryPath);
for (const entry of entries) {
if (!entry.isFile()) continue;
const isNeverReachableProviderFile = PRISMA_RUNTIME_NEVER_REACHABLE_PROVIDER_MARKERS.some(
(marker) => entry.name.includes(marker),
);
const isSourceMap = entry.name.endsWith('.map');
if (isNeverReachableProviderFile || isSourceMap) {
await rm(join(runtimeDirectoryPath, entry.name), { force: true });
}
}
}

export async function pruneServerPrismaArtifactsForTarget({
payloadDir,
target,
}: {
payloadDir: string;
target: BinaryTarget;
}): Promise<void> {
await pruneNonTargetPrismaEngineFiles(join(payloadDir, 'node_modules', '.prisma', 'client'), target);

const generatedDir = join(payloadDir, 'generated');
const generatedEntries = await readdirOrEmptyIfMissing(generatedDir);
for (const entry of generatedEntries) {
if (entry.isDirectory() && entry.name.endsWith('-client')) {
await pruneNonTargetPrismaEngineFiles(join(generatedDir, entry.name), target);
}
}

await pruneUnreachablePrismaRuntimeFiles(join(payloadDir, 'node_modules', '@prisma', 'client', 'runtime'));
}

async function validateServerPrismaEnginesForTarget({
payloadDir,
target,
Expand Down Expand Up @@ -159,6 +231,8 @@ export async function buildServerBinaryArtifactPayload({
});
}

await pruneServerPrismaArtifactsForTarget({ payloadDir, target });

await validateServerPrismaEnginesForTarget({
payloadDir,
target,
Expand Down