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

[WIP] combining coupled PRs into one to see if tests pass #1003

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
42f13b0
Re-add "realpath" to LanguageServiceHost
cspotcode Mar 2, 2020
99cfd15
fix linter failures
cspotcode Mar 2, 2020
18c1c54
fix failure on old ts version
cspotcode Mar 2, 2020
c9addde
Merge remote-tracking branch 'origin/master' into ab/realpath-and-emi…
cspotcode Mar 3, 2020
be77977
normalized filename
sylc Apr 5, 2020
1202b3b
Add failing test
cspotcode Apr 7, 2020
6c45a1b
bump projectVersion every time getScriptFileNames changes
cspotcode Apr 7, 2020
8622dca
tweak issue #884 regression test so it triggers on master
cspotcode Apr 7, 2020
3d9cb55
Simplify while stil reproducing the issue
cspotcode Apr 7, 2020
7eb0c30
getScriptFileNames returns rootFileNames, not everything from memory …
cspotcode Apr 8, 2020
16654a6
Merge remote-tracking branch 'origin/master' into ab/merging-many-things
cspotcode Apr 8, 2020
80ff1ce
Merge branch 'ab/tweak-884-regression-test' into ab/merging-many-things
cspotcode Apr 8, 2020
eb02cc2
Merge branch 'ab/fix-996' into ab/merging-many-things
cspotcode Apr 8, 2020
f7b1419
Merge remote-tracking branch 'origin/ab/realpath-and-emit-node_module…
cspotcode Apr 8, 2020
ca4a5fa
fix type error
cspotcode Apr 8, 2020
a236df9
Switch to normalizing once in our compile() function, so that the res…
cspotcode Apr 8, 2020
05b2b0d
Merge branch 'scau_fix_windows' into ab/merging-many-things
cspotcode Apr 8, 2020
1aaa2e1
add failing test for #1004
cspotcode Apr 12, 2020
5f28488
Fix #1004
cspotcode Apr 12, 2020
dc1f23e
Merge branch 'ab/resolve-entrypoint-symlink' into ab/merging-many-things
cspotcode Apr 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/bin.ts
Expand Up @@ -7,7 +7,7 @@ import Module = require('module')
import arg = require('arg')
import { diffLines } from 'diff'
import { Script } from 'vm'
import { readFileSync, statSync } from 'fs'
import { readFileSync, statSync, realpathSync } from 'fs'
import { homedir } from 'os'
import { VERSION, TSError, parse, Register, register } from './index'

Expand Down Expand Up @@ -154,6 +154,7 @@ export function main (argv: string[]) {
}

const cwd = dir || process.cwd()
/** Unresolved. May point to a symlink, not realpath. May be missing file extension */
const scriptPath = args._.length ? resolve(cwd, args._[0]) : undefined
const state = new EvalState(scriptPath || join(cwd, EVAL_FILENAME))

Expand Down Expand Up @@ -251,7 +252,21 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) {
throw new TypeError('Script mode cannot be combined with `--dir`')
}

return dirname(scriptPath)
// Use node's own resolution behavior to ensure we follow symlinks
// This may affect which tsconfig we discover
// This happens before we are registered, so tell node's resolver to consider .ts and tsx files
// TODO in extremely rare cases, if a foo.js and foo.ts both exist, we may follow the wrong one,
// because we are not obeying `--prefer-ts-exts`
const hadTsExt = hasOwnProperty(require.extensions, '.ts') // tslint:disable-line
const hadTsxExt = hasOwnProperty(require.extensions, '.tsx') // tslint:disable-line
try {
if(!hadTsExt) require.extensions['.ts'] = function() {} // tslint:disable-line
if(!hadTsxExt) require.extensions['.tsx'] = function() {} // tslint:disable-line
return dirname(require.resolve(scriptPath))
} finally {
if(!hadTsExt) delete require.extensions['.ts'] // tslint:disable-line
if(!hadTsxExt) delete require.extensions['.tsx'] // tslint:disable-line
}
}

return dir
Expand Down Expand Up @@ -481,6 +496,11 @@ function isRecoverable (error: TSError) {
return error.diagnosticCodes.every(code => RECOVERY_CODES.has(code))
}

/** Safe `hasOwnProperty` */
function hasOwnProperty (object: any, property: string): boolean {
return Object.prototype.hasOwnProperty.call(object, property)
}

if (require.main === module) {
main(process.argv.slice(2))
}
15 changes: 15 additions & 0 deletions src/index.spec.ts
Expand Up @@ -379,6 +379,14 @@ describe('ts-node', function () {
expect(err).to.equal(null)
expect(stdout).to.equal('.ts\n')

return done()
})
})
it('should read tsconfig relative to realpath, not symlink, in scriptMode', function (done) {
exec(`node ${BIN_SCRIPT_PATH} tests/main-realpath/symlink/symlink.tsx`, function (err, stdout) {
expect(err).to.equal(null)
expect(stdout).to.equal('')

return done()
})
})
Expand Down Expand Up @@ -467,6 +475,13 @@ describe('ts-node', function () {
})
})
})

it('should transpile files inside a node_modules directory when not ignored', function (done) {
exec(`${cmd} --skip-ignore tests/from-node-modules`, function (err, stdout) {
if (err) return done('Unexpected error')
done()
})
})
})

describe('register', function () {
Expand Down
78 changes: 64 additions & 14 deletions src/index.ts
Expand Up @@ -73,6 +73,21 @@ export interface TSCommon {
formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext
}

/**
* Compiler APIs we use that are marked internal and not included in TypeScript's public API declarations
*/
interface TSInternal {
// https://github.com/microsoft/TypeScript/blob/4a34294908bed6701dcba2456ca7ac5eafe0ddff/src/compiler/core.ts#L1906-L1909
createGetCanonicalFileName (useCaseSensitiveFileNames: boolean): TSInternal.GetCanonicalFileName

// https://github.com/microsoft/TypeScript/blob/4a34294908bed6701dcba2456ca7ac5eafe0ddff/src/compiler/core.ts#L1430-L1445
toFileNameLowerCase (x: string): string
}
namespace TSInternal {
// https://github.com/microsoft/TypeScript/blob/4a34294908bed6701dcba2456ca7ac5eafe0ddff/src/compiler/core.ts#L1906
export type GetCanonicalFileName = (fileName: string) => string
}

/**
* Export the current version.
*/
Expand Down Expand Up @@ -453,13 +468,13 @@ export function create (rawOptions: CreateOptions = {}): Register {
// Use full language services when the fast option is disabled.
if (!transpileOnly) {
const fileContents = new Map<string, string>()
const rootFileNames = config.fileNames.slice()
const rootFileNames = new Set(config.fileNames)
const cachedReadFile = cachedLookup(debugFn('readFile', readFile))

// Use language services by default (TODO: invert next major version).
if (!options.compilerHost) {
let projectVersion = 1
const fileVersions = new Map(rootFileNames.map(fileName => [fileName, 0]))
const fileVersions = new Map(Array.from(rootFileNames).map(fileName => [fileName, 0]))

const getCustomTransformers = () => {
if (typeof transformers === 'function') {
Expand All @@ -471,9 +486,9 @@ export function create (rawOptions: CreateOptions = {}): Register {
}

// Create the compiler host for type checking.
const serviceHost: _ts.LanguageServiceHost = {
const serviceHost: _ts.LanguageServiceHost & Required<Pick<_ts.LanguageServiceHost, 'fileExists' | 'readFile'>> = {
getProjectVersion: () => String(projectVersion),
getScriptFileNames: () => Array.from(fileVersions.keys()),
getScriptFileNames: () => Array.from(rootFileNames),
getScriptVersion: (fileName: string) => {
const version = fileVersions.get(fileName)
return version ? version.toString() : ''
Expand All @@ -497,21 +512,55 @@ export function create (rawOptions: CreateOptions = {}): Register {
getDirectories: cachedLookup(debugFn('getDirectories', ts.sys.getDirectories)),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(debugFn('directoryExists', ts.sys.directoryExists)),
realpath: ts.sys.realpath ? cachedLookup(debugFn('realpath', ts.sys.realpath)) : undefined,
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
getCompilationSettings: () => config.options,
getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options),
getCustomTransformers: getCustomTransformers
getCustomTransformers: getCustomTransformers,
/*
* NOTE:
* Older ts versions do not pass `redirectedReference` nor `options`.
* We must pass `redirectedReference` to newer ts versions, but cannot rely on `options`
*/
resolveModuleNames (moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: _ts.ResolvedProjectReference | undefined, options: _ts.CompilerOptions): (_ts.ResolvedModule | undefined)[] {
return moduleNames.map(moduleName => {
const { resolvedModule } = ts.resolveModuleName(moduleName, containingFile, config.options, serviceHost, moduleResolutionCache, redirectedReference)
if (resolvedModule) fixupResolvedModule(resolvedModule)
return resolvedModule
})
},
getResolvedModuleWithFailedLookupLocationsFromCache (moduleName, containingFile): _ts.ResolvedModuleWithFailedLookupLocations | undefined {
const ret = ts.resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache)
if (ret && ret.resolvedModule) {
fixupResolvedModule(ret.resolvedModule)
}
return ret
}
}
/**
* If we need to emit JS for a file, force TS to consider it non-external
*/
const fixupResolvedModule = (resolvedModule: _ts.ResolvedModule) => {
const canonical = getCanonicalFileName(resolvedModule.resolvedFileName)
if (Array.from(rootFileNames).some(rootFileName => canonical === getCanonicalFileName(rootFileName))) {
resolvedModule.isExternalLibraryImport = false
}
}
const getCanonicalFileName = (ts as unknown as TSInternal).createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames)
const moduleResolutionCache = ts.createModuleResolutionCache(cwd, (s) => getCanonicalFileName(s), config.options)

const registry = ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, cwd)
const service = ts.createLanguageService(serviceHost, registry)

const updateMemoryCache = (contents: string, fileName: string) => {
// Add to `rootFiles` when discovered for the first time.
if (!fileVersions.has(fileName)) {
rootFileNames.push(fileName)
// Add to `rootFiles` if not already there
// This is necessary to force TS to emit output
if (!rootFileNames.has(fileName)) {
rootFileNames.add(fileName)
// Increment project version for every change to rootFileNames.
projectVersion++
}

const previousVersion = fileVersions.get(fileName) || 0
Expand Down Expand Up @@ -613,14 +662,14 @@ export function create (rawOptions: CreateOptions = {}): Register {
// Fallback for older TypeScript releases without incremental API.
let builderProgram = ts.createIncrementalProgram
? ts.createIncrementalProgram({
rootNames: rootFileNames.slice(),
rootNames: Array.from(rootFileNames),
options: config.options,
host: host,
configFileParsingDiagnostics: config.errors,
projectReferences: config.projectReferences
})
: ts.createEmitAndSemanticDiagnosticsBuilderProgram(
rootFileNames.slice(),
Array.from(rootFileNames),
config.options,
host,
undefined,
Expand All @@ -641,13 +690,13 @@ export function create (rawOptions: CreateOptions = {}): Register {

// Add to `rootFiles` when discovered by compiler for the first time.
if (sourceFile === undefined) {
rootFileNames.push(fileName)
rootFileNames.add(fileName)
}

// Update program when file changes.
if (sourceFile === undefined || sourceFile.text !== contents) {
builderProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
rootFileNames.slice(),
Array.from(rootFileNames),
config.options,
host,
builderProgram,
Expand Down Expand Up @@ -756,8 +805,9 @@ export function create (rawOptions: CreateOptions = {}): Register {

// Create a simple TypeScript compiler proxy.
function compile (code: string, fileName: string, lineOffset = 0) {
const [value, sourceMap] = getOutput(code, fileName, lineOffset)
const output = updateOutput(value, fileName, sourceMap, getExtension)
const normalizedFileName = normalizeSlashes(fileName)
const [value, sourceMap] = getOutput(code, normalizedFileName, lineOffset)
const output = updateOutput(value, normalizedFileName, sourceMap, getExtension)
outputCache.set(fileName, output)
return output
}
Expand Down
7 changes: 7 additions & 0 deletions tests/issue-884/index-2.ts
@@ -0,0 +1,7 @@
export {};

const timeout = setTimeout(() => {}, 0);

if (timeout.unref) {
timeout.unref();
}
8 changes: 3 additions & 5 deletions tests/issue-884/index.ts
@@ -1,5 +1,3 @@
const timeout = setTimeout(() => {}, 0);

if (timeout.unref) {
timeout.unref();
}
// 2x index files required so that memory cache is populated with all build-in lib and @types
// declarations *before* this require() call.
require('./index-2');
1 change: 1 addition & 0 deletions tests/main-realpath/symlink/symlink.tsx
1 change: 1 addition & 0 deletions tests/main-realpath/symlink/tsconfig.json
@@ -0,0 +1 @@
this tsconfig is intentionally invalid, to confirm that ts-node does *not* attempt to parse it
4 changes: 4 additions & 0 deletions tests/main-realpath/target/target.tsx
@@ -0,0 +1,4 @@
// Will throw a compiler error unless ./tsconfig.json is parsed, which enables JSX
function foo() {
<div></div>
}
5 changes: 5 additions & 0 deletions tests/main-realpath/target/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "react"
}
}