diff --git a/docs/guides/project-layout.md b/docs/guides/project-layout.md index ce30e0d0d..006ab6ec1 100644 --- a/docs/guides/project-layout.md +++ b/docs/guides/project-layout.md @@ -2,14 +2,9 @@ - Nexus honours settings within `tsconfig.json`. - This ensures that Nexus and your IDE perform identical static analysis. -- If no `tsconfig.json` is present then Nexus will scaffold one for you. +- If no `tsconfig.json` is present in the project root then Nexus will scaffold one for you. This will make ([VSCode treat it as the project root too](https://vscode.readthedocs.io/en/latest/languages/typescript/#typescript-files-and-projects)). - Nexus interacts with `tsconfig.json` in the following ways. -##### Project Root - -- Project Root is the CWD (current working directory) for all CLI invocations. -- Nexus ([like VSCode](https://vscode.readthedocs.io/en/latest/languages/typescript/#typescript-files-and-projects)) considers the folder containing a `tsconfig.json` to be the project root. - ##### Source Root - Source Root is the base from which your source code layout starts. So, all of your app code must live within the source root. Your JavaScript build output layout will mirror it. @@ -44,6 +39,16 @@ Autocomplete with Nexus TS LSP: Nexus imposes a few requirements about how you structure your codebase. +### Project Root + +The project root is the directory from which all all Nexus CLI commands base their CWD upon. It is also the directory that configuration paths in Nexus (e.g. `--entrypoint` flag) are often relative to as well (in other cases it can be source root). + +To find the project root Nexus starts with the current working directory (CWD). This usually means the current directory you're in when invoking the Nexus CLI. From this location Nexus will do the following: + +1. If a directory in the current hierarchy, including CWD, contains a [valid](https://docs.npmjs.com/creating-a-package-json-file#required-name-and-version-fields) `package.json` then it will be considered the project root. In case multiple such files are present in the hierarchy, only the first one is considered (in other words the one closest to CWD). + +2. If no `package.json` files exist then the CWD itself is taken to be the project root. + ### Nexus module(s) ##### Pattern diff --git a/src/cli/commands/create/app.ts b/src/cli/commands/create/app.ts index e18a3b447..b808cc385 100644 --- a/src/cli/commands/create/app.ts +++ b/src/cli/commands/create/app.ts @@ -555,7 +555,7 @@ async function scaffoldBaseFiles(options: InternalConfig) { 'tsconfig.json', tsconfigTemplate({ sourceRootRelative, - outRootRelative: Layout.DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, + outRootRelative: Layout.DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT, }) ), diff --git a/src/lib/build/build.ts b/src/lib/build/build.ts index a289eaf4d..88b2710c4 100644 --- a/src/lib/build/build.ts +++ b/src/lib/build/build.ts @@ -45,7 +45,7 @@ export async function buildNexusApp(settings: BuildSettings) { buildOutputDir: buildOutput, asBundle: settings.asBundle, entrypointPath: settings.entrypoint, - cwd: settings.cwd, + projectRoot: settings.cwd, }) ) diff --git a/src/lib/build/deploy-target.ts b/src/lib/build/deploy-target.ts index fb6577193..1db6eed44 100644 --- a/src/lib/build/deploy-target.ts +++ b/src/lib/build/deploy-target.ts @@ -2,7 +2,7 @@ import chalk from 'chalk' import { stripIndent } from 'common-tags' import * as fs from 'fs-jetpack' import * as Path from 'path' -import { DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, Layout } from '../../lib/layout' +import { DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT, Layout } from '../../lib/layout' import { findFileRecurisvelyUpwardSync } from '../fs' import { rootLogger } from '../nexus-logger' import { fatal } from '../process' @@ -41,7 +41,7 @@ export function normalizeTarget(inputDeployTarget: string | undefined): Supporte const TARGET_TO_BUILD_OUTPUT: Record = { vercel: 'dist', - heroku: DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, + heroku: DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT, } export function computeBuildOutputFromTarget(target: SupportedTargets | null) { diff --git a/src/lib/layout/build.ts b/src/lib/layout/build.ts new file mode 100644 index 000000000..34fc008ef --- /dev/null +++ b/src/lib/layout/build.ts @@ -0,0 +1,118 @@ +import * as Path from 'path' +import { START_MODULE_NAME } from '../../runtime/start/start-module' +import { ScanResult } from './layout' + +/** + * The temporary ts build folder used when bundling is enabled + * + * Note: It **should not** be nested in a sub-folder. This might "corrupt" the relative paths of the bundle build. + */ +export const TMP_TS_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT = '.tmp_build' + +export const DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT = '.nexus/build' + +export type BuildLayout = { + startModuleOutPath: string + startModuleInPath: string + tsOutputDir: string + bundleOutputDir: string | null + /** + * The final path to the start module. When bundling disbaled, same as `startModuleOutPath`. + */ + startModule: string + /** + * The final output dir. If bundler is enabled then this is `bundleOutputDir`. + * Otherwise it is `tsOutputDir`. + * + * When bundle case, this accounts for the bundle environment, which makes it + * **DIFFERENT** than the source root. For example: + * + * ``` + * /node_modules/ + * /api/app.ts + * /api/index.ts + * ``` + */ + root: string + /** + * If bundler is enabled then the final output dir where the **source** is + * located. Otherwise same as `tsOutputDir`. + * + * When bundle case, this is different than `root` because it tells you where + * the source starts, not the build environment. + * + * For example, here `source_root` is `/api` becuase the user has + * set their root dir to `api`: + * + * ``` + * /node_modules/ + * /api/app.ts + * /api/index.ts + * ``` + * + * But here, `source_root` is `` because the user has set their root + * dir to `.`: + * + * ``` + * /node_modules/ + * /app.ts + * /index.ts + * ``` + */ + sourceRoot: string +} + +export function getBuildLayout( + buildOutput: string | undefined, + scanResult: ScanResult, + asBundle?: boolean +): BuildLayout { + const tsOutputDir = getBuildOutputDir(scanResult.projectRoot, buildOutput, scanResult) + const startModuleInPath = Path.join(scanResult.sourceRoot, START_MODULE_NAME + '.ts') + const startModuleOutPath = Path.join(tsOutputDir, START_MODULE_NAME + '.js') + + if (!asBundle) { + return { + tsOutputDir, + startModuleInPath, + startModuleOutPath, + bundleOutputDir: null, + startModule: startModuleOutPath, + root: tsOutputDir, + sourceRoot: tsOutputDir, + } + } + + const tsBuildInfo = getBuildLayout(TMP_TS_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, scanResult, false) + const relativeRootDir = Path.relative(scanResult.projectRoot, scanResult.tsConfig.content.options.rootDir!) + const sourceRoot = Path.join(tsOutputDir, relativeRootDir) + + return { + ...tsBuildInfo, + bundleOutputDir: tsOutputDir, + root: tsOutputDir, + startModule: Path.join(sourceRoot, START_MODULE_NAME + '.js'), + sourceRoot, + } +} + +/** + * Get the absolute build output dir + * Precedence: User's input > tsconfig.json's outDir > default + */ +function getBuildOutputDir( + projectRoot: string, + buildOutput: string | undefined, + scanResult: ScanResult +): string { + const output = + buildOutput ?? + scanResult.tsConfig.content.options.outDir ?? + DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT + + if (Path.isAbsolute(output)) { + return output + } + + return Path.join(projectRoot, output) +} diff --git a/src/lib/layout/cache.ts b/src/lib/layout/cache.ts new file mode 100644 index 000000000..053f2897c --- /dev/null +++ b/src/lib/layout/cache.ts @@ -0,0 +1,32 @@ +import { Either, right } from 'fp-ts/lib/Either' +import { rootLogger } from '../nexus-logger' +import { create, createFromData, Data, Layout } from './layout' + +const ENV_VAR_DATA_NAME = 'NEXUS_LAYOUT' + +const log = rootLogger.child('layout') + +export function saveDataForChildProcess(layout: Layout): { NEXUS_LAYOUT: string } { + return { + [ENV_VAR_DATA_NAME]: JSON.stringify(layout.data), + } +} + +/** + * Load the layout data from a serialized version stored in the environment. If + * it is not found then a warning will be logged and it will be recalculated. + * For this reason the function is async however under normal circumstances it + * should be as-if sync. + */ +export async function loadDataFromParentProcess(): Promise> { + const savedData: undefined | string = process.env[ENV_VAR_DATA_NAME] + if (!savedData) { + log.trace( + 'WARNING an attempt to load saved layout data was made but no serialized data was found in the environment. This may represent a bug. Layout is being re-calculated as a fallback solution. This should result in the same layout data (if not, another probably bug, compounding confusion) but at least adds latentency to user experience.' + ) + return create({}) // todo no build output... + } else { + // todo guard against corrupted env data + return right(createFromData(JSON.parse(savedData) as Data)) + } +} diff --git a/src/lib/layout/index.spec.ts b/src/lib/layout/index.spec.ts index bf1645e86..f4cc366c2 100644 --- a/src/lib/layout/index.spec.ts +++ b/src/lib/layout/index.spec.ts @@ -70,10 +70,10 @@ const ctx = TC.create( ? replaceEvery(x, ctx.tmpDir, '__DYNAMIC__') : repalceInObject(ctx.tmpDir, '__DYNAMIC__', x) }, - async scanThrow(opts?: { entrypointPath?: string; buildOutput?: string }) { + async createLayoutThrow(opts?: { entrypointPath?: string; buildOutput?: string }) { const data = rightOrThrow( await Layout.create({ - cwd: ctx.tmpDir, + projectRoot: ctx.tmpDir, entrypointPath: opts?.entrypointPath, buildOutputDir: opts?.buildOutput, asBundle: false, @@ -82,9 +82,9 @@ const ctx = TC.create( mockedStdoutBuffer = mockedStdoutBuffer.split(ctx.tmpDir).join('__DYNAMIC__') return repalceInObject(ctx.tmpDir, '__DYNAMIC__', data.data) }, - async scan(opts?: { entrypointPath?: string; buildOutput?: string }) { + async createLayout(opts?: { entrypointPath?: string; buildOutput?: string }) { return Layout.create({ - cwd: ctx.tmpDir, + projectRoot: ctx.tmpDir, entrypointPath: opts?.entrypointPath, buildOutputDir: opts?.buildOutput, asBundle: false, @@ -94,15 +94,58 @@ const ctx = TC.create( }) ) +const nestTmpDir = () => { + const projectRootPath = ctx.fs.path('project-root') + ctx.fs.dir(projectRootPath) + ctx.fs = ctx.fs.cwd(projectRootPath) +} + /** * Tests */ +describe('projectRoot', () => { + it('can be forced', () => { + const projectRoot = ctx.fs.path('./foobar') + ctx.fs.write('./foobar/app.ts', '') + ctx.fs.dir(projectRoot) + expect(Layout.create({ projectRoot }).then(rightOrThrow)).resolves.toMatchObject({ projectRoot }) + }) + it('otherwise uses first dir in hierarchy with a package.json', () => { + nestTmpDir() + ctx.fs.write('../package.json', { version: '0.0.0', name: 'foo' }) + ctx.fs.write('app.ts', '') + expect(Layout.create({ cwd: ctx.fs.cwd() }).then(rightOrThrow)).resolves.toMatchObject({ + projectRoot: ctx.fs.path('..'), + }) + }) + it('otherwise finally falls back to process cwd', () => { + ctx.fs.write('app.ts', '') + expect(Layout.create({ cwd: ctx.fs.cwd() }).then(rightOrThrow)).resolves.toMatchObject({ + projectRoot: ctx.fs.cwd(), + }) + }) +}) + +describe('sourceRoot', () => { + it('defaults to project dir', async () => { + ctx.setup({ 'tsconfig.json': '' }) + const result = await ctx.createLayoutThrow() + expect(result.sourceRoot).toEqual('__DYNAMIC__') + expect(result.projectRoot).toEqual('__DYNAMIC__') + }) + it('honours the value in tsconfig rootDir', async () => { + ctx.setup({ 'tsconfig.json': tsconfigContent({ compilerOptions: { rootDir: 'api' } }) }) + const result = await ctx.createLayoutThrow() + expect(result.sourceRoot).toMatchInlineSnapshot(`"__DYNAMIC__/api"`) + }) +}) + it('fails if empty file tree', async () => { ctx.setup() try { - await ctx.scanThrow() + await ctx.createLayoutThrow() } catch (err) { expect(err.message).toContain("Path you want to find stuff in doesn't exist") } @@ -114,7 +157,7 @@ describe('tsconfig', () => { }) it('will scaffold tsconfig if not present', async () => { - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` "▲ nexus:tsconfig We could not find a \\"tsconfig.json\\" file ▲ nexus:tsconfig We scaffolded one for you at __DYNAMIC__/tsconfig.json @@ -155,7 +198,7 @@ describe('tsconfig', () => { include: ['.'], }), }) - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` "▲ nexus:tsconfig You have set compilerOptions.tsBuildInfoFile in your tsconfig.json but it will be ignored by Nexus. Nexus manages this value internally. ▲ nexus:tsconfig You have set compilerOptions.incremental in your tsconfig.json but it will be ignored by Nexus. Nexus manages this value internally. @@ -167,7 +210,7 @@ describe('tsconfig', () => { ctx.setup({ 'tsconfig.json': '', }) - const layout = await ctx.scanThrow() + const layout = await ctx.createLayoutThrow() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` "▲ nexus:tsconfig You have not setup the Nexus TypeScript Language Service Plugin. Add this to your tsconfig compiler options: @@ -189,7 +232,7 @@ describe('tsconfig', () => { }), }) - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` "▲ nexus:tsconfig You have not added the Nexus TypeScript Language Service Plugin to your configured TypeScript plugins. Add this to your tsconfig compiler options: @@ -203,7 +246,7 @@ describe('tsconfig', () => { ctx.setup({ 'tsconfig.json': 'bad json', }) - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(stripAnsi(mockedStdoutBuffer)).toMatchInlineSnapshot(` "✕ nexus:tsconfig Unable to read your tsconifg.json @@ -230,7 +273,7 @@ describe('tsconfig', () => { ctx.setup({ 'tsconfig.json': '{ "exclude": "bad" }', }) - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(stripAnsi(mockedStdoutBuffer)).toMatchInlineSnapshot(` "▲ nexus:tsconfig You have not setup the Nexus TypeScript Language Service Plugin. Add this to your tsconfig compiler options: @@ -251,7 +294,7 @@ describe('tsconfig', () => { }) }) -it('fails if no entrypoint and no graphql modules', async () => { +it('fails if no entrypoint and no nexus modules', async () => { ctx.setup({ ...fsTsConfig, src: { @@ -260,7 +303,7 @@ it('fails if no entrypoint and no graphql modules', async () => { }, }) - await ctx.scanThrow() + await ctx.createLayoutThrow() expect(mockedStdoutBuffer).toMatchInlineSnapshot(` "■ nexus:layout We could not find any modules that imports 'nexus' or app.ts entrypoint @@ -277,29 +320,30 @@ it('fails if no entrypoint and no graphql modules', async () => { expect(mockExit).toHaveBeenCalledWith(1) }) -it('finds nested nexus modules', async () => { - ctx.setup({ - ...fsTsConfig, - src: { - 'app.ts': '', - graphql: { - '1.ts': `import { schema } from 'nexus'`, - '2.ts': `import { schema } from 'nexus'`, +describe('nexusModules', () => { + it('finds nested nexus modules', async () => { + ctx.setup({ + ...fsTsConfig, + src: { + 'app.ts': '', graphql: { - '3.ts': `import { schema } from 'nexus'`, - '4.ts': `import { schema } from 'nexus'`, + '1.ts': `import { schema } from 'nexus'`, + '2.ts': `import { schema } from 'nexus'`, graphql: { - '5.ts': `import { schema } from 'nexus'`, - '6.ts': `import { schema } from 'nexus'`, + '3.ts': `import { schema } from 'nexus'`, + '4.ts': `import { schema } from 'nexus'`, + graphql: { + '5.ts': `import { schema } from 'nexus'`, + '6.ts': `import { schema } from 'nexus'`, + }, }, }, }, - }, - }) + }) - const result = await ctx.scanThrow() + const result = await ctx.createLayoutThrow() - expect(result.nexusModules).toMatchInlineSnapshot(` + expect(result.nexusModules).toMatchInlineSnapshot(` Array [ "__DYNAMIC__/src/graphql/1.ts", "__DYNAMIC__/src/graphql/2.ts", @@ -309,101 +353,106 @@ it('finds nested nexus modules', async () => { "__DYNAMIC__/src/graphql/graphql/graphql/6.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.createLayoutThrow({ entrypointPath: './app.ts' }) + expect({ + app: result.app, + nexusModules: result.nexusModules, + }).toMatchInlineSnapshot(` + Object { + "app": Object { + "exists": true, + "path": "__DYNAMIC__/app.ts", + }, + "nexusModules": Array [ + "__DYNAMIC__/graphql.ts", + ], + } + `) + }) }) -it('detects yarn as package manager', async () => { - ctx.setup({ ...fsTsConfig, 'app.ts': '', 'yarn.lock': '' }) - const result = await ctx.scanThrow() - expect(result.packageManagerType).toMatchInlineSnapshot(`"yarn"`) +describe('packageManagerType', () => { + it('detects yarn as package manager', async () => { + ctx.setup({ ...fsTsConfig, 'app.ts': '', 'yarn.lock': '' }) + const result = await ctx.createLayoutThrow() + expect(result.packageManagerType).toMatchInlineSnapshot(`"yarn"`) + }) }) -it('finds app.ts entrypoint', async () => { - ctx.setup({ ...fsTsConfig, 'app.ts': '' }) - const result = await ctx.scanThrow() - expect(result.app).toMatchInlineSnapshot(` +describe('entrypoint', () => { + it('finds app.ts entrypoint', async () => { + ctx.setup({ ...fsTsConfig, 'app.ts': '' }) + const result = await ctx.createLayoutThrow() + expect(result.app).toMatchInlineSnapshot(` Object { "exists": true, "path": "__DYNAMIC__/app.ts", } `) -}) + }) -it('set app.exists = false if no entrypoint', async () => { - await ctx.setup({ ...fsTsConfig, 'graphql.ts': '' }) - const result = await ctx.scanThrow() - expect(result.app).toMatchInlineSnapshot(` + it('set app.exists = false if no entrypoint', async () => { + await ctx.setup({ ...fsTsConfig, 'graphql.ts': '' }) + const result = await ctx.createLayoutThrow() + expect(result.app).toMatchInlineSnapshot(` Object { "exists": false, "path": null, } `) -}) + }) -it('uses custom relative entrypoint when defined', async () => { - await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) - const result = await ctx.scanThrow({ entrypointPath: './index.ts' }) - expect(result.app).toMatchInlineSnapshot(` + it('uses custom relative entrypoint when defined', async () => { + await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) + const result = await ctx.createLayoutThrow({ entrypointPath: './index.ts' }) + expect(result.app).toMatchInlineSnapshot(` Object { "exists": true, "path": "__DYNAMIC__/index.ts", } `) -}) + }) -it('uses custom absolute entrypoint when defined', async () => { - await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) - const result = await ctx.scanThrow({ entrypointPath: ctx.fs.path('index.ts') }) - expect(result.app).toMatchInlineSnapshot(` + it('uses custom absolute entrypoint when defined', async () => { + await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) + const result = await ctx.createLayoutThrow({ entrypointPath: ctx.fs.path('index.ts') }) + expect(result.app).toMatchInlineSnapshot(` Object { "exists": true, "path": "__DYNAMIC__/index.ts", } `) -}) - -it('fails if custom entrypoint does not exist', async () => { - await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) - const result = await ctx.scan({ entrypointPath: './wrong-path.ts' }) - expect(JSON.stringify(result)).toMatchInlineSnapshot( - `"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint does not exist\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/wrong-path.ts\\"}}}"` - ) -}) + }) -it('fails if custom entrypoint is not a .ts file', async () => { - await ctx.setup({ ...fsTsConfig, 'index.ts': ``, 'index.js': `console.log('entrypoint')` }) - const result = await ctx.scan({ entrypointPath: './index.js' }) - expect(JSON.stringify(result)).toMatchInlineSnapshot( - `"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint must be a .ts file\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/index.js\\"}}}"` - ) -}) + it('fails if custom entrypoint does not exist', async () => { + await ctx.setup({ ...fsTsConfig, 'index.ts': `console.log('entrypoint')` }) + const result = await ctx.createLayout({ entrypointPath: './wrong-path.ts' }) + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint does not exist\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/wrong-path.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'`, + it('fails if custom entrypoint is not a .ts file', async () => { + await ctx.setup({ ...fsTsConfig, 'index.ts': ``, 'index.js': `console.log('entrypoint')` }) + const result = await ctx.createLayout({ entrypointPath: './index.js' }) + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint must be a .ts file\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/index.js\\"}}}"` + ) }) - const result = await ctx.scanThrow({ entrypointPath: './app.ts' }) - expect({ - app: result.app, - nexusModules: result.nexusModules, - }).toMatchInlineSnapshot(` - Object { - "app": Object { - "exists": true, - "path": "__DYNAMIC__/app.ts", - }, - "nexusModules": Array [ - "__DYNAMIC__/graphql.ts", - ], - } - `) }) -describe('build output', () => { +describe('build', () => { it(`defaults to .nexus/build`, async () => { await ctx.setup({ ...fsTsConfig, 'graphql.ts': '' }) - const result = await ctx.scanThrow() + const result = await ctx.createLayoutThrow() expect({ tsOutputDir: result.build.tsOutputDir, @@ -429,7 +478,7 @@ describe('build output', () => { }), 'graphql.ts': '', }) - const result = await ctx.scanThrow() + const result = await ctx.createLayoutThrow() expect({ tsOutputDir: result.build.tsOutputDir, @@ -454,7 +503,7 @@ describe('build output', () => { }), 'graphql.ts': '', }) - const result = await ctx.scanThrow({ buildOutput: 'custom-output' }) + const result = await ctx.createLayoutThrow({ buildOutput: 'custom-output' }) expect({ tsOutputDir: result.build.tsOutputDir, @@ -470,27 +519,8 @@ describe('build output', () => { }) }) -describe('source root', () => { - it('defaults to project dir', async () => { - ctx.setup({ 'tsconfig.json': '' }) - const result = await ctx.scanThrow() - expect(result.sourceRoot).toEqual('__DYNAMIC__') - expect(result.projectRoot).toEqual('__DYNAMIC__') - }) - it('honours the value in tsconfig rootDir', async () => { - ctx.setup({ 'tsconfig.json': tsconfigContent({ compilerOptions: { rootDir: 'api' } }) }) - const result = await ctx.scanThrow() - expect(result.sourceRoot).toMatchInlineSnapshot(`"__DYNAMIC__/api"`) - }) -}) - -describe.only('scanProjectType', () => { +describe('scanProjectType', () => { const pjdata = { version: '0.0.0', name: 'foo' } - const nestTmpDir = () => { - const projectRootPath = ctx.fs.path('project_root') - ctx.fs.dir(projectRootPath) - ctx.fs = ctx.fs.cwd(projectRootPath) - } describe('if package.json with nexus dep then nexus project', () => { it('in cwd', async () => { diff --git a/src/lib/layout/index.ts b/src/lib/layout/index.ts index 36545de52..cc3c9bbdb 100644 --- a/src/lib/layout/index.ts +++ b/src/lib/layout/index.ts @@ -1,22 +1,11 @@ +export * from './build' +export * from './cache' export { create, createFromData, Data, - DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, - TMP_TS_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, findAppModule, + findNexusModules, Layout, - loadDataFromParentProcess, - saveDataForChildProcess, scanProjectType, - findNexusModules } from './layout' - -// todo refactor with TS 3.8 namespace re-export -// once https://github.com/prettier/prettier/issues/7263 - -import { emptyExceptionMessage } from './schema-modules' - -export const schemaModules = { - emptyExceptionMessage, -} diff --git a/src/lib/layout/layout.ts b/src/lib/layout/layout.ts index 38eb573fb..4f3d829ea 100644 --- a/src/lib/layout/layout.ts +++ b/src/lib/layout/layout.ts @@ -5,36 +5,26 @@ import { Either, isLeft, left, right } from 'fp-ts/lib/Either' import * as FS from 'fs-jetpack' import * as Path from 'path' import * as ts from 'ts-morph' -import { PackageJson } from 'type-fest' import type { ParsedCommandLine } from 'typescript' import { findFile, isEmptyDir } from '../../lib/fs' -import { START_MODULE_NAME } from '../../runtime/start/start-module' import { rootLogger } from '../nexus-logger' import * as PJ from '../package-json' import * as PackageManager from '../package-manager' import { createContextualError } from '../utils' +import { BuildLayout, getBuildLayout } from './build' +import { saveDataForChildProcess } from './cache' import { readOrScaffoldTsconfig } from './tsconfig' -export const DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT = '.nexus/build' -/** - * The temporary ts build folder used when bundling is enabled - * - * Note: It **should not** be nested in a sub-folder. This might "corrupt" the relative paths of the bundle build. - */ -export const TMP_TS_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT = '.tmp_build' - const log = rootLogger.child('layout') +// todo allow user to configure these for their project +const CONVENTIONAL_ENTRYPOINT_MODULE_NAME = 'app' +const CONVENTIONAL_ENTRYPOINT_FILE_NAME = `${CONVENTIONAL_ENTRYPOINT_MODULE_NAME}.ts` + /** * The part of layout data resulting from the dynamic file/folder inspection. */ export type ScanResult = { - // build: { - // dir: string - // } - // source: { - // isNested: string - // } app: | { exists: true @@ -56,84 +46,20 @@ export type ScanResult = { path: string } packageManagerType: PackageManager.PackageManager['type'] - packageJson: null | { - dir: string - path: string - content: PackageJson - } - // schema: - // | { - // exists: boolean - // multiple: true - // paths: string[] - // } - // | { - // exists: boolean - // multiple: false - // path: null | string - // } - // context: { - // exists: boolean - // path: null | string - // } -} - -type OutputInfo = { - startModuleOutPath: string - startModuleInPath: string - tsOutputDir: string - bundleOutputDir: string | null - /** - * The final path to the start module. When bundling disbaled, same as `startModuleOutPath`. - */ - startModule: string - /** - * The final output dir. If bundler is enabled then this is `bundleOutputDir`. - * Otherwise it is `tsOutputDir`. - * - * When bundle case, this accounts for the bundle environment, which makes it - * **DIFFERENT** than the source root. For example: - * - * ``` - * /node_modules/ - * /api/app.ts - * /api/index.ts - * ``` - */ - root: string - /** - * If bundler is enabled then the final output dir where the **source** is - * located. Otherwise same as `tsOutputDir`. - * - * When bundle case, this is different than `root` because it tells you where - * the source starts, not the build environment. - * - * For example, here `source_root` is `/api` becuase the user has - * set their root dir to `api`: - * - * ``` - * /node_modules/ - * /api/app.ts - * /api/index.ts - * ``` - * - * But here, `source_root` is `` because the user has set their root - * dir to `.`: - * - * ``` - * /node_modules/ - * /app.ts - * /index.ts - * ``` - */ - sourceRoot: string } /** * The combination of manual datums the user can specify about the layout plus * the dynamic scan results. */ -export type Data = ScanResult & { build: OutputInfo } +export type Data = ScanResult & { + build: BuildLayout + packageJson: null | { + dir: string + path: string + content: PJ.ValidPackageJson + } +} /** * Layout represents the important edges of the project to support things like @@ -171,17 +97,25 @@ interface Options { */ entrypointPath?: string /** - * Directory in which the layout should be performed + * Force the project root directory. Defaults to being detected automatically. */ - cwd?: string + projectRoot?: string /** * Whether the build should be outputted as a bundle */ asBundle?: boolean -} - -const optionDefaults = { - buildOutput: DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, + /** + * Force the current working directory. + * + * @default + * + * process.cwd() + * + * @remarks + * + * Interplay between this and projectRoot option: When the projectRoot is not forced then the cwd is utilized for various logic. + */ + cwd?: string } /** @@ -189,23 +123,88 @@ const optionDefaults = { */ export async function create(options?: Options): Promise> { const cwd = options?.cwd ?? process.cwd() - const errNormalizedEntrypoint = normalizeEntrypoint(options?.entrypointPath, cwd) + /** + * Find the project root directory. This can be different than the source root + * directory. For example the classic project structure where there is a root + * `src` folder. `src` folder would be considered the "source root". + * + * Project root is considered to be the first package.json found from cwd upward + * to disk root. If not package.json is found then cwd is taken to be the + * project root. + * + */ + + let projectRoot = options?.projectRoot + let packageJson: null | Data['packageJson'] = null + + if (!projectRoot) { + const maybeErrPackageJson = PJ.findRecurisvelyUpwardSync({ cwd }) + + if (!maybeErrPackageJson) { + projectRoot = cwd + } else if (isLeft(maybeErrPackageJson.content)) { + return maybeErrPackageJson.content + } else { + projectRoot = maybeErrPackageJson.dir + packageJson = { + ...maybeErrPackageJson, + content: maybeErrPackageJson.content.right, + } + } + } + + const errNormalizedEntrypoint = normalizeEntrypoint(options?.entrypointPath, projectRoot) if (isLeft(errNormalizedEntrypoint)) return errNormalizedEntrypoint const normalizedEntrypoint = errNormalizedEntrypoint.right - // TODO lodash merge defaults or something + const packageManagerType = await PackageManager.detectProjectPackageManager({ projectRoot }) + const maybeAppModule = normalizedEntrypoint ?? findAppModule({ projectRoot }) + const tsConfig = await readOrScaffoldTsconfig({ + projectRoot, + }) + const nexusModules = findNexusModules(tsConfig, maybeAppModule) + const project = packageJson + ? { + name: packageJson.content.name, + isAnonymous: false, + } + : { + name: 'anonymous', + isAnonymous: true, + } + + const scanResult = { + app: + maybeAppModule === null + ? ({ exists: false, path: maybeAppModule } as const) + : ({ exists: true, path: maybeAppModule } as const), + projectRoot, + sourceRoot: tsConfig.content.options.rootDir!, + nexusModules, + project, + tsConfig, + packageManagerType, + } + + if (scanResult.app.exists === false && scanResult.nexusModules.length === 0) { + // todo return left + log.error(checks.no_app_or_nexus_modules.explanations.problem) + log.error(checks.no_app_or_nexus_modules.explanations.solution) + process.exit(1) + } - const errScanResult = await scan({ cwd, entrypointPath: normalizedEntrypoint }) - if (isLeft(errScanResult)) return errScanResult - const scanResult = errScanResult.right + // const errScanResult = await scan({ projectRoot, packageJson, entrypointPath: normalizedEntrypoint }) + // if (isLeft(errScanResult)) return errScanResult + // const scanResult = errScanResult.right - const buildInfo = getBuildInfo(options?.buildOutputDir, scanResult, options?.asBundle) + const buildInfo = getBuildLayout(options?.buildOutputDir, scanResult, options?.asBundle) log.trace('layout build info', { data: buildInfo }) const layout = createFromData({ ...scanResult, + packageJson, build: buildInfo, }) @@ -221,7 +220,7 @@ export async function create(options?: Options): Promise> } /** - * Create a layout based on received data. Useful for taking in serialized scan + * Create a layout instance with given layout data. Useful for taking in serialized scan * data from another process that would be wasteful to re-calculate. */ export function createFromData(layoutData: Data): Layout { @@ -256,61 +255,8 @@ export function createFromData(layoutData: Data): Layout { return layout } -/** - * Analyze the user's project files/folders for how conventions are being used - * and where key modules exist. - */ -export async function scan(opts?: { - cwd?: string - entrypointPath?: string -}): Promise> { - log.trace('starting scan') - const projectRoot = opts?.cwd ?? process.cwd() - const packageManagerType = await PackageManager.detectProjectPackageManager({ projectRoot }) - const maybeErrPackageJson = PJ.findRecurisvelyUpwardSync({ cwd: projectRoot }) - const maybeAppModule = opts?.entrypointPath ?? findAppModule({ projectRoot }) - const tsConfig = await readOrScaffoldTsconfig({ - projectRoot, - }) - const nexusModules = findNexusModules(tsConfig, maybeAppModule) - - if (maybeErrPackageJson && isLeft(maybeErrPackageJson.contents)) { - return maybeErrPackageJson.contents - } - - const result: ScanResult = { - app: - maybeAppModule === null - ? ({ exists: false, path: maybeAppModule } as const) - : ({ exists: true, path: maybeAppModule } as const), - projectRoot, - sourceRoot: tsConfig.content.options.rootDir!, - nexusModules, - project: readProjectInfo(opts), - tsConfig, - packageManagerType, - packageJson: maybeErrPackageJson - ? { ...maybeErrPackageJson, content: rightOrThrow(maybeErrPackageJson.contents) } - : maybeErrPackageJson, - } - - log.trace('completed scan', { result }) - - 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) - } - - return right(result) -} - -// todo allow user to configure these for their project -const CONVENTIONAL_ENTRYPOINT_MODULE_NAME = 'app' -const CONVENTIONAL_ENTRYPOINT_FILE_NAME = `${CONVENTIONAL_ENTRYPOINT_MODULE_NAME}.ts` - const checks = { - no_app_or_schema_modules: { + no_app_or_nexus_modules: { code: 'no_app_or_schema_modules', // prettier-ignore explanations: { @@ -336,22 +282,6 @@ export function findAppModule(opts: { projectRoot: string }): string | null { return path } -/** - * Find the project root directory. This can be different than the source root - * directory. For example the classic project structure where there is a root - * `src` folder. `src` folder would be considered the "source root". - * - * Project root is considered to be the first package.json found from cwd upward - * to disk root. If not package.json is found then cwd is taken to be the - * project root. - * - * todo update jsdoc or make it true again - * - */ -export function findProjectDir(): string { - return process.cwd() -} - /** * Detect whether or not CWD is inside a nexus project. nexus project is * defined as there being a package.json in or above CWD with nexus as a @@ -377,14 +307,14 @@ export async function scanProjectType(opts: { return { type: 'unknown' } } - if (isLeft(packageJson.contents)) { + if (isLeft(packageJson.content)) { return { type: 'malformed_package_json', - error: packageJson.contents.left, + error: packageJson.content.left, } } - const pjc = rightOrThrow(packageJson.contents) // will never throw, check above + const pjc = rightOrThrow(packageJson.content) // will never throw, check above if (pjc.dependencies?.['nexus']) { return { type: 'NEXUS_project', @@ -400,58 +330,18 @@ export async function scanProjectType(opts: { } } -const ENV_VAR_DATA_NAME = 'NEXUS_LAYOUT' - -export function saveDataForChildProcess(layout: Layout): { NEXUS_LAYOUT: string } { - return { - [ENV_VAR_DATA_NAME]: JSON.stringify(layout.data), - } -} - /** - * Load the layout data from a serialized version stored in the environment. If - * it is not found then a warning will be logged and it will be recalculated. - * For this reason the function is async however under normal circumstances it - * should be as-if sync. + * Validate the given entrypoint and normalize it into an absolute path. */ -export async function loadDataFromParentProcess(): Promise> { - const savedData: undefined | string = process.env[ENV_VAR_DATA_NAME] - if (!savedData) { - log.trace( - 'WARNING an attempt to load saved layout data was made but no serialized data was found in the environment. This may represent a bug. Layout is being re-calculated as a fallback solution. This should result in the same layout data (if not, another probably bug, compounding confusion) but at least adds latentency to user experience.' - ) - return create({}) // todo no build output... - } else { - // todo guard against corrupted env data - return right(createFromData(JSON.parse(savedData) as Data)) - } -} - -function readProjectInfo(opts?: { cwd?: string }): ScanResult['project'] { - const localFS = FS.cwd(opts?.cwd ?? process.cwd()) - try { - const packageJson: PackageJson = require(localFS.path('package.json')) - - if (packageJson.name) { - return { - name: packageJson.name, - isAnonymous: false, - } - } - } catch {} - - return { - name: 'anonymous', - isAnonymous: true, - } -} - -function normalizeEntrypoint(entrypoint: string | undefined, cwd: string): Either { +function normalizeEntrypoint( + entrypoint: string | undefined, + projectRoot: string +): Either { if (!entrypoint) { return right(undefined) } - const absoluteEntrypoint = entrypoint.startsWith('/') ? entrypoint : Path.join(cwd, entrypoint) + const absoluteEntrypoint = Path.isAbsolute(entrypoint) ? entrypoint : Path.join(projectRoot, entrypoint) if (!absoluteEntrypoint.endsWith('.ts')) { const error = createContextualError('Entrypoint must be a .ts file', { path: absoluteEntrypoint }) @@ -467,20 +357,9 @@ function normalizeEntrypoint(entrypoint: string | undefined, cwd: string): Eithe } /** - * Get the relative output build path - * Precedence: User's input > tsconfig.json's outDir > default + * Find the modules in the project that import nexus */ -function getBuildOutput(buildOutput: string | undefined, scanResult: ScanResult): string { - const output = buildOutput ?? scanResult.tsConfig.content.options.outDir ?? optionDefaults.buildOutput - - if (Path.isAbsolute(output)) { - return output - } - - return Path.join(scanResult.projectRoot, output) -} - -export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModule: string | null): string[] { +export function findNexusModules(tsConfig: Data['tsConfig'], maybeAppModule: string | null): string[] { try { log.trace('finding nexus modules') const project = new ts.Project({ @@ -505,41 +384,8 @@ export function findNexusModules(tsConfig: ScanResult['tsConfig'], maybeAppModul return modules } catch (error) { + // todo return left log.error('We could not find your nexus modules', { error }) return [] } } - -function getBuildInfo( - buildOutput: string | undefined, - scanResult: ScanResult, - asBundle?: boolean -): OutputInfo { - const tsOutputDir = getBuildOutput(buildOutput, scanResult) - const startModuleInPath = Path.join(scanResult.sourceRoot, START_MODULE_NAME + '.ts') - const startModuleOutPath = Path.join(tsOutputDir, START_MODULE_NAME + '.js') - - if (!asBundle) { - return { - tsOutputDir, - startModuleInPath, - startModuleOutPath, - bundleOutputDir: null, - startModule: startModuleOutPath, - root: tsOutputDir, - sourceRoot: tsOutputDir, - } - } - - const tsBuildInfo = getBuildInfo(TMP_TS_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, scanResult, false) - const relativeRootDir = Path.relative(scanResult.projectRoot, scanResult.tsConfig.content.options.rootDir!) - const sourceRoot = Path.join(tsOutputDir, relativeRootDir) - - return { - ...tsBuildInfo, - bundleOutputDir: tsOutputDir, - root: tsOutputDir, - startModule: Path.join(sourceRoot, START_MODULE_NAME + '.js'), - sourceRoot, - } -} diff --git a/src/lib/layout/schema-modules.ts b/src/lib/layout/schema-modules.ts deleted file mode 100644 index 58b0f7024..000000000 --- a/src/lib/layout/schema-modules.ts +++ /dev/null @@ -1,7 +0,0 @@ -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. 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/layout/tsconfig.ts b/src/lib/layout/tsconfig.ts index 69b66d976..4ae29b326 100644 --- a/src/lib/layout/tsconfig.ts +++ b/src/lib/layout/tsconfig.ts @@ -5,7 +5,7 @@ import * as Path from 'path' import { TsConfigJson } from 'type-fest' import * as ts from 'typescript' import { rootLogger } from '../nexus-logger' -import { DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT } from './layout' +import { DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT } from './build' export const NEXUS_TS_LSP_IMPORT_ID = 'nexus/typescript-language-service' @@ -27,7 +27,7 @@ export async function readOrScaffoldTsconfig(input: { tsconfigPath = Path.join(input.projectRoot, 'tsconfig.json') const tsconfigContent = tsconfigTemplate({ sourceRootRelative: '.', - outRootRelative: DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT, + outRootRelative: DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT, }) log.warn('We could not find a "tsconfig.json" file') log.warn(`We scaffolded one for you at ${tsconfigPath}`) @@ -123,7 +123,7 @@ export async function readOrScaffoldTsconfig(input: { if (input.overrides?.outRoot !== undefined) { tscfg.compilerOptions.outDir = input.overrides.outRoot } else if (!tscfg.compilerOptions.outDir) { - tscfg.compilerOptions.outDir = DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT + tscfg.compilerOptions.outDir = DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT } // check it diff --git a/src/lib/package-json.ts b/src/lib/package-json.ts index b73c13936..33616ff76 100644 --- a/src/lib/package-json.ts +++ b/src/lib/package-json.ts @@ -3,9 +3,11 @@ import * as FS from 'fs-jetpack' import { isEmpty, isPlainObject, isString } from 'lodash' import parseJson from 'parse-json' import * as Path from 'path' -import { PackageJson } from 'type-fest' +import * as TypeFest from 'type-fest' import { exceptionType } from './utils' +export type ValidPackageJson = TypeFest.PackageJson & { name: string; version: string } + const malformedPackageJson = exceptionType<'MalformedPackageJson', { path: string; reason: string }>( 'MalformedPackageJson', (c) => `package.json at ${c.path} was malformed\n\n${c.reason}` @@ -16,7 +18,7 @@ export type MalformedPackageJsonError = ReturnType export type Result = { path: string dir: string - contents: Either + content: Either } | null /** @@ -34,8 +36,8 @@ export function findRecurisvelyUpwardSync(opts: { cwd: string }): Result { const rawContents = localFS.read(filePath) if (rawContents) { - const contents = parse(rawContents, filePath) - found = { dir: currentDir, path: filePath, contents } + const content = parse(rawContents, filePath) + found = { dir: currentDir, path: filePath, content } break } @@ -70,5 +72,5 @@ export function parse(contents: string, path: string) { return left(malformedPackageJson({ path, reason: 'Package.json version field is not a string' })) if (isEmpty(rawData.version)) return left(malformedPackageJson({ path, reason: 'Package.json version field is empty' })) - return right(rawData as PackageJson & { name: string; version: string }) + return right(rawData as ValidPackageJson) } diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index bdac14e96..5b2ca9071 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -2,7 +2,6 @@ import * as NexusLogger from '@nexus/logger' import * as NexusSchema from '@nexus/schema' import { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql' import * as HTTP from 'http' -import { emptyExceptionMessage } from '../../lib/layout/schema-modules' import { createNexusSchemaStateful, NexusSchemaStatefulBuilders } from '../../lib/nexus-schema-stateful' import { RuntimeContributions } from '../../lib/plugin' import { Index, MaybePromise } from '../../lib/utils' @@ -130,3 +129,7 @@ export function create(appState: AppState): SchemaInternal { }, } } + +function emptyExceptionMessage() { + 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/website/content/02-guides/06-project-layout.mdx b/website/content/02-guides/06-project-layout.mdx index 07d7d1019..1d77c8451 100644 --- a/website/content/02-guides/06-project-layout.mdx +++ b/website/content/02-guides/06-project-layout.mdx @@ -2,20 +2,13 @@ title: Project Layout --- -## Project Layout - ## Working With tsconfig.json - Nexus honours settings within `tsconfig.json`. - This ensures that Nexus and your IDE perform identical static analysis. -- If no `tsconfig.json` is present then Nexus will scaffold one for you. +- If no `tsconfig.json` is present in the project root then Nexus will scaffold one for you. This will make ([VSCode treat it as the project root too](https://vscode.readthedocs.io/en/latest/languages/typescript/#typescript-files-and-projects)). - Nexus interacts with `tsconfig.json` in the following ways. -### Project Root - -- Project Root is the CWD (current working directory) for all CLI invocations. -- Nexus ([like VSCode](https://vscode.readthedocs.io/en/latest/languages/typescript/#typescript-files-and-projects)) considers the folder containing a `tsconfig.json` to be the project root. - ### Source Root - Source Root is the base from which your source code layout starts. So, all of your app code must live within the source root. Your JavaScript build output layout will mirror it. @@ -50,6 +43,16 @@ Autocomplete with Nexus TS LSP: Nexus imposes a few requirements about how you structure your codebase. +### Project Root + +The project root is the directory from which all all Nexus CLI commands base their CWD upon. It is also the directory that configuration paths in Nexus (e.g. `--entrypoint` flag) are often relative to as well (in other cases it can be source root). + +To find the project root Nexus starts with the current working directory (CWD). This usually means the current directory you're in when invoking the Nexus CLI. From this location Nexus will do the following: + +1. If a directory in the current hierarchy, including CWD, contains a [valid](https://docs.npmjs.com/creating-a-package-json-file#required-name-and-version-fields) `package.json` then it will be considered the project root. In case multiple such files are present in the hierarchy, only the first one is considered (in other words the one closest to CWD). + +2. If no `package.json` files exist then the CWD itself is taken to be the project root. + ### Nexus module(s) #### Pattern