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 });