Skip to content

Commit

Permalink
feat: treat fist package.json as project root (#1034)
Browse files Browse the repository at this point in the history
closes #1012

BREAKING CHANGE:

When invoking the CLI within a Nexus project, it will use the location of the package.json as its project root rather than blindly use the real CWD. Mostly this should just fix things but some users might have learnt the previous behaviour.
  • Loading branch information
Jason Kuhrt committed Jun 12, 2020
1 parent db6cce9 commit 19bb322
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 427 deletions.
17 changes: 11 additions & 6 deletions docs/guides/project-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/create/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
),

Expand Down
2 changes: 1 addition & 1 deletion src/lib/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function buildNexusApp(settings: BuildSettings) {
buildOutputDir: buildOutput,
asBundle: settings.asBundle,
entrypointPath: settings.entrypoint,
cwd: settings.cwd,
projectRoot: settings.cwd,
})
)

Expand Down
4 changes: 2 additions & 2 deletions src/lib/build/deploy-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -41,7 +41,7 @@ export function normalizeTarget(inputDeployTarget: string | undefined): Supporte

const TARGET_TO_BUILD_OUTPUT: Record<SupportedTargets, string> = {
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) {
Expand Down
118 changes: 118 additions & 0 deletions src/lib/layout/build.ts
Original file line number Diff line number Diff line change
@@ -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:
*
* ```
* <out_root>/node_modules/
* <out_root>/api/app.ts
* <out_root>/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 `<out_root>/api` becuase the user has
* set their root dir to `api`:
*
* ```
* <out_root>/node_modules/
* <out_root>/api/app.ts
* <out_root>/api/index.ts
* ```
*
* But here, `source_root` is `<out_root>` because the user has set their root
* dir to `.`:
*
* ```
* <out_source_root>/node_modules/
* <out_source_root>/app.ts
* <out_source_root>/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)
}
32 changes: 32 additions & 0 deletions src/lib/layout/cache.ts
Original file line number Diff line number Diff line change
@@ -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<Either<Error, Layout>> {
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))
}
}

0 comments on commit 19bb322

Please sign in to comment.