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

feat: app assembly #784

Merged
merged 4 commits into from
May 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions docs/api/modules/main/exports/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,6 @@

Use the server to run the HTTP server that clients will connect to.

### `start`

Make the server start listening for incoming client connections.

Calling start while already started is a no-op.

Normally you should not need to use this method. When your app does not call `server.start`, Nexus will do so for you automatically.

### `stop`

Make the server stop listening for incoming client connections.

Calling stop while the server is already stopped is a no-op.

Normally you should not need to use this method.

### `express`

Gives you access to the underlying `express` instance.
Expand Down
26 changes: 26 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Reflection

todo

## Assembly

todo

## Build Flow

1. The app layout is calculated
Expand Down Expand Up @@ -37,3 +45,21 @@ what follows is a stub
1. validate imported value
1. load plugin
1. catch any load errors

## 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 lifecycle.

### 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.

### Reflection

The general idea of the app source code or its state at runtime being analyzed to extract data.
12 changes: 4 additions & 8 deletions src/lib/reflection/fork-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function run() {
throw new Error('process.env.NEXUS_REFLECTION_LAYOUT is required')
}

const app = require('../../').default as App.InternalApp
const app = require('../../').default as App.PrivateApp
const layout = Layout.createFromData(JSON.parse(process.env.NEXUS_REFLECTION_LAYOUT) as Layout.Data)
const appRunner = createDevAppRunner(layout, app, {
catchUnhandledErrors: false,
Expand All @@ -24,28 +24,24 @@ async function run() {
sendErrorToParent(err)
}

const plugins = app.__state.plugins()

if (isReflectionStage('plugin')) {
sendDataToParent({
type: 'success-plugin',
data: {
plugins,
plugins: app.private.state.plugins,
},
})

return
}

if (isReflectionStage('typegen')) {
const graphqlSchema = app.__state.schema()

try {
await writeArtifacts({
graphqlSchema,
graphqlSchema: app.private.state.schemaComponent.schema!,
layout,
schemaSettings: app.settings.current.schema,
plugins: Plugin.importAndLoadRuntimePlugins(plugins),
plugins: Plugin.importAndLoadRuntimePlugins(app.private.state.plugins),
})
sendDataToParent({
type: 'success-typegen',
Expand Down
7 changes: 7 additions & 0 deletions src/lib/reflection/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ export function saveReflectionStageEnv(type: ReflectionType) {
export function isReflectionStage(type: ReflectionType) {
return process.env[REFLECTION_ENV_VAR] === type
}

/**
* Check whether the app is executing in its reflection stage
*/
export function isReflection() {
return process.env[REFLECTION_ENV_VAR] !== undefined
}
209 changes: 83 additions & 126 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as Logger from '@nexus/logger'
import { stripIndent } from 'common-tags'
import * as Lo from 'lodash'
import * as Plugin from '../lib/plugin'
import * as Reflection from '../lib/reflection'
import * as Schema from './schema'
import * as Server from './server'
import * as NexusSchema from '@nexus/schema'
import * as Reflection from '../lib/reflection'
import * as Settings from './settings'

const log = Logger.create({ name: 'app' })

Expand All @@ -27,168 +26,126 @@ export interface App {
/**
* todo
*/
settings: Settings
settings: Settings.Settings
/**
* [API Reference](https://www.nexusjs.org/#/api/modules/main/exports/schema) // [Guide](todo)
*
* ### todo
*/
schema: Schema.Schema

/**
* todo
*/
use(plugin: Plugin.Plugin): void
}

type SettingsInput = {
logger?: Logger.SettingsInput
schema?: Schema.SettingsInput
server?: Server.SettingsInput
}

export type SettingsData = Readonly<{
logger: Logger.SettingsData
schema: Schema.SettingsData
server: Server.SettingsData
}>

/**
* todo
*/
export type Settings = {
/**
* todo
*/
original: SettingsData
assemble(): any
/**
* todo
*/
start(): any
/**
* todo
*/
current: SettingsData
stop(): any
/**
* todo
*/
change(newSetting: SettingsInput): void
}

export type AppState = {
plugins: () => Plugin.Plugin[]
schema: () => NexusSchema.core.NexusGraphQLSchema
isWasServerStartCalled: boolean
plugins: Plugin.Plugin[]
// schema: () => NexusSchema.core.NexusGraphQLSchema
/**
* Once the app is started incremental component APIs can no longer be used. This
* flag let's those APIs detect that they are being used after app start. Then
* they can do something useful like tell the user about their mistake.
*/
assembled: boolean
running: boolean
schemaComponent: Schema.LazyState
}

export type InternalApp = App & {
__state: AppState
export type PrivateApp = App & {
private: {
state: AppState
}
}

/**
* Create new app state. Be careful to pass this state to components to complete its
* data. The data returned only contains core state, despite what the return
* type says.
*/
export function createAppState(): AppState {
const appState = {
assembled: false,
running: false,
plugins: [],
} as Omit<AppState, 'schemaComponent'>

return appState as any
}

/**
* Crate an app instance
* Create an app instance
*/
export function create(): App {
let _plugins: Plugin.Plugin[] = []
let _schema: NexusSchema.core.NexusGraphQLSchema | null = null
const appState = createAppState()
const serverComponent = Server.create(appState)
const schemaComponent = Schema.create(appState)
const settingsComponent = Settings.create({
serverSettings: serverComponent.private.settings,
schemaSettings: schemaComponent.private.settings,
log,
})
const api: PrivateApp = {
log: log,
settings: settingsComponent.public,
schema: schemaComponent.public,
server: serverComponent.public,
// todo call this in the start module
assemble() {
if (appState.assembled) return

const __state: InternalApp['__state'] = {
plugins() {
return _plugins
},
schema() {
if (_schema === null) {
throw new Error('GraphQL schema was not built yet. server.start() needs to be run first')
}
appState.assembled = true

return _schema
},
isWasServerStartCalled: false,
}
if (Reflection.isReflectionStage('plugin')) return

const server = Server.create()
const schemaComponent = Schema.create(__state)
const loadedRuntimePlugins = Plugin.importAndLoadRuntimePlugins(appState.plugins)

const settings: Settings = {
change(newSettings) {
if (newSettings.logger) {
log.settings(newSettings.logger)
}
if (newSettings.schema) {
schemaComponent.private.settings.change(newSettings.schema)
}
if (newSettings.server) {
server.settings.change(newSettings.server)
}
schemaComponent.private.assemble(loadedRuntimePlugins)

if (Reflection.isReflectionStage('typegen')) return

schemaComponent.private.checks()

serverComponent.private.assemble(loadedRuntimePlugins, appState.schemaComponent.schema!)
},
current: {
logger: log.settings,
schema: schemaComponent.private.settings.data,
server: server.settings.data,
async start() {
if (Reflection.isReflection()) return
if (appState.running) return
await serverComponent.private.start()
appState.running = true
},
async stop() {
if (Reflection.isReflection()) return
if (!appState.running) return
await serverComponent.private.stop()
appState.running = false
},
original: Lo.cloneDeep({
logger: log.settings,
schema: schemaComponent.private.settings.data,
server: server.settings.data,
}),
}

const api: InternalApp = {
__state,
log: log,
settings: settings,
schema: schemaComponent.public,
use(plugin) {
if (__state.isWasServerStartCalled === true) {
if (appState.assembled === true) {
log.warn(stripIndent`
A plugin was ignored because it was loaded after the server was started
Make sure to call \`use\` before you call \`server.start\`
`)
}

_plugins.push(plugin)
appState.plugins.push(plugin)
},
server: {
express: server.express,
/**
* Start the server. If you do not call this explicitly then nexus will
* for you. You should not normally need to call this function yourself.
*/
async start() {
// Track the start call so that we can know in entrypoint whether to run
// or not start for the user.
__state.isWasServerStartCalled = true

/**
* If loading plugins, we need to return here, before loading the runtime plugins
*/
if (Reflection.isReflectionStage('plugin')) {
return Promise.resolve()
}

const plugins = Plugin.importAndLoadRuntimePlugins(__state.plugins())
const { schema, assertValidSchema } = schemaComponent.private.makeSchema(plugins)

/**
* Set the schema in the app state so that the reflection step can access it
*/
_schema = schema

/**
* If execution in in reflection stage, do not run the server
*/
if (Reflection.isReflectionStage('typegen')) {
return Promise.resolve()
}

/**
* This needs to be done **after** the reflection step,
* to make sure typegen can still run in case of missing types
*/
assertValidSchema()

await server.setupAndStart({
schema,
plugins,
contextContributors: schemaComponent.private.state.contextContributors,
})
},
stop() {
return server.stop()
},
private: {
state: appState,
},
}

Expand Down
12 changes: 7 additions & 5 deletions src/runtime/schema/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { plugin } from '@nexus/schema'
import { create } from './schema'
import { AppState, createAppState } from '../app'
import { create, SchemaInternal } from './schema'
import { mapSettingsToNexusSchemaConfig } from './settings'

let schema: ReturnType<typeof create>
let schema: SchemaInternal
let appState: AppState

// TODO: Figure out how to deal with `schema` and `plugins`
beforeEach(() => {
schema = create({ isWasServerStartCalled: false, plugins: () => [], schema: () => null as any })
appState = createAppState()
schema = create(appState)
})

it('defaults to outputs being nullable by default', () => {
Expand Down Expand Up @@ -39,6 +41,6 @@ describe('use', () => {
it('incrementally adds plugins', () => {
schema.public.use(plugin({ name: 'foo' }))
schema.public.use(plugin({ name: 'bar' }))
expect(schema.private.state.schemaPlugins.length).toEqual(2)
expect(appState.schemaComponent.plugins.length).toEqual(2)
})
})
Loading