Skip to content

Commit 4e53bb8

Browse files
author
Jason Kuhrt
authored
refactor: remove fatals from layout tsconfig (#1046)
1 parent 6aa8059 commit 4e53bb8

File tree

7 files changed

+151
-114
lines changed

7 files changed

+151
-114
lines changed

src/lib/glocal/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Either, isLeft, isRight } from 'fp-ts/lib/Either'
22
import * as Path from 'path'
3+
import { inspect } from 'util'
34
import { findFileRecurisvelyUpwardSync } from '../fs'
45
import { fatal } from '../process'
56

@@ -47,6 +48,14 @@ export function rightOrThrow<A extends Error, B>(x: Either<A, B>): B {
4748
return x.right
4849
}
4950

51+
/**
52+
* Extract the left value from an Either or throw.
53+
*/
54+
export function leftOrThrow<A, B>(x: Either<A, B>): A {
55+
if (isLeft(x)) return x.left
56+
throw new Error(`Unexpected Either.right:\n${inspect(x.right)}`)
57+
}
58+
5059
/**
5160
* Extract the right value from an Either or throw.
5261
*/

src/lib/layout/index.spec.ts

Lines changed: 57 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { log } from '@nexus/logger'
22
import { defaultsDeep } from 'lodash'
3-
import stripAnsi from 'strip-ansi'
43
import { TsConfigJson } from 'type-fest'
54
import * as Layout from '.'
65
import { FSSpec, writeFSSpec } from '../../lib/testing-utils'
7-
import { rightOrThrow } from '../glocal/utils'
6+
import { leftOrThrow, rightOrThrow } from '../glocal/utils'
87
import * as TC from '../test-context'
98
import { repalceInObject, replaceEvery } from '../utils'
109
import { NEXUS_TS_LSP_IMPORT_ID } from './tsconfig'
@@ -136,33 +135,42 @@ describe('projectRoot', () => {
136135

137136
describe('sourceRoot', () => {
138137
it('defaults to project dir', async () => {
139-
ctx.setup({ 'tsconfig.json': '' })
140-
const result = await ctx.createLayoutThrow()
141-
expect(result.sourceRoot).toEqual('__DYNAMIC__')
142-
expect(result.projectRoot).toEqual('__DYNAMIC__')
138+
ctx.setup({ 'tsconfig.json': '', 'app.ts': '' })
139+
const res = await ctx.createLayout().then(rightOrThrow)
140+
expect(res.sourceRoot).toEqual('__DYNAMIC__')
141+
expect(res.projectRoot).toEqual('__DYNAMIC__')
143142
})
144-
it('honours the value in tsconfig rootDir', async () => {
145-
ctx.setup({ 'tsconfig.json': tsconfigSource({ compilerOptions: { rootDir: 'api' } }) })
146-
const result = await ctx.createLayoutThrow()
147-
expect(result.sourceRoot).toMatchInlineSnapshot(`"__DYNAMIC__/api"`)
143+
it('uses the value in tsconfig compilerOptions.rootDir if present', async () => {
144+
ctx.setup({ 'tsconfig.json': tsconfigSource({ compilerOptions: { rootDir: 'api' } }), 'api/app.ts': '' })
145+
const res = await ctx.createLayout().then(rightOrThrow)
146+
expect(res.sourceRoot).toMatchInlineSnapshot(`"__DYNAMIC__/api"`)
148147
})
149148
})
150149

151-
it('fails if empty file tree', async () => {
152-
ctx.setup()
153-
154-
try {
155-
await ctx.createLayoutThrow()
156-
} catch (err) {
157-
expect(err.message).toContain("Path you want to find stuff in doesn't exist")
158-
}
159-
})
160-
161150
describe('tsconfig', () => {
162151
beforeEach(() => {
163152
ctx.setup({ 'app.ts': '' })
164153
})
165154

155+
it('fails if tsconfig settings does not lead to matching any source files', async () => {
156+
ctx.fs.remove('app.ts')
157+
const res = await ctx.createLayout().then(leftOrThrow)
158+
expect(res).toMatchInlineSnapshot(`
159+
Object {
160+
"context": Object {
161+
"diagnostics": Array [
162+
Object {
163+
"category": 1,
164+
"code": 18003,
165+
"messageText": "No inputs were found in config file '__DYNAMIC__/tsconfig.json'. Specified 'include' paths were '[\\".\\"]' and 'exclude' paths were '[]'.",
166+
},
167+
],
168+
},
169+
"type": "invalid_tsconfig",
170+
}
171+
`)
172+
})
173+
166174
it('will scaffold tsconfig if not present', async () => {
167175
await ctx.createLayoutThrow()
168176
expect(logs).toMatchInlineSnapshot(`
@@ -320,56 +328,44 @@ describe('tsconfig', () => {
320328
})
321329
})
322330

323-
it('will fatal message and exit if error reading file', async () => {
331+
it('will return exception if error reading file', async () => {
324332
ctx.setup({
325333
'tsconfig.json': 'bad json',
326334
})
327-
await ctx.createLayoutThrow()
328-
expect(stripAnsi(logs)).toMatchInlineSnapshot(`
329-
"✕ nexus:tsconfig Unable to read your tsconifg.json
330-
331-
../../../../..__DYNAMIC__/tsconfig.json:1:1 - error TS1005: '{' expected.
332-
333-
1 bad json
334-
~~~
335-
336-
337-
338-
--- process.exit(1) ---
339-
340-
▲ nexus:tsconfig You have not setup the Nexus TypeScript Language Service Plugin. Add this to your compiler options:
335+
const res = await ctx.createLayout().then(leftOrThrow)
336+
expect(res).toMatchInlineSnapshot(`
337+
Object {
338+
"context": Object {},
339+
"message": "Unable to read your tsconifg.json
341340
342-
\\"plugins\\": [{ \\"name\\": \\"nexus/typescript-language-service\\" }]
341+
[96m../../../../..__DYNAMIC__/tsconfig.json[0m:[93m1[0m:[93m1[0m - [91merror[0m[90m TS1005: [0m'{' expected.
343342
344-
▲ nexus:tsconfig Please set \`compilerOptions.rootDir\` to \\".\\"
345-
▲ nexus:tsconfig Please set \`include\` to have \\".\\"
346-
▲ nexus:tsconfig Please set \`compilerOptions.noEmit\` to true. This will ensure you do not accidentally emit using \`$ tsc\`. Use \`$ nexus build\` to build your app and emit JavaScript.
347-
"
343+
1 bad json
344+
  ~~~
345+
",
346+
"type": "generic",
347+
}
348348
`)
349349
})
350350

351-
it('will fatal message and exit if invalid tsconfig schema', async () => {
351+
it('will return exception if invalid tsconfig schema', async () => {
352352
ctx.setup({
353353
'tsconfig.json': '{ "exclude": "bad" }',
354354
})
355-
await ctx.createLayoutThrow()
356-
expect(stripAnsi(logs)).toMatchInlineSnapshot(`
357-
"▲ nexus:tsconfig You have not setup the Nexus TypeScript Language Service Plugin. Add this to your compiler options:
358-
359-
\\"plugins\\": [{ \\"name\\": \\"nexus/typescript-language-service\\" }]
360-
361-
▲ nexus:tsconfig Please set \`compilerOptions.rootDir\` to \\".\\"
362-
▲ nexus:tsconfig Please set \`include\` to have \\".\\"
363-
▲ nexus:tsconfig Please set \`compilerOptions.noEmit\` to true. This will ensure you do not accidentally emit using \`$ tsc\`. Use \`$ nexus build\` to build your app and emit JavaScript.
364-
✕ nexus:tsconfig Your tsconfig.json is invalid
365-
366-
error TS5024: Compiler option 'exclude' requires a value of type Array.
367-
368-
369-
370-
--- process.exit(1) ---
371-
372-
"
355+
const res = await ctx.createLayout().then(leftOrThrow)
356+
expect(res).toMatchInlineSnapshot(`
357+
Object {
358+
"context": Object {
359+
"diagnostics": Array [
360+
Object {
361+
"category": 1,
362+
"code": 5024,
363+
"messageText": "Compiler option 'exclude' requires a value of type Array.",
364+
},
365+
],
366+
},
367+
"type": "invalid_tsconfig",
368+
}
373369
`)
374370
})
375371
})
@@ -516,7 +512,7 @@ describe('entrypoint', () => {
516512
await ctx.setup({ 'tsconfig.json': tsconfigSource(), 'index.ts': `console.log('entrypoint')` })
517513
const result = await ctx.createLayout({ entrypointPath: './wrong-path.ts' })
518514
expect(JSON.stringify(result)).toMatchInlineSnapshot(
519-
`"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint does not exist\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/wrong-path.ts\\"}}}"`
515+
`"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint does not exist\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/wrong-path.ts\\"},\\"type\\":\\"generic\\"}}"`
520516
)
521517
})
522518

@@ -528,7 +524,7 @@ describe('entrypoint', () => {
528524
})
529525
const result = await ctx.createLayout({ entrypointPath: './index.js' })
530526
expect(JSON.stringify(result)).toMatchInlineSnapshot(
531-
`"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint must be a .ts file\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/index.js\\"}}}"`
527+
`"{\\"_tag\\":\\"Left\\",\\"left\\":{\\"message\\":\\"Entrypoint must be a .ts file\\",\\"context\\":{\\"path\\":\\"__DYNAMIC__/index.js\\"},\\"type\\":\\"generic\\"}}"`
532528
)
533529
})
534530
})

src/lib/layout/layout.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { findFile, isEmptyDir } from '../../lib/fs'
1010
import { rootLogger } from '../nexus-logger'
1111
import * as PJ from '../package-json'
1212
import * as PackageManager from '../package-manager'
13-
import { createContextualError } from '../utils'
13+
import { exception } from '../utils'
1414
import { BuildLayout, getBuildLayout } from './build'
1515
import { saveDataForChildProcess } from './cache'
1616
import { readOrScaffoldTsconfig } from './tsconfig'
@@ -160,9 +160,11 @@ export async function create(options?: Options): Promise<Either<Error, Layout>>
160160

161161
const packageManagerType = await PackageManager.detectProjectPackageManager({ projectRoot })
162162
const maybeAppModule = normalizedEntrypoint ?? findAppModule({ projectRoot })
163-
const tsConfig = await readOrScaffoldTsconfig({
164-
projectRoot,
165-
})
163+
164+
const errTsConfig = await readOrScaffoldTsconfig({ projectRoot })
165+
if (isLeft(errTsConfig)) return errTsConfig
166+
const tsConfig = errTsConfig.right
167+
166168
const nexusModules = findNexusModules(tsConfig, maybeAppModule)
167169
const project = packageJson
168170
? {
@@ -340,12 +342,12 @@ function normalizeEntrypoint(
340342
const absoluteEntrypoint = Path.isAbsolute(entrypoint) ? entrypoint : Path.join(projectRoot, entrypoint)
341343

342344
if (!absoluteEntrypoint.endsWith('.ts')) {
343-
const error = createContextualError('Entrypoint must be a .ts file', { path: absoluteEntrypoint })
345+
const error = exception('Entrypoint must be a .ts file', { path: absoluteEntrypoint })
344346
return left(error)
345347
}
346348

347349
if (!FS.exists(absoluteEntrypoint)) {
348-
const error = createContextualError('Entrypoint does not exist', { path: absoluteEntrypoint })
350+
const error = exception('Entrypoint does not exist', { path: absoluteEntrypoint })
349351
return left(error)
350352
}
351353

src/lib/layout/tsconfig.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import chalk from 'chalk'
22
import { stripIndent } from 'common-tags'
3+
import { Either, left, right } from 'fp-ts/lib/Either'
34
import * as fs from 'fs-jetpack'
45
import { cloneDeep } from 'lodash'
56
import { EOL } from 'os'
@@ -8,6 +9,7 @@ import { TsConfigJson } from 'type-fest'
89
import * as ts from 'typescript'
910
import { isDeepStrictEqual } from 'util'
1011
import { rootLogger } from '../nexus-logger'
12+
import { exception, exceptionType } from '../utils'
1113
import { DEFAULT_BUILD_DIR_PATH_RELATIVE_TO_PROJECT_ROOT } from './build'
1214

1315
export const NEXUS_TS_LSP_IMPORT_ID = 'nexus/typescript-language-service'
@@ -20,12 +22,20 @@ const diagnosticHost: ts.FormatDiagnosticsHost = {
2022
getCanonicalFileName: (path) => path,
2123
}
2224

25+
/**
26+
* An error following the parsing of tsconfig. Kinds of errors include type validations like if include field is an array.
27+
*/
28+
const invalidTsConfig = exceptionType<'invalid_tsconfig', { diagnostics: ts.Diagnostic[] }>(
29+
'invalid_tsconfig',
30+
({ diagnostics }) =>
31+
'Your tsconfig.json is invalid\n\n' + ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost)
32+
)
33+
2334
export async function readOrScaffoldTsconfig(input: {
2435
projectRoot: string
2536
overrides?: { outRoot?: string }
26-
}): Promise<{ content: ts.ParsedCommandLine; path: string }> {
37+
}): Promise<Either<Error, { content: ts.ParsedCommandLine; path: string }>> {
2738
log.trace('start read')
28-
2939
let tsconfigPath = ts.findConfigFile(input.projectRoot, ts.sys.fileExists, 'tsconfig.json')
3040

3141
if (!tsconfigPath) {
@@ -44,12 +54,16 @@ export async function readOrScaffoldTsconfig(input: {
4454
const tscfgReadResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile)
4555

4656
if (tscfgReadResult.error) {
47-
// todo either
48-
log.fatal(
49-
'Unable to read your tsconifg.json\n\n' +
50-
ts.formatDiagnosticsWithColorAndContext([tscfgReadResult.error], diagnosticHost)
57+
return left(
58+
exception(
59+
'Unable to read your tsconifg.json\n\n' +
60+
ts.formatDiagnosticsWithColorAndContext([tscfgReadResult.error], diagnosticHost),
61+
{
62+
// todo leads to circ ref error in json serialize
63+
// diagnostics: [tscfgReadResult.error],
64+
}
65+
)
5166
)
52-
process.exit(1)
5367
}
5468

5569
const tsconfigSource: TsConfigJson = tscfgReadResult.config
@@ -70,9 +84,16 @@ export async function readOrScaffoldTsconfig(input: {
7084
// If the include is present but not array it must mean a mal-formed tsconfig.
7185
// Exit early, if we contintue we will have a runtime error when we try .push on a non-array.
7286
// todo testme once we're not relying on mock process exit
73-
checkNoTsConfigErrors(
74-
ts.parseJsonConfigFileContent(tsconfigSource, ts.sys, projectRoot, undefined, tsconfigPath)
75-
)
87+
const diagnostics = ts.parseJsonConfigFileContent(
88+
tsconfigSource,
89+
ts.sys,
90+
projectRoot,
91+
undefined,
92+
tsconfigPath
93+
).errors
94+
if (diagnostics.length > 0) {
95+
return left(invalidTsConfig({ diagnostics }))
96+
}
7697
}
7798

7899
const tsconfigSourceOriginal = cloneDeep(tsconfigSource)
@@ -210,22 +231,13 @@ export async function readOrScaffoldTsconfig(input: {
210231
* Validate the tsconfig
211232
*/
212233

213-
checkNoTsConfigErrors(tsconfigParsed)
234+
if (tsconfigParsed.errors.length > 0) {
235+
return left(invalidTsConfig({ diagnostics: tsconfigParsed.errors }))
236+
}
214237

215238
log.trace('finished read')
216239

217-
return { content: tsconfigParsed, path: tsconfigPath }
218-
}
219-
220-
function checkNoTsConfigErrors(tsconfig: ts.ParsedCommandLine) {
221-
if (tsconfig.errors.length > 0) {
222-
// Kinds of errors include type validations like if include field is an array.
223-
log.fatal(
224-
'Your tsconfig.json is invalid\n\n' +
225-
ts.formatDiagnosticsWithColorAndContext(tsconfig.errors, diagnosticHost)
226-
)
227-
process.exit(1)
228-
}
240+
return right({ content: tsconfigParsed, path: tsconfigPath })
229241
}
230242

231243
export function tsconfigTemplate(input: { sourceRootRelative: string; outRootRelative: string }): string {

src/lib/plugin/index.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ describe('manifest', () => {
4646
"plugin": Object {
4747
"packageJsonPath": "<project root>/package.json",
4848
},
49-
},
50-
"message": "Failed to read the the package.json file.
49+
"reason": "Failed to read the the package.json file.
5150
5251
Error: Cannot find module '<project root>/package.json' from 'src/lib/plugin/manifest.ts'",
52+
},
53+
"type": "get_manifest_error",
5354
},
5455
}
5556
`)
@@ -66,8 +67,9 @@ describe('manifest', () => {
6667
"plugin": Object {
6768
"packageJsonPath": "<project root>/package.json",
6869
},
70+
"reason": "\`name\` property is missing in the package.json",
6971
},
70-
"message": "\`name\` property is missing in package.json",
72+
"type": "get_manifest_error",
7173
},
7274
}
7375
`)
@@ -85,8 +87,9 @@ describe('manifest', () => {
8587
"plugin": Object {
8688
"packageJsonPath": "<project root>/package.json",
8789
},
90+
"reason": "\`main\` property is missing in the package.json",
8891
},
89-
"message": "\`main\` property is missing in package.json",
92+
"type": "get_manifest_error",
9093
},
9194
}
9295
`)

0 commit comments

Comments
 (0)