Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Be usable with nextjs #648

Closed
9 of 14 tasks
jasonkuhrt opened this issue Apr 13, 2020 · 8 comments
Closed
9 of 14 tasks

Be usable with nextjs #648

jasonkuhrt opened this issue Apr 13, 2020 · 8 comments

Comments

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Apr 13, 2020

What

Make it easy to write projects with next + nexus

Why

Next is a popular way to write client apps

How

needs research

Notes

TODOs

Big TODOs

  • Serverless server mode (different platforms, vercel first) Serverless mode #782
  • nexus/sdk ... cli features as an api ... for creating a nextjs plugin (?)
    • dev
    • build
@jasonkuhrt jasonkuhrt added the needs/discussion Open-ended conversation about something (ideation, design, analysis, ...) label Apr 13, 2020
@yovanoc
Copy link

yovanoc commented Apr 18, 2020

I would love this too, in typescript for sure it will be perfect for modern app!

@Weakky
Copy link
Collaborator

Weakky commented Apr 23, 2020

Status update

This comment is a summary of findings and rough reflexions after blindly trying to get Nexus and Next to work together for a day.

1. Support serverless environments

NextJS' pages/api/**/*.ts files gets compiled to individual lambdas. To properly support NextJS with Vercel (previously called ZEIT now), we need to make Nexus support serverless. Here's what an ideal file structure might look like:

.
├── README.md
├── graphql <------------------ Nexus' schema modules
│   ├── mutation.ts
│   ├── query.ts
│   └── user.ts
├── package.json
├── pages
│   ├── about.tsx
│   ├── api
│   │   └── graphql.ts <------  Nexus entrypoint (exporting serverless handler)
│   └── index.tsx
├── tsconfig.json
└── yarn.lock

2. Custom entrypoint

You might notice on the file structure above, that the nexus entrypoint is completely separated from its schema modules. That's because each file in the pages/api folder end up as a lambda, so we need to make sure we only have 1 file in pages/api and that should be our entrypoint.

Also, each file name in that folder is used as api route. Nexus' default entrypoint file name is app.ts, but here it wouldn't really make sense to have the GraphQL lambda exposed on /api/app. Instead, we want /api/graphql hence the pages/api/graphql.ts file.

Note: There might be a way to override the default api routes via configuration

3. Support next build command

Reasons to wrap nexus build in next build:

  • By following the steps above, we try to make Nexus comply with NextJS' conventions. At that point, it just makes more sense to let next build do its job based on its conventions
  • next build command is a lot more advanced than nexus build as it brings a ton of nice feature for the web that we could benefit from (eg: bundler, tree-shaker, minifier etc)
  • next build output something that is 100% compatible with Now. If we manage to get Nexus to work with the NextJS' compiler options, the last bit we need to figure out is how to inject our start module

To achieve the steps above, we need:

a. Support NextJS TS compiler options

NextJS uses the following TS compiler options:

  • "isolatedModules": "true" (requirement for babel)
  • "module": "esnext" (for dynamic import() support)
  • "moduleResolution": "node" (to match webpack resolution)

nexus build output must work with these compiler options

b. Hook into NextJS' build process

In order to get next build to compile Nexus code, we will need some kind of NextJS plugin to hook into the next build process so that we can write our start module and do our usual build checks

c. Let users pass Nexus entrypoint to the NextJS plugin

If nexus build was in charge of compiling the project, we could've just added a --entrypoint option. But here we're talking about wrapping nexus build inside next build. There are two ways we can achieve that:

  • With convention: No configuration needed, we create the convention that with NextJS, the Nexus entrypoint should be pages/api/graphql.ts
  • With configuration:
    • Ideally, the NextJS plugin allow us to extend the CLI to add flags to the next build command. eg: next build --nexus-entrypoint pages/api/graphql.ts
    • On the plugin itself. eg: nexusPlugin({ entrypoint: 'pages/api/graphql.ts' })

4. Development time

We have two approaches possible here. Wrapping nexus dev in next dev, or keeping the two separate.

Both have trade-offs, I'll try to go over them.

Keeping nexus dev and next dev separate

In dev, nexus dev would run on a "serverful" express instance and the frontend should connect to that serverful url. It means that in dev, next dev would compile the pages/api files but they wouldn't be used.

It's only when creating a production build with next build that the nexus plugin would compile the nexus code and make it serverless so that it can be used alongside nexus.

What are the side-effects of that approach?

I see 3 side-effects:

  • On Nexus side, user will need need to enable cors in dev to accept requests from the frontend
  • On Next side, they will need to change the GraphQL endpoint depending on whether they're in dev or prod. eg:
    • dev: http://localhost:4000/api/graphql
    • next: https://localhost:3000/api/graphql
  • Unexpected errors might appear in production because users will be switch from serverful in dev to serverless in prod

Wrapping nexus dev in next dev

Non-goals:

  1. Ending up with something that looks like yarn concurrently "next dev" "nexus dev"

While at first glance this approach is better than keeping the two commands separate, it brings a whole lot of complexity that I'm not sure we're ready for yet.

Keeping the experience tight

Complexity apart, my biggest reservation is about logs. next dev can output a metric ton of logs/warning with TS typecheck logs (that nexus dev fortunately don't have, but we might optionally enable them) + all the babel plugins that it has. next dev logs could very easily sink the nexus dev logs after each refresh.

Then, there's the inherent complexity of merging the two commands.

If we don't want to end up with the non-goal n°1, I don't think we can have two watchers. Otherwise the two dev commands won't be in sync, they will do stuff on their own, we'll potentially have two restarts on every change etc.

So that brings us to the fact that we'd need to hook into NextJS' watcher to keep everything tight.

a. Watcher hooks

And to keep nexus dev working, including the nexus plugins, NextJS needs to expose as much watcher hooks as Nexus does. We could then do the plumbing of routing the NextJS watcher events to Nexus events.

Here are the hooks we currently support (raw shape):

    onStart?: Utils.SideEffector
    onBeforeWatcherRestart?: Utils.SideEffector
    onBeforeWatcherStartOrRestart?: (change: Watcher.ChangeEvent) => Utils.MaybePromise<void | Watcher.RunnerOptions>
    onAfterWatcherRestart?: Utils.SideEffector
    onFileWatcherEvent?: Chokidar.FileWatcherEventCallback
    addToWatcherSettings: {
      /**
       * Set additional files to be watched for the app and plugin listeners
       */
      watchFilePatterns?: string[]
      listeners?: {
        /**
         * Define the watcher settings for the app listener
         */
        app?: {
          /**
           * Set files patterns that should not trigger a server restart by the app
           */
          ignoreFilePatterns?: string[]
        }
        /**
         * Define the watcher settings for your plugin listener
         */
        plugin?: {
          /**
           * Set file patterns that should trigger `dev.onFileWatcherEvent`
           * When set without `plugin.ignoreFilePatterns`, `dev.onFileWatcherEvent` will only react to changes made to the files which matches the `plugin.allowFilePatterns` patterns
           * When set with `plugin.ignoreFilePatterns`, `dev.onFileWatcherEvent` will only react to changes made to the files which matches the `plugin.allowFilePatterns` patterns, minus the files which matches `plugin.ignoreFilePatterns`
           */
          allowFilePatterns?: string[]
          /**
           * Set file patterns that should not trigger `dev.onFileWatcherEvent`
           * When set without `plugin.allowFilePatterns`, `dev.onFileWatcherEvent` will react to changes made to all files watched except the files which matches the `plugin.ignoreFilePatterns` patterns
           * When set with `plugin.allowFilePatterns`, , `dev.onFileWatcherEvent` will react to changes made to all files matched by `plugin.allowFilesPatterns` except the files which matches the `plugin.ignoreFilePatterns` patterns
           */
          ignoreFilePatterns?: string[]
        }
      }
    }

b. Build hooks

We also need to hook into NextJS build process during dev, so that we can apply our build process there too

c. Nexus SDK

Finally, for all the steps above to happen, we need to come up with an SDK that'll allow up to rip apart part of Nexus into resusable components so that we can reuse them in the plugin.

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented Apr 27, 2020

Support for es modules is shipped #720. It seems there are no issues with moduleResolution=node and isolateModules.

However, minor bump:

The nextjs-blog app uses noEmit tsc compiler option. This conflicts with Nexus #702 which would see it marked as not usable. Also, when present, there is an error in the nexus build:

❯ yarn -s nexus build 
3051 ▲ nexus:tsconfig Please set your tsconfig.json include to have "."
  11 ● nexus:build getting used plugins
  56 ● nexus:build starting artifact generation
2336 ● nexus:build building typescript program
1195 ● nexus:build starting addToContext type extraction
   3 ● nexus:build Awaiting artifact generation & addToContext type extraction
   3 ● nexus:build Compiling a production build
Error: error TS5053: Option 'noEmit' cannot be specified with option 'incremental'.

    at Object.compile (/Users/jasonkuhrt/projects/graphql-nexus/nexus/dist/lib/tsc.js:63:15)
    at Object.buildNexusApp (/Users/jasonkuhrt/projects/graphql-nexus/nexus/dist/lib/build/build.js:63:11)
    at async Build.parse (/Users/jasonkuhrt/projects/graphql-nexus/nexus/dist/cli/commands/build.js:31:9)

It seems that removing the noEmit option from tsconfig to satisfy Nexus has no impact on nextjs. E.g. next build works fine without it.

--- edit

Just discovered that nextjs is a bit more agressive than first thought about noEmit:

❯ yarn -s next dev          
[ wait ]  starting the development server ...
[ info ]  waiting on http://localhost:3000 ...
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
The following changes are being made to your tsconfig.json file:
  - compilerOptions.noEmit to be suggested value: true (this can be changed)

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented Apr 27, 2020

We've made some progress since @Weakky's post. The tsc compiler options and entrypoint issues have been largely resolved. Three major issues remain, ordered by how we think we will tackle them:

  1. serverless support
  2. build mode integration
  3. dev mode integration

Support for serverless should enable users to do something like this.

yarn add nexus-plugin-nextjs
// pages/api/graphql.ts
import { server, use } from 'nexus'
import { nextjs } from 'nexus-plugin-nextjs'

use(nextjs())

const handler = server.handler

export default handler
  • the nextjs plugin would turn server.handler into something tailored for nextjs

  • it would turn server middleware server middleware #523 into something typed/compatible with nextjs

  • Anything here should work https://nextjs.org/docs/api-routes/api-middlewares, for example users could do this (taken from nextjs docs):

     // pages/api/graphql.ts
     import Cors from 'cors'
     import { server, use } from 'nexus'
     import { nextjs } from 'nexus-plugin-nextjs'
     
     use(nextjs())
     
     export default server.handler
     
     // Initializing the cors middleware
     const cors = Cors({
       methods: ['GET', 'HEAD'],
     })
     
     // Helper method to wait for a middleware to execute before continuing
     // And to throw an error when an error happens in a middleware
     function runMiddleware(req, res, fn) {
       return new Promise((resolve, reject) => {
         fn(req, res, result => {
           if (result instanceof Error) {
             return reject(result)
           }
     
           return resolve(result)
         })
       })
     }
     
     async function handler(req, res) {
       // Run the middleware
       await runMiddleware(req, res, cors)
     
       // Rest of the API logic
       server.handler(req, res)  // <---- nexus handler here
     }
     
     export default handler

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented Apr 29, 2020

Update

  • We got something deployed today. The nextjs-blog example, tweaked with Nexus, can be seen here: https://nextjs-blog-one-lyart.now.sh. Its just a tech prototype showing graphql running:

    • Playground (when you open, change URL path to be /api/graphql)
    • graphql (navigating to this in your browser will cause an expected error)
  • We are able to use Next to build the nexus app, in a sense

  • We achieve this by manually coding a version of the start module into every nextjs api endpoint implemented by nexus

Looking at some next steps

  • NextJS does not permit having a module dynamically injected into its build, so let's work with that constraint for now.

    • need an api to make the app. It is currently done when server start is called. But that presumably doesn't exist in serverless mode.

    • Need an api for process.env.NEXUS_SHOULD_GENERATE_ARTIFACTS, e.g.

       settings.change({ framework: { generateArtifacts: false } })
      

      But probably/maybe this should be viewed more as an SDK thing.

  • need nexus to expose a serverless handler

  • More of a serverless thing than a nextjs only thing... Also not super important as we can live with a single entrypoint today...

    In serverless every endpoint is an entrypoint. Effectively each of these entrypoints are their own apps. There is no state shared between entrpoints. So one entrypoint could use certain plugins while another entrypoint not. This is wildly different than the current Nexus operational model right now where there is one entrypoint, one app. For example How would Nexus deal with this:

     // pages/api/a.ts
     import { use, schema } from 'nexus'
     import { prisma } from 'prisma'
     
     use(prisma())
     
     schema.mutationType({
       definition(t) {
         t.crud.createUser()
       }
     })
     // pages/api/b.ts
     import { use, schema } from 'nexus'
     
     schema.mutationType({
       definition(t) {
         t.crud.createUser()   // <-- this should be an error, etc.
       }
     })

    Effectively we need typegen scoped to the serverless function level. Nexus is not equipped to do this today.

@jasonkuhrt
Copy link
Member Author

While we do not expect dev modes to be integrated at least for a while, we do think there should be a way to run nexus dev mode for just reflection. Currently it runs start but in the context of nextjs dev it is nextjs that is responsible for running the app.

nexus dev --reflection | -r

@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 5, 2020

Following up on graphql-nexus/nexus#648 (comment).

We will need build to not emit, as that will be the job of nextjs. Therefore the comment references seems obsolete. NoEmit isn't in conflict, it is expected.

Some ideas:

  1. new build flag

    nexus build --no-emit
    # or
    nexus build --check
    # or...
  2. We rely/honour on the tsconfig option of noEmit

  3. new command

    nexus doctor
    # or
    nexus check
    # or...

I think honouring tsconfig makes the most sense for us.

However I don't think it negates #702. Instead a nextjs nexus plugin should disable the warning.

@jasonkuhrt jasonkuhrt changed the title Integrate with Nextjs Be usable with next.js May 6, 2020
@jasonkuhrt jasonkuhrt changed the title Be usable with next.js Be usable with nextjs May 6, 2020
@jasonkuhrt
Copy link
Member Author

jasonkuhrt commented May 6, 2020

Hey everyone, thanks for following this issue. We have shipped this sprint (0.21.0) enough serverless support to unblock rough integration with nextjs. Check it out now at https://github.com/graphql-nexus/examples#nextjs. To keep things flowing we will close this issue and open new ones to track more specific integration improvements with nextjs, like build and dev mode unification.

@jasonkuhrt jasonkuhrt added type/design and removed needs/discussion Open-ended conversation about something (ideation, design, analysis, ...) labels May 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants