Skip to content

Commit f41196f

Browse files
author
Jason Kuhrt
authored
feat(lifecycle): add new app component lifecycle (#1211)
closes #758 supports #1201
1 parent 7778cc1 commit f41196f

File tree

14 files changed

+290
-61
lines changed

14 files changed

+290
-61
lines changed

src/lib/reflection/fork-script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async function run() {
2929
try {
3030
const plugins = Plugin.importAndLoadRuntimePlugins(
3131
app.private.state.plugins,
32-
app.private.state.schemaComponent.scalars
32+
app.private.state.components.schema.scalars
3333
)
3434
const artifactsRes = await writeArtifacts({
3535
graphqlSchema: app.private.state.assembled!.schema,

src/lib/reflection/reflect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as Layout from '../layout'
66
import { rootLogger } from '../nexus-logger'
77
import { Plugin } from '../plugin'
88
import { deserializeError, SerializedError } from '../utils'
9-
import { getReflectionStageEnv, removeReflectionStage, setReflectionStage } from './stage'
9+
import { getReflectionStageEnv, setReflectionStage, unsetReflectionStage } from './stage'
1010

1111
const log = rootLogger.child('reflection')
1212

@@ -72,7 +72,7 @@ export async function runPluginsReflectionOnMainThread(
7272
try {
7373
await appRunner.start()
7474

75-
removeReflectionStage()
75+
unsetReflectionStage()
7676

7777
return { success: true, plugins: app.private.state.plugins }
7878
} catch (error) {

src/lib/reflection/stage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ export function setReflectionStage(type: ReflectionType) {
1515
process.env[REFLECTION_ENV_VAR] = type
1616
}
1717

18-
export function removeReflectionStage() {
19-
process.env[REFLECTION_ENV_VAR] = undefined
18+
export function unsetReflectionStage() {
19+
// assigning `undefined` will result in envar becoming string 'undefined'
20+
delete process.env[REFLECTION_ENV_VAR]
2021
}
2122

2223
/**

src/runtime/app.spec.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { log } from '@nexus/logger'
2+
import * as GraphQL from 'graphql'
23
import * as HTTP from 'http'
4+
import 'jest-extended'
35
import * as Lo from 'lodash'
4-
import { removeReflectionStage, setReflectionStage } from '../lib/reflection'
6+
import { setReflectionStage, unsetReflectionStage } from '../lib/reflection'
57
import * as App from './app'
8+
import * as Lifecycle from './lifecycle'
69

710
let app: App.PrivateApp
811

@@ -21,11 +24,13 @@ describe('reset', () => {
2124
schema: { connections: { foo: {} } },
2225
})
2326
app.schema.objectType({ name: 'Foo', definition() {} })
27+
app.on.start(() => {})
2428
app.assemble()
2529
app.reset()
2630
expect(app.settings.current.server.path).toEqual(app.settings.original.server.path)
2731
expect(app.settings.current.schema).toEqual(app.settings.original.schema)
2832
expect(app.private.state).toEqual(originalAppState)
33+
expect(app.private.state.components.lifecycle).toEqual(Lifecycle.createLazyState())
2934
})
3035

3136
it('calling before assemble is fine', () => {
@@ -68,6 +73,40 @@ describe('assemble', () => {
6873
})
6974
})
7075

76+
describe('lifecycle', () => {
77+
beforeEach(() => {
78+
app.settings.change({ server: { port: 7583 } })
79+
app.schema.queryType({
80+
definition(t) {
81+
t.string('foo')
82+
},
83+
})
84+
})
85+
afterEach(async () => {
86+
await app.stop()
87+
})
88+
describe('start', () => {
89+
it('callback is called with data when app is started', async () => {
90+
const fn = jest.fn()
91+
app.on.start(fn)
92+
app.assemble()
93+
await app.start()
94+
expect(fn.mock.calls[0][0].schema instanceof GraphQL.GraphQLSchema).toBeTrue()
95+
})
96+
it('if callback throws error then Nexus shows a nice error', async () => {
97+
app.on.start(() => {
98+
throw new Error('error from user code')
99+
})
100+
app.assemble()
101+
expect(await app.start().catch((e: Error) => e)).toMatchInlineSnapshot(`
102+
[Error: Lifecycle callback error on event "start":
103+
104+
error from user code]
105+
`)
106+
})
107+
})
108+
})
109+
71110
describe('checks', () => {
72111
const spy = createLogSpy()
73112

@@ -96,7 +135,7 @@ describe('server', () => {
96135
const p = app.server.handlers.playground as any
97136
expect(g()).toBeUndefined()
98137
expect(p()).toBeUndefined()
99-
removeReflectionStage()
138+
unsetReflectionStage()
100139
})
101140

102141
// todo, process exit poop

src/runtime/app.ts

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { rootLogger } from '../lib/nexus-logger'
44
import * as Plugin from '../lib/plugin'
55
import { RuntimeContributions } from '../lib/plugin'
66
import * as Reflection from '../lib/reflection/stage'
7+
import { builtinScalars } from '../lib/scalars'
78
import { Index } from '../lib/utils'
9+
import * as Lifecycle from './lifecycle'
810
import * as Schema from './schema'
911
import * as Server from './server'
1012
import { ContextCreator } from './server/server'
1113
import * as Settings from './settings'
1214
import { assertAppNotAssembled } from './utils'
13-
import { builtinScalars } from '../lib/scalars'
1415

1516
const log = Logger.log.child('app')
1617

@@ -33,6 +34,12 @@ export interface App {
3334
* [API Reference](https://nxs.li/docs/api/settings) ⌁ [Issues](https://nxs.li/issues/components/settings)
3435
*/
3536
settings: Settings.Settings
37+
/**
38+
* [API Reference](https://nxs.li/docs/api/on) ⌁ [Issues](https://nxs.li/issues/components/lifecycle)
39+
*
40+
* Use the lifecycle component to tap into application events.
41+
*/
42+
on: Lifecycle.Lifecycle
3643
/**
3744
* [API Reference](https://nxs.li/docs/api/use-plugins) ⌁ [Issues](https://nxs.li/issues/components/plugins)
3845
*/
@@ -84,7 +91,10 @@ export type AppState = {
8491
createContext: ContextCreator
8592
}
8693
running: boolean
87-
schemaComponent: Schema.LazyState
94+
components: {
95+
schema: Schema.LazyState
96+
lifecycle: Lifecycle.LazyState
97+
}
8898
}
8999

90100
export type PrivateApp = App & {
@@ -99,45 +109,71 @@ export type PrivateApp = App & {
99109
* type says.
100110
*/
101111
export function createAppState(): AppState {
102-
const appState = {
112+
const appState: AppState = {
103113
assembled: null,
104114
running: false,
105115
plugins: [],
106-
} as Omit<AppState, 'schemaComponent'>
116+
components: {} as any, // populated by components
117+
}
107118

108-
return appState as any
119+
return appState
109120
}
110121

111122
/**
112123
* Create an app instance
113124
*/
114125
export function create(): App {
115-
const appState = createAppState()
116-
const serverComponent = Server.create(appState)
117-
const schemaComponent = Schema.create(appState)
118-
const settingsComponent = Settings.create(appState, {
126+
const state = createAppState()
127+
const serverComponent = Server.create(state)
128+
const schemaComponent = Schema.create(state)
129+
const settingsComponent = Settings.create(state, {
119130
serverSettings: serverComponent.private.settings,
120131
schemaSettings: schemaComponent.private.settings,
121132
log: Logger.log,
122133
})
134+
const lifecycleComponent = Lifecycle.create(state)
123135

124136
const app: App = {
125137
log: log,
126138
settings: settingsComponent.public,
127139
schema: schemaComponent.public,
128140
server: serverComponent.public,
141+
on: lifecycleComponent.public,
129142
reset() {
130143
// todo once we have log filtering, make this debug level
131144
rootLogger.trace('resetting state')
132145
schemaComponent.private.reset()
133146
serverComponent.private.reset()
134147
settingsComponent.private.reset()
135-
appState.assembled = null
136-
appState.plugins = []
137-
appState.running = false
148+
lifecycleComponent.private.reset()
149+
state.assembled = null
150+
state.plugins = []
151+
state.running = false
152+
},
153+
async start() {
154+
if (Reflection.isReflection()) return
155+
if (state.running) return
156+
if (!state.assembled) {
157+
throw new Error('Must call app.assemble before calling app.start')
158+
}
159+
lifecycleComponent.private.trigger.start({
160+
schema: state.assembled!.schema,
161+
})
162+
await serverComponent.private.start()
163+
state.running = true
164+
},
165+
async stop() {
166+
if (Reflection.isReflection()) return
167+
if (!state.running) return
168+
await serverComponent.private.stop()
169+
state.running = false
170+
},
171+
use(plugin) {
172+
assertAppNotAssembled(state, 'app.use', 'The plugin you attempted to use will be ignored')
173+
state.plugins.push(plugin)
138174
},
139175
assemble() {
140-
if (appState.assembled) return
176+
if (state.assembled) return
141177

142178
schemaComponent.private.beforeAssembly()
143179

@@ -150,44 +186,25 @@ export function create(): App {
150186
*/
151187
if (Reflection.isReflectionStage('plugin')) return
152188

153-
appState.assembled = {} as AppState['assembled']
189+
state.assembled = {} as AppState['assembled']
154190

155-
const loadedPlugins = Plugin.importAndLoadRuntimePlugins(
156-
appState.plugins,
157-
appState.schemaComponent.scalars
158-
)
159-
appState.assembled!.loadedPlugins = loadedPlugins
191+
const loadedPlugins = Plugin.importAndLoadRuntimePlugins(state.plugins, state.components.schema.scalars)
192+
state.assembled!.loadedPlugins = loadedPlugins
160193

161194
const { schema, missingTypes } = schemaComponent.private.assemble(loadedPlugins)
162-
appState.assembled!.schema = schema
163-
appState.assembled!.missingTypes = missingTypes
195+
state.assembled!.schema = schema
196+
state.assembled!.missingTypes = missingTypes
164197

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

167200
const { createContext } = serverComponent.private.assemble(loadedPlugins, schema)
168-
appState.assembled!.createContext = createContext
201+
state.assembled!.createContext = createContext
169202

170203
const { settings } = settingsComponent.private.assemble()
171-
appState.assembled!.settings = settings
204+
state.assembled!.settings = settings
172205

173206
schemaComponent.private.checks()
174207
},
175-
async start() {
176-
if (Reflection.isReflection()) return
177-
if (appState.running) return
178-
await serverComponent.private.start()
179-
appState.running = true
180-
},
181-
async stop() {
182-
if (Reflection.isReflection()) return
183-
if (!appState.running) return
184-
await serverComponent.private.stop()
185-
appState.running = false
186-
},
187-
use(plugin) {
188-
assertAppNotAssembled(appState, 'app.use', 'The plugin you attempted to use will be ignored')
189-
appState.plugins.push(plugin)
190-
},
191208
}
192209

193210
/**
@@ -209,7 +226,7 @@ export function create(): App {
209226
return {
210227
...app,
211228
private: {
212-
state: appState,
229+
state: state,
213230
},
214231
} as App
215232
}

src/runtime/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export const schema = app.schema
3737
*/
3838
export const settings = app.settings
3939

40+
/**
41+
* [API Reference](https://nxs.li/docs/api/on) ⌁ [Issues](https://nxs.li/issues/components/lifecycle)
42+
*
43+
* Use the lifecycle component to tap into application events.
44+
*/
45+
export const on = app.on
46+
4047
/**
4148
* [API Reference](https://nxs.li/docs/api/use-plugins) ⌁ [Issues](https://nxs.li/issues/components/plugins)
4249
*/

src/runtime/lifecycle/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lifecycle'

0 commit comments

Comments
 (0)