Skip to content

Commit

Permalink
feat: Add support for Custom tsconfig.json, Resolve extended configs
Browse files Browse the repository at this point in the history
This addresses both oclif/oclif#299 and oclif/oclif#488

`loadTSConfig()` in `src/ts-node.ts` was not properly resolving extended configuration files, because while `parseConfigFileTextToJson` will load and parse a given `tsconfig.json` file, it does not merge compilerOptions from configs specified in the `extends` option.

This implementation is based on the solutions suggested in microsoft/TypeScript#5276
and https://stackoverflow.com/questions/53804566/how-to-get-compileroptions-from-tsconfig-json which rely on Typescript's utilities for loading and parsing `tsconfig.json` files.

Specifically, by adding  a call to `parseJsonConfigFileContent()`,  the merged compilerOptions will be resolved as expected.

Additionally, this adds support for user-defined tsconfig files through the `tsConfig` option under `oclif` key in their `package.json`.
  • Loading branch information
Saeris committed Mar 15, 2021
1 parent 0779d64 commit b52dec1
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 44 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"mocha": "^8.2.1",
"proxyquire": "^2.1.0",
"ts-node": "^9.0.0",
"typescript": "3.8.3"
"typescript": "4.2.3"
},
"engines": {
"node": ">=8.0.0"
Expand Down
6 changes: 3 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,18 @@ export class Config implements IConfig {
s3.templates = {
...s3.templates,
target: {
...s3.templates && s3.templates.target,
baseDir: '<%- bin %>',
unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>",
versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>",
manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>",
...s3.templates && s3.templates.target,
},
vanilla: {
...s3.templates && s3.templates.vanilla,
unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>",
versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>",
baseDir: '<%- bin %>',
manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version",
...s3.templates && s3.templates.vanilla,
},
}

Expand Down Expand Up @@ -327,7 +327,7 @@ export class Config implements IConfig {
return Promise.all((p.hooks[event] || [])
.map(async hook => {
try {
const f = tsPath(p.root, hook)
const f = tsPath(p.root, hook, this.pjson.oclif.tsConfig)
debug('start', f)
const search = (m: any): Hook<T> => {
if (typeof m === 'function') return m
Expand Down
13 changes: 9 additions & 4 deletions src/pjson.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export interface PJSON {
[k: string]: any;
dependencies?: {[name: string]: string};
dependencies?: { [name: string]: string };
oclif: {
schema?: number;
tsConfig?: string;
};
}

Expand All @@ -14,7 +15,7 @@ export namespace PJSON {
schema?: number;
title?: string;
description?: string;
hooks?: { [name: string]: (string | string[]) };
hooks?: { [name: string]: string | string[] };
commands?: string;
plugins?: string[];
devPlugins?: string[];
Expand Down Expand Up @@ -76,10 +77,14 @@ export namespace PJSON {
export interface User extends PJSON {
private?: boolean;
oclif: PJSON['oclif'] & {
plugins?: (string | PluginTypes.User | PluginTypes.Link)[]; };
plugins?: (string | PluginTypes.User | PluginTypes.Link)[];
};
}

export type PluginTypes = PluginTypes.User | PluginTypes.Link | {root: string}
export type PluginTypes =
| PluginTypes.User
| PluginTypes.Link
| { root: string };
export namespace PluginTypes {
export interface User {
type: 'user';
Expand Down
2 changes: 1 addition & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class Plugin implements IPlugin {
}

get commandsDir() {
return tsPath(this.root, this.pjson.oclif.commands)
return tsPath(this.root, this.pjson.oclif.commands, this.pjson.oclif.tsConfig)
}

get commandIDs() {
Expand Down
63 changes: 35 additions & 28 deletions src/ts-node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs'
import * as path from 'path'
import * as TSNode from 'ts-node'
import type * as TS from 'typescript'

import Debug from './debug'
// eslint-disable-next-line new-cap
Expand All @@ -11,46 +12,51 @@ const rootDirs: string[] = []
const typeRoots = [`${__dirname}/../node_modules/@types`]

export interface TSConfig {
compilerOptions: {
rootDir?: string;
rootDirs?: string[];
outDir?: string;
target?: string;
esModuleInterop?: boolean;
experimentalDecorators?: boolean;
emitDecoratorMetadata?: boolean;
};
compilerOptions: TS.CompilerOptions;
}

function loadTSConfig(root: string): TSConfig | undefined {
const tsconfigPath = path.join(root, 'tsconfig.json')
let typescript: typeof import('typescript') | undefined
function loadTSConfig(root: string, configPath = 'tsconfig.json'): TSConfig | undefined {
let typescript: typeof TS | undefined
try {
typescript = require('typescript')
} catch {
try {
typescript = require(root + '/node_modules/typescript')
} catch { }
}

if (fs.existsSync(tsconfigPath) && typescript) {
const tsconfig = typescript.parseConfigFileTextToJson(
tsconfigPath,
fs.readFileSync(tsconfigPath, 'utf8'),
).config
if (!tsconfig || !tsconfig.compilerOptions) {
throw new Error(
`Could not read and parse tsconfig.json at ${tsconfigPath}, or it ` +
if (typescript) {
const {findConfigFile, readConfigFile, parseJsonConfigFileContent, sys} = typescript
const tsconfigPath = findConfigFile(
root,
sys.fileExists,
configPath,
)
if (tsconfigPath) {
// Read the user's raw tsconfig file
const readFile = (path: string): string | undefined => fs.readFileSync(path).toString('utf8')
const tsconfig = readConfigFile(tsconfigPath, readFile).config
// Parse the raw config, resolving any configuration files it extends from
const compilerOptions = parseJsonConfigFileContent(
tsconfig,
sys,
root,
).options
console.log({root, tsconfig, compilerOptions})
if (!tsconfig || !compilerOptions) {
throw new Error(
`Could not read and parse tsconfig.json at ${tsconfigPath}, or it ` +
'did not contain a "compilerOptions" section.')
}
// Return only the combined compiler options
return {compilerOptions}
}
return tsconfig
}
}

function registerTSNode(root: string) {
function registerTSNode(root: string, configPath?: string) {
if (process.env.OCLIF_TS_NODE === '0') return
if (tsconfigs[root]) return
const tsconfig = loadTSConfig(root)
const tsconfig = loadTSConfig(root, configPath)
if (!tsconfig) return
debug('registering ts-node at', root)
const tsNodePath = require.resolve('ts-node', {paths: [root, __dirname]})
Expand All @@ -71,6 +77,7 @@ function registerTSNode(root: string) {
// cache: false,
// typeCheck: true,
compilerOptions: {
...tsconfig.compilerOptions,
esModuleInterop: tsconfig.compilerOptions.esModuleInterop,
target: tsconfig.compilerOptions.target || 'es2017',
experimentalDecorators: tsconfig.compilerOptions.experimentalDecorators || false,
Expand All @@ -92,13 +99,13 @@ function registerTSNode(root: string) {
* this is for developing typescript plugins/CLIs
* if there is a tsconfig and the original sources exist, it attempts to require ts-
*/
export function tsPath(root: string, orig: string): string
export function tsPath(root: string, orig: string | undefined): string | undefined
export function tsPath(root: string, orig: string | undefined): string | undefined {
export function tsPath(root: string, orig: string, configPath?: string): string
export function tsPath(root: string, orig: string | undefined, configPath?: string): string | undefined
export function tsPath(root: string, orig: string | undefined, configPath?: string): string | undefined {
if (!orig) return orig
orig = path.join(root, orig)
try {
registerTSNode(root)
registerTSNode(root, configPath)
const tsconfig = tsconfigs[root]
if (!tsconfig) return orig
const {rootDir, rootDirs, outDir} = tsconfig.compilerOptions
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/typescript/tsconfig.custom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json"
}

1 change: 1 addition & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Config from '../src'
export const fancy = base
.register('resetConfig', () => ({
run(ctx: {config: Config.IConfig}) {
// @ts-expect-error
delete ctx.config
},
}))
Expand Down
7 changes: 6 additions & 1 deletion test/ts-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'path'
import * as proxyquire from 'proxyquire'
import * as tsNode from 'ts-node'

import {TSConfig} from '../src/ts-node'
import {TSConfig, tsPath} from '../src/ts-node'

import {expect, fancy} from './test'

Expand Down Expand Up @@ -40,6 +40,11 @@ describe('tsPath', () => {
expect(result).to.equal(path.join(root, orig))
})

it('should resolve a .ts file using custom config using "extends"', () => {
const result = tsPath(root, orig, 'tsconfig.custom.json')
expect(result).to.equal(path.join(root, orig))
})

withMockTsConfig()
.it('should leave esModuleInterop undefined by default', ctx => {
ctx.tsNodePlugin.tsPath(root, orig)
Expand Down
11 changes: 5 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1706,12 +1706,11 @@ lodash.zip@^4.2.0:
resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020"
integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=

lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.20:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==


[email protected]:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
Expand Down Expand Up @@ -2776,10 +2775,10 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==

typescript@3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
typescript@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==

uglify-js@^3.1.4:
version "3.7.3"
Expand Down

0 comments on commit b52dec1

Please sign in to comment.