Skip to content

Commit

Permalink
fix: ncc support (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Aug 24, 2021
1 parent 627aa54 commit 9c7e552
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 98 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/generator/models/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
68 changes: 57 additions & 11 deletions src/helpers/prisma.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>

// 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 =
Expand Down
131 changes: 83 additions & 48 deletions tests/__helpers__/testProject.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -48,74 +52,105 @@ 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(),
})
},
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
}
12 changes: 12 additions & 0 deletions tests/e2e/__snapshots__/ncc.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`works with ncc 1`] = `
"type Foo {
id: ID!
}
type Query {
foos: [Foo]
}
"
`;
Loading

0 comments on commit 9c7e552

Please sign in to comment.