Repositories and packages

Repos & Packages

Reflection

🚧 Work in progress.

Assembly

🚧 Work in progress.

Plugins

Diagrams

Plugin Tree Shaking

plugin-tree-shaking

Glossary

Entrypoint

A plugin entrypoint is responsible for returning the plugin's manifest input. This is what users actually deal with at the app layer.

1import { prisma } from 'nexus-plugin-prisma'
2// ~~~~~~ <------------- The entrypoint, returns a manifest input
3import { use } from 'nexus'
4
5use(prisma())

Manifest

A plugin manifest describes logistical information about a plugin like where its package file is located on disk, what versions of Nexus it is compatible with, and more.

Plugin manifests are created by Nexus by processing the manifest inputs returned by plugin entrypoints.

1import { prisma } from 'nexus-plugin-prisma'
2
3const prismaPluginManifestInput = prisma()

This lazy approach allows Nexus the flexibility it needs to provide features like automatic environment variable plugin settings injection and tree-shaking.

Manifest Input

A manifest input is the view of manifests that plugin authors work with. It models the minimum information that Nexus needs to build the plugin manifest. It is also more human friendly, for example minimizing the number of required fields. Developer experience is not the primary motivation here however. We want Nexus to control as much as possible, so the less the human has to specify the more we achieve this goal. For example we ask for a path to package json rather than the contents of it. We want Nexus to be the one that reads it, when and how it wants.

Dimension

A plugin dimension represents a high-level area of plugin concern that are separated by module from other dimensions. In other words, each dimension has its own entrypoint. Nexus will directly import it. A dimension is found by Nexus from Nexus reading the Plugin manifest.

There are three dimensions: worktime, testtime, and runtime. Worktime allows plugins to tap into the CLI, the builder, dev mode, and more. Testtime allows plugins to tap into features of the Nexus testing component. Runtime allows plugins to tap into the API.

Directionally Nexus is on a good track, but there is work still left to do. The names are a bit confusing when you dig into the details, and the supposed separation between worktime/runtime has undesirable "loopholes" because of reflection. Details;

  1. Runtime dimension does not mean plugging exclusively into what is run in your production code. There are actually reasons to plug into the runtime for ostensibly worktime benefit... This is due to Nexus' so-called reflection system, wherein the app is run in the background during development for development purposes.

  2. The rationale for splitting worktime from runtime is clear, tree-shaking alone makes the case for it. However the separation between worktime and testing is less clear, perhaps nonsense, and so may be revisited in the future.

  3. We've talked about motivation for separating worktime from runtime, yet there are runtime parts for worktime (reflection). What this means is that expensive dependencies can make there way into the runtime dimension that a user should actually not be paying for in production runtime.

Lens

A plugin lens is just a specialized api into a subset of Nexus to hook into, extend, manipulate, and react to it during execution. The name "lens" is arbitrary. The choice comes from it being "view" into Nexus. Each dimension has its own specialized lens.

Dimension Entrypoint

Just like the plugin has a top level entrypoint so to does each dimension within the plugin have its own entrypoint. These sub-entrypoints can be thought as sub-plugins, with the top-level plugin just being a grouping mechanism.

Comparisons to Other Systems

Rollup

  • Like Rollup plugins are prefixed with <tool-name>-plugin-<plugin-name>
  • We have considered but so far not put first-party Nexus plugins under the pattern @nexus/plugin-<plugin-name>. Rollup made this transition retroactively.
  • Rollup suggests plugins have a default export so that are much easier to use on the command line. Nexus suggests plugins also have default exports for similar system-usability reasons (not cli in Nexus' case, but other future features maybe like auto-use).

Loading Flow

🚧 Work in progress.
what follows is a stub

  1. capture the used plugins in the app

  2. validate entrypoints

  3. transform entrypoints into manifests

  4. for each dimension (work, test, run) in the manifest

    1. import i t

    2. catch any import errors

    3. validate imported value

    4. load plugin

    5. catch any load errors

Build Flow

  1. The app layout is calculated
    We discover things like where the entrypoint is, if any, and where Nexus modules are, if any.

  2. Worktime plugins are loaded (see Plugin Loading Flow)

  3. Typegen is acquired
    This step is about processes that reflect upon the app's source code to extract type information that will be automatically used in other parts of the app. This approach is relatively novel among Node tools. There are dynamic and static processes. The static ones use the TypeScript compiler API while the dynamic ones literally run the app with node in a special reflective mode.

    Dynamic has the benefit of being able to produce types that IDE's can pick up for use in not just TypeScript but also JavaScript. It works for the schema typegen because the GraphQL Schema builders permit generating accurate derived TypeScript. Dynamic works regardless of the abstractions users through over them. On the downside, dynamic is riskier because runtime errors in the app can halt its completion. When the types to be generated are based upon arbitrary code, the task becomes one of effectively re-writing TypeScript and thus impractical.

    Static doesn't have to deal with the unpredictabilities of running an app and so has the benefit of being easier to reason about in a sense. It also has the benefit of extracting accurate type information using the native TS system whereas dynamic relies on building TS types from scratch. This makes static a fit for arbitrary code. On the downside, robust AST processing is hard work, and so, so far, static restricts how certain expressions can be written, otherwise AST traversal fails.

    1. A start module is created in memory. It imports the entrypoint and all Nexus modules. It registers an extension hook to transpile the TypeScript app on the fly as it is run. The transpilation uses the project's tsconfig but overrides target and module so that it is runnable by Node (10 and up). Specificaly es2015 target and commonjs module. For example if user had module of esnext the transpilation result would not be runnable by Node.
    2. The start module is run in a sub-process for maximum isolation. (we're looking at running within workers #752)
    3. In parallel, a TypeScript instance is created and the app source is statically analyzed to extract context types. This does not require running the app at all. TypeScript cache called tsbuildinfo is stored under node_modules/.nexus.
  4. A new TypeScript instance is created so that the types generated in the previous step are picked up by the checker. This should be faster because it reuses the TypeScript cache created in the previous step.

  5. The app is type checked

  6. The app is transpiled

  7. The app is emitted into .nexus/build. This convention keeps derived files in a well known generally ignored location.

  8. A production-oriented start module is generated differing in the following ways:

    • paths are relative
    • typescript not hooked into module extensions
    • plugins are imported for tree-shaking

Dependency Philosophy

Nexus takes a different perspective on the arguably mainstream approach of Node dependency management. In Node it is common practice to write and share tiny reusable modules. Then, users build their applications by assembling these many smaller packages how they need to.

Nexus takes a batteries included approach. Aside from being featureful, it also means bundling things that would typically be peer dependencies in other systems: graphql and @nexus/schema for Nexus itself, and for prisma plugin @prisma/cli, @prisma/sdk, and @prisma/client. There are numerous benefits to this.

  1. Setting up dependency automation tools like renovate lead to simpler experiences.

    For example when graphql 0.15 was released, bothering nexus users to upgrade it wouldn't have made sense since @nexus/schema didn't support it yet.

  2. Integration bugs are owned by the core team

    For example when TypeScript 3.9 was released nexus-prisma was incompatible with it. The types it generated led to static errors that were previously not there due to a change in how TS dealt with any as a type generic default.

  3. Simpler upgrade paths for you

    It is easier to piece together an upgrade path from one dep to another than a combination of n deps to another combination of n deps. In pratice what happens is the migration guide is written with a discrete set of n rather than all possible n.

  4. More "honest" than peer dependenies

    Peer dependencies in a nutshell are a loose contract encoding thus: "I, author of lib A, declare that A supports a range of lib B that, you, user, will bring to the party". This sounds great, but how is this claim enforced? Its very likely not, at least rigorously. Trust in semver and a test suite against one version of B is probably how the majority of situations go. And so of course a not insubstantial burden falls onto users' laps.

    If Nexus, especially with Prisma plugin, were going to leverage peer dependencies (implying a range of lib support or what's the point?) doing it honestly would mean a test suite running against a matrix of the combinations of the supported ranges of peer deps. Nexus already runs a matrix of Node versions and OSs. Adding peer deps to the mix would further strain an already strained test setup. Then, of course, there would be the actual burden of supporting whatever idiosyncracies exist along the ranges. Our internal implementations would complexify, more tests would be needed, more room for bugs.

  5. Yarn resolutions to the rescue

    But what if I really need an older or newer version of X transient dep!?

    If the bundled versions of deps in Nexus or Prisma plugin aren't meeting your needs and there is an urgent issue you can use the yarn resolutions option. If you aren't using yarn or a package manager with similar capability, and you aren't willing to, then you probably don't have an urgent issue.

    Of course by doing this you're going outside what a given version of Nexus officially supports, so, YMMV "your mileage may vary"! But its not like if Nexus were using peer dep ranges that this caveat would magically go away.

In conclusion, from both a DX and engineering resource perspective we believe the best dep strategy for Nexus is not peer deps but internalized deps. Recapping the tradeoffs:

  • Cost: Giving you less flexibility to tailor the packages you use
    ...but even then not really. You can have final say via Yarn's resolutions setting.

  • Benefit: For us, lowering our implementation complexity, tests, and test infrastructure; The number of bugs which we in turn have to spend more time triaging, reproducting, and investigating. For you, less things to install, think about; More confidence that integration works; More overhead around dep management, changelog consumption, upgrade paths, and, invariably, bug reporting.

Glossary

Assembly

The process in which the app "configuration" (settings, schema type defs, used plugins, etc.) is processed into a runnable state. Nexus apps are only ever assembled once in their process lifecycle.

Reflection

The general idea of the app source code or its state at runtime being analyzed to extract data.

Dynamic Reflection

The process in which the app is run in order to extract data out of it. The data in turn can be used for anything. For example typegen to provide better TypeScript types or GraphQL SDL generation.

Static Reflection

The process in which the app is analyzed with the TS API to extract data out of it. The data in turn can be used for anything. For example typegen to provide better TypeScript types.

Edit this page on Github