Skip to content

Commit

Permalink
feat: malformed json case on project type scan
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Jun 12, 2020
1 parent e5f51e6 commit 75de26c
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 52 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"http-errors": "^1.7.3",
"lodash": "^4.17.15",
"node-fetch": "^2.6.0",
"parse-json": "^5.0.0",
"pirates": "^4.0.1",
"prompts": "^2.3.0",
"rxjs": "^6.5.4",
Expand All @@ -72,6 +73,7 @@
"@types/jest": "25.2.3",
"@types/lodash": "4.14.152",
"@types/node": "13.13.9",
"@types/parse-json": "^4.0.0",
"docsify": "4.11.3",
"docsify-cli": "4.4.0",
"doctoc": "1.4.0",
Expand Down
8 changes: 7 additions & 1 deletion src/cli/commands/__default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'fs-jetpack'
import { Command } from '../../lib/cli'
import * as Layout from '../../lib/layout'
import { rootLogger } from '../../lib/nexus-logger'
import { CWDProjectNameOrGenerate, generateProjectName } from '../../lib/utils'
import { casesHandled, CWDProjectNameOrGenerate, generateProjectName } from '../../lib/utils'
import { run as runCreateApp } from './create/app'
import { Dev } from './dev'

Expand Down Expand Up @@ -72,6 +72,12 @@ export class __Default implements Command {
console.log() // space after codeblock

break
case 'malformed_package_json':
// todo test this case
log.fatal(projectType.error.message)
break
default:
casesHandled(projectType)
}
}
}
12 changes: 12 additions & 0 deletions src/lib/contextual-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class ContextualError<Context extends object> extends Error {
constructor(message: string, public context: Context) {
super(message)
this.name = this.constructor.name
}
}

export function rewordError<E extends ContextualError<{}>>(message: string, e: E): E {
// todo copy instead of mutate
e.message = message
return e
}
9 changes: 9 additions & 0 deletions src/lib/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,12 @@ export function stripExt(filePath: string): string {
const { dir, name } = Path.parse(filePath)
return Path.join(dir, name)
}

/**
* Check if the CWD is empty of any files or folders.
* TODO we should make nice exceptions for known meaningless files, like .DS_Store
*/
export async function isEmptyDir(dirPath: string): Promise<boolean> {
const contents = await FS.listAsync(dirPath)
return contents === undefined || contents.length === 0
}
90 changes: 39 additions & 51 deletions src/lib/layout/layout.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rightOrThrow } from '@nexus/logger/dist/utils'
import Chalk from 'chalk'
import { stripIndent } from 'common-tags'
import { Either, isLeft, left, right } from 'fp-ts/lib/Either'
Expand All @@ -6,9 +7,11 @@ import * as Path from 'path'
import * as ts from 'ts-morph'
import { PackageJson } from 'type-fest'
import type { ParsedCommandLine } from 'typescript'
import { findFile, findFileRecurisvelyUpwardSync } from '../../lib/fs'
import { findFile, isEmptyDir } from '../../lib/fs'
import { START_MODULE_NAME } from '../../runtime/start/start-module'
import { rewordError } from '../contextual-error'
import { rootLogger } from '../nexus-logger'
import * as PJ from '../package-json'
import * as PackageManager from '../package-manager'
import { createContextualError } from '../utils'
import { readOrScaffoldTsconfig } from './tsconfig'
Expand Down Expand Up @@ -194,7 +197,10 @@ export async function create(options?: Options): Promise<Either<Error, Layout>>

// TODO lodash merge defaults or something

const scanResult = await scan({ cwd, entrypointPath: normalizedEntrypoint })
const errScanResult = await scan({ cwd, entrypointPath: normalizedEntrypoint })
if (isLeft(errScanResult)) return errScanResult
const scanResult = errScanResult.right

const buildInfo = getBuildInfo(options?.buildOutputDir, scanResult, options?.asBundle)

log.trace('layout build info', { data: buildInfo })
Expand Down Expand Up @@ -255,17 +261,24 @@ export function createFromData(layoutData: Data): 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<ScanResult> {
export async function scan(opts?: {
cwd?: string
entrypointPath?: string
}): Promise<Either<Error, ScanResult>> {
log.trace('starting scan')
const projectRoot = opts?.cwd ?? process.cwd()
const packageManagerType = await PackageManager.detectProjectPackageManager({ projectRoot })
const maybePackageJson = findPackageJson({ 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
Expand All @@ -277,7 +290,9 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr
project: readProjectInfo(opts),
tsConfig,
packageManagerType,
packageJson: maybePackageJson,
packageJson: maybeErrPackageJson
? { ...maybeErrPackageJson, content: rightOrThrow(maybeErrPackageJson.contents) }
: maybeErrPackageJson,
}

log.trace('completed scan', { result })
Expand All @@ -288,7 +303,7 @@ export async function scan(opts?: { cwd?: string; entrypointPath?: string }): Pr
process.exit(1)
}

return result
return right(result)
}

// todo allow user to configure these for their project
Expand Down Expand Up @@ -347,46 +362,46 @@ export async function scanProjectType(opts: {
cwd: string
}): Promise<
| { type: 'unknown' | 'new' }
| { type: 'malformed_package_json'; error: PJ.MalformedPackageJsonError }
| {
type: 'NEXUS_project' | 'node_project'
packageJson: {}
packageJsonLocation: { path: string; dir: string }
}
> {
const packageJsonLocation = findPackageJsonRecursivelyUpward(opts)
const packageJson = PJ.findRecurisvelyUpwardSync(opts)

if (packageJsonLocation === null) {
if (await isEmptyCWD()) {
if (packageJson === null) {
if (await isEmptyDir(opts.cwd)) {
return { type: 'new' }
}
return { type: 'unknown' }
}

const packageJson = FS.read(packageJsonLocation.path, 'json')
if (packageJson?.dependencies?.['nexus']) {
if (isLeft(packageJson.contents)) {
const e = packageJson.contents.left
return {
type: 'malformed_package_json',
error: rewordError(`A package.json was found at ${e.context.path} but it was malformed`, e),
}
}

const pjc = rightOrThrow(packageJson.contents) // will never throw, check above
if (pjc.dependencies?.['nexus']) {
return {
type: 'NEXUS_project',
packageJson: packageJsonLocation,
packageJsonLocation: packageJsonLocation,
packageJson: packageJson,
packageJsonLocation: packageJson,
}
}

return {
type: 'node_project',
packageJson: packageJsonLocation,
packageJsonLocation: packageJsonLocation,
packageJson: packageJson,
packageJsonLocation: packageJson,
}
}

/**
* Check if the CWD is empty of any files or folders.
* TODO we should make nice exceptions for known meaningless files, like .DS_Store
*/
async function isEmptyCWD(): Promise<boolean> {
const contents = await FS.listAsync()
return contents === undefined || contents.length === 0
}

const ENV_VAR_DATA_NAME = 'NEXUS_LAYOUT'

export function saveDataForChildProcess(layout: Layout): { NEXUS_LAYOUT: string } {
Expand Down Expand Up @@ -433,33 +448,6 @@ function readProjectInfo(opts?: { cwd?: string }): ScanResult['project'] {
}
}

/**
* Find the package.json file path. Looks recursively upward to disk root.
* Starts looking in CWD If no package.json found along search, returns null.
*/
function findPackageJsonRecursivelyUpward(opts: { cwd: string }) {
return findFileRecurisvelyUpwardSync('package.json', opts)
}

/**
*
*/
function findPackageJson(opts: { projectRoot: string }): ScanResult['packageJson'] {
const packageJsonPath = FS.path(opts.projectRoot, 'package.json')

try {
const content = FS.read(packageJsonPath, 'json')

return {
content,
path: packageJsonPath,
dir: Path.dirname(packageJsonPath),
}
} catch {
return null
}
}

function normalizeEntrypoint(entrypoint: string | undefined, cwd: string): Either<Error, string | undefined> {
if (!entrypoint) {
return right(undefined)
Expand Down
68 changes: 68 additions & 0 deletions src/lib/package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Either, isLeft, left, right, toError, tryCatch } from 'fp-ts/lib/Either'
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 { ContextualError } from './contextual-error'

export class MalformedPackageJsonError extends ContextualError<{ path: string }> {}

export type Result = {
path: string
dir: string
contents: Either<MalformedPackageJsonError, PackageJson>
} | null

/**
* Find the package.json file path. Looks recursively upward to disk root.
* Starts looking in CWD If no package.json found along search, returns null.
* If packge.json fonud but fails to be parsed or fails validation than an error is returned.
*/
export function findRecurisvelyUpwardSync(opts: { cwd: string }): Result {
let found: Result = null
let currentDir = opts.cwd
const localFS = FS.cwd(currentDir)

while (true) {
const filePath = Path.join(currentDir, 'package.json')
const rawContents = localFS.read(filePath)

if (rawContents) {
const contents = parse(rawContents, filePath)
found = { dir: currentDir, path: filePath, contents }
break
}

if (currentDir === '/') {
break
}

currentDir = Path.join(currentDir, '..')
}

return found
}

/**
* Parse package.json contents.
*/
export function parse(contents: string, path: string) {
const errRawData = tryCatch(
() => parseJson(contents),
(e) => new MalformedPackageJsonError(toError(e).message, { path })
)
if (isLeft(errRawData)) return errRawData
const rawData = errRawData.right
if (!isPlainObject(rawData))
return left(new MalformedPackageJsonError('Package.json data is not an object', { path }))
if (!isString(rawData.name))
return left(new MalformedPackageJsonError('Package.json name field is not a string', { path }))
if (isEmpty(rawData.name))
return left(new MalformedPackageJsonError('Package.json name field is empty', { path }))
if (!isString(rawData.version))
return left(new MalformedPackageJsonError('Package.json version field is not a string', { path }))
if (isEmpty(rawData.version))
return left(new MalformedPackageJsonError('Package.json version field is empty', { path }))
return right(rawData as PackageJson & { name: string; version: string })
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,11 @@
dependencies:
"@types/node" "*"

"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==

"@types/prettier@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4"
Expand Down

0 comments on commit 75de26c

Please sign in to comment.