From 9c7e5520659481a91ab89c36532dfedabdbfecb5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 24 Aug 2021 15:58:16 -0400 Subject: [PATCH] fix: ncc support (#113) --- README.md | 17 ++- src/generator/models/javascript.ts | 6 +- src/helpers/prisma.ts | 68 ++++++++-- tests/__helpers__/testProject.ts | 131 +++++++++++++------- tests/e2e/__snapshots__/ncc.test.ts.snap | 12 ++ tests/e2e/e2e.test.ts | 66 +++++----- tests/e2e/fixtures/ncc/.gitignore | 1 + tests/e2e/fixtures/ncc/main.ts | 26 ++++ tests/e2e/fixtures/ncc/package.json | 24 ++++ tests/e2e/fixtures/ncc/prisma/db.sqlite | Bin 0 -> 20480 bytes tests/e2e/fixtures/ncc/prisma/schema.prisma | 16 +++ tests/e2e/fixtures/ncc/tsconfig.json | 6 + tests/e2e/ncc.test.ts | 18 +++ 13 files changed, 293 insertions(+), 98 deletions(-) create mode 100644 tests/e2e/__snapshots__/ncc.test.ts.snap create mode 100644 tests/e2e/fixtures/ncc/.gitignore create mode 100644 tests/e2e/fixtures/ncc/main.ts create mode 100644 tests/e2e/fixtures/ncc/package.json create mode 100644 tests/e2e/fixtures/ncc/prisma/db.sqlite create mode 100644 tests/e2e/fixtures/ncc/prisma/schema.prisma create mode 100644 tests/e2e/fixtures/ncc/tsconfig.json create mode 100644 tests/e2e/ncc.test.ts diff --git a/README.md b/README.md index 4cc402c8e..bed50e754 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Official Prisma plugin for Nexus. - [Project relation with custom resolver logic](#project-relation-with-custom-resolver-logic) - [Supply custom custom scalars to your GraphQL schema](#supply-custom-custom-scalars-to-your-graphql-schema) - [Notes](#notes) + - [Working with Bundlers](#working-with-bundlers) + - [Disable Peer Dependency Check](#disable-peer-dependency-check) + - [General Support](#general-support) - [For users of `nexus-prisma@=<0.20`](#for-users-of-nexus-prisma020) - [For users of `prisma@=<2.17`](#for-users-of-prisma217) - [For users of `nexus@=<1.0`](#for-users-of-nexus10) @@ -809,7 +812,7 @@ When your project is in a state where the generated Nexus Prisma part is missing When `nexus-prisma` is imported it will validate that your project has peer dependencies setup correctly. -If a peer dependency is not installed it `nexus-prisma` will log an error and then exit 1. If its version does not satify the range supported by the current version of `nexus-prisma` that you have installed, then a warning will be logged. If you want to opt-out of this validation then set an envar as follows: +If a peer dependency is not installed it `nexus-prisma` will log an error and then exit 1. If its version does not satify the range supported by the current version of `nexus-prisma` that you have installed, then a warning will be logged. If you want to opt-out of this validation (e.g. you're [using a bundler](#Disable-Peer-Dependency-Check)) then set an envar as follows: ``` NO_PEER_DEPENDENCY_CHECK=true|1 @@ -887,6 +890,18 @@ makeSchema({ ## Notes +### Working with Bundlers + +#### Disable Peer Dependency Check + +When working with bundlers, it probably makes sense to disable the rutnime peer dependency check system since the bundle step is merging the dependency tree into a single file and may be moved to run standalone away from the original project manifest (e.g. in a docker container). + +Instructions to do this can be found [here](#Peer-Dependency-Validation). + +#### General Support + +`nexus-prisma` has tests showing that it supports `ncc`. Other bundlers are not tested and may or may not work. It is our goal however that nexus-prisma not be the reason for any popular bundler to not work on your project. So if you encounter a problem with one (e.g. `parcel`), open an issue here and we'll fix the issue including an addition to our test suite. + ### For users of `nexus-prisma@=<0.20` Versions of `nexus-prisma` package prior to `0.20` were a completely different version of the API, and had also become deprecated at one point to migrate to `nexus-plugin-prisma` when Nexus Framework was being worked on. All of that is history. diff --git a/src/generator/models/javascript.ts b/src/generator/models/javascript.ts index b24db4d43..932af8e1f 100644 --- a/src/generator/models/javascript.ts +++ b/src/generator/models/javascript.ts @@ -47,7 +47,11 @@ export function createModuleSpec(gentimeSettings: Gentime.Settings): ModuleSpec const gentimeSettings = ${JSON.stringify(gentimeSettings.data, null, 2)} - const dmmf = getPrismaClientDmmf(gentimeSettings.prismaClientImportId) + const dmmf = getPrismaClientDmmf({ + require: () => require('${gentimeSettings.data.prismaClientImportId}'), + importId: gentimeSettings.prismaClientImportId, + importIdResolved: require.resolve('${gentimeSettings.data.prismaClientImportId}') + }) const models = ModelsGenerator.JS.createNexusTypeDefConfigurations(dmmf, { runtime: Runtime.settings, diff --git a/src/helpers/prisma.ts b/src/helpers/prisma.ts index 11e156b89..f57820b5a 100644 --- a/src/helpers/prisma.ts +++ b/src/helpers/prisma.ts @@ -1,48 +1,94 @@ import { DMMF } from '@prisma/client/runtime' import dedent from 'dindist' import ono from 'ono' +import { inspect } from 'util' import { detectProjectPackageManager, renderRunBin } from '../lib/packageManager' import { d } from './debugNexusPrisma' import { GITHUB_NEW_DISCUSSION_LINK } from './errorMessages' import kleur = require('kleur') -export const getPrismaClientDmmf = (importId = '@prisma/client'): DMMF.Document => { +/** + * Given a package loader, attempt to get the Prisma Client DMMF. + * + * @remarks Only the given require function is truly import, the rest is used for better error messages. + * + * This function is designed to support working with bundlers. Specifically `ncc` has been + * tested. + * + * This function intentionally does not do the `require`/`import` itself, leaving that to + * upstream code to handle in static ways that bundlers will be able to process. + */ +export const getPrismaClientDmmf = (packageLoader: { + /** + * A function that must return the Prisma Client Package + */ + require: () => unknown + /** + * The import specifier being used (the from "..." part) + */ + importId: string + /** + * The resolved import specifier being used. This can be different than important ID in two ways: + * + * 1. NodeJS lookp algorithm + * 2. Bundlers that rewrite import paths + */ + importIdResolved: string +}): DMMF.Document => { d('get dmmf from @prisma/client') - let maybeDmmf: DMMF.Document | undefined + let prismaClientPackage: unknown + + // prettier-ignore + const printedImportId = `${kleur.yellow(packageLoader.importId)} (resolved as ${packageLoader.importIdResolved})` try { - // We duck type check below // eslint-disable-next-line - maybeDmmf = require(importId).dmmf + prismaClientPackage = packageLoader.require() } catch (error) { // prettier-ignore throw ono(error, dedent` - Failed to get Prisma Client DMMF. An error occured while trying to import it from ${kleur.yellow(importId)}. + Failed to get Prisma Client DMMF. An error occured while trying to import it from ${printedImportId}. `) } - if (maybeDmmf === undefined) { + if (!(typeof prismaClientPackage === 'object' && prismaClientPackage !== null)) { // prettier-ignore throw new Error(dedent` - Failed to get Prisma Client DMMF. It was imported from ${kleur.yellow(importId)} but was \`undefined\`. + Failed to get Prisma Client DMMF. It was imported from ${printedImportId} but was not the expected type. Got: + + ${inspect(prismaClientPackage)} + `) + } + + const prismaClientPackageObject = prismaClientPackage as Record + + // eslint-disable-next-line + if (!prismaClientPackageObject.dmmf) { + // prettier-ignore + throw new Error(dedent` + Failed to get Prisma Client DMMF. It was imported from ${printedImportId} but did not contain "dmmf" data. Got: + + ${inspect(prismaClientPackage)} + This usually means that you need to run Prisma Client generation. Please run ${renderRunBin(detectProjectPackageManager(), `prisma generate`)}. If that does not solve your problem, you can get community help by opening a discussion at ${kleur.yellow(GITHUB_NEW_DISCUSSION_LINK)}. `) } - /** Simple duck type to sanity check we got good data at runtime. */ + // Simple duck type to sanity check we got good data at runtime. + + const dmmf = prismaClientPackageObject.dmmf as DMMF.Document - const dmmf = maybeDmmf const expectedFields = ['datamodel', 'schema', 'mappings'] as const if (expectedFields.find((fieldName) => dmmf[fieldName] && typeof dmmf[fieldName] !== 'object')) { throw new Error(dedent` - The DMMF imported from ${importId} appears to be invalid. Missing one/all of expected fields: + The DMMF imported from ${packageLoader.importId} appears to be invalid. Missing one/all of expected fields: `) } - return maybeDmmf + return dmmf } export type PrismaScalarType = diff --git a/tests/__helpers__/testProject.ts b/tests/__helpers__/testProject.ts index 7f7fda600..060962487 100644 --- a/tests/__helpers__/testProject.ts +++ b/tests/__helpers__/testProject.ts @@ -1,16 +1,20 @@ -import execa from 'execa' +import { debug } from 'debug' +import * as Execa from 'execa' import * as fs from 'fs-jetpack' import { FSJetpack } from 'fs-jetpack/types' import { GraphQLClient } from 'graphql-request' import { merge } from 'lodash' import { PackageJson, TsConfigJson } from 'type-fest' +const d = debug(`testProject`) + export interface TestProject { fs: FSJetpack info: TestProjectInfo - run(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue - runAsync(command: string, options?: execa.SyncOptions): execa.ExecaChildProcess - runOrThrow(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue + run(command: string, options?: Execa.SyncOptions): Execa.ExecaSyncReturnValue + runAsync(command: string, options?: Execa.SyncOptions): Execa.ExecaChildProcess + runOrThrow(command: string, options?: Execa.SyncOptions): Execa.ExecaSyncReturnValue + runOrThrowNpmScript(command: string, options?: Execa.SyncOptions): Execa.ExecaSyncReturnValue client: GraphQLClient } @@ -48,68 +52,50 @@ export class TestProjectInfo { } } -export function setupTestProject({ - packageJson, - tsconfigJson, -}: { - packageJson?: PackageJson - tsconfigJson?: TsConfigJson -} = {}): TestProject { +export function setupTestProject( + params: { + fixture?: string + files?: { + packageJson?: PackageJson + tsconfigJson?: TsConfigJson + } + } = {} +): TestProject { + const thisPackageName = `nexus-prisma` const tpi = new TestProjectInfo() - /** * Allow reusing a test project directory. This can be helpful when debugging things. */ - let fs_ = tpi.isReusingEnabled ? fs.cwd(tpi.getOrSetGet().dir) : fs.tmpDir() - - fs_.write( - 'package.json', - merge( - { - name: 'some-test-project', - version: '1.0.0', - }, - packageJson - ) - ) + const fs_ = tpi.isReusingEnabled ? fs.cwd(tpi.getOrSetGet().dir) : fs.tmpDir() - fs_.write( - 'tsconfig.json', - merge( - { - compilerOptions: { - strict: true, - target: 'ES2018', - module: 'CommonJS', - moduleResolution: 'node', - rootDir: 'src', - outDir: 'build', - esModuleInterop: true, // for ApolloServer b/c ws dep :( - }, - include: ['src'], - } as TsConfigJson, - tsconfigJson - ) - ) - - const api: TestProject = { + const testProject: TestProject = { fs: fs_, info: tpi, run(command, options) { - return execa.commandSync(command, { + // console.log(`${command} ...`) + return Execa.commandSync(command, { reject: false, ...options, cwd: fs_.cwd(), }) }, runOrThrow(command, options) { - return execa.commandSync(command, { + // console.log(`${command} ...`) + return Execa.commandSync(command, { + ...options, + cwd: fs_.cwd(), + }) + }, + runOrThrowNpmScript(command, options) { + // console.log(`${command} ...`) + return Execa.commandSync(`npm run --silent ${command}`, { ...options, cwd: fs_.cwd(), }) }, runAsync(command, options) { - return execa.command(command, { + // console.log(`${command} ...`) + return Execa.command(command, { ...options, cwd: fs_.cwd(), }) @@ -117,5 +103,54 @@ export function setupTestProject({ client: new GraphQLClient('http://localhost:4000'), } - return api + if (params.fixture) { + testProject.fs.copy(params.fixture, testProject.fs.cwd(), { + overwrite: true, + }) + } else { + testProject.fs.write( + 'package.json', + merge( + { + name: 'some-test-project', + version: '1.0.0', + }, + params.files?.packageJson + ) + ) + + testProject.fs.write( + 'tsconfig.json', + merge( + { + compilerOptions: { + strict: true, + target: 'ES2018', + module: 'CommonJS', + moduleResolution: 'node', + rootDir: 'src', + outDir: 'build', + esModuleInterop: true, // for ApolloServer b/c ws dep :( + }, + include: ['src'], + } as TsConfigJson, + params.files?.tsconfigJson + ) + ) + } + + if (testProject.info.isReusing) { + d(`starting project setup cleanup for reuse`) + testProject.fs.remove(`node_modules/${thisPackageName}`) + testProject.runOrThrow(`yalc add ${thisPackageName}`) + d(`done project setup cleanup for reuse`) + } else { + d(`starting project setup`) + Execa.commandSync(`yalc publish --no-scripts`, { stdio: 'inherit' }) + testProject.runOrThrow(`yalc add ${thisPackageName}`, { stdio: 'inherit' }) + testProject.runOrThrow(`npm install --legacy-peer-deps`, { stdio: 'inherit' }) + d(`done project setup`) + } + + return testProject } diff --git a/tests/e2e/__snapshots__/ncc.test.ts.snap b/tests/e2e/__snapshots__/ncc.test.ts.snap new file mode 100644 index 000000000..ac0c02b79 --- /dev/null +++ b/tests/e2e/__snapshots__/ncc.test.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`works with ncc 1`] = ` +"type Foo { + id: ID! +} + +type Query { + foos: [Foo] +} +" +`; diff --git a/tests/e2e/e2e.test.ts b/tests/e2e/e2e.test.ts index 0e278c6ec..a6c2f81d9 100644 --- a/tests/e2e/e2e.test.ts +++ b/tests/e2e/e2e.test.ts @@ -2,12 +2,12 @@ import debug from 'debug' import dedent from 'dindist' import * as Execa from 'execa' import { gql } from 'graphql-request' +import * as GQLScalars from 'graphql-scalars' import stripAnsi from 'strip-ansi' import { inspect } from 'util' import { assertBuildPresent } from '../__helpers__/helpers' import { createPrismaSchema } from '../__helpers__/testers' import { setupTestProject, TestProject } from '../__helpers__/testProject' -import * as GQLScalars from 'graphql-scalars' const d = debug('e2e') @@ -56,48 +56,40 @@ function runTestProjectBuild(testProject: TestProject): ProjectResult { function setupTestNexusPrismaProject(): TestProject { const testProject = setupTestProject({ - tsconfigJson: {}, - packageJson: { - license: 'MIT', - scripts: { - reflect: 'yarn -s reflect:prisma && yarn -s reflect:nexus', - 'reflect:prisma': "cross-env DEBUG='*' prisma generate", - // peer dependency check will fail since we're using yalc, e.g.: - // " ... nexus-prisma@0.0.0-dripip+c2653557 does not officially support @prisma/client@2.22.1 ... " - 'reflect:nexus': 'cross-env REFLECT=true ts-node --transpile-only src/schema', - build: 'tsc', - start: 'node build/server', - 'dev:server': 'yarn ts-node-dev --transpile-only server', - 'db:migrate': 'prisma db push --force-reset && ts-node prisma/seed', - }, - dependencies: { - dotenv: '^9.0.0', - 'apollo-server': '^2.24.0', - 'cross-env': '^7.0.1', - '@prisma/client': '^2.18.0', - '@types/node': '^14.14.32', - graphql: '^15.5.0', - nexus: '^1.0.0', - prisma: '^2.18.0', - 'ts-node': '^9.1.1', - 'ts-node-dev': '^1.1.6', - typescript: '^4.2.3', + files: { + tsconfigJson: {}, + packageJson: { + license: 'MIT', + scripts: { + reflect: 'yarn -s reflect:prisma && yarn -s reflect:nexus', + 'reflect:prisma': "cross-env DEBUG='*' prisma generate", + // peer dependency check will fail since we're using yalc, e.g.: + // " ... nexus-prisma@0.0.0-dripip+c2653557 does not officially support @prisma/client@2.22.1 ... " + 'reflect:nexus': 'cross-env REFLECT=true ts-node --transpile-only src/schema', + build: 'tsc', + start: 'node build/server', + 'dev:server': 'yarn ts-node-dev --transpile-only server', + 'db:migrate': 'prisma db push --force-reset && ts-node prisma/seed', + }, + dependencies: { + dotenv: '^9.0.0', + 'apollo-server': '^2.24.0', + 'cross-env': '^7.0.1', + '@prisma/client': '^2.18.0', + '@types/node': '^14.14.32', + graphql: '^15.5.0', + nexus: '^1.0.0', + prisma: '^2.18.0', + 'ts-node': '^9.1.1', + 'ts-node-dev': '^1.1.6', + typescript: '^4.2.3', + }, }, }, }) if (testProject.info.isReusing) { - d(`starting project setup cleanup for reuse`) testProject.fs.remove(TYPEGEN_FILE_PATH) - testProject.fs.remove('node_modules/nexus-prisma') - testProject.runOrThrow(`yalc add nexus-prisma`) - d(`done project setup cleanup for reuse`) - } else { - d(`starting project setup`) - Execa.commandSync(`yalc publish --no-scripts`, { stdio: 'inherit' }) - testProject.runOrThrow(`yalc add nexus-prisma`, { stdio: 'inherit' }) - testProject.runOrThrow(`npm install`, { stdio: 'inherit' }) - d(`done project setup`) } return testProject diff --git a/tests/e2e/fixtures/ncc/.gitignore b/tests/e2e/fixtures/ncc/.gitignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/tests/e2e/fixtures/ncc/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/tests/e2e/fixtures/ncc/main.ts b/tests/e2e/fixtures/ncc/main.ts new file mode 100644 index 000000000..1a4b8bdfe --- /dev/null +++ b/tests/e2e/fixtures/ncc/main.ts @@ -0,0 +1,26 @@ +import { printSchema } from 'graphql' +import { makeSchema, objectType, queryType } from 'nexus' +import { Foo } from 'nexus-prisma' + +const schema = makeSchema({ + types: [ + objectType({ + name: Foo.$name, + definition(t) { + t.field(Foo.id) + }, + }), + queryType({ + definition(t) { + t.list.field('foos', { + type: 'Foo', + resolve(_, __, ctx) { + return ctx.prisma.foo.findMany() + }, + }) + }, + }), + ], +}) + +console.log(printSchema(schema)) diff --git a/tests/e2e/fixtures/ncc/package.json b/tests/e2e/fixtures/ncc/package.json new file mode 100644 index 000000000..2c0eb27eb --- /dev/null +++ b/tests/e2e/fixtures/ncc/package.json @@ -0,0 +1,24 @@ +{ + "name": "nexus-prisma-ncc-test", + "version": "0.0.0", + "main": "index.js", + "scripts": { + "build": "npm run build:prisma && npm run build:ncc", + "build:prisma": "prisma generate", + "build:ncc": "ncc build --no-cache main.ts", + "start:dist": "cross-env NO_PEER_DEPENDENCY_CHECK=true node ./dist", + "start:ts": "ts-node main" + }, + "author": "Jason Kuhrt", + "license": "ISC", + "dependencies": { + "@prisma/client": "2.30", + "cross-env": "7.0.1", + "@vercel/ncc": "0.29.2", + "graphql": "15.5.1", + "nexus": "1.1.0", + "prisma": "2.30.0", + "ts-node": "10.2.1", + "typescript": "4.3.5" + } +} diff --git a/tests/e2e/fixtures/ncc/prisma/db.sqlite b/tests/e2e/fixtures/ncc/prisma/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..697f34514d38df8b5e77794ea38f320cd85aeaec GIT binary patch literal 20480 zcmeI&K}*9h6u|MM?TQXY20QE;3W8pQ;mMm-D#)sHvw}NFsjR|aTGS495WM&W{WgLZ zU&X;ZOgwog|3_Y$hLVt9P9eP*#?vylx5eXKR@yz)R$8kAYn4*IWJ7Wpjx@aGi#+v8 zdtK94yMyPJc~_okEAwjJOxs+q`=Q*9<=9Gcbo>Z=QZd9f% zNo_uq6aoPR5I_I{1Q0*~0R#|0009K*C*W$wWKCHY=l}X`E-OL+0R#|0009ILKmY** z5J2Etz;n%JR^9*0PyYl02q1s}0tg_000IagfB*sr)L5Xp|L6Q);|*qM2q1s}0tg_0 O00IagfB*srh`=ZKt55>~ literal 0 HcmV?d00001 diff --git a/tests/e2e/fixtures/ncc/prisma/schema.prisma b/tests/e2e/fixtures/ncc/prisma/schema.prisma new file mode 100644 index 000000000..2b9672a1c --- /dev/null +++ b/tests/e2e/fixtures/ncc/prisma/schema.prisma @@ -0,0 +1,16 @@ +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator client { + provider = "prisma-client-js" +} + +generator nexusPrisma { + provider = "nexus-prisma" +} + +model Foo { + id String @id @default(cuid()) +} diff --git a/tests/e2e/fixtures/ncc/tsconfig.json b/tests/e2e/fixtures/ncc/tsconfig.json new file mode 100644 index 000000000..d4e2d7632 --- /dev/null +++ b/tests/e2e/fixtures/ncc/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "moduleResolution": "node" + }, + "include": ["."] +} diff --git a/tests/e2e/ncc.test.ts b/tests/e2e/ncc.test.ts new file mode 100644 index 000000000..0298b8eb3 --- /dev/null +++ b/tests/e2e/ncc.test.ts @@ -0,0 +1,18 @@ +import * as Path from 'path' +import { setupTestProject } from '../__helpers__/testProject' + +it('works with ncc', async () => { + const testProject = setupTestProject({ + fixture: Path.join(__dirname, 'fixtures/ncc'), + }) + + await testProject.runOrThrowNpmScript(`build`) + + // Remove this to ensure that when the ncc build is run in the next step + // it is truly running independent of any node_modules. + await testProject.fs.remove('node_modules') + + const result = await testProject.runOrThrowNpmScript(`start:dist`) + + expect(result.stdout).toMatchSnapshot() +})