From 3b5b9c2eb7c853cc0a685cd59dafe72934c19038 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 26 Feb 2021 20:27:13 -0500 Subject: [PATCH] Implement #1202: default @tsconfig/bases (#1236) * 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 --- node10/tsconfig.json | 3 ++ node12/tsconfig.json | 3 ++ node14/tsconfig.json | 3 ++ package-lock.json | 15 ++++++ package.json | 13 ++++- src/index.spec.ts | 54 +++++++++++++++++++- src/index.ts | 61 ++++++++++++++++------- src/tsconfigs.ts | 33 ++++++++++++ tests/tsconfig-bases/node10/tsconfig.json | 3 ++ tests/tsconfig-bases/node12/tsconfig.json | 3 ++ tests/tsconfig-bases/node14/tsconfig.json | 3 ++ 11 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 node10/tsconfig.json create mode 100644 node12/tsconfig.json create mode 100644 node14/tsconfig.json create mode 100644 src/tsconfigs.ts create mode 100644 tests/tsconfig-bases/node10/tsconfig.json create mode 100644 tests/tsconfig-bases/node12/tsconfig.json create mode 100644 tests/tsconfig-bases/node14/tsconfig.json diff --git a/node10/tsconfig.json b/node10/tsconfig.json new file mode 100644 index 000000000..7079f3adb --- /dev/null +++ b/node10/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node10/tsconfig.json" +} diff --git a/node12/tsconfig.json b/node12/tsconfig.json new file mode 100644 index 000000000..76603f1cd --- /dev/null +++ b/node12/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", +} diff --git a/node14/tsconfig.json b/node14/tsconfig.json new file mode 100644 index 000000000..b08285106 --- /dev/null +++ b/node14/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json" +} diff --git a/package-lock.json b/package-lock.json index 3050c4e6e..849393ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -394,6 +394,21 @@ "defer-to-connect": "^1.0.1" } }, + "@tsconfig/node10": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.7.tgz", + "integrity": "sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==" + }, + "@tsconfig/node12": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.7.tgz", + "integrity": "sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==" + }, + "@tsconfig/node14": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.0.tgz", + "integrity": "sha512-RKkL8eTdPv6t5EHgFKIVQgsDapugbuOptNd9OOunN/HAkzmmTnZELx1kNCK0rSdUYGmiFMM3rRQMAWiyp023LQ==" + }, "@types/chai": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.10.tgz", diff --git a/package.json b/package.json index 5d1c68402..0ff87b8a4 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", @@ -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", diff --git a/src/index.spec.ts b/src/index.spec.ts index d7db0f5ed..e6ea53eff 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -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' @@ -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) => { @@ -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`) diff --git a/src/index.ts b/src/index.ts index 0866d293d..f9fb36593 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 @@ -131,6 +132,7 @@ export interface TSCommon { parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent formatDiagnostics: typeof _ts.formatDiagnostics formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext + libs?: string[] } /** @@ -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({}, DEFAULTS, tsconfigOptions || {}, rawOptions) + const { configFilePath, config, tsNodeOptionsFromTsconfig } = readConfig(cwd, ts, rawOptions) + const options = assign({}, DEFAULTS, tsNodeOptionsFromTsconfig || {}, rawOptions) options.require = [ - ...tsconfigOptions.require || [], + ...tsNodeOptionsFromTsconfig.require || [], ...rawOptions.require || [] ] @@ -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) { @@ -1206,7 +1218,7 @@ function readConfig ( return { configFilePath, config: { errors: [result.error], fileNames: [], options: {} }, - options: {} + tsNodeOptionsFromTsconfig: {} } } @@ -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 ) @@ -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 } } /** diff --git a/src/tsconfigs.ts b/src/tsconfigs.ts new file mode 100644 index 000000000..2ce944a4d --- /dev/null +++ b/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)) + ) + } +} diff --git a/tests/tsconfig-bases/node10/tsconfig.json b/tests/tsconfig-bases/node10/tsconfig.json new file mode 100644 index 000000000..f8b881e4c --- /dev/null +++ b/tests/tsconfig-bases/node10/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "ts-node/node10/tsconfig.json" +} diff --git a/tests/tsconfig-bases/node12/tsconfig.json b/tests/tsconfig-bases/node12/tsconfig.json new file mode 100644 index 000000000..eda168e10 --- /dev/null +++ b/tests/tsconfig-bases/node12/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "ts-node/node12/tsconfig.json" +} diff --git a/tests/tsconfig-bases/node14/tsconfig.json b/tests/tsconfig-bases/node14/tsconfig.json new file mode 100644 index 000000000..8a496b8a8 --- /dev/null +++ b/tests/tsconfig-bases/node14/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "ts-node/node14/tsconfig.json" +}