From 9686229842ce8a30e73322355a63f251297c6d15 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 18:30:14 +0200 Subject: [PATCH 1/8] feat: require all files that imports nexus --- src/cli/commands/create/app.ts | 11 +++--- src/cli/commands/dev.ts | 54 ++++++++++++++++++---------- src/lib/layout/index.ts | 8 ++--- src/lib/layout/layout.ts | 60 +++++++++++++++++++++++-------- src/lib/layout/schema-modules.ts | 28 +-------------- src/lib/tsc.ts | 2 +- src/lib/watcher/link.ts | 1 + src/lib/watcher/watcher.ts | 1 + src/runtime/schema/schema.ts | 2 +- src/runtime/start/start-module.ts | 2 +- 10 files changed, 97 insertions(+), 72 deletions(-) diff --git a/src/cli/commands/create/app.ts b/src/cli/commands/create/app.ts index 54bc213d8..996def083 100644 --- a/src/cli/commands/create/app.ts +++ b/src/cli/commands/create/app.ts @@ -384,7 +384,7 @@ const templates: Record = { return { files: [ { - path: path.join(internalConfig.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME), + path: path.join(internalConfig.sourceRoot, 'graphql.ts'), content: stripIndent` import { schema } from "nexus"; @@ -466,7 +466,9 @@ async function scaffoldBaseFiles(options: InternalConfig) { // Having at least one of these satisfies minimum Nexus requirements. // We put both to setup vscode debugger config with an entrypoint that is // unlikely to change. - fs.writeAsync(appEntrypointPath, stripIndent` + fs.writeAsync( + appEntrypointPath, + stripIndent` /** * This file is your server entrypoint. Don't worry about its emptyness, Nexus handles everything for you. * However, if you need to add settings, enable plugins, schema middleware etc, this is place to do it. @@ -507,8 +509,9 @@ async function scaffoldBaseFiles(options: InternalConfig) { // import { prisma } from 'nexus-plugin-prisma' // // use(prisma()) - `), - fs.writeAsync(path.join(options.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME), ''), + ` + ), + fs.writeAsync(path.join(options.sourceRoot, 'graphql.ts'), ''), // An exhaustive .gitignore tailored for Node can be found here: // https://github.com/github/gitignore/blob/master/Node.gitignore // We intentionally stay minimal here, as we want the default ignore file diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 015d7a3be..0c9abe50b 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -73,6 +73,14 @@ export class Dev implements Command { dev: { addToWatcherSettings: {}, async onBeforeWatcherStartOrRestart(change) { + if (change.type === 'change') { + const nexusModules = Layout.findNexusModules( + layout.tsConfig, + layout.app.exists ? layout.app.path : null + ) + layout.update({ nexusModules }) + } + if ( change.type === 'init' || change.type === 'add' || @@ -89,33 +97,21 @@ export class Dev implements Command { } runDebouncedReflection(layout) + + //log.info('new start module', { startModule: getTranspiledStartModule(layout) }) + + return { + entrypointScript: getTranspiledStartModule(layout), + } }, }, } - const startModule = createStartModuleContent({ - registerTypeScript: { - ...layout.tsConfig.content.options, - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2015, - }, - internalStage: 'dev', - runtimePluginManifests: [], // tree-shaking not needed - layout, - absoluteModuleImports: true, - }) - - const transpiledStartModule = transpileModule(startModule, { - ...layout.tsConfig.content.options, - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2015, - }) - /** * We use an empty script when in reflection mode so that the user's app doesn't run. * The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above */ - const entrypointScript = args['--reflection'] ? '' : transpiledStartModule + const entrypointScript = args['--reflection'] ? '' : getTranspiledStartModule(layout) const watcher = await createWatcher({ entrypointScript, @@ -141,3 +137,23 @@ export class Dev implements Command { ` } } + +function getTranspiledStartModule(layout: Layout.Layout) { + const startModule = createStartModuleContent({ + registerTypeScript: { + ...layout.tsConfig.content.options, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2015, + }, + internalStage: 'dev', + runtimePluginManifests: [], // tree-shaking not needed + layout, + absoluteModuleImports: true, + }) + + return transpileModule(startModule, { + ...layout.tsConfig.content.options, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2015, + }) +} diff --git a/src/lib/layout/index.ts b/src/lib/layout/index.ts index bf9e24527..9267d64d8 100644 --- a/src/lib/layout/index.ts +++ b/src/lib/layout/index.ts @@ -8,16 +8,14 @@ export { loadDataFromParentProcess, saveDataForChildProcess, scanProjectType, + findNexusModules } from './layout' // todo refactor with TS 3.8 namespace re-export // once https://github.com/prettier/prettier/issues/7263 -import { CONVENTIONAL_SCHEMA_FILE_NAME, DIR_NAME, emptyExceptionMessage, MODULE_NAME } from './schema-modules' +import { emptyExceptionMessage } from './schema-modules' -export const schema = { +export const schemaModules = { emptyExceptionMessage, - DIR_NAME, - MODULE_NAME, - CONVENTIONAL_SCHEMA_FILE_NAME, } diff --git a/src/lib/layout/layout.ts b/src/lib/layout/layout.ts index d72d2ae80..48f7de133 100644 --- a/src/lib/layout/layout.ts +++ b/src/lib/layout/layout.ts @@ -9,8 +9,8 @@ import { START_MODULE_NAME } from '../../runtime/start/start-module' import { rootLogger } from '../nexus-logger' import * as PackageManager from '../package-manager' import { fatal } from '../process' -import * as Schema from './schema-modules' import { readOrScaffoldTsconfig } from './tsconfig' +import * as ts from 'ts-morph' export const DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT = 'node_modules/.build' @@ -41,7 +41,7 @@ export type ScanResult = { } sourceRoot: string projectRoot: string - schemaModules: string[] + nexusModules: string[] tsConfig: { content: ParsedCommandLine path: string @@ -93,9 +93,12 @@ export type Layout = Data & { projectPath(...subPaths: string[]): string sourceRelative(filePath: string): string sourcePath(...subPaths: string[]): string + update(options: UpdateableLayoutData): void packageManager: PackageManager.PackageManager } +interface UpdateableLayoutData { nexusModules?: string[] } + interface Options { /** * The place to output the build, relative to project root. @@ -148,7 +151,7 @@ export async function create(options?: Options): Promise { * data from another process that would be wasteful to re-calculate. */ export function createFromData(layoutData: Data): Layout { - return { + let layout: Layout = { ...layoutData, data: layoutData, projectRelative: Path.relative.bind(null, layoutData.projectRoot), @@ -162,7 +165,15 @@ export function createFromData(layoutData: Data): Layout { packageManager: PackageManager.createPackageManager(layoutData.packageManagerType, { projectRoot: layoutData.projectRoot, }), + update(options) { + if (options.nexusModules) { + layout.nexusModules = options.nexusModules + layout.data.nexusModules = options.nexusModules + } + }, } + + return layout } /** @@ -175,15 +186,10 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr const packageManagerType = await PackageManager.detectProjectPackageManager({ projectRoot }) const maybePackageJson = findPackageJson({ projectRoot }) const maybeAppModule = opts?.entrypointPath ?? findAppModule({ projectRoot }) - let maybeSchemaModules = Schema.findDirOrModules({ projectRoot }) const tsConfig = await readOrScaffoldTsconfig({ projectRoot, }) - - // Remove entrypoint from schema modules (can happen if the entrypoint is named graphql.ts or inside a graphql/ folder) - if (maybeSchemaModules && maybeAppModule && maybeSchemaModules.includes(maybeAppModule)) { - maybeSchemaModules = maybeSchemaModules.filter((s) => s !== maybeAppModule) - } + let nexusModules = findNexusModules(tsConfig, maybeAppModule) const result: ScanResult = { app: @@ -192,7 +198,7 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr : ({ exists: true, path: maybeAppModule } as const), projectRoot, sourceRoot: tsConfig.content.options.rootDir!, - schemaModules: maybeSchemaModules, + nexusModules, project: readProjectInfo(opts), tsConfig, packageManagerType, @@ -201,7 +207,7 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr log.trace('completed scan', { result }) - if (result.app.exists === false && result.schemaModules.length === 0) { + if (result.app.exists === false && result.nexusModules.length === 0) { log.error(checks.no_app_or_schema_modules.explanations.problem) log.error(checks.no_app_or_schema_modules.explanations.solution) process.exit(1) @@ -219,12 +225,11 @@ const checks = { code: 'no_app_or_schema_modules', // prettier-ignore explanations: { - problem: `We could not find any ${Schema.MODULE_NAME} modules or app entrypoint`, + problem: `We could not find any modules that imports 'nexus' or ${CONVENTIONAL_ENTRYPOINT_FILE_NAME} entrypoint`, solution: stripIndent` Please do one of the following: - 1. Create a (${Chalk.yellow(Schema.CONVENTIONAL_SCHEMA_FILE_NAME)} file and write your GraphQL type definitions in it. - 2. Create a ${Chalk.yellow(Schema.DIR_NAME)} directory and write your GraphQL type definitions inside files there. + 1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it. 3. Create an ${Chalk.yellow(CONVENTIONAL_ENTRYPOINT_FILE_NAME)} file. `, } @@ -410,3 +415,30 @@ function getBuildOutput(buildOutput: string | undefined, scanResult: ScanResult) return Path.join(scanResult.projectRoot, output) } + +export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModule: string | null) { + log.info('finding nexus modules') + const project = new ts.Project({ + tsConfigFilePath: tsConfig.path, + skipFileDependencyResolution: true, + }) + + const modules = project + .getSourceFiles() + .filter((s) => { + // Do not add app module to nexus modules + if (s.getFilePath().toString() === maybeAppModule) { + return false + } + + const nexusImport = s.getImportDeclarations().find((i) => { + return i.getModuleSpecifier()?.getLiteralText() === 'nexus' + }) + + return nexusImport !== undefined + }) + .map((s) => s.getFilePath().toString()) + + log.info('done finding nexus modules', { modules }) + return modules +} diff --git a/src/lib/layout/schema-modules.ts b/src/lib/layout/schema-modules.ts index 494906131..58b0f7024 100644 --- a/src/lib/layout/schema-modules.ts +++ b/src/lib/layout/schema-modules.ts @@ -1,33 +1,7 @@ -import Chalk from 'chalk' -import * as fs from 'fs-jetpack' -import { baseIgnores } from '../../lib/fs' - -export const MODULE_NAME = 'graphql' -export const CONVENTIONAL_SCHEMA_FILE_NAME = MODULE_NAME + '.ts' -export const DIR_NAME = 'graphql' - export function emptyExceptionMessage() { // todo when the file is present but empty this error message is shown just // the same. That is poor user feedback because the instructions are wrong in // that case. The instructions in that case should be something like "you have // schema files setup correctly but they are empty" - return `Your GraphQL schema is empty. This is normal if you have not defined any GraphQL types yet. But if you did, check that the file name follows the convention: all ${Chalk.yellow( - CONVENTIONAL_SCHEMA_FILE_NAME - )} modules or direct child modules within a ${Chalk.yellow(DIR_NAME)} directory are automatically imported.` -} - -/** - * Find all modules called schema modules or directories having the trigger - * name. This does not grab the child modules of the directory instances! - */ -export function findDirOrModules(opts: { projectRoot: string }): string[] { - const localFS = fs.cwd(opts.projectRoot) - // TODO async - const files = localFS.find({ - files: true, - recursive: true, - matching: [CONVENTIONAL_SCHEMA_FILE_NAME, `**/${MODULE_NAME}/**/*.ts`, ...baseIgnores], - }) - - return files.map((f) => localFS.path(f)) + return `Your GraphQL schema is empty. This is normal if you have not defined any GraphQL types yet. If you did however, check that your files are contained in the same directory specified in the \`rootDir\` property of your tsconfig.json file.` } diff --git a/src/lib/tsc.ts b/src/lib/tsc.ts index 9e9839556..9245fe6f8 100644 --- a/src/lib/tsc.ts +++ b/src/lib/tsc.ts @@ -29,7 +29,7 @@ export function createTSProgram( log.trace('Create TypeScript program') const builder = ts.createIncrementalProgram({ - rootNames: layout.schemaModules.concat(layout.app.exists ? [layout.app.path] : []), + rootNames: layout.nexusModules.concat(layout.app.exists ? [layout.app.path] : []), options: { ...compilerCacheOptions, ...layout.tsConfig.content.options, diff --git a/src/lib/watcher/link.ts b/src/lib/watcher/link.ts index 154b2f7b2..4c426afcf 100644 --- a/src/lib/watcher/link.ts +++ b/src/lib/watcher/link.ts @@ -7,6 +7,7 @@ const log = rootLogger.child('dev').child('link') interface ChangeableOptions { environmentAdditions?: Record + entrypointScript?: string } interface Options extends ChangeableOptions { diff --git a/src/lib/watcher/watcher.ts b/src/lib/watcher/watcher.ts index 32d9c52fd..b6df57313 100644 --- a/src/lib/watcher/watcher.ts +++ b/src/lib/watcher/watcher.ts @@ -27,6 +27,7 @@ export type PostInitChangeEvent = export interface RunnerOptions { environmentAdditions?: Record + entrypointScript?: string } export interface Watcher { diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index 832715351..f1c9b5935 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -121,7 +121,7 @@ export function create(appState: AppState): SchemaInternal { checks() { NexusSchema.core.assertNoMissingTypes(appState.assembled!.schema, appState.assembled!.missingTypes) if (statefulNexusSchema.state.types.length === 0) { - log.warn(Layout.schema.emptyExceptionMessage()) + log.warn(Layout.schemaModules.emptyExceptionMessage()) } }, }, diff --git a/src/runtime/start/start-module.ts b/src/runtime/start/start-module.ts index 6cbdec9d4..e47b9f01d 100644 --- a/src/runtime/start/start-module.ts +++ b/src/runtime/start/start-module.ts @@ -176,7 +176,7 @@ export function prepareStartModule( * in the source/build root. */ export function printStaticImports(layout: Layout.Layout, opts?: { absolutePaths?: boolean }): string { - return layout.schemaModules.reduce((script, modulePath) => { + return layout.nexusModules.reduce((script, modulePath) => { const path = opts?.absolutePaths ? stripExt(modulePath) : relativeTranspiledImportPath(layout, modulePath) return `${script}\n${printSideEffectsImport(path)}` }, '') From 91653342cac68733b7512c7d142ef465193711f7 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 18:42:49 +0200 Subject: [PATCH 2/8] remove useless logs --- src/cli/commands/dev.ts | 4 +--- src/lib/layout/layout.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 0c9abe50b..9b0190ad8 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -75,7 +75,7 @@ export class Dev implements Command { async onBeforeWatcherStartOrRestart(change) { if (change.type === 'change') { const nexusModules = Layout.findNexusModules( - layout.tsConfig, + layout.tsConfig.path, layout.app.exists ? layout.app.path : null ) layout.update({ nexusModules }) @@ -98,8 +98,6 @@ export class Dev implements Command { runDebouncedReflection(layout) - //log.info('new start module', { startModule: getTranspiledStartModule(layout) }) - return { entrypointScript: getTranspiledStartModule(layout), } diff --git a/src/lib/layout/layout.ts b/src/lib/layout/layout.ts index 48f7de133..fe29295f8 100644 --- a/src/lib/layout/layout.ts +++ b/src/lib/layout/layout.ts @@ -97,7 +97,9 @@ export type Layout = Data & { packageManager: PackageManager.PackageManager } -interface UpdateableLayoutData { nexusModules?: string[] } +interface UpdateableLayoutData { + nexusModules?: string[] +} interface Options { /** @@ -189,7 +191,7 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr const tsConfig = await readOrScaffoldTsconfig({ projectRoot, }) - let nexusModules = findNexusModules(tsConfig, maybeAppModule) + const nexusModules = findNexusModules(tsConfig.path, maybeAppModule) const result: ScanResult = { app: @@ -416,10 +418,10 @@ function getBuildOutput(buildOutput: string | undefined, scanResult: ScanResult) return Path.join(scanResult.projectRoot, output) } -export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModule: string | null) { - log.info('finding nexus modules') +export function findNexusModules(tsConfigFilePath: string, maybeAppModule: string | null) { + log.trace('finding nexus modules') const project = new ts.Project({ - tsConfigFilePath: tsConfig.path, + tsConfigFilePath, skipFileDependencyResolution: true, }) @@ -439,6 +441,6 @@ export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModul }) .map((s) => s.getFilePath().toString()) - log.info('done finding nexus modules', { modules }) + log.trace('done finding nexus modules', { modules }) return modules } From f58c4975f28b9481e8595ced23e4d38014f2e8d4 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 19:20:29 +0200 Subject: [PATCH 3/8] improve usage of ts-morph --- src/cli/commands/dev.ts | 2 +- src/lib/layout/index.spec.ts | 58 +++++++++++++----------------------- src/lib/layout/layout.ts | 47 +++++++++++++++-------------- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 9b0190ad8..bb3903544 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -75,7 +75,7 @@ export class Dev implements Command { async onBeforeWatcherStartOrRestart(change) { if (change.type === 'change') { const nexusModules = Layout.findNexusModules( - layout.tsConfig.path, + layout.tsConfig, layout.app.exists ? layout.app.path : null ) layout.update({ nexusModules }) diff --git a/src/lib/layout/index.spec.ts b/src/lib/layout/index.spec.ts index 4966e4e0e..e8d7fd0cb 100644 --- a/src/lib/layout/index.spec.ts +++ b/src/lib/layout/index.spec.ts @@ -195,11 +195,10 @@ it('fails if no entrypoint and no graphql modules', async () => { await ctx.scan() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` - "■ nexus:layout We could not find any graphql modules or app entrypoint + "■ nexus:layout We could not find any modules that imports 'nexus' or app.ts entrypoint ■ nexus:layout Please do one of the following: - 1. Create a (graphql.ts file and write your GraphQL type definitions in it. - 2. Create a graphql directory and write your GraphQL type definitions inside files there. + 1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it. 3. Create an app.ts file. @@ -210,20 +209,20 @@ it('fails if no entrypoint and no graphql modules', async () => { expect(mockExit).toHaveBeenCalledWith(1) }) -it('finds nested graphql modules', async () => { +it('finds nested nexus modules', async () => { ctx.setup({ ...fsTsConfig, src: { 'app.ts': '', graphql: { - '1.ts': '', - '2.ts': '', + '1.ts': `import { schema } from 'nexus'`, + '2.ts': `import { schema } from 'nexus'`, graphql: { - '3.ts': '', - '4.ts': '', + '3.ts': `import { schema } from 'nexus'`, + '4.ts': `import { schema } from 'nexus'`, graphql: { - '5.ts': '', - '6.ts': '', + '5.ts': `import { schema } from 'nexus'`, + '6.ts': `import { schema } from 'nexus'`, }, }, }, @@ -232,7 +231,7 @@ it('finds nested graphql modules', async () => { const result = await ctx.scan() - expect(result.schemaModules).toMatchInlineSnapshot(` + expect(result.nexusModules).toMatchInlineSnapshot(` Array [ "__DYNAMIC__/src/graphql/1.ts", "__DYNAMIC__/src/graphql/2.ts", @@ -320,39 +319,24 @@ it('fails if custom entrypoint is not a .ts file', async () => { `) }) -it('does not take custom entrypoint as schema module if its named graphql.ts', async () => { - await ctx.setup({ ...fsTsConfig, 'graphql.ts': '', graphql: { 'user.ts': '' } }) - const result = await ctx.scan({ entrypointPath: './graphql.ts' }) - expect({ - app: result.app, - schemaModules: result.schemaModules, - }).toMatchInlineSnapshot(` - Object { - "app": Object { - "exists": true, - "path": "__DYNAMIC__/graphql.ts", - }, - "schemaModules": Array [ - "__DYNAMIC__/graphql/user.ts", - ], - } - `) -}) - -it('does not take custom entrypoint as schema module if its inside a graphql/ folder', async () => { - await ctx.setup({ ...fsTsConfig, graphql: { 'user.ts': '', 'graphql.ts': '' } }) - const result = await ctx.scan({ entrypointPath: './graphql/graphql.ts' }) +it('does not take custom entrypoint as nexus module if contains a nexus import', async () => { + await ctx.setup({ + ...fsTsConfig, + 'app.ts': `import { schema } from 'nexus'`, + 'graphql.ts': `import { schema } from 'nexus'`, + }) + const result = await ctx.scan({ entrypointPath: './app.ts' }) expect({ app: result.app, - schemaModules: result.schemaModules, + nexusModules: result.nexusModules, }).toMatchInlineSnapshot(` Object { "app": Object { "exists": true, - "path": "__DYNAMIC__/graphql/graphql.ts", + "path": "__DYNAMIC__/app.ts", }, - "schemaModules": Array [ - "__DYNAMIC__/graphql/user.ts", + "nexusModules": Array [ + "__DYNAMIC__/graphql.ts", ], } `) diff --git a/src/lib/layout/layout.ts b/src/lib/layout/layout.ts index fe29295f8..4fc77edba 100644 --- a/src/lib/layout/layout.ts +++ b/src/lib/layout/layout.ts @@ -191,7 +191,7 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr const tsConfig = await readOrScaffoldTsconfig({ projectRoot, }) - const nexusModules = findNexusModules(tsConfig.path, maybeAppModule) + const nexusModules = findNexusModules(tsConfig, maybeAppModule) const result: ScanResult = { app: @@ -232,7 +232,7 @@ const checks = { Please do one of the following: 1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it. - 3. Create an ${Chalk.yellow(CONVENTIONAL_ENTRYPOINT_FILE_NAME)} file. + 2. Create an ${Chalk.yellow(CONVENTIONAL_ENTRYPOINT_FILE_NAME)} file. `, } }, @@ -418,29 +418,32 @@ function getBuildOutput(buildOutput: string | undefined, scanResult: ScanResult) return Path.join(scanResult.projectRoot, output) } -export function findNexusModules(tsConfigFilePath: string, maybeAppModule: string | null) { - log.trace('finding nexus modules') - const project = new ts.Project({ - tsConfigFilePath, - skipFileDependencyResolution: true, - }) +export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModule: string | null): string[] { + try { + log.trace('finding nexus modules') + const project = new ts.Project({ + addFilesFromTsConfig: false, // Prevent ts-morph from re-parsing the tsconfig + }) - const modules = project - .getSourceFiles() - .filter((s) => { - // Do not add app module to nexus modules - if (s.getFilePath().toString() === maybeAppModule) { - return false - } + tsConfig.content.fileNames.forEach((f) => project.addSourceFileAtPath(f)) - const nexusImport = s.getImportDeclarations().find((i) => { - return i.getModuleSpecifier()?.getLiteralText() === 'nexus' + const modules = project + .getSourceFiles() + .filter((s) => { + // Do not add app module to nexus modules + if (s.getFilePath().toString() === maybeAppModule) { + return false + } + + return s.getImportDeclaration('nexus') !== undefined }) + .map((s) => s.getFilePath().toString()) - return nexusImport !== undefined - }) - .map((s) => s.getFilePath().toString()) + log.trace('done finding nexus modules', { modules }) - log.trace('done finding nexus modules', { modules }) - return modules + return modules + } catch (error) { + log.error('We could not find your nexus modules', { error }) + return [] + } } From 7f5d71d8e91d60b8220f906b5240b50ef0782af6 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 19:23:00 +0200 Subject: [PATCH 4/8] fix tests --- test/__helpers/e2e/kitchen-sink.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/__helpers/e2e/kitchen-sink.ts b/test/__helpers/e2e/kitchen-sink.ts index 15d412621..1a9df5214 100644 --- a/test/__helpers/e2e/kitchen-sink.ts +++ b/test/__helpers/e2e/kitchen-sink.ts @@ -3,7 +3,6 @@ import { ConnectableObservable, Subscription } from 'rxjs' import { refCount } from 'rxjs/operators' import { createE2EContext, E2EContext } from '../../../src/lib/e2e-testing' import { DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT } from '../../../src/lib/layout' -import { CONVENTIONAL_SCHEMA_FILE_NAME } from '../../../src/lib/layout/schema-modules' import { rootLogger } from '../../../src/lib/nexus-logger' import { bufferOutput, takeUntilServerListening } from './utils' @@ -41,7 +40,7 @@ export async function e2eKitchenSink(app: E2EContext) { // Cover addToContext feature await app.fs.writeAsync( - `./api/add-to-context/${CONVENTIONAL_SCHEMA_FILE_NAME}`, + `./api/add-to-context/graphql.ts`, ` import { schema } from 'nexus' @@ -68,7 +67,7 @@ export async function e2eKitchenSink(app: E2EContext) { // Cover backing-types feature await app.fs.writeAsync( - `./api/backing-types/${CONVENTIONAL_SCHEMA_FILE_NAME}`, + `./api/backing-types/graphql.ts`, ` import { schema } from 'nexus' From f6112ef63752112702750b598ffbd137d4f13740 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 19:25:42 +0200 Subject: [PATCH 5/8] fix tests --- src/lib/layout/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/layout/index.spec.ts b/src/lib/layout/index.spec.ts index e8d7fd0cb..043025e34 100644 --- a/src/lib/layout/index.spec.ts +++ b/src/lib/layout/index.spec.ts @@ -199,7 +199,7 @@ it('fails if no entrypoint and no graphql modules', async () => { ■ nexus:layout Please do one of the following: 1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it. - 3. Create an app.ts file. + 2. Create an app.ts file. --- process.exit(1) --- From e3749f4e6e9365ef0d5df92a886dcd9adc9d64b4 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Sat, 9 May 2020 19:34:57 +0200 Subject: [PATCH 6/8] do not update entrypoint script in reflection mode --- src/cli/commands/dev.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index bb3903544..c2984762f 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -37,6 +37,7 @@ export class Dev implements Command { } const entrypointPath = args['--entrypoint'] + const reflectionMode = args['--reflection'] === true let layout = await Layout.create({ entrypointPath }) const pluginReflectionResult = await Reflection.reflect(layout, { usedPlugins: true, onMainThread: true }) @@ -99,20 +100,14 @@ export class Dev implements Command { runDebouncedReflection(layout) return { - entrypointScript: getTranspiledStartModule(layout), + entrypointScript: getTranspiledStartModule(layout, reflectionMode), } }, }, } - /** - * We use an empty script when in reflection mode so that the user's app doesn't run. - * The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above - */ - const entrypointScript = args['--reflection'] ? '' : getTranspiledStartModule(layout) - const watcher = await createWatcher({ - entrypointScript, + entrypointScript: getTranspiledStartModule(layout, reflectionMode), sourceRoot: layout.sourceRoot, cwd: process.cwd(), plugins: [devPlugin].concat(worktimePlugins.map((p) => p.hooks)), @@ -136,7 +131,15 @@ export class Dev implements Command { } } -function getTranspiledStartModule(layout: Layout.Layout) { +function getTranspiledStartModule(layout: Layout.Layout, reflectionMode: boolean) { + /** + * We use an empty script when in reflection mode so that the user's app doesn't run. + * The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above + */ + if (reflectionMode === true) { + return '' + } + const startModule = createStartModuleContent({ registerTypeScript: { ...layout.tsConfig.content.options, From 4d9ee532b7d2c8ae48d34c3ec1f44b3c885353e8 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 18 May 2020 14:33:47 +0200 Subject: [PATCH 7/8] fix merge error --- src/lib/layout/layout.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/layout/layout.ts b/src/lib/layout/layout.ts index 0477a63f4..0b059267f 100644 --- a/src/lib/layout/layout.ts +++ b/src/lib/layout/layout.ts @@ -459,6 +459,9 @@ export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModul } catch (error) { log.error('We could not find your nexus modules', { error }) return [] + } +} + function getBuildInfo( buildOutput: string | undefined, scanResult: ScanResult, From 11048ae4442eeb28e38d6f8817949efd7fd8cdf4 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 18 May 2020 15:26:13 +0200 Subject: [PATCH 8/8] docs --- docs/architecture.md | 4 ++-- docs/guides/project-layout.md | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 2a3e369ed..252a44732 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,7 +94,7 @@ what follows is a stub ## Build Flow 1. The app layout is calculated - We discover things like where the entrypoint is, if any, and where graphql modules are, if any. + We discover things like where the entrypoint is, if any, and where [Nexus modules](/guides/project-layout?id=nexus-modules) are, if any. 1. Worktime plugins are loaded (see [Plugin Loading Flow](#plugin-loading-flow)) 1. Typegen is acquired This step is about processes that reflect upon the app's source code to extract type information that will be automatically used in other parts of the app. This approach is relatively novel among Node tools. There are dynamic and static processes. The static ones use the TypeScript compiler API while the dynamic ones literally run the app with node in a special reflective mode. @@ -103,7 +103,7 @@ what follows is a stub Static doesn't have to deal with the unpredictabilities of running an app and so has the benefit of being easier to reason about in a sense. It also has the benefit of extracting accurate type information using the native TS system whereas dynamic relies on building TS types from scratch. This makes static a fit for arbitrary code. On the downside, robust AST processing is hard work, and so, so far, static restricts how certain expressions can be written, otherwise AST traversal fails. - 1. A start module is created in memory. It imports the entrypoint and all graphql modules. It registers an extension hook to transpile the TypeScript app on the fly as it is run. The transpilation uses the project's tsconfig but overrides target and module so that it is runnable by Node (10 and up). Specificaly es2015 target and commonjs module. For example if user had module of `esnext` the transpilation result would not be runnable by Node. + 1. A start module is created in memory. It imports the entrypoint and all [Nexus modules](/guides/project-layout?id=nexus-modules). It registers an extension hook to transpile the TypeScript app on the fly as it is run. The transpilation uses the project's tsconfig but overrides target and module so that it is runnable by Node (10 and up). Specificaly es2015 target and commonjs module. For example if user had module of `esnext` the transpilation result would not be runnable by Node. 1. The start module is run in a sub-process for maximum isolation. (we're looking at running within workers [#752](https://github.com/graphql-nexus/nexus/issues/752)) 1. In parallel, a TypeScript instance is created and the app source is statically analyzed to extract context types. This does not require running the app at all. TypeScript cache called tsbuildinfo is stored under `node_modules/.nexus`. diff --git a/docs/guides/project-layout.md b/docs/guides/project-layout.md index 159141786..8a434903f 100644 --- a/docs/guides/project-layout.md +++ b/docs/guides/project-layout.md @@ -32,21 +32,19 @@ if `compilerOptions.noEmit` is set to `true` then Nexus will not output the buil Nexus imposes a few requirements about how you structure your codebase. -### Schema Module(s) +### Nexus module(s) ##### Pattern -A `graphql` module or directory of modules `graphql.ts` `graphql/*.ts`. - -This may be repeated multiple times in your source tree. +A file importing `nexus`. eg: `import { schema } from 'nexus'` ##### About -This convention is optional if entrypoint is present, required otherwise. +Nexus looks for modules that import `nexus` and uses codegen to statically import them before the server starts. -This is where you should write your GraphQL type definitions. +Beware if you have module-level side-effects coming from something else than Nexus, as these side-effects will always be run when your app starts. -In dev mode the modules are synchronously found and imported when the server starts. Conversely, at build time, codegen runs making the modules statically imported. This is done to support tree-shaking and decrease application start time. +> *Note: `require` is not supported. ### Entrypoint @@ -58,4 +56,4 @@ A custom entrypoint can also be configured using the `--entrypoint` or `-e` CLI ##### About -This convention is optional if schema modules are present, required otherwise. +This convention is optional if Nexus modules are present, required otherwise.