Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #1202: default @tsconfig/bases #1236

Merged
merged 17 commits into from Feb 27, 2021
Merged
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"
}