Skip to content

Commit efc3ee3

Browse files
author
Jason Kuhrt
authored
feat: app assembly (#784)
This feature removes documented server methods and adds three new but undocumented app methods: start, stop, assemble. We will see if we document these in a future change. Also these methods are not statically exported as named exports. Motivations: What has been done is mostly about refactoring. - decouple concept of starting server from assembly to support future serverless feature #782 - assembly is the process of gathering the lazy state into a final runnable one - settings is now a component like schema and server - the components are designed to look a lot more symmetrical now - root app state is passed down to components which attach their namespaced state to, easier to reason about, think redux - introduce concept of checks that runs after schema assembly BREAKING CHANGE: - `server.start` and `server.stop` are no longer exposed. If you had a use-case for them please open an issue to discuss.
1 parent 2017db6 commit efc3ee3

18 files changed

+817
-450
lines changed

docs/api/modules/main/exports/server.md

-16
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,6 @@
44

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

7-
### `start`
8-
9-
Make the server start listening for incoming client connections.
10-
11-
Calling start while already started is a no-op.
12-
13-
Normally you should not need to use this method. When your app does not call `server.start`, Nexus will do so for you automatically.
14-
15-
### `stop`
16-
17-
Make the server stop listening for incoming client connections.
18-
19-
Calling stop while the server is already stopped is a no-op.
20-
21-
Normally you should not need to use this method.
22-
237
### `express`
248

259
Gives you access to the underlying `express` instance.

docs/architecture.md

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## Reflection
2+
3+
todo
4+
5+
## Assembly
6+
7+
todo
8+
19
## Build Flow
210

311
1. The app layout is calculated
@@ -37,3 +45,21 @@ what follows is a stub
3745
1. validate imported value
3846
1. load plugin
3947
1. catch any load errors
48+
49+
## Glossary
50+
51+
### Assembly
52+
53+
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.
54+
55+
### Dynamic Reflection
56+
57+
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.
58+
59+
### Static Reflection
60+
61+
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.
62+
63+
### Reflection
64+
65+
The general idea of the app source code or its state at runtime being analyzed to extract data.

src/lib/reflection/fork-script.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async function run() {
1212
throw new Error('process.env.NEXUS_REFLECTION_LAYOUT is required')
1313
}
1414

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

27-
const plugins = app.__state.plugins()
28-
2927
if (isReflectionStage('plugin')) {
3028
sendDataToParent({
3129
type: 'success-plugin',
3230
data: {
33-
plugins,
31+
plugins: app.private.state.plugins,
3432
},
3533
})
3634

3735
return
3836
}
3937

4038
if (isReflectionStage('typegen')) {
41-
const graphqlSchema = app.__state.schema()
42-
4339
try {
4440
await writeArtifacts({
45-
graphqlSchema,
41+
graphqlSchema: app.private.state.schemaComponent.schema!,
4642
layout,
4743
schemaSettings: app.settings.current.schema,
48-
plugins: Plugin.importAndLoadRuntimePlugins(plugins),
44+
plugins: Plugin.importAndLoadRuntimePlugins(app.private.state.plugins),
4945
})
5046
sendDataToParent({
5147
type: 'success-typegen',

src/lib/reflection/stage.ts

+7
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ export function saveReflectionStageEnv(type: ReflectionType) {
1717
export function isReflectionStage(type: ReflectionType) {
1818
return process.env[REFLECTION_ENV_VAR] === type
1919
}
20+
21+
/**
22+
* Check whether the app is executing in its reflection stage
23+
*/
24+
export function isReflection() {
25+
return process.env[REFLECTION_ENV_VAR] !== undefined
26+
}

src/runtime/app.ts

+83-126
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import * as Logger from '@nexus/logger'
22
import { stripIndent } from 'common-tags'
3-
import * as Lo from 'lodash'
43
import * as Plugin from '../lib/plugin'
4+
import * as Reflection from '../lib/reflection'
55
import * as Schema from './schema'
66
import * as Server from './server'
7-
import * as NexusSchema from '@nexus/schema'
8-
import * as Reflection from '../lib/reflection'
7+
import * as Settings from './settings'
98

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

@@ -27,168 +26,126 @@ export interface App {
2726
/**
2827
* todo
2928
*/
30-
settings: Settings
29+
settings: Settings.Settings
3130
/**
3231
* [API Reference](https://www.nexusjs.org/#/api/modules/main/exports/schema) // [Guide](todo)
3332
*
3433
* ### todo
3534
*/
3635
schema: Schema.Schema
37-
36+
/**
37+
* todo
38+
*/
3839
use(plugin: Plugin.Plugin): void
39-
}
40-
41-
type SettingsInput = {
42-
logger?: Logger.SettingsInput
43-
schema?: Schema.SettingsInput
44-
server?: Server.SettingsInput
45-
}
46-
47-
export type SettingsData = Readonly<{
48-
logger: Logger.SettingsData
49-
schema: Schema.SettingsData
50-
server: Server.SettingsData
51-
}>
52-
53-
/**
54-
* todo
55-
*/
56-
export type Settings = {
5740
/**
5841
* todo
5942
*/
60-
original: SettingsData
43+
assemble(): any
44+
/**
45+
* todo
46+
*/
47+
start(): any
6148
/**
6249
* todo
6350
*/
64-
current: SettingsData
51+
stop(): any
6552
/**
6653
* todo
6754
*/
68-
change(newSetting: SettingsInput): void
6955
}
7056

7157
export type AppState = {
72-
plugins: () => Plugin.Plugin[]
73-
schema: () => NexusSchema.core.NexusGraphQLSchema
74-
isWasServerStartCalled: boolean
58+
plugins: Plugin.Plugin[]
59+
// schema: () => NexusSchema.core.NexusGraphQLSchema
60+
/**
61+
* Once the app is started incremental component APIs can no longer be used. This
62+
* flag let's those APIs detect that they are being used after app start. Then
63+
* they can do something useful like tell the user about their mistake.
64+
*/
65+
assembled: boolean
66+
running: boolean
67+
schemaComponent: Schema.LazyState
7568
}
7669

77-
export type InternalApp = App & {
78-
__state: AppState
70+
export type PrivateApp = App & {
71+
private: {
72+
state: AppState
73+
}
74+
}
75+
76+
/**
77+
* Create new app state. Be careful to pass this state to components to complete its
78+
* data. The data returned only contains core state, despite what the return
79+
* type says.
80+
*/
81+
export function createAppState(): AppState {
82+
const appState = {
83+
assembled: false,
84+
running: false,
85+
plugins: [],
86+
} as Omit<AppState, 'schemaComponent'>
87+
88+
return appState as any
7989
}
8090

8191
/**
82-
* Crate an app instance
92+
* Create an app instance
8393
*/
8494
export function create(): App {
85-
let _plugins: Plugin.Plugin[] = []
86-
let _schema: NexusSchema.core.NexusGraphQLSchema | null = null
95+
const appState = createAppState()
96+
const serverComponent = Server.create(appState)
97+
const schemaComponent = Schema.create(appState)
98+
const settingsComponent = Settings.create({
99+
serverSettings: serverComponent.private.settings,
100+
schemaSettings: schemaComponent.private.settings,
101+
log,
102+
})
103+
const api: PrivateApp = {
104+
log: log,
105+
settings: settingsComponent.public,
106+
schema: schemaComponent.public,
107+
server: serverComponent.public,
108+
// todo call this in the start module
109+
assemble() {
110+
if (appState.assembled) return
87111

88-
const __state: InternalApp['__state'] = {
89-
plugins() {
90-
return _plugins
91-
},
92-
schema() {
93-
if (_schema === null) {
94-
throw new Error('GraphQL schema was not built yet. server.start() needs to be run first')
95-
}
112+
appState.assembled = true
96113

97-
return _schema
98-
},
99-
isWasServerStartCalled: false,
100-
}
114+
if (Reflection.isReflectionStage('plugin')) return
101115

102-
const server = Server.create()
103-
const schemaComponent = Schema.create(__state)
116+
const loadedRuntimePlugins = Plugin.importAndLoadRuntimePlugins(appState.plugins)
104117

105-
const settings: Settings = {
106-
change(newSettings) {
107-
if (newSettings.logger) {
108-
log.settings(newSettings.logger)
109-
}
110-
if (newSettings.schema) {
111-
schemaComponent.private.settings.change(newSettings.schema)
112-
}
113-
if (newSettings.server) {
114-
server.settings.change(newSettings.server)
115-
}
118+
schemaComponent.private.assemble(loadedRuntimePlugins)
119+
120+
if (Reflection.isReflectionStage('typegen')) return
121+
122+
schemaComponent.private.checks()
123+
124+
serverComponent.private.assemble(loadedRuntimePlugins, appState.schemaComponent.schema!)
116125
},
117-
current: {
118-
logger: log.settings,
119-
schema: schemaComponent.private.settings.data,
120-
server: server.settings.data,
126+
async start() {
127+
if (Reflection.isReflection()) return
128+
if (appState.running) return
129+
await serverComponent.private.start()
130+
appState.running = true
131+
},
132+
async stop() {
133+
if (Reflection.isReflection()) return
134+
if (!appState.running) return
135+
await serverComponent.private.stop()
136+
appState.running = false
121137
},
122-
original: Lo.cloneDeep({
123-
logger: log.settings,
124-
schema: schemaComponent.private.settings.data,
125-
server: server.settings.data,
126-
}),
127-
}
128-
129-
const api: InternalApp = {
130-
__state,
131-
log: log,
132-
settings: settings,
133-
schema: schemaComponent.public,
134138
use(plugin) {
135-
if (__state.isWasServerStartCalled === true) {
139+
if (appState.assembled === true) {
136140
log.warn(stripIndent`
137141
A plugin was ignored because it was loaded after the server was started
138142
Make sure to call \`use\` before you call \`server.start\`
139143
`)
140144
}
141-
142-
_plugins.push(plugin)
145+
appState.plugins.push(plugin)
143146
},
144-
server: {
145-
express: server.express,
146-
/**
147-
* Start the server. If you do not call this explicitly then nexus will
148-
* for you. You should not normally need to call this function yourself.
149-
*/
150-
async start() {
151-
// Track the start call so that we can know in entrypoint whether to run
152-
// or not start for the user.
153-
__state.isWasServerStartCalled = true
154-
155-
/**
156-
* If loading plugins, we need to return here, before loading the runtime plugins
157-
*/
158-
if (Reflection.isReflectionStage('plugin')) {
159-
return Promise.resolve()
160-
}
161-
162-
const plugins = Plugin.importAndLoadRuntimePlugins(__state.plugins())
163-
const { schema, assertValidSchema } = schemaComponent.private.makeSchema(plugins)
164-
165-
/**
166-
* Set the schema in the app state so that the reflection step can access it
167-
*/
168-
_schema = schema
169-
170-
/**
171-
* If execution in in reflection stage, do not run the server
172-
*/
173-
if (Reflection.isReflectionStage('typegen')) {
174-
return Promise.resolve()
175-
}
176-
177-
/**
178-
* This needs to be done **after** the reflection step,
179-
* to make sure typegen can still run in case of missing types
180-
*/
181-
assertValidSchema()
182-
183-
await server.setupAndStart({
184-
schema,
185-
plugins,
186-
contextContributors: schemaComponent.private.state.contextContributors,
187-
})
188-
},
189-
stop() {
190-
return server.stop()
191-
},
147+
private: {
148+
state: appState,
192149
},
193150
}
194151

src/runtime/schema/index.spec.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { plugin } from '@nexus/schema'
2-
import { create } from './schema'
2+
import { AppState, createAppState } from '../app'
3+
import { create, SchemaInternal } from './schema'
34
import { mapSettingsToNexusSchemaConfig } from './settings'
45

5-
let schema: ReturnType<typeof create>
6+
let schema: SchemaInternal
7+
let appState: AppState
68

7-
// TODO: Figure out how to deal with `schema` and `plugins`
89
beforeEach(() => {
9-
schema = create({ isWasServerStartCalled: false, plugins: () => [], schema: () => null as any })
10+
appState = createAppState()
11+
schema = create(appState)
1012
})
1113

1214
it('defaults to outputs being nullable by default', () => {
@@ -39,6 +41,6 @@ describe('use', () => {
3941
it('incrementally adds plugins', () => {
4042
schema.public.use(plugin({ name: 'foo' }))
4143
schema.public.use(plugin({ name: 'bar' }))
42-
expect(schema.private.state.schemaPlugins.length).toEqual(2)
44+
expect(appState.schemaComponent.plugins.length).toEqual(2)
4345
})
4446
})

0 commit comments

Comments
 (0)