From d6e46172c38699635a180c4b543b3b98ca1c91fc Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Thu, 14 Mar 2019 20:58:55 +0100 Subject: [PATCH 1/2] Support `index.ts` in `resolversPath` --- packages/yoga/src/server.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/yoga/src/server.ts b/packages/yoga/src/server.ts index 1118be4..4a90ba3 100644 --- a/packages/yoga/src/server.ts +++ b/packages/yoga/src/server.ts @@ -1,6 +1,7 @@ import { ApolloServer } from 'apollo-server-express' import { watch as nativeWatch } from 'chokidar' import express from 'express' +import { existsSync } from 'fs' import { Server } from 'http' import { makeSchema } from 'nexus' import { makePrismaSchema } from 'nexus-prisma' @@ -150,9 +151,17 @@ function importArtifacts( context?: any /** Context | ContextFunction */ expressMiddleware?: (app: Express.Application) => Promise | void } { - const types = findFileByExtension(resolversPath, '.ts').map(file => - importFile(file), - ) + const resolversIndexPath = path.join(resolversPath, 'index.ts') + let types: any = null + + if (existsSync(resolversIndexPath)) { + types = importFile(resolversIndexPath) + } else { + types = findFileByExtension(resolversPath, '.ts').map(file => + importFile(file), + ) + } + let context = undefined let express = undefined From 77636d415645def0b71ec7647a0008fec62fcce4 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Thu, 14 Mar 2019 20:59:48 +0100 Subject: [PATCH 2/2] Implement eject command --- examples/minimal/tsconfig.json | 2 +- packages/yoga/src/cli/commands/build/index.ts | 258 +++++------------- .../yoga/src/cli/commands/build/renderers.ts | 223 +++++++++++++++ packages/yoga/src/cli/commands/eject/index.ts | 27 ++ .../yoga/src/cli/commands/scaffold/index.ts | 30 +- packages/yoga/src/cli/index.ts | 2 + packages/yoga/src/helpers.ts | 29 ++ 7 files changed, 356 insertions(+), 215 deletions(-) create mode 100644 packages/yoga/src/cli/commands/build/renderers.ts create mode 100644 packages/yoga/src/cli/commands/eject/index.ts diff --git a/examples/minimal/tsconfig.json b/examples/minimal/tsconfig.json index 76ba725..15412a8 100644 --- a/examples/minimal/tsconfig.json +++ b/examples/minimal/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "moduleResolution": "node", "target": "es5", - "rootDir": "./src", + "rootDir": ".", "outDir": "dist", "sourceMap": true, "lib": ["es2015", "esnext.asynciterable", "es2017", "dom"], diff --git a/packages/yoga/src/cli/commands/build/index.ts b/packages/yoga/src/cli/commands/build/index.ts index 99add13..002aa21 100644 --- a/packages/yoga/src/cli/commands/build/index.ts +++ b/packages/yoga/src/cli/commands/build/index.ts @@ -1,11 +1,15 @@ import { existsSync, writeFileSync } from 'fs' -import { EOL } from 'os' import * as path from 'path' import * as ts from 'typescript' import { findConfigFile, importYogaConfig } from '../../../config' -import { findFileByExtension } from '../../../helpers' import { ConfigWithInfo } from '../../../types' import { DEFAULTS } from '../../../yogaDefaults' +import { + renderIndexFile, + renderPrismaEjectFile, + renderSimpleIndexFile, + renderResolversIndex, +} from './renderers' const diagnosticHost: ts.FormatDiagnosticsHost = { getNewLine: () => ts.sys.newLine, @@ -15,40 +19,15 @@ const diagnosticHost: ts.FormatDiagnosticsHost = { export default () => { const info = importYogaConfig() - const tsConfigPath = findConfigFile('tsconfig.json', { required: true }) - const tsConfigContent = ts.readConfigFile(tsConfigPath, ts.sys.readFile) - - if (tsConfigContent.error) { - throw new Error( - ts.formatDiagnosticsWithColorAndContext( - [tsConfigContent.error], - diagnosticHost, - ), - ) - } - - const inputConfig = ts.parseJsonConfigFileContent( - tsConfigContent.config, - ts.sys, - info.projectDir, - undefined, - tsConfigPath, - ) - const config = fixConfig(inputConfig, info.projectDir) + const config = readConfigFromTsConfig(info) compile(config.fileNames, config.options) - if (!info.yogaConfig.ejectFilePath) { - const ejectFilePath = path.join( - info.projectDir, - path.dirname(DEFAULTS.ejectFilePath!), - 'index.ts', - ) + const ejectFilePath = writeEjectFiles(info, (filePath, content) => { + outputFile(filePath, content, config.options, info) + }) - writeEntryPoint(info, ejectFilePath, config) - } else { - useEntryPoint(info, info.yogaConfig.ejectFilePath, config) - } + useEntryPoint(info, ejectFilePath, config) } function compile(rootNames: string[], options: ts.CompilerOptions) { @@ -68,177 +47,41 @@ function compile(rootNames: string[], options: ts.CompilerOptions) { } } -/** - * Do post-processing on config options to support `ts-node`. - */ -function fixConfig(config: ts.ParsedCommandLine, projectDir: string) { - // Target ES5 output by default (instead of ES3). - if (config.options.target === undefined) { - config.options.target = ts.ScriptTarget.ES5 - } - - // Target CommonJS modules by default (instead of magically switching to ES6 when the target is ES6). - if (config.options.module === undefined) { - config.options.module = ts.ModuleKind.CommonJS - } - - if (config.options.outDir === undefined) { - config.options.outDir = 'dist' - } - - config.options.rootDir = projectDir - - return config -} - function useEntryPoint( info: ConfigWithInfo, ejectFilePath: string, config: ts.ParsedCommandLine, ) { - const indexFile = ` - import yoga from '${getRelativePath( - path.dirname(ejectFilePath), - ejectFilePath, - )}' - - async function main() { - const serverInstance = await yoga.server() - - return yoga.startServer(serverInstance) - } - - main() - ` + const indexFile = renderIndexFile(ejectFilePath) const indexFilePath = path.join(path.dirname(ejectFilePath), 'index.ts') - outputFile(indexFile, indexFilePath, config.options, info) + outputFile(indexFilePath, indexFile, config.options, info) } -function writeEntryPoint( +export function writeEjectFiles( info: ConfigWithInfo, - ejectFilePath: string, - config: ts.ParsedCommandLine, + writeFile: (filePath: string, content: string) => void, ) { + if (info.yogaConfig.ejectFilePath) { + return info.yogaConfig.ejectFilePath + } + + const ejectFilePath = path.join(info.projectDir, DEFAULTS.ejectFilePath!) const ejectFile = info.yogaConfig.prisma - ? prismaIndexFile(path.dirname(ejectFilePath), info) - : simpleIndexfile(path.dirname(ejectFilePath), info) + ? renderPrismaEjectFile(ejectFilePath, info) + : renderSimpleIndexFile(ejectFilePath, info) - outputFile(ejectFile, ejectFilePath, config.options, info) + writeFile(ejectFilePath, ejectFile) const resolverIndexPath = path.join(info.yogaConfig.resolversPath, 'index.ts') if (!existsSync(resolverIndexPath)) { - writeResolversIndexFile(info, resolverIndexPath, config) - } -} + const resolverIndexFile = renderResolversIndex(info) -function prismaIndexFile(filePath: string, info: ConfigWithInfo) { - return ` - import { ApolloServer, makePrismaSchema, express } from 'yoga' - import datamodelInfo from '${getRelativePath( - filePath, - info.datamodelInfoDir!, - )}' - import { prisma } from '${getRelativePath(filePath, info.prismaClientDir!)}' - ${ - info.yogaConfig.contextPath - ? `import context from '${getRelativePath( - filePath, - info.yogaConfig.contextPath, - )}'` - : '' + writeFile(resolverIndexPath, resolverIndexFile) } - import * as types from '${getRelativePath( - filePath, - info.yogaConfig.resolversPath, - )}' - - - const schema = makePrismaSchema({ - types, - prisma: { - datamodelInfo, - client: prisma - }, - outputs: false - }) - - const apolloServer = new ApolloServer.ApolloServer({ - schema, - ${info.yogaConfig.contextPath ? 'context' : ''} - }) - const app = express() - apolloServer.applyMiddleware({ app, path: '/' }) - - app.listen({ port: 4000 }, () => { - console.log( - \`🚀 Server ready at http://localhost:4000/\`, - ) - }) - ` -} - -function simpleIndexfile(filePath: string, info: ConfigWithInfo) { - return `\ -import { ApolloServer, makeSchema, express } from 'yoga' -${ - info.yogaConfig.contextPath - ? `import context from '${getRelativePath( - filePath, - info.yogaConfig.contextPath, - )}'` - : '' -} -import * as types from '${getRelativePath( - filePath, - info.yogaConfig.resolversPath, - )}' - -const schema = makeSchema({ - types, - outputs: false -}) - -const apolloServer = new ApolloServer.ApolloServer({ - schema, - ${info.yogaConfig.contextPath ? 'context' : ''} -}) - -const app = express() - -apolloServer.applyMiddleware({ app, path: '/' }) - -app.listen({ port: 4000 }, () => { - console.log( - \`🚀 Server ready at http://localhost:4000/\`, - ) -}) -` -} - -function writeResolversIndexFile( - info: ConfigWithInfo, - resolverIndexPath: string, - config: ts.ParsedCommandLine, -) { - const resolversFile = findFileByExtension( - info.yogaConfig.resolversPath, - '.ts', - ) - const resolverIndexFile = `\ - ${resolversFile - .map( - filePath => - `export * from '${getRelativePath( - info.yogaConfig.resolversPath, - filePath, - )}'`, - ) - .join(EOL)} - ` - outputFile(resolverIndexFile, resolverIndexPath, config.options, info) + return ejectFilePath } export function getTranspiledPath( @@ -253,8 +96,8 @@ export function getTranspiledPath( return path.join(outDir, pathToJsFile) } -export function getRelativePath(dir: string, filePath: string): string { - let relativePath = path.relative(dir, filePath) +export function getRelativePath(sourceDir: string, targetPath: string): string { + let relativePath = path.relative(sourceDir, targetPath) if (!relativePath.startsWith('.')) { relativePath = './' + relativePath @@ -280,8 +123,8 @@ function transpileModule( } function outputFile( - fileContent: string, filePath: string, + fileContent: string, compilerOptions: ts.CompilerOptions, info: ConfigWithInfo, ) { @@ -294,3 +137,46 @@ function outputFile( writeFileSync(outFilePath, transpiled) } + +function fixConfig(config: ts.ParsedCommandLine, projectDir: string) { + // Target ES5 output by default (instead of ES3). + if (config.options.target === undefined) { + config.options.target = ts.ScriptTarget.ES5 + } + + // Target CommonJS modules by default (instead of magically switching to ES6 when the target is ES6). + if (config.options.module === undefined) { + config.options.module = ts.ModuleKind.CommonJS + } + + if (config.options.outDir === undefined) { + config.options.outDir = 'dist' + } + + config.options.rootDir = projectDir + + return config +} + +export function readConfigFromTsConfig(info: ConfigWithInfo) { + const tsConfigPath = findConfigFile('tsconfig.json', { required: true }) + const tsConfigContent = ts.readConfigFile(tsConfigPath, ts.sys.readFile) + + if (tsConfigContent.error) { + throw new Error( + ts.formatDiagnosticsWithColorAndContext( + [tsConfigContent.error], + diagnosticHost, + ), + ) + } + + const inputConfig = ts.parseJsonConfigFileContent( + tsConfigContent.config, + ts.sys, + info.projectDir, + undefined, + tsConfigPath, + ) + return fixConfig(inputConfig, info.projectDir) +} diff --git a/packages/yoga/src/cli/commands/build/renderers.ts b/packages/yoga/src/cli/commands/build/renderers.ts new file mode 100644 index 0000000..7755f69 --- /dev/null +++ b/packages/yoga/src/cli/commands/build/renderers.ts @@ -0,0 +1,223 @@ +import { EOL } from 'os' +import * as path from 'path' +import { getRelativePath } from '.' +import { findFileByExtension } from '../../../helpers' +import { ConfigWithInfo } from '../../../types' + +export function renderIndexFile(ejectFilePath: string) { + return ` + import yoga from '${getRelativePath( + path.dirname(ejectFilePath), + ejectFilePath, + )}' + + async function main() { + const serverInstance = await yoga.server() + + return yoga.startServer(serverInstance) + } + + main() + ` +} + +export function renderPrismaEjectFile(filePath: string, info: ConfigWithInfo) { + const fileDir = path.dirname(filePath) + + return ` + import * as path from 'path' + import { ApolloServer, makePrismaSchema, express, yogaEject } from 'yoga' + ${renderImportIf('* as types', fileDir, info.yogaConfig.resolversPath)} + ${renderImportIf('context', fileDir, info.yogaConfig.contextPath)} + ${renderImportIf('expressMiddleware', fileDir, info.yogaConfig.expressPath)} + ${renderImportIf('datamodelInfo', fileDir, info.datamodelInfoDir)} + ${renderImportIf('{ prisma }', fileDir, info.prismaClientDir)} + + export default yogaEject({ + async server() { + const schema = makePrismaSchema({ + types, + prisma: { + datamodelInfo, + client: prisma + }, + outputs: { + schema: ${renderPathJoin(fileDir, info.yogaConfig.output.schemaPath)}, + typegen: ${renderPathJoin( + fileDir, + info.yogaConfig.output.typegenPath, + )} + }, + nonNullDefaults: { + input: true, + output: true, + }, + typegenAutoConfig: { + sources: [ + ${ + info.yogaConfig.contextPath + ? `{ + source: ${renderPathJoin(fileDir, info.yogaConfig.contextPath)}, + alias: 'ctx', + }` + : '' + }, + ${ + info.yogaConfig.prisma + ? `{ + source: ${renderPathJoin( + fileDir, + path.join( + info.yogaConfig.prisma.datamodelInfo.clientPath, + 'index.ts', + ), + )}, + alias: 'prisma', + }` + : '' + }, + ${ + info.yogaConfig.typesPath + ? `{ + source: ${renderPathJoin(fileDir, info.yogaConfig.typesPath)}, + alias: 'types', + }` + : '' + }, + ], + ${info.yogaConfig.contextPath ? `contextType: 'ctx.Context'` : ''} + } + }) + const apolloServer = new ApolloServer.ApolloServer({ + schema, + ${info.yogaConfig.contextPath ? 'context' : ''} + }) + const app = express() + + ${info.yogaConfig.expressPath ? 'await expressMiddleware(app)' : ''} + apolloServer.applyMiddleware({ app, path: '/' }) + + return app + }, + async startServer(app) { + return app.listen({ port: 4000 }, () => { + console.log( + \`🚀 Server ready at http://localhost:4000/\`, + ) + }) + }, + async stopServer(http) { + http.close() + } + }) + ` +} + +export function renderSimpleIndexFile(filePath: string, info: ConfigWithInfo) { + const fileDir = path.dirname(filePath) + + return `\ +import * as path from 'path' +import { ApolloServer, makeSchema, express, yogaEject } from 'yoga' +${renderImportIf('* as types', fileDir, info.yogaConfig.resolversPath)} +${renderImportIf('context', fileDir, info.yogaConfig.contextPath)} +${renderImportIf('expressMiddleware', fileDir, info.yogaConfig.expressPath)} + +export default yogaEject({ + async server() { + const schema = makeSchema({ + types, + outputs: { + schema: ${renderPathJoin(fileDir, info.yogaConfig.output.schemaPath)}, + typegen: ${renderPathJoin(fileDir, info.yogaConfig.output.typegenPath)} + }, + nonNullDefaults: { + input: true, + output: true, + }, + typegenAutoConfig: { + sources: [ + ${ + info.yogaConfig.contextPath + ? `{ + source: ${renderPathJoin(fileDir, info.yogaConfig.contextPath)}, + alias: 'ctx', + }` + : '' + }, + ${ + info.yogaConfig.typesPath + ? `{ + source: ${renderPathJoin(fileDir, info.yogaConfig.typesPath)}, + alias: 'types', + }` + : '' + }, + ], + contextType: 'ctx.Context' + } + }) + const apolloServer = new ApolloServer.ApolloServer({ + schema, + ${info.yogaConfig.contextPath ? 'context' : ''} + }) + const app = express() + + ${info.yogaConfig.expressPath ? 'await expressMiddleware(app)' : ''} + apolloServer.applyMiddleware({ app, path: '/' }) + + return app + }, + async startServer(app) { + return app.listen({ port: 4000 }, () => { + console.log( + \`🚀 Server ready at http://localhost:4000/\`, + ) + }) + }, + async stopServer(http) { + http.close() + } +}) +` +} + +function renderPathJoin(sourceDir: string, targetPath: string) { + let relativePath = path.relative(sourceDir, targetPath) + + if (!relativePath.startsWith('.')) { + relativePath = './' + relativePath + } + + return `path.join(__dirname, '${relativePath}')` +} + +export function renderResolversIndex(info: ConfigWithInfo) { + const resolversFile = findFileByExtension( + info.yogaConfig.resolversPath, + '.ts', + ) + return `\ +${resolversFile + .map( + filePath => + `export * from '${getRelativePath( + info.yogaConfig.resolversPath, + filePath, + )}'`, + ) + .join(EOL)} + ` +} + +export function renderImportIf( + importName: string, + sourceDir: string, + targetPath: string | undefined, +) { + if (!targetPath) { + return '' + } + + return `import ${importName} from '${getRelativePath(sourceDir, targetPath)}'` +} diff --git a/packages/yoga/src/cli/commands/eject/index.ts b/packages/yoga/src/cli/commands/eject/index.ts new file mode 100644 index 0000000..1a97d8e --- /dev/null +++ b/packages/yoga/src/cli/commands/eject/index.ts @@ -0,0 +1,27 @@ +import { importYogaConfig } from '../../../config' +import { existsSync, writeFileSync } from 'fs' +import chalk from 'chalk' +import { writeEjectFiles } from '../build' +import { relative } from 'path' +import { resolvePrettierOptions, prettify } from '../../../helpers' + +export default async () => { + const info = importYogaConfig() + const prettierOptions = await resolvePrettierOptions(info.projectDir) + + if ( + info.yogaConfig.ejectFilePath && + existsSync(info.yogaConfig.ejectFilePath) + ) { + console.log( + `${chalk.yellow( + 'You are already ejected', + )}. If you want to run the command, please delete ${chalk.yellow( + relative(info.projectDir, info.yogaConfig.ejectFilePath), + )}`, + ) + process.exit(1) + } + + writeEjectFiles(info, (filePath, content) => writeFileSync(filePath, prettify(content, prettierOptions))) +} diff --git a/packages/yoga/src/cli/commands/scaffold/index.ts b/packages/yoga/src/cli/commands/scaffold/index.ts index d884fd0..3368587 100644 --- a/packages/yoga/src/cli/commands/scaffold/index.ts +++ b/packages/yoga/src/cli/commands/scaffold/index.ts @@ -3,8 +3,8 @@ import * as inquirer from 'inquirer' import yaml from 'js-yaml' import * as path from 'path' import pluralize from 'pluralize' -import * as prettier from 'prettier' import { findPrismaConfigFile, importYogaConfig } from '../../../config' +import { prettify, resolvePrettierOptions } from '../../../helpers' import { Config } from '../../../types' import { spawnAsync } from '../../spawnAsync' import execa = require('execa') @@ -158,7 +158,7 @@ async function scaffoldType( const prettierOptions = await resolvePrettierOptions(process.cwd()) try { - fs.writeFileSync(typePath, format(content, prettierOptions)) + fs.writeFileSync(typePath, prettify(content, prettierOptions)) } catch (e) { console.error(e) } @@ -347,29 +347,3 @@ async function runCommand(command: string) { return childProcess } - -async function resolvePrettierOptions(path: string): Promise { - const options = (await prettier.resolveConfig(path)) || {} - - return options -} - -function format( - code: string, - options: prettier.Options = {}, - parser: prettier.BuiltInParserName = 'typescript', -) { - try { - return prettier.format(code, { - ...options, - parser, - }) - } catch (e) { - console.log( - `There is a syntax error in generated code, unformatted code printed, error: ${JSON.stringify( - e, - )}`, - ) - return code - } -} diff --git a/packages/yoga/src/cli/index.ts b/packages/yoga/src/cli/index.ts index 34e5688..d8e5e42 100644 --- a/packages/yoga/src/cli/index.ts +++ b/packages/yoga/src/cli/index.ts @@ -6,6 +6,7 @@ import build from './commands/build' import scaffold from './commands/scaffold' import start from './commands/start' import watch from './commands/watch' +import eject from './commands/eject' function run() { // tslint:disable-next-line:no-unused-expression @@ -16,6 +17,7 @@ function run() { .command('dev', 'Start the server in dev mode', {}, watch) .command('scaffold', 'Scaffold a new GraphQL type', {}, scaffold) .command('build', 'Build a yoga server', {}, build) + .command('eject', 'Eject your project', {}, eject) .alias('h', 'help') .help('help') .showHelpOnFail(true, 'Specify --help for available options') diff --git a/packages/yoga/src/helpers.ts b/packages/yoga/src/helpers.ts index 7788733..5a8f70e 100644 --- a/packages/yoga/src/helpers.ts +++ b/packages/yoga/src/helpers.ts @@ -1,6 +1,7 @@ import * as fs from 'fs' import * as path from 'path' import decache from 'decache' +import * as prettier from 'prettier' /** * Find all files recursively in a directory based on an extension @@ -65,3 +66,31 @@ export function importFile( return exportName ? importedModule[exportName] : importedModule } + +export async function resolvePrettierOptions( + path: string, +): Promise { + const options = (await prettier.resolveConfig(path)) || {} + + return options +} + +export function prettify( + code: string, + options: prettier.Options = {}, + parser: prettier.BuiltInParserName = 'typescript', +) { + try { + return prettier.format(code, { + ...options, + parser, + }) + } catch (e) { + console.log( + `There is a syntax error in generated code, unformatted code printed, error: ${JSON.stringify( + e, + )}`, + ) + return code + } +}