From c6f11deae0e4f2f16e5cd7ed8131862d8b93c97c Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 28 Sep 2026 10:10:34 +0200 Subject: [PATCH] fix: install MongoDB projects with npm without ERESOLVE alchemy declares an optional peer dependency on mongodb 6 and @prisma/orm-mongo requires mongodb 7. npm stops on that conflict. The scaffold now overrides the mongodb peer of alchemy for npm, and allows it for pnpm. The scaffolded project never loads the alchemy modules that import mongodb. Fixes #114 Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- src/tasks/install.ts | 21 +++++- .../_package-manager/pnpm-workspace.yaml.hbs | 5 ++ tests/e2e/create-prisma.e2e.test.ts | 68 ++++++++++++++++++- tests/install.test.ts | 10 ++- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/tasks/install.ts b/src/tasks/install.ts index 5fdc870..0165f72 100644 --- a/src/tasks/install.ts +++ b/src/tasks/install.ts @@ -2,6 +2,7 @@ import { Effect, FileSystem } from "effect"; import path from "node:path"; import { + dependencyVersionMap, getCreateTemplateDependencies, getDependencyVersion, PRISMA_DENO_CLI_PACKAGE, @@ -17,7 +18,7 @@ type PackageJson = { devDependencies?: Record; scripts?: Record; resolutions?: Record; - overrides?: Record; + overrides?: Record>; [key: string]: unknown; }; @@ -156,6 +157,21 @@ export const addPackageDependencyEffect = Effect.fn("Dependencies.add")(function yield* writePackageJson(packageJsonPath, packageJson); }); +// alchemy declares an optional peer on mongodb 6 for resources the scaffold +// never loads, and @prisma/orm-mongo requires mongodb 7. npm fails the install +// on that conflict; the other package managers only warn. +const resolveAlchemyMongoPeerForNpm = Effect.fn("Dependencies.resolveAlchemyMongoPeerForNpm")( + function* (projectDir: string) { + const packageJsonPath = path.join(projectDir, "package.json"); + const packageJson = yield* readPackageJson(packageJsonPath); + packageJson.overrides = { + ...packageJson.overrides, + alchemy: { mongodb: dependencyVersionMap.mongodb }, + }; + yield* writePackageJson(packageJsonPath, packageJson); + }, +); + export const writePrismaDependenciesEffect = Effect.fn("Dependencies.writePrisma")(function* ( provider: DatabaseProvider, packageManager: PackageManager, @@ -187,6 +203,9 @@ export const writePrismaDependenciesEffect = Effect.fn("Dependencies.writePrisma projectDir: path.join(projectDir, "packages/database"), }); } + if (provider === "mongo" && packageManager === "npm") { + yield* resolveAlchemyMongoPeerForNpm(projectDir); + } }); export const writeCreateTemplateDependenciesEffect = Effect.fn("Dependencies.writeTemplate")( diff --git a/templates/create/_package-manager/pnpm-workspace.yaml.hbs b/templates/create/_package-manager/pnpm-workspace.yaml.hbs index e1af88d..9f3ab37 100644 --- a/templates/create/_package-manager/pnpm-workspace.yaml.hbs +++ b/templates/create/_package-manager/pnpm-workspace.yaml.hbs @@ -18,4 +18,9 @@ minimumReleaseAgeExclude: - "@prisma/*" overrides: effect: "4.0.0-rc.115" +{{#if (eq provider "mongo")}} +peerDependencyRules: + allowedVersions: + "alchemy>mongodb": "7" +{{/if}} {{/if}} diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index fba5adf..586fa63 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -13,6 +13,8 @@ import { import { tmpdir } from "node:os"; import path from "node:path"; +import { MongoMemoryReplSet } from "mongodb-memory-server"; + import { runCreateCommand } from "../../src/commands/create"; import { dependencyVersionMap } from "../../src/constants/dependencies"; import type { CreateCommandResult } from "../../src/result"; @@ -128,11 +130,16 @@ async function fetchUntilResponding(url: string, deadline: number): Promise = {}, +) { const process = Bun.spawn({ cmd: ["node", "dist/server.mjs"], cwd: projectDir, - env: { ...Bun.env, PORT: String(port) }, + env: { ...Bun.env, ...env, PORT: String(port) }, stdout: "pipe", stderr: "pipe", }); @@ -682,6 +689,63 @@ describe("create-prisma e2e", () => { TEST_TIMEOUT, ); + test( + "installs a MongoDB app with npm and runs it against MongoDB", + async () => { + const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-mongo-npm-e2e-")); + tempRoots.push(rootDir); + const { result, exitCode } = await runCreatePrismaJson(rootDir, [ + "mongo-npm-app", + "--template", + "minimal", + "--provider", + "mongodb", + "--authoring", + "psl", + "--package-manager", + "npm", + "--no-deploy", + "--json", + ]); + + expect(result).toMatchObject({ + ok: true, + project: { databaseProvider: "mongo", packageManager: "npm" }, + deployment: null, + }); + expect(exitCode).toBe(0); + + const projectDir = path.join(rootDir, "mongo-npm-app"); + const packageJson = JSON.parse( + await readFile(path.join(projectDir, "package.json"), "utf8"), + ) as Record; + expect(packageJson.overrides.alchemy).toEqual({ mongodb: dependencyVersionMap.mongodb }); + const installedDriver = JSON.parse( + await readFile(path.join(projectDir, "node_modules/mongodb/package.json"), "utf8"), + ) as { version: string }; + expect(installedDriver.version).toMatch(/^7\./); + expect( + await pathExists(path.join(projectDir, "node_modules/alchemy/node_modules/mongodb")), + ).toBe(false); + + await runCommand(projectDir, ["npm", "run", "contract:emit"]); + await runCommand(projectDir, ["npm", "run", "build"]); + await runCommand(projectDir, ["npx", "tsc", "--noEmit"]); + + const mongod = await MongoMemoryReplSet.create({ replSet: { count: 1 } }); + try { + const body = await verifyBuiltServer(projectDir, 46_160, 200, { + DATABASE_URL: mongod.getUri("app"), + }); + const { users } = JSON.parse(body) as { users: Array<{ name: string }> }; + expect(users.map((user) => user.name)).toEqual(["Alice", "Bob", "Carol"]); + } finally { + await mongod.stop(); + } + }, + TEST_TIMEOUT, + ); + test( "generates and checks a minimal Deno Prisma Postgres app", async () => { diff --git a/tests/install.test.ts b/tests/install.test.ts index b258ea2..9729cbb 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -577,13 +577,21 @@ describe("generated templates", () => { ' - "@prisma/*"', "overrides:", ` effect: "${dependencyVersionMap.effect}"`, + ...(provider === "mongo" + ? ["peerDependencyRules:", " allowedVersions:", ' "alchemy>mongodb": "7"'] + : []), "", ].join("\n"), ); } else if (packageManager === "yarn") { expect(packageJson.resolutions?.effect).toBe(dependencyVersionMap.effect); } else { - expect(packageJson.overrides?.effect).toBe(dependencyVersionMap.effect); + expect(packageJson.overrides).toEqual({ + effect: dependencyVersionMap.effect, + ...(provider === "mongo" && packageManager === "npm" + ? { alchemy: { mongodb: dependencyVersionMap.mongodb } } + : {}), + }); } } finally { await rm(projectDir, { recursive: true, force: true });