diff --git a/packages/client/src/generation/TSClient/TSClient.ts b/packages/client/src/generation/TSClient/TSClient.ts index c077eb90b271..f09e07ec07e4 100644 --- a/packages/client/src/generation/TSClient/TSClient.ts +++ b/packages/client/src/generation/TSClient/TSClient.ts @@ -110,7 +110,6 @@ export class TSClient implements Generatable { const code = `${commonCodeJS({ ...this.options, browser: false })} ${buildRequirePath(edge)} -${buildDirname(edge, relativeOutdir)} /** * Enums @@ -132,8 +131,8 @@ ${buildDMMF(dataProxy && engineProtocol === 'graphql', this.options.document)} * Create the Client */ const config = ${JSON.stringify(config, null, 2)} -config.dirname = dirname config.document = dmmf +${buildDirname(edge, relativeOutdir)} ${await buildInlineSchema(dataProxy, schemaPath)} ${buildInlineDatasource(dataProxy, datasources)} ${buildInjectableEdgeEnv(edge, datasources)} diff --git a/packages/client/src/generation/TSClient/common.ts b/packages/client/src/generation/TSClient/common.ts index db3cb9d1e9fc..7f3cc7fc086c 100644 --- a/packages/client/src/generation/TSClient/common.ts +++ b/packages/client/src/generation/TSClient/common.ts @@ -34,7 +34,6 @@ import { objectEnumValues, makeStrictEnum, Extensions, - findSync } from '${runtimeDir}/edge-esm.js'` : browser ? ` @@ -63,7 +62,7 @@ const { objectEnumValues, makeStrictEnum, Extensions, - findSync + warnOnce, } = require('${runtimeDir}/${runtimeName}') ` } diff --git a/packages/client/src/generation/utils/buildDirname.ts b/packages/client/src/generation/utils/buildDirname.ts index 967d84afba9e..9ccc11e6c301 100644 --- a/packages/client/src/generation/utils/buildDirname.ts +++ b/packages/client/src/generation/utils/buildDirname.ts @@ -1,5 +1,3 @@ -import path from 'path' - /** * Builds a `dirname` variable that holds the location of the generated client. * @param edge @@ -24,37 +22,28 @@ export function buildDirname(edge: boolean, relativeOutdir: string) { * moved and copied out of its original spot. It all fails, it falls-back to * `findSync`, when `__dirname` is not available (eg. bundle, electron) or * nothing has been found around `__dirname`. - * @param defaultRelativeOutdir + * @param relativeOutdir * @param runtimePath * @returns */ -function buildDirnameFind(defaultRelativeOutdir: string) { - // potential client location on serverless envs - const serverlessRelativeOutdir = defaultRelativeOutdir.split(path.sep).slice(1).join(path.sep) - +function buildDirnameFind(relativeOutdir: string) { return ` const fs = require('fs') -// some frameworks or bundlers replace or totally remove __dirname -const hasDirname = typeof __dirname !== 'undefined' && __dirname !== '/' - -// will work in most cases, ie. if the client has not been bundled -const regularDirname = hasDirname && fs.existsSync(path.join(__dirname, 'schema.prisma')) && __dirname - -// if the client has been bundled, we need to look for the folders -const foundDirname = !regularDirname && findSync(process.cwd(), [ - ${defaultRelativeOutdir ? `${JSON.stringify(defaultRelativeOutdir)},` : ''} - ${serverlessRelativeOutdir ? `${JSON.stringify(serverlessRelativeOutdir)},` : ''} -], ['d'], ['d'], 1)[0] - -const dirname = regularDirname || foundDirname || __dirname` +config.dirname = __dirname +if (!fs.existsSync(path.join(__dirname, 'schema.prisma'))) { + warnOnce('bundled-warning-1', 'Your generated Prisma Client could not immediately find its \`schema.prisma\`, falling back to finding it via the current working directory.') + warnOnce('bundled-warning-2', 'We are interested in learning about your project setup. We\\'d appreciate if you could take the time to share some information with us.') + warnOnce('bundled-warning-3', 'Please help us by answering a few questions: https://pris.ly/bundler-investigation') + config.dirname = path.join(process.cwd(), ${JSON.stringify(relativeOutdir)}) + config.isBundled = true +}` } -// TODO: 👆 all this complexity could fade away if we embed the schema /** * Builds a simple `dirname` for when it is not important to have one. * @returns */ function buildDirnameDefault() { - return `const dirname = '/'` + return `config.dirname = '/'` } diff --git a/packages/client/src/generation/utils/buildWarnEnvConflicts.ts b/packages/client/src/generation/utils/buildWarnEnvConflicts.ts index a1df4ded66d7..fe44067edce0 100644 --- a/packages/client/src/generation/utils/buildWarnEnvConflicts.ts +++ b/packages/client/src/generation/utils/buildWarnEnvConflicts.ts @@ -13,7 +13,7 @@ export function buildWarnEnvConflicts(edge: boolean, runtimeDir: string, runtime const { warnEnvConflicts } = require('${runtimeDir}/${runtimeName}') warnEnvConflicts({ - rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(dirname, config.relativeEnvPaths.rootEnvPath), - schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(dirname, config.relativeEnvPaths.schemaEnvPath) + rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.rootEnvPath), + schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.schemaEnvPath) })` } diff --git a/packages/client/src/runtime/getPrismaClient.ts b/packages/client/src/runtime/getPrismaClient.ts index bba985cc6804..8e2f622f00ac 100644 --- a/packages/client/src/runtime/getPrismaClient.ts +++ b/packages/client/src/runtime/getPrismaClient.ts @@ -282,6 +282,13 @@ export interface GetPrismaClientConfig { * @remarks used to error for Vercel/Netlify for schema caching issues */ ciName?: string + + /** + * Information about whether we have not found a schema.prisma file in the + * default location, and that we fell back to finding the schema.prisma file + * in the current working directory. This usually means it has been bundled. + */ + isBundled?: boolean } const TX_ID = Symbol.for('prisma.client.transaction.id') @@ -440,6 +447,7 @@ export function getPrismaClient(config: GetPrismaClientConfig) { tracingConfig: this._tracingConfig, logEmitter: logEmitter, engineProtocol, + isBundled: config.isBundled, } debug('clientVersion', config.clientVersion) diff --git a/packages/client/src/runtime/index.ts b/packages/client/src/runtime/index.ts index 2b4fcf9d7df8..74aa09dc33e0 100644 --- a/packages/client/src/runtime/index.ts +++ b/packages/client/src/runtime/index.ts @@ -20,7 +20,6 @@ export { objectEnumValues } from './object-enums' export { makeDocument, PrismaClientValidationError, transformDocument, unpack } from './query' export { makeStrictEnum } from './strictEnum' export type { DecimalJsLike } from './utils/decimalJsLike' -export { findSync } from './utils/find' export { NotFoundError } from './utils/rejectOnNotFound' export { warnEnvConflicts } from './warnEnvConflicts' export { Debug } from '@prisma/debug' @@ -40,6 +39,7 @@ export { decompressFromBase64 } export { Types } export { Extensions } +export { warnOnce } from '@prisma/internals' /** * Payload is already exported via Types but tsc will complain that it isn't reachable diff --git a/packages/client/src/runtime/utils/find.ts b/packages/client/src/runtime/utils/find.ts deleted file mode 100644 index 42b921ecd9db..000000000000 --- a/packages/client/src/runtime/utils/find.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* eslint-disable @typescript-eslint/no-inferrable-types */ -import fs from 'fs' -import path from 'path' - -const readdirSync = fs.readdirSync -const realpathSync = fs.realpathSync -const statSync = fs.statSync - -type ItemType = 'd' | 'f' | 'l' -type Handler = (base: string, item: string, type: ItemType) => boolean | string - -/** - * Transform a dirent to a file type - * @param dirent - * @returns - */ -function direntToType(dirent: fs.Dirent | fs.Stats) { - return dirent.isFile() ? 'f' : dirent.isDirectory() ? 'd' : dirent.isSymbolicLink() ? 'l' : undefined -} - -/** - * Is true if at least one matched - * @param string to match against - * @param regexs to be matched with - * @returns - */ -function isMatched(string: string, regexs: (RegExp | string)[]) { - for (const regex of regexs) { - if (typeof regex === 'string') { - if (string.includes(regex)) { - return true - } - } else if (regex.exec(string)) { - return true - } - } - - return false -} - -/** - * Find paths that match a set of regexes - * @param root to start from - * @param match to match against - * @param types to select files, folders, links - * @param deep to recurse in the directory tree - * @param limit to limit the results - * @param handler to further filter results - * @param found to add to already found - * @param seen to add to already seen - * @returns found paths (symlinks preserved) - */ -export function findSync( - root: string, - match: (RegExp | string)[], - types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'], - deep: ('d' | 'l')[] = [], - limit: number = Infinity, - handler: Handler = () => true, - found: string[] = [], - seen: Record = {}, -) { - try { - const realRoot = realpathSync(root) - - // we make sure not to loop infinitely - if (seen[realRoot]) { - return found - } - - // we stop if we found enough results - if (limit - found.length <= 0) { - return found - } - - // we check that the root is a directory - if (direntToType(statSync(realRoot)) !== 'd') { - return found - } - - // we list the items in the current root - const items = readdirSync(root, { withFileTypes: true }) - - seen[realRoot] = true - for (const item of items) { - // we get the file info for each item - const itemName = item.name - const itemType = direntToType(item) - const itemPath = path.join(root, item.name) - - // if the item is one of the selected - if (itemType && types.includes(itemType)) { - // if the path of an item has matched - if (isMatched(itemPath, match)) { - const value = handler(root, itemName, itemType) - - // if we changed the path value - if (typeof value === 'string') { - found.push(value) - } - // if we kept the default path - else if (value === true) { - found.push(itemPath) - } - } - } - - // dive within the directory tree - if (deep.includes(itemType as any)) { - // we recurse and continue mutating `found` - findSync(itemPath, match, types, deep, limit, handler, found, seen) - } - } - } catch {} - - return found -} - -/** - * Like `findSync` but moves to the parent folder if nothing is found - * @param root to start from - * @param match to match against - * @param types to select files, folders, links - * @param deep to recurse in the directory tree - * @param limit to limit the results - * @param filter to further filter results - * @param found to add to already found - * @param seen to add to already seen - * @returns found paths (symlinks preserved) - */ -export function findUpSync( - root: string, - match: (RegExp | string)[], - types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'], - deep: ('d' | 'l')[] = [], - limit: number = Infinity, - handler: Handler = () => true, - found: string[] = [], - seen: Record = {}, -) { - // stop if we cannot go any higher than this root - if (path.resolve(root) === path.resolve(root, '..')) { - return found - } - - findSync(root, match, types, deep, limit, handler, found, seen) - - if (found.length === 0) { - const parent = path.join(root, '..') - - findUpSync(parent, match, types, deep, limit, handler, found, seen) - } - - return found -} - -/** - * Find paths that match a set of regexes - * @param root to start from - * @param match to match against - * @param types to select files, folders, links - * @param deep to recurse in the directory tree - * @param limit to limit the results - * @param filter to further filter results - * @param found to add to already found - * @param seen to add to already seen - * @returns found paths (symlinks preserved) - */ -export async function findAsync( - root: string, - match: (RegExp | string)[], - types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'], - deep: ('d' | 'l')[] = [], - limit: number = Infinity, - handler: Handler = () => true, - found: string[] = [], - seen: Record = {}, -) { - try { - const realRoot = await fs.promises.realpath(root) - - // we make sure not to loop infinitely - if (seen[realRoot]) { - return found - } - - // we stop if we found enough results - if (limit - found.length <= 0) { - return found - } - - // we check that the root is a directory - if (direntToType(await fs.promises.stat(realRoot)) !== 'd') { - return found - } - - // we list the items in the current root - const items = await fs.promises.readdir(root, { withFileTypes: true }) - - seen[realRoot] = true - for (const item of items) { - // we get the file info for each item - const itemName = item.name - const itemType = direntToType(item) - const itemPath = path.join(root, item.name) - - // if the item is one of the selected - if (itemType && types.includes(itemType)) { - // if the path of an item has matched - if (isMatched(itemPath, match)) { - const value = handler(root, itemName, itemType) - - // if we changed the path value - if (typeof value === 'string') { - found.push(value) - } - // if we kept the default path - else if (value === true) { - found.push(itemPath) - } - } - } - - // dive within the directory tree - if (deep.includes(itemType as any)) { - // we recurse and continue mutating `found` - await findAsync(itemPath, match, types, deep, limit, handler, found, seen) - } - } - } catch {} - - return found -} - -/** - * Like `findSync` but moves to the parent folder if nothing is found - * @param root to start from - * @param match to match against - * @param types to select files, folders, links - * @param deep to recurse in the directory tree - * @param limit to limit the results - * @param filter to further filter results - * @param found to add to already found - * @param seen to add to already seen - * @returns found paths (symlinks preserved) - */ -export async function findUpAsync( - root: string, - match: (RegExp | string)[], - types: ('f' | 'd' | 'l')[] = ['f', 'd', 'l'], - deep: ('d' | 'l')[] = [], - limit: number = Infinity, - handler: Handler = () => true, - found: string[] = [], - seen: Record = {}, -) { - // stop if we cannot go any higher than this root - if (path.resolve(root) === path.resolve(root, '..')) { - return found - } - - await findAsync(root, match, types, deep, limit, handler, found, seen) - - if (found.length === 0) { - const parent = path.join(root, '..') - - await findUpAsync(parent, match, types, deep, limit, handler, found, seen) - } - - return found -} diff --git a/packages/client/tests/e2e/bundler-detection-error/_steps.ts b/packages/client/tests/e2e/bundler-detection-error/_steps.ts new file mode 100644 index 000000000000..25a3d63b5a07 --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/_steps.ts @@ -0,0 +1,19 @@ +import { $ } from 'zx' + +import { executeSteps } from '../_utils/executeSteps' + +void executeSteps({ + setup: async () => { + await $`pnpm install` + await $`pnpm exec prisma db push --force-reset` + await $`pnpm exec prisma generate` + }, + test: async () => { + await $`pnpm exec prisma -v` + await $`pnpm exec esbuild src/index.ts --bundle --outdir=dist --platform=node` + await $`pnpm exec jest` + }, + finish: async () => { + await $`echo "done"` + }, +}) diff --git a/packages/client/tests/e2e/bundler-detection-error/package.json b/packages/client/tests/e2e/bundler-detection-error/package.json new file mode 100644 index 000000000000..8bb012302c89 --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "version": "0.0.0", + "main": "index.js", + "scripts": {}, + "dependencies": { + "@prisma/client": "../prisma-client-0.0.0.tgz" + }, + "devDependencies": { + "@types/node": "16.18.11", + "prisma": "../prisma-0.0.0.tgz" + } +} diff --git a/packages/client/tests/e2e/bundler-detection-error/prisma/schema.prisma b/packages/client/tests/e2e/bundler-detection-error/prisma/schema.prisma new file mode 100644 index 000000000000..60dc7945ff25 --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/prisma/schema.prisma @@ -0,0 +1,36 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + output = "../generated/client" +} + +datasource db { + provider = "sqlite" + url = "file:./db" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] + profile Profile? +} + +model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int +} + +model Profile { + id Int @id @default(autoincrement()) + bio String? + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} diff --git a/packages/client/tests/e2e/bundler-detection-error/readme.md b/packages/client/tests/e2e/bundler-detection-error/readme.md new file mode 100644 index 000000000000..c9ed25673a08 --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/readme.md @@ -0,0 +1,3 @@ +# Readme + +Test that makes sure we are displaying a warning and an appropriate error when the schema isn't found when a client is bundled. diff --git a/packages/client/tests/e2e/bundler-detection-error/src/index.ts b/packages/client/tests/e2e/bundler-detection-error/src/index.ts new file mode 100644 index 000000000000..9565544e132f --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/src/index.ts @@ -0,0 +1,11 @@ +import { PrismaClient } from '../generated/client' + +export async function somePrismaCall() { + const prisma = new PrismaClient() + + const user = await prisma.user.create({ + data: { email: 'john@doe.io' }, + }) + + return user +} diff --git a/packages/client/tests/e2e/bundler-detection-error/tests/main.ts b/packages/client/tests/e2e/bundler-detection-error/tests/main.ts new file mode 100644 index 000000000000..a1fac902cf8b --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/tests/main.ts @@ -0,0 +1,50 @@ +import { $ } from 'zx' + +const consoleMock = jest.spyOn(global.console, 'warn').mockImplementation() + +afterEach(() => { + consoleMock.mockClear() +}) + +test('importing the the bundled prisma client produces warning messages', () => { + require('../dist/index.js') + + expect(consoleMock.mock.calls[0]).toMatchInlineSnapshot(` +[ + "prisma:warn Your generated Prisma Client could not immediately find its \`schema.prisma\`, falling back to finding it via the current working directory.", +] +`) + expect(consoleMock.mock.calls[1]).toMatchInlineSnapshot(` +[ + "prisma:warn We are interested in learning about your project setup. We'd appreciate if you could take the time to share some information with us.", +] +`) + expect(consoleMock.mock.calls[2]).toMatchInlineSnapshot(` +[ + "prisma:warn Please help us by answering a few questions: https://pris.ly/bundler-investigation", +] +`) +}) + +test('bundled prisma client will re-use the schema.prisma via cwd', async () => { + const { somePrismaCall } = require('../dist/index.js') + + const user = await somePrismaCall() + + expect(user).not.toBeNull() + expect(user).not.toBeUndefined() +}) + +// ! this test must be run last because it deletes the generated client +test('bundled prisma client will fail if generated client is gone', async () => { + await $`rm -rf generated` + + const { somePrismaCall } = require('../dist/index.js') + + await expect(somePrismaCall()).rejects.toThrowErrorMatchingInlineSnapshot(` +"Prisma Client could not find its \`schema.prisma\`. This is likely caused by a bundling step, which leads to \`schema.prisma\` not being copied near the resulting bundle. We would appreciate if you could take the time to share some information with us. +Please help us by answering a few questions: https://pris.ly/bundler-investigation" +`) +}) + +export {} diff --git a/packages/client/tests/e2e/bundler-detection-error/tsconfig.json b/packages/client/tests/e2e/bundler-detection-error/tsconfig.json new file mode 100644 index 000000000000..6249bed01061 --- /dev/null +++ b/packages/client/tests/e2e/bundler-detection-error/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src/*"] +} diff --git a/packages/engine-core/src/common/Engine.ts b/packages/engine-core/src/common/Engine.ts index afeb60dec878..8f019f1349b2 100644 --- a/packages/engine-core/src/common/Engine.ts +++ b/packages/engine-core/src/common/Engine.ts @@ -150,6 +150,13 @@ export interface EngineConfig { * @remarks enabling is determined by the client */ tracingConfig: TracingConfig + + /** + * Information about whether we have not found a schema.prisma file in the + * default location, and that we fell back to finding the schema.prisma file + * in the current working directory. This usually means it has been bundled. + */ + isBundled?: boolean } export type GetConfigResult = { diff --git a/packages/engine-core/src/library/LibraryEngine.ts b/packages/engine-core/src/library/LibraryEngine.ts index b602d3f8329f..3eeddb9e1ca5 100644 --- a/packages/engine-core/src/library/LibraryEngine.ts +++ b/packages/engine-core/src/library/LibraryEngine.ts @@ -108,6 +108,12 @@ export class LibraryEngine extends Engine { Find out why and learn how to fix this: https://pris.ly/d/schema-not-found-nextjs`, config.clientVersion!, ) + } else if (config.isBundled === true) { + throw new PrismaClientInitializationError( + `Prisma Client could not find its \`schema.prisma\`. This is likely caused by a bundling step, which leads to \`schema.prisma\` not being copied near the resulting bundle. We would appreciate if you could take the time to share some information with us. +Please help us by answering a few questions: https://pris.ly/bundler-investigation`, + config.clientVersion!, + ) } throw e