From 1805b3c58f5e3d96289d4a43cb7979583a74623c Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 2 Mar 2019 16:51:37 +0100 Subject: [PATCH] Support an `express.ts` file in which the express instance is passed --- packages/yoga/package.json | 5 +- packages/yoga/src/cli/commands/start/index.ts | 4 +- packages/yoga/src/helpers.ts | 8 +- packages/yoga/src/index.ts | 25 +++- packages/yoga/src/server.ts | 118 ++++++++++-------- packages/yoga/src/types.ts | 29 ++--- packages/yoga/src/yogaDefaults.ts | 11 ++ packages/yoga/tslint.json | 3 + 8 files changed, 127 insertions(+), 76 deletions(-) diff --git a/packages/yoga/package.json b/packages/yoga/package.json index 32588ab..6a55604 100644 --- a/packages/yoga/package.json +++ b/packages/yoga/package.json @@ -16,7 +16,7 @@ "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", @@ -24,7 +24,7 @@ "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", @@ -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", diff --git a/packages/yoga/src/cli/commands/start/index.ts b/packages/yoga/src/cli/commands/start/index.ts index 644dff7..beec87b 100644 --- a/packages/yoga/src/cli/commands/start/index.ts +++ b/packages/yoga/src/cli/commands/start/index.ts @@ -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()) } diff --git a/packages/yoga/src/helpers.ts b/packages/yoga/src/helpers.ts index 9ec71c4..7788733 100644 --- a/packages/yoga/src/helpers.ts +++ b/packages/yoga/src/helpers.ts @@ -54,14 +54,14 @@ export function importUncached( */ export function importFile( filePath: string, - exportName: string, + exportName?: string, invalidateModule: boolean = false, ): T { - const importedModule = importUncached(filePath, invalidateModule) + const importedModule = importUncached(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 } diff --git a/packages/yoga/src/index.ts b/packages/yoga/src/index.ts index 53824d0..1bc1d1a 100644 --- a/packages/yoga/src/index.ts +++ b/packages/yoga/src/index.ts @@ -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(opts: Yoga) { + return opts +} + +export function express(fn: (app: Application) => core.MaybePromise) { + return fn +} + +export function context(ctx: ((ctx: ExpressContext) => object) | object) { + return ctx +} diff --git a/packages/yoga/src/server.ts b/packages/yoga/src/server.ts index 252595f..6584fcd 100644 --- a/packages/yoga/src/server.ts +++ b/packages/yoga/src/server.ts @@ -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': { @@ -31,6 +32,7 @@ register({ export async function watch(): Promise { logger.clearConsole() logger.info('Starting development server...') + logger.warn('DEV') let info = importYogaConfig() let filesToWatch = [path.join(info.projectDir, '**', '*.ts')] @@ -39,11 +41,7 @@ export async function watch(): Promise { 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, { @@ -71,27 +69,21 @@ export async function watch(): Promise { 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)) } @@ -123,26 +115,19 @@ function getIgnoredFiles( } export async function start( - yogaConfig: Config, - prismaClientDir: string | undefined, + info: ConfigWithInfo, withLog: boolean = false, ): Promise { 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)) } @@ -154,30 +139,43 @@ 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 context?: any /** Context | ContextFunction */ + expressMiddleware?: (app: Express.Application) => Promise | 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, } } @@ -185,21 +183,28 @@ function importGraphqlTypesAndContext( * * @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({ @@ -207,21 +212,32 @@ function getYogaServer( 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 + } } const yogaServer = importFile(config.ejectFilePath, 'default') diff --git a/packages/yoga/src/types.ts b/packages/yoga/src/types.ts index e442ca2..03be429 100644 --- a/packages/yoga/src/types.ts +++ b/packages/yoga/src/types.ts @@ -1,10 +1,6 @@ -import { PrismaClientInput } from 'nexus-prisma/dist/types' +import { PrismaClientInput, PrismaSchemaConfig } from 'nexus-prisma/dist/types' -export interface DatamodelInfo { - uniqueFieldsByModel: Record - clientPath: string - schema: { __schema: any } -} +export type DatamodelInfo = PrismaSchemaConfig['prisma']['datamodelInfo'] export type InputPrismaConfig = { /** @@ -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 ..) */ @@ -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 = { @@ -90,7 +91,7 @@ export type ConfigWithInfo = { } export interface Yoga { - server: (dirname: string) => Server | Promise + server: () => Server | Promise startServer: (server: Server) => any | Promise stopServer: (server: Server) => any | Promise } diff --git a/packages/yoga/src/yogaDefaults.ts b/packages/yoga/src/yogaDefaults.ts index 44f224b..6690582 100644 --- a/packages/yoga/src/yogaDefaults.ts +++ b/packages/yoga/src/yogaDefaults.ts @@ -40,6 +40,7 @@ export const DEFAULTS: Config = { $graphql: null as any, // FIXME }, }, + expressPath: './src/express.ts', } export const DEFAULT_META_SCHEMA_DIR = './.yoga/nexus-prisma/' @@ -56,6 +57,7 @@ export function normalizeConfig( contextPath: contextPath(projectDir, config.contextPath), resolversPath: resolversPath(projectDir, config.resolversPath), ejectFilePath: ejectFilePath(projectDir, config.ejectFilePath), + expressPath: expressPath(projectDir, config.expressPath), output: output(projectDir, config.output), prisma: prisma(projectDir, config.prisma), } @@ -156,6 +158,15 @@ function ejectFilePath( return optional(path, input, buildError(projectDir, path, 'ejectFilePath')) } +function expressPath( + projectDir: string, + input: string | undefined, +): string | undefined { + const path = inputOrDefaultPath(projectDir, input, DEFAULTS.expressPath!) + + return optional(path, input, buildError(projectDir, path, 'expressPath')) +} + function output( projectDir: string, input: InputOutputFilesConfig | undefined, diff --git a/packages/yoga/tslint.json b/packages/yoga/tslint.json index cd6c029..29de186 100644 --- a/packages/yoga/tslint.json +++ b/packages/yoga/tslint.json @@ -1,5 +1,8 @@ { "extends": ["tslint-config-standard", "tslint-config-prettier"], + "linterOptions": { + "exclude": ["node_modules/**"] + }, "rules": { "no-use-before-declare": false, "space-before-function-paren": false