diff --git a/src/configuration.ts b/src/configuration.ts index b9bc9da5b..8acff0503 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -10,7 +10,7 @@ import { import type { TSInternal } from './ts-compiler-types'; import { createTsInternals } from './ts-internals'; import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; -import { assign, createRequire, trace } from './util'; +import { assign, createRequire } from './util'; /** * TypeScript compiler option values required by `ts-node` which cannot be overridden. @@ -94,6 +94,7 @@ export function readConfig( readFile = ts.sys.readFile, skipProject = DEFAULTS.skipProject, project = DEFAULTS.project, + tsTrace = DEFAULTS.tsTrace, } = rawApiOptions; // Read project configuration when available. @@ -137,7 +138,7 @@ export function readConfig( readDirectory: ts.sys.readDirectory, readFile, useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, - trace, + trace: tsTrace, }, bp, errors, diff --git a/src/index.ts b/src/index.ts index 1fc07cd88..b3506a4cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -353,6 +353,12 @@ export interface CreateOptions { * the configuration loader, so it is *not* necessary for their source to be set here. */ optionBasePaths?: OptionBasePaths; + /** + * A function to collect trace messages from the TypeScript compiler, for example when `traceResolution` is enabled. + * + * @default console.log + */ + tsTrace?: (str: string) => void; } /** @internal */ @@ -389,6 +395,7 @@ export interface TsConfigOptions | 'cwd' | 'projectSearchDir' | 'optionBasePaths' + | 'tsTrace' > {} /** @@ -424,6 +431,7 @@ export const DEFAULTS: RegisterOptions = { compilerHost: yn(env.TS_NODE_COMPILER_HOST), logError: yn(env.TS_NODE_LOG_ERROR), experimentalReplAwait: yn(env.TS_NODE_EXPERIMENTAL_REPL_AWAIT) ?? undefined, + tsTrace: console.log.bind(console), }; /** @@ -883,6 +891,7 @@ export function create(rawOptions: CreateOptions = {}): Service { getCompilationSettings: () => config.options, getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options), getCustomTransformers: getCustomTransformers, + trace: options.tsTrace, }; const { resolveModuleNames, @@ -891,7 +900,7 @@ export function create(rawOptions: CreateOptions = {}): Service { isFileKnownToBeInternal, markBucketOfFilenameInternal, } = createResolverFunctions({ - serviceHost, + host: serviceHost, getCanonicalFileName, ts, cwd, @@ -1036,13 +1045,14 @@ export function create(rawOptions: CreateOptions = {}): Service { ), useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, }; + host.trace = options.tsTrace; const { resolveModuleNames, resolveTypeReferenceDirectives, isFileKnownToBeInternal, markBucketOfFilenameInternal, } = createResolverFunctions({ - serviceHost: host, + host, cwd, configFilePath, config, @@ -1057,7 +1067,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ? ts.createIncrementalProgram({ rootNames: Array.from(rootFileNames), options: config.options, - host: host, + host, configFileParsingDiagnostics: config.errors, projectReferences: config.projectReferences, }) diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index e76528155..df7db9aab 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -7,14 +7,14 @@ import type * as _ts from 'typescript'; */ export function createResolverFunctions(kwargs: { ts: typeof _ts; - serviceHost: _ts.ModuleResolutionHost; + host: _ts.ModuleResolutionHost; cwd: string; getCanonicalFileName: (filename: string) => string; config: _ts.ParsedCommandLine; configFilePath: string | undefined; }) { const { - serviceHost, + host, ts, config, cwd, @@ -93,7 +93,7 @@ export function createResolverFunctions(kwargs: { moduleName, containingFile, config.options, - serviceHost, + host, moduleResolutionCache, redirectedReference ); @@ -132,7 +132,7 @@ export function createResolverFunctions(kwargs: { typeDirectiveName, containingFile, config.options, - serviceHost, + host, redirectedReference ); if (typeDirectiveName === 'node' && !resolvedTypeReferenceDirective) { @@ -157,7 +157,7 @@ export function createResolverFunctions(kwargs: { ...config.options, typeRoots, }, - serviceHost, + host, redirectedReference )); } diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 7b613db66..3b128afdc 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -331,6 +331,26 @@ test.suite('ts-node', (test) => { }); } + test.suite('should support `traceResolution` compiler option', (test) => { + test('prints traces before running code when enabled', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} --compiler-options="{ \\"traceResolution\\": true }" -e "console.log('ok')"` + ); + expect(err).toBeNull(); + expect(stdout).toContain('======== Resolving module'); + expect(stdout.endsWith('ok\n')).toBe(true); + }); + + test('does NOT print traces when not enabled', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} -e "console.log('ok')"` + ); + expect(err).toBeNull(); + expect(stdout).not.toContain('======== Resolving module'); + expect(stdout.endsWith('ok\n')).toBe(true); + }); + }); + if (semver.gte(process.version, '12.16.0')) { test('swc transpiler supports native ESM emit', async () => { const { err, stdout } = await exec( diff --git a/src/test/repl/helpers.ts b/src/test/repl/helpers.ts index cc4edfcf7..310b8e221 100644 --- a/src/test/repl/helpers.ts +++ b/src/test/repl/helpers.ts @@ -45,6 +45,7 @@ export async function contextReplHelpers( ...replService.evalAwarePartialHost, project: `${TEST_DIR}/tsconfig.json`, ...createServiceOpts, + tsTrace: replService.console.log.bind(replService.console), }); replService.setService(service); t.teardown(async () => { diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 2ef41d15d..8e1ceab04 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -4,12 +4,14 @@ import * as expect from 'expect'; import { CMD_TS_NODE_WITH_PROJECT_FLAG, contextTsNodeUnderTest, + getStream, TEST_DIR, } from '../helpers'; import { createExec, createExecTester } from '../exec-helpers'; import { upstreamTopLevelAwaitTests } from './node-repl-tla'; import { _test } from '../testlib'; import { contextReplHelpers } from './helpers'; +import { promisify } from 'util'; const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); @@ -412,6 +414,47 @@ test.suite( } ); +test.suite('REPL works with traceResolution', (test) => { + test.serial( + 'startup traces should print before the prompt appears when traceResolution is enabled', + async (t) => { + const repl = t.context.createReplViaApi({ + registerHooks: false as true, + createServiceOpts: { + compilerOptions: { + traceResolution: true, + }, + }, + }); + + repl.replService.start(); + + repl.stdin.end(); + + await promisify(setTimeout)(3e3); + + repl.stdout.end(); + const stdout = await getStream(repl.stdout); + + expect(stdout).toContain('======== Resolving module'); + expect(stdout.endsWith('> ')).toBe(true); + } + ); + + test.serial( + 'traces should NOT appear when traceResolution is not enabled', + async (t) => { + const { stdout, stderr } = await t.context.executeInRepl('1', { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: '1\n>', + }); + expect(stderr).toBe(''); + expect(stdout).not.toContain('======== Resolving module'); + } + ); +}); + test.serial('REPL declares types for node built-ins within REPL', async (t) => { const { stdout, stderr } = await t.context.executeInRepl( `util.promisify(setTimeout)("should not be a string" as string) diff --git a/src/util.ts b/src/util.ts index 23e19bba2..da6cc53be 100644 --- a/src/util.ts +++ b/src/util.ts @@ -90,9 +90,3 @@ export function cachedLookup(fn: (arg: T) => R): (arg: T) => R { return cache.get(arg)!; }; } - -/** - * We do not support ts's `trace` option yet. In the meantime, rather than omit - * `trace` options in hosts, I am using this placeholder. - */ -export function trace(s: string): void {}