Skip to content
This repository has been archived by the owner on Jul 27, 2020. It is now read-only.

Commit

Permalink
Support an express.ts file in which the express instance is passed
Browse files Browse the repository at this point in the history
  • Loading branch information
Weakky committed Mar 2, 2019
1 parent 18c4c2f commit 1805b3c
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 76 deletions.
5 changes: 3 additions & 2 deletions packages/yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
"yoga": "dist/cli/index.js"
},
"dependencies": {
"apollo-server": "^2.4.2",
"apollo-server-express": "^2.4.8",
"chalk": "^2.4.2",
"chokidar": "^2.1.1",
"create-yoga": "0.0.2",
"decache": "^4.5.1",
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0",
"inquirer": "^6.2.1",
"js-yaml": "^3.12.1",
"nexus": "0.9.17",
"nexus": "0.10.0",
"nexus-prisma": "0.3.3",
"pluralize": "^7.0.0",
"pretty-error": "2.2.0-rc.1",
Expand All @@ -33,6 +33,7 @@
},
"devDependencies": {
"@types/chokidar": "1.7.5",
"@types/express": "^4.16.1",
"@types/graphql": "14.0.7",
"@types/inquirer": "0.0.44",
"@types/js-yaml": "3.12.0",
Expand Down
4 changes: 1 addition & 3 deletions packages/yoga/src/cli/commands/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@ import { start } from '../../../server'
import { importYogaConfig } from '../../../config'

export default async () => {
const { yogaConfig, prismaClientDir } = importYogaConfig()

return start(yogaConfig, prismaClientDir)
return start(importYogaConfig())
}
8 changes: 4 additions & 4 deletions packages/yoga/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ export function importUncached<T extends any = any>(
*/
export function importFile<T extends any = any>(
filePath: string,
exportName: string,
exportName?: string,
invalidateModule: boolean = false,
): T {
const importedModule = importUncached(filePath, invalidateModule)
const importedModule = importUncached<T>(filePath, invalidateModule)

if (importedModule[exportName] === undefined) {
if (exportName && importedModule[exportName] === undefined) {
throw new Error(`\`${filePath}\` must have a '${exportName}' export`)
}

return importedModule[exportName]
return exportName ? importedModule[exportName] : importedModule
}
25 changes: 23 additions & 2 deletions packages/yoga/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import * as ApolloServer from 'apollo-server-express'
import { ExpressContext } from 'apollo-server-express/dist/ApolloServer'
import { InputConfig, Yoga } from './types'
import { core } from 'nexus/dist'
import { Application } from 'express'

export * from 'nexus'
export * from 'nexus-prisma'
export * from 'apollo-server'
export { InputConfig, Yoga } from './types'
export { ApolloServer }

export function config(opts: InputConfig) {
return opts
}

export function eject<T extends any = any>(opts: Yoga<T>) {
return opts
}

export function express(fn: (app: Application) => core.MaybePromise<void>) {
return fn
}

export function context(ctx: ((ctx: ExpressContext) => object) | object) {
return ctx
}
118 changes: 67 additions & 51 deletions packages/yoga/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ApolloServer } from 'apollo-server'
import { ApolloServer } from 'apollo-server-express'
import { watch as nativeWatch } from 'chokidar'
import express from 'express'
import { makeSchema } from 'nexus'
import { makePrismaSchema } from 'nexus-prisma'
import * as path from 'path'
import PrettyError from 'pretty-error'
import { register } from 'ts-node'
import { importYogaConfig } from './config'
import { findFileByExtension, importFile, importUncached } from './helpers'
import { findFileByExtension, importFile } from './helpers'
import * as logger from './logger'
import { makeSchemaDefaults } from './nexusDefaults'
import { Config, Yoga } from './types'
import { Config, ConfigWithInfo, Yoga } from './types'

const pe = new PrettyError().appendStyle({
'pretty-error': {
Expand All @@ -31,6 +32,7 @@ register({
export async function watch(): Promise<void> {
logger.clearConsole()
logger.info('Starting development server...')
logger.warn('DEV')
let info = importYogaConfig()
let filesToWatch = [path.join(info.projectDir, '**', '*.ts')]

Expand All @@ -39,11 +41,7 @@ export async function watch(): Promise<void> {
filesToWatch.push(info.datamodelInfoDir)
}

let oldServer: any | undefined = await start(
info.yogaConfig,
info.prismaClientDir,
true,
)
let oldServer: any | undefined = await start(info, true)
let filesToReloadBatched = [] as string[]

nativeWatch(filesToWatch, {
Expand Down Expand Up @@ -71,27 +69,21 @@ export async function watch(): Promise<void> {
return Promise.resolve(true)
}
}
console.clear()
logger.clearConsole()
logger.info('Compiling')

const yogaServer = getYogaServer(info.yogaConfig, info.prismaClientDir)
const { server, startServer, stopServer } = getYogaServer(info)

if (oldServer !== undefined) {
await yogaServer.stopServer(oldServer)
await stopServer(oldServer)
}

const serverInstance = await yogaServer.server(
info.yogaConfig.ejectFilePath
? path.dirname(info.yogaConfig.ejectFilePath)
: __dirname,
)

oldServer = serverInstance
const serverInstance = await server()

logger.clearConsole()
logger.done('Compiled succesfully')

await yogaServer.startServer(serverInstance)
oldServer = await startServer(serverInstance)
} catch (e) {
console.error(pe.render(e))
}
Expand Down Expand Up @@ -123,26 +115,19 @@ function getIgnoredFiles(
}

export async function start(
yogaConfig: Config,
prismaClientDir: string | undefined,
info: ConfigWithInfo,
withLog: boolean = false,
): Promise<any> {
try {
const yogaServer = getYogaServer(yogaConfig, prismaClientDir)
const serverInstance = await yogaServer.server(
yogaConfig.ejectFilePath
? path.dirname(yogaConfig.ejectFilePath)
: __dirname,
)
const { server, startServer } = getYogaServer(info)
const serverInstance = await server()

if (withLog) {
logger.clearConsole()
logger.done('Compiled successfully')
}

await yogaServer.startServer(serverInstance)

return serverInstance
return startServer(serverInstance)
} catch (e) {
console.error(pe.render(e))
}
Expand All @@ -154,74 +139,105 @@ export async function start(
*
* @param resolversPath The `resolversPath` property from the `yoga.config.ts` file
* @param contextPath The `contextPath` property from the `yoga.config.ts` file
* @param expressPath The `expressPath` property from the `yoga.config.ts` file
*/
function importGraphqlTypesAndContext(
function importTypesContextExpressMiddleware(
resolversPath: string,
contextPath: string | undefined,
expressPath: string | undefined,
): {
types: Record<string, any>
context?: any /** Context<any> | ContextFunction<any> */
expressMiddleware?: (app: Express.Application) => Promise<void> | void
} {
const tsFiles = findFileByExtension(resolversPath, '.ts')
const types = findFileByExtension(resolversPath, '.ts').map(file =>
importFile(file),
)
let context = undefined
let express = undefined

if (contextPath !== undefined) {
context = importFile(contextPath, 'default')

if (typeof context !== 'function') {
throw new Error('Context must be a default exported function')
throw new Error(`${contextPath} must default export a function`)
}
}

const types = tsFiles.map(file => importUncached(file))
if (expressPath !== undefined) {
express = importFile(expressPath, 'default')

if (typeof express !== 'function') {
throw new Error(`${expressPath} must default export a function`)
}
}

return {
context,
types: types.reduce((a, b) => ({ ...a, ...b }), {}),
expressMiddleware: express,
types,
}
}

/**
*
* @param config The yoga config object
*/
function getYogaServer(
config: Config,
prismaClientDir: string | undefined,
): Yoga {
function getYogaServer(info: ConfigWithInfo): Yoga {
const { yogaConfig: config } = info

if (!config.ejectFilePath) {
return {
async server() {
const { types, context } = importGraphqlTypesAndContext(
const app = express()
const {
types,
context,
expressMiddleware,
} = importTypesContextExpressMiddleware(
config.resolversPath,
config.contextPath,
config.expressPath,
)
const allTypes: any[] = [types]

const makeSchemaOptions = makeSchemaDefaults(
config,
types,
prismaClientDir,
allTypes,
info.prismaClientDir,
)
const schema = config.prisma
? makePrismaSchema({
...makeSchemaOptions,
prisma: config.prisma,
})
: makeSchema(makeSchemaOptions)

return new ApolloServer({
const server = new ApolloServer({
schema,
context,
})

if (expressMiddleware) {
await expressMiddleware(app)
}

server.applyMiddleware({ app, path: '/' })

return { express: app, server }
},
startServer(server) {
return server
.listen()
.then(({ url }) => console.log(`🚀 Server ready at ${url}`))
async startServer({ express, server }) {
const httpServer = await express.listen({ port: 4000 }, () => {
console.log(
`🚀 Server ready at http://localhost:4000${server.graphqlPath}`,
)
})

return { express: httpServer, server }
},
stopServer(server) {
return server.stop()
stopServer({ express }) {
return express.close()
},
} as Yoga<ApolloServer>
}
}

const yogaServer = importFile<Yoga>(config.ejectFilePath, 'default')
Expand Down
29 changes: 15 additions & 14 deletions packages/yoga/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { PrismaClientInput } from 'nexus-prisma/dist/types'
import { PrismaClientInput, PrismaSchemaConfig } from 'nexus-prisma/dist/types'

export interface DatamodelInfo {
uniqueFieldsByModel: Record<string, string[]>
clientPath: string
schema: { __schema: any }
}
export type DatamodelInfo = PrismaSchemaConfig['prisma']['datamodelInfo']

export type InputPrismaConfig = {
/**
Expand Down Expand Up @@ -37,23 +33,30 @@ export type InputOutputFilesConfig = {

export type InputConfig = {
/**
* Path to the directory where your resolvers are defined.
* Path to the resolvers directory.
* **Path has to exist**
* @default ./src/graphql/
*/
resolversPath?: string
/**
* Path to your context.ts file. **If provided, path has to exist**
* Path to the context.ts file. **If provided, path has to exist**
* @default ./src/context.ts
*/
contextPath?: string
/**
* Path to an `server.ts` file to eject from default configuration file `yoga.config.ts`.
* Path to the `server.ts` file to eject from default configuration file `yoga.config.ts`.
* When provided, all other configuration properties are ignored and should be configured programatically.
* **If provided, path has to exist**
* @default ./src/server.ts
*/
ejectFilePath?: string
/**
* Path to the `express.ts` file.
* This file gets injected the underlying express instance (to add routes, or middlewares etc...)
* **If provided, path has to exist**
* @default ./src/express.ts
*/
expressPath?: string
/**
* Config for the outputted files (schema, typings ..)
*/
Expand All @@ -74,10 +77,8 @@ export type Config = {
contextPath?: RequiredProperty<'contextPath'>
ejectFilePath?: RequiredProperty<'ejectFilePath'>
output: RequiredProperty<'output'>
prisma?: {
datamodelInfo: DatamodelInfo
client: PrismaClientInput
}
prisma?: PrismaSchemaConfig['prisma']
expressPath?: RequiredProperty<'expressPath'>
}

export type ConfigWithInfo = {
Expand All @@ -90,7 +91,7 @@ export type ConfigWithInfo = {
}

export interface Yoga<Server = any> {
server: (dirname: string) => Server | Promise<Server>
server: () => Server | Promise<Server>
startServer: (server: Server) => any | Promise<any>
stopServer: (server: Server) => any | Promise<any>
}
Loading

0 comments on commit 1805b3c

Please sign in to comment.