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

#1202 supplemental: add --no-implicit-compiler-options which disables loading of implicit @tsconfig/bases #1249

Closed
wants to merge 12 commits into from
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
4 changes: 4 additions & 0 deletions src/bin.ts
Expand Up @@ -45,6 +45,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
'--compiler-host': Boolean,
'--pretty': Boolean,
'--skip-project': Boolean,
'--no-implicit-compiler-options': Boolean,
'--skip-ignore': Boolean,
'--prefer-ts-exts': Boolean,
'--log-error': Boolean,
Expand Down Expand Up @@ -90,6 +91,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
'--files': files,
'--compiler': compiler,
'--compiler-options': compilerOptions,
'--no-implicit-compiler-options': noImplicitCompilerOptions,
'--project': project,
'--ignore-diagnostics': ignoreDiagnostics,
'--ignore': ignore,
Expand Down Expand Up @@ -132,6 +134,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
--files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup
--pretty Use pretty diagnostic formatter (usually enabled by default)
--skip-project Skip reading \`tsconfig.json\`
--no-implicit-compiler-options Do not use a default \`tsconfig.json\` from @tsconfig/bases matching your node version.
--skip-ignore Skip \`--ignore\` checks
--prefer-ts-exts Prefer importing TypeScript files over JavaScript files
--log-error Logs TypeScript errors to stderr instead of throwing exceptions
Expand Down Expand Up @@ -172,6 +175,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
compiler,
ignoreDiagnostics,
compilerOptions,
noImplicitCompilerOptions,
require: argsRequire,
readFile: code !== undefined ? evalAwarePartialHost.readFile : undefined,
fileExists: code !== undefined ? evalAwarePartialHost.fileExists : undefined
Expand Down
11 changes: 10 additions & 1 deletion src/index.spec.ts
Expand Up @@ -6,7 +6,7 @@ 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 } from 'fs'
import * as promisify from 'util.promisify'
import { sync as rimrafSync } from 'rimraf'
import type _createRequire from 'create-require'
Expand Down Expand Up @@ -538,6 +538,15 @@ test.suite('ts-node', (test) => {
})
})

test('should use implicit @tsconfig/bases config when one is not loaded from disk', async (t) => {
const tempDir = mkdtempSync('ts-node-spec')
// const fixtureDir = join(TEST_DIR, 'implicit-tsconfig')
// copyFileSync(join(fixtureDir, 'script.ts'), join(tempDir, 'script.ts'))
const { err, stdout, stderr } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir })
expect(err).to.equal(null)
expect(stdout).to.equal('10n\n')
})

test.suite('compiler host', (test) => {
test('should execute cli', async () => {
const { err, stdout } = await exec(`${cmd} --compiler-host hello-world`)
Expand Down
74 changes: 54 additions & 20 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 @@ -262,6 +264,15 @@ export interface CreateOptions {
* @default false
*/
skipProject?: boolean
/**
* Do not apply compiler options from the @tsconfig/bases configuration matching your node version
*
* This option has no effect if a tsconfig.json is loaded, because loading a tsconfig.json disables
* implicit compiler options.
*
* @default false
*/
noImplicitCompilerOptions?: boolean
/**
* Skip ignore check, so that compilation will be attempted for all files with matching extensions.
*
Expand Down Expand Up @@ -320,6 +331,7 @@ export interface TsConfigOptions extends Omit<RegisterOptions,
| 'readFile'
| 'fileExists'
| 'skipProject'
| 'noImplicitCompilerOptions'
| 'project'
| 'dir'
| 'cwd'
Expand Down Expand Up @@ -373,7 +385,8 @@ export const DEFAULTS: RegisterOptions = {
typeCheck: yn(env.TS_NODE_TYPE_CHECK),
compilerHost: yn(env.TS_NODE_COMPILER_HOST),
logError: yn(env.TS_NODE_LOG_ERROR),
experimentalEsmLoader: false
experimentalEsmLoader: false,
noImplicitCompilerOptions: false
}

/**
Expand Down Expand Up @@ -521,10 +534,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 +1180,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 +1229,7 @@ function readConfig (
return {
configFilePath,
config: { errors: [result.error], fileNames: [], options: {} },
options: {}
tsNodeOptionsFromTsconfig: {}
}
}

Expand All @@ -1216,22 +1239,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 || (rawApiOptions.noImplicitCompilerOptions ?? DEFAULTS.noImplicitCompilerOptions) // 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 +1276,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))
)
}
}