Skip to content

Commit

Permalink
Implement #1202: default @tsconfig/bases (#1236)
Browse files Browse the repository at this point in the history
* Implementation

* fix

* fix lint

* fix

* fix

* cleanup

* fallback to older @tsconfig/node* config when we detect an incompatibility with the lib or target options

* lint fix

* WIP

* Add CLI and programmatic option to disable implicit compiler options

* Remove --no-implicit-compiler-options flag and programmatic option; it is implemented in another PR

* add tests

* fix tests

* fix tests

* fix tests
  • Loading branch information
cspotcode committed Feb 27, 2021
1 parent 78af045 commit 3b5b9c2
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 22 deletions.
3 changes: 3 additions & 0 deletions node10/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "@tsconfig/node10/tsconfig.json"
}
3 changes: 3 additions & 0 deletions node12/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "@tsconfig/node12/tsconfig.json",
}
3 changes: 3 additions & 0 deletions node14/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "@tsconfig/node14/tsconfig.json"
}
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions package.json
Expand Up @@ -22,7 +22,10 @@
"./esm": "./esm.mjs",
"./esm.mjs": "./esm.mjs",
"./esm/transpile-only": "./esm/transpile-only.mjs",
"./esm/transpile-only.mjs": "./esm/transpile-only.mjs"
"./esm/transpile-only.mjs": "./esm/transpile-only.mjs",
"./node10/tsconfig.json": "./node10/tsconfig.json",
"./node12/tsconfig.json": "./node12/tsconfig.json",
"./node14/tsconfig.json": "./node14/tsconfig.json"
},
"types": "dist/index.d.ts",
"bin": {
Expand All @@ -40,7 +43,10 @@
"esm.mjs",
"LICENSE",
"tsconfig.schema.json",
"tsconfig.schemastore-schema.json"
"tsconfig.schemastore-schema.json",
"node10/",
"node12/",
"node14/"
],
"scripts": {
"lint": "tslint \"src/**/*.ts\" --project tsconfig.json",
Expand Down Expand Up @@ -131,6 +137,9 @@
"typescript": ">=2.7"
},
"dependencies": {
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
Expand Down
54 changes: 53 additions & 1 deletion src/index.spec.ts
Expand Up @@ -2,11 +2,12 @@ import { test, TestInterface } from './testlib'
import { expect } from 'chai'
import { ChildProcess, exec as childProcessExec, ExecException, ExecOptions } from 'child_process'
import { join, resolve, sep as pathSep } from 'path'
import { tmpdir } from 'os'
import semver = require('semver')
import ts = require('typescript')
import proxyquire = require('proxyquire')
import type * as tsNodeTypes from './index'
import { unlinkSync, existsSync, lstatSync } from 'fs'
import { unlinkSync, existsSync, lstatSync, mkdtempSync, fstat, copyFileSync, writeFileSync } from 'fs'
import * as promisify from 'util.promisify'
import { sync as rimrafSync } from 'rimraf'
import type _createRequire from 'create-require'
Expand Down Expand Up @@ -97,6 +98,10 @@ test.suite('ts-node', (test) => {
testsDirRequire.resolve('ts-node/esm.mjs')
testsDirRequire.resolve('ts-node/esm/transpile-only')
testsDirRequire.resolve('ts-node/esm/transpile-only.mjs')

testsDirRequire.resolve('ts-node/node10/tsconfig.json')
testsDirRequire.resolve('ts-node/node12/tsconfig.json')
testsDirRequire.resolve('ts-node/node14/tsconfig.json')
})

test.suite('cli', (test) => {
Expand Down Expand Up @@ -538,6 +543,53 @@ test.suite('ts-node', (test) => {
})
})

test.suite('should use implicit @tsconfig/bases config when one is not loaded from disk', _test => {
const test = _test.context(async t => ({
tempDir: mkdtempSync(join(tmpdir(), 'ts-node-spec'))
}))
if (semver.gte(ts.version, '3.5.0') && semver.gte(process.versions.node, '14.0.0')) {
test('implicitly uses @tsconfig/node14 compilerOptions when both TS and node versions support it', async t => {
const { context: { tempDir } } = t
const { err: err1, stdout: stdout1, stderr: stderr1 } = await exec(`${BIN_PATH} --showConfig`, { cwd: tempDir })
expect(err1).to.equal(null)
t.like(JSON.parse(stdout1), {
compilerOptions: {
target: 'es2020',
lib: ['es2020']
}
})
const { err: err2, stdout: stdout2, stderr: stderr2 } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir })
expect(err2).to.equal(null)
expect(stdout2).to.equal('10n\n')
})
} else {
test('implicitly uses @tsconfig/* lower than node14 (node10 or node12) when either TS or node versions do not support @tsconfig/node14', async ({ context: { tempDir } }) => {
const { err, stdout, stderr } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir })
expect(err).to.not.equal(null)
expect(stderr).to.match(/BigInt literals are not available when targeting lower than|error TS2304: Cannot find name 'n'/)
})
}
})

if (semver.gte(ts.version, '3.2.0')) {
test.suite('should bundle @tsconfig/bases to be used in your own tsconfigs', test => {
const macro = test.macro((nodeVersion: string) => async t => {
const config = require(`@tsconfig/${ nodeVersion }/tsconfig.json`)
const { err, stdout, stderr } = await exec(`${BIN_PATH} --showConfig -e 10n`, { cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion) })
expect(err).to.equal(null)
t.like(JSON.parse(stdout), {
compilerOptions: {
target: config.compilerOptions.target,
lib: config.compilerOptions.lib
}
})
})
test(`ts-node/node10/tsconfig.json`, macro, 'node10')
test(`ts-node/node12/tsconfig.json`, macro, 'node12')
test(`ts-node/node14/tsconfig.json`, macro, 'node14')
})
}

test.suite('compiler host', (test) => {
test('should execute cli', async () => {
const { err, stdout } = await exec(`${cmd} --compiler-host hello-world`)
Expand Down
61 changes: 42 additions & 19 deletions src/index.ts
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'
import type * as _ts from 'typescript'
import { Module, createRequire as nodeCreateRequire, createRequireFromPath as nodeCreateRequireFromPath } from 'module'
import type _createRequire from 'create-require'
import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'

/** @internal */
export const createRequire = nodeCreateRequire ?? nodeCreateRequireFromPath ?? require('create-require') as typeof _createRequire // tslint:disable-line:deprecation
Expand Down Expand Up @@ -131,6 +132,7 @@ export interface TSCommon {
parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent
formatDiagnostics: typeof _ts.formatDiagnostics
formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext
libs?: string[]
}

/**
Expand Down Expand Up @@ -521,10 +523,10 @@ export function create (rawOptions: CreateOptions = {}): Service {
let { compiler, ts } = loadCompiler(compilerName, rawOptions.projectSearchDir ?? rawOptions.project ?? cwd)

// Read config file and merge new options between env and CLI options.
const { configFilePath, config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions)
const options = assign<RegisterOptions>({}, DEFAULTS, tsconfigOptions || {}, rawOptions)
const { configFilePath, config, tsNodeOptionsFromTsconfig } = readConfig(cwd, ts, rawOptions)
const options = assign<RegisterOptions>({}, DEFAULTS, tsNodeOptionsFromTsconfig || {}, rawOptions)
options.require = [
...tsconfigOptions.require || [],
...tsNodeOptionsFromTsconfig.require || [],
...rawOptions.require || []
]

Expand Down Expand Up @@ -1167,30 +1169,40 @@ function fixConfig (ts: TSCommon, config: _ts.ParsedCommandLine) {
/**
* Load TypeScript configuration. Returns the parsed TypeScript config and
* any `ts-node` options specified in the config file.
*
* Even when a tsconfig.json is not loaded, this function still handles merging
* compilerOptions from various sources: API, environment variables, etc.
*/
function readConfig (
cwd: string,
ts: TSCommon,
rawOptions: CreateOptions
rawApiOptions: CreateOptions
): {
// Path of tsconfig file
/**
* Path of tsconfig file if one was loaded
*/
configFilePath: string | undefined,
// Parsed TypeScript configuration.
/**
* Parsed TypeScript configuration with compilerOptions merged from all other sources (env vars, etc)
*/
config: _ts.ParsedCommandLine
// Options pulled from `tsconfig.json`.
options: TsConfigOptions
/**
* ts-node options pulled from `tsconfig.json`, NOT merged with any other sources. Merging must happen outside
* this function.
*/
tsNodeOptionsFromTsconfig: TsConfigOptions
} {
let config: any = { compilerOptions: {} }
let basePath = cwd
let configFilePath: string | undefined = undefined
const projectSearchDir = resolve(cwd, rawOptions.projectSearchDir ?? cwd)
const projectSearchDir = resolve(cwd, rawApiOptions.projectSearchDir ?? cwd)

const {
fileExists = ts.sys.fileExists,
readFile = ts.sys.readFile,
skipProject = DEFAULTS.skipProject,
project = DEFAULTS.project
} = rawOptions
} = rawApiOptions

// Read project configuration when available.
if (!skipProject) {
Expand All @@ -1206,7 +1218,7 @@ function readConfig (
return {
configFilePath,
config: { errors: [result.error], fileNames: [], options: {} },
options: {}
tsNodeOptionsFromTsconfig: {}
}
}

Expand All @@ -1216,22 +1228,33 @@ function readConfig (
}

// Fix ts-node options that come from tsconfig.json
const tsconfigOptions: TsConfigOptions = Object.assign({}, filterRecognizedTsConfigTsNodeOptions(config['ts-node']))
const tsNodeOptionsFromTsconfig: TsConfigOptions = Object.assign({}, filterRecognizedTsConfigTsNodeOptions(config['ts-node']))

// Remove resolution of "files".
const files = rawOptions.files ?? tsconfigOptions.files ?? DEFAULTS.files
const files = rawApiOptions.files ?? tsNodeOptionsFromTsconfig.files ?? DEFAULTS.files
if (!files) {
config.files = []
config.include = []
}

// Override default configuration options `ts-node` requires.
// Only if a config file is *not* loaded, load an implicit configuration from @tsconfig/bases
const skipDefaultCompilerOptions = configFilePath != null // tslint:disable-line
const defaultCompilerOptionsForNodeVersion = skipDefaultCompilerOptions ? undefined : getDefaultTsconfigJsonForNodeVersion(ts).compilerOptions

// Merge compilerOptions from all sources
config.compilerOptions = Object.assign(
{},
// automatically-applied options from @tsconfig/bases
defaultCompilerOptionsForNodeVersion,
// tsconfig.json "compilerOptions"
config.compilerOptions,
// from env var
DEFAULTS.compilerOptions,
tsconfigOptions.compilerOptions,
rawOptions.compilerOptions,
// tsconfig.json "ts-node": "compilerOptions"
tsNodeOptionsFromTsconfig.compilerOptions,
// passed programmatically
rawApiOptions.compilerOptions,
// overrides required by ts-node, cannot be changed
TS_NODE_COMPILER_OPTIONS
)

Expand All @@ -1242,15 +1265,15 @@ function readConfig (
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames
}, basePath, undefined, configFilePath))

if (tsconfigOptions.require) {
if (tsNodeOptionsFromTsconfig.require) {
// Modules are found relative to the tsconfig file, not the `dir` option
const tsconfigRelativeRequire = createRequire(configFilePath!)
tsconfigOptions.require = tsconfigOptions.require.map((path: string) => {
tsNodeOptionsFromTsconfig.require = tsNodeOptionsFromTsconfig.require.map((path: string) => {
return tsconfigRelativeRequire.resolve(path)
})
}

return { configFilePath, config: fixedConfig, options: tsconfigOptions }
return { configFilePath, config: fixedConfig, tsNodeOptionsFromTsconfig }
}

/**
Expand Down
33 changes: 33 additions & 0 deletions src/tsconfigs.ts
@@ -0,0 +1,33 @@
import { TSCommon } from '.'

const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
/**
* return parsed JSON of the bundled @tsconfig/bases config appropriate for the
* running version of nodejs
* @internal
*/
export function getDefaultTsconfigJsonForNodeVersion (ts: TSCommon): any {
if (nodeMajor >= 14) {
const config = require('@tsconfig/node14/tsconfig.json')
if (configCompatible(config)) return config
}
if (nodeMajor >= 12) {
const config = require('@tsconfig/node12/tsconfig.json')
if (configCompatible(config)) return config
}
return require('@tsconfig/node10/tsconfig.json')

// Verify that tsconfig target and lib options are compatible with TypeScript compiler
function configCompatible (config: {
compilerOptions: {
lib: string[],
target: string
}
}) {
return (
typeof (ts.ScriptTarget as any)[config.compilerOptions.target.toUpperCase()] === 'number' &&
ts.libs &&
config.compilerOptions.lib.every(lib => ts.libs!.includes(lib))
)
}
}
3 changes: 3 additions & 0 deletions tests/tsconfig-bases/node10/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "ts-node/node10/tsconfig.json"
}
3 changes: 3 additions & 0 deletions tests/tsconfig-bases/node12/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "ts-node/node12/tsconfig.json"
}
3 changes: 3 additions & 0 deletions tests/tsconfig-bases/node14/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "ts-node/node14/tsconfig.json"
}

0 comments on commit 3b5b9c2

Please sign in to comment.