diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 99e92b21f959..55ab6ddfaf26 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -420,6 +420,13 @@ export const options = { 'Will not fail if no tests are found (for example while using `--testPathPattern`.)', type: 'boolean', }, + preserveSymlinks: { + default: false, + description: + 'Find symlinked files and do not expand their file paths during' + + 'module resolution', + type: 'boolean', + }, preset: { description: "A preset that is used as a base for Jest's configuration.", type: 'string', diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index b1832b94af6a..22ddb8a4e35a 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -176,6 +176,7 @@ const groupOptions = ( modulePathIgnorePatterns: options.modulePathIgnorePatterns, modulePaths: options.modulePaths, name: options.name, + preserveSymlinks: options.preserveSymlinks, prettierPath: options.prettierPath, resetMocks: options.resetMocks, resetModules: options.resetModules, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index ef6886ce2a3b..af74696cc148 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -648,6 +648,7 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'onlyChanged': case 'outputFile': case 'passWithNoTests': + case 'preserveSymlinks': case 'replname': case 'reporters': case 'resetMocks': diff --git a/packages/jest-haste-map/src/crawlers/node.js b/packages/jest-haste-map/src/crawlers/node.js index 53f4194df54a..c53506cb3cd0 100644 --- a/packages/jest-haste-map/src/crawlers/node.js +++ b/packages/jest-haste-map/src/crawlers/node.js @@ -78,9 +78,13 @@ function findNative( roots: Array, extensions: Array, ignore: IgnoreMatcher, + preserveSymlinks: boolean, callback: Callback, ): void { const args = [].concat(roots); + if (preserveSymlinks) { + args.unshift('-L'); + } args.push('-type', 'f'); if (extensions.length) { args.push('('); @@ -137,6 +141,7 @@ module.exports = function nodeCrawl( extensions, forceNodeFilesystemAPI, ignore, + preserveSymlinks, rootDir, roots, } = options; @@ -163,7 +168,7 @@ module.exports = function nodeCrawl( if (forceNodeFilesystemAPI || process.platform === 'win32') { find(roots, extensions, ignore, callback); } else { - findNative(roots, extensions, ignore, callback); + findNative(roots, extensions, ignore, preserveSymlinks, callback); } }); }; diff --git a/packages/jest-haste-map/src/index.js b/packages/jest-haste-map/src/index.js index ef5392efd5fe..53d35f4d4c26 100644 --- a/packages/jest-haste-map/src/index.js +++ b/packages/jest-haste-map/src/index.js @@ -62,6 +62,7 @@ type Options = { mocksPattern?: string, name: string, platforms: Array, + preserveSymlinks: boolean, providesModuleNodeModules?: Array, resetCache?: boolean, retainAllFiles: boolean, @@ -86,6 +87,7 @@ type InternalOptions = { mocksPattern: ?RegExp, name: string, platforms: Array, + preserveSymlinks: boolean, resetCache: ?boolean, retainAllFiles: boolean, rootDir: string, @@ -246,12 +248,16 @@ class HasteMap extends EventEmitter { : null, name: options.name, platforms: options.platforms, + preserveSymlinks: options.preserveSymlinks, resetCache: options.resetCache, retainAllFiles: options.retainAllFiles, rootDir: options.rootDir, roots: Array.from(new Set(options.roots)), throwOnModuleCollision: !!options.throwOnModuleCollision, - useWatchman: options.useWatchman == null ? true : options.useWatchman, + // Watchman can not handle symlinks: https://github.com/facebook/watchman/issues/105 + useWatchman: options.preserveSymlinks == true + ? false + : options.useWatchman == null ? true : options.useWatchman, watch: !!options.watch, }; this._console = options.console || global.console; @@ -705,6 +711,7 @@ class HasteMap extends EventEmitter { forceNodeFilesystemAPI: options.forceNodeFilesystemAPI, ignore, mapper: options.mapper, + preserveSymlinks: options.preserveSymlinks, rootDir: options.rootDir, roots: options.roots, }).catch(e => { @@ -726,6 +733,7 @@ class HasteMap extends EventEmitter { extensions: options.extensions, forceNodeFilesystemAPI: options.forceNodeFilesystemAPI, ignore, + preserveSymlinks: options.preserveSymlinks, rootDir: options.rootDir, roots: options.roots, }).catch(retry); diff --git a/packages/jest-haste-map/src/types.js b/packages/jest-haste-map/src/types.js index 8ee12a279fb5..1e6af41825ad 100644 --- a/packages/jest-haste-map/src/types.js +++ b/packages/jest-haste-map/src/types.js @@ -35,6 +35,7 @@ export type CrawlerOptions = {| forceNodeFilesystemAPI: boolean, ignore: IgnoreMatcher, mapper?: ?Mapper, + preserveSymlinks: boolean, rootDir: string, roots: Array, |}; diff --git a/packages/jest-resolve/src/defaultResolver.js b/packages/jest-resolve/src/defaultResolver.js index 02f311c22ece..fa625145c852 100644 --- a/packages/jest-resolve/src/defaultResolver.js +++ b/packages/jest-resolve/src/defaultResolver.js @@ -22,6 +22,7 @@ type ResolverOptions = {| extensions?: Array, moduleDirectory?: Array, paths?: ?Array, + preserveSymlinks: boolean, rootDir: ?Path, |}; @@ -36,6 +37,7 @@ export default function defaultResolver( extensions: options.extensions, moduleDirectory: options.moduleDirectory, paths: options.paths, + preserveSymlinks: options.preserveSymlinks, rootDir: options.rootDir, }); } @@ -50,7 +52,7 @@ function resolveSync(target: Path, options: ResolverOptions): Path { if (REGEX_RELATIVE_IMPORT.test(target)) { // resolve relative import const resolveTarget = path.resolve(basedir, target); - const result = tryResolve(resolveTarget); + const result = tryResolve(resolveTarget, options.preserveSymlinks); if (result) { return result; } @@ -62,7 +64,7 @@ function resolveSync(target: Path, options: ResolverOptions): Path { }); for (let i = 0; i < dirs.length; i++) { const resolveTarget = path.join(dirs[i], target); - const result = tryResolve(resolveTarget); + const result = tryResolve(resolveTarget, options.preserveSymlinks); if (result) { return result; } @@ -82,13 +84,13 @@ function resolveSync(target: Path, options: ResolverOptions): Path { /* * contextual helper functions */ - function tryResolve(name: Path): ?Path { + function tryResolve(name: Path, preserveSymlinks: boolean): ?Path { const dir = path.dirname(name); let result; if (isDirectory(dir)) { - result = resolveAsFile(name) || resolveAsDirectory(name); + result = resolveAsFile(name) || resolveAsDirectory(name, preserveSymlinks); } - if (result) { + if (result && !preserveSymlinks) { // Dereference symlinks to ensure we don't create a separate // module instance depending on how it was referenced. result = fs.realpathSync(result); @@ -111,7 +113,7 @@ function resolveSync(target: Path, options: ResolverOptions): Path { return undefined; } - function resolveAsDirectory(name: Path): ?Path { + function resolveAsDirectory(name: Path, preserveSymlinks: boolean): ?Path { if (!isDirectory(name)) { return undefined; } @@ -125,7 +127,7 @@ function resolveSync(target: Path, options: ResolverOptions): Path { if (pkgmain && !isCurrentDirectory(pkgmain)) { const resolveTarget = path.resolve(name, pkgmain); - const result = tryResolve(resolveTarget); + const result = tryResolve(resolveTarget, preserveSymlinks); if (result) { return result; } diff --git a/packages/jest-resolve/src/index.js b/packages/jest-resolve/src/index.js index cf3ba0d85093..53f374a8419f 100644 --- a/packages/jest-resolve/src/index.js +++ b/packages/jest-resolve/src/index.js @@ -28,6 +28,7 @@ type ResolverConfig = {| moduleNameMapper: ?Array, modulePaths: Array, platforms?: Array, + preserveSymlinks?: boolean, resolver: ?Path, rootDir: ?Path, |}; @@ -38,6 +39,7 @@ type FindNodeModuleConfig = {| extensions?: Array, moduleDirectory?: Array, paths?: Array, + preserveSymlinks?: boolean, resolver?: ?Path, rootDir?: ?Path, |}; @@ -80,6 +82,7 @@ class Resolver { moduleNameMapper: options.moduleNameMapper, modulePaths: options.modulePaths, platforms: options.platforms, + preserveSymlinks: options.preserveSymlinks, resolver: options.resolver, rootDir: options.rootDir, }; @@ -106,6 +109,7 @@ class Resolver { extensions: options.extensions, moduleDirectory: options.moduleDirectory, paths: paths ? (nodePaths || []).concat(paths) : nodePaths, + preserveSymlinks: options.preserveSymlinks, rootDir: options.rootDir, }); } catch (e) {} @@ -161,6 +165,7 @@ class Resolver { extensions, moduleDirectory, paths, + preserveSymlinks: this._options.preserveSymlinks, resolver: this._options.resolver, rootDir: this._options.rootDir, }); @@ -380,6 +385,7 @@ class Resolver { extensions, moduleDirectory, paths, + preserveSymlinks: this._options.preserveSymlinks, resolver, rootDir: this._options.rootDir, }); diff --git a/packages/jest-resolve/src/nodeModulesPaths.js b/packages/jest-resolve/src/nodeModulesPaths.js index 84cb68a2c3fb..3040e964c021 100644 --- a/packages/jest-resolve/src/nodeModulesPaths.js +++ b/packages/jest-resolve/src/nodeModulesPaths.js @@ -16,6 +16,7 @@ import {sync as realpath} from 'realpath-native'; type NodeModulesPathsOptions = {| moduleDirectory?: Array, paths?: ?Array, + preserveSymlinks?: boolean, |}; export default function nodeModulesPaths( @@ -42,7 +43,11 @@ export default function nodeModulesPaths( // traverses parents of the physical path, not the symlinked path let physicalBasedir; try { - physicalBasedir = realpath(basedirAbs); + if (!options.preserveSymlinks) { + physicalBasedir = realpath(basedirAbs); + } else { + physicalBasedir = basedirAbs; + } } catch (err) { // realpath can throw, e.g. on mapped drives physicalBasedir = basedirAbs; diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index f91e481a2925..3d25dd9c9622 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -266,6 +266,7 @@ class Runtime { moduleNameMapper: getModuleNameMapper(config), modulePaths: config.modulePaths, platforms: config.haste.platforms, + preserveSymlinks: config.preserveSymlinks, resolver: config.resolver, rootDir: config.rootDir, }); diff --git a/packages/jest-runtime/src/script_transformer.js b/packages/jest-runtime/src/script_transformer.js index a25683156afd..b901c4ed4873 100644 --- a/packages/jest-runtime/src/script_transformer.js +++ b/packages/jest-runtime/src/script_transformer.js @@ -54,6 +54,8 @@ const projectCaches: WeakMap = new WeakMap(); // To reset the cache for specific changesets (rather than package version). const CACHE_VERSION = '1'; +const preserveSymlinks = true; + export default class ScriptTransformer { static EVAL_RESULT_VARIABLE: string; _cache: ProjectCache; @@ -197,7 +199,7 @@ export default class ScriptTransformer { } transformSource(filepath: Path, content: string, instrument: boolean) { - const filename = this._getRealPath(filepath); + const filename = preserveSymlinks ? filepath : this._getRealPath(filepath); const transform = this._getTransformer(filename); const cacheFilePath = this._getFileCachePath(filename, content, instrument); let sourceMapPath = cacheFilePath + '.map'; diff --git a/types/Argv.js b/types/Argv.js index 966c82327dbd..f4d420ef3598 100644 --- a/types/Argv.js +++ b/types/Argv.js @@ -60,6 +60,7 @@ export type Argv = {| notifyMode: string, onlyChanged: boolean, outputFile: string, + preserveSymlinks: boolean, preset: ?string, projects: Array, replname: ?string, diff --git a/types/Config.js b/types/Config.js index 39eac782b764..78ef6cdcd8a0 100644 --- a/types/Config.js +++ b/types/Config.js @@ -268,6 +268,7 @@ export type ProjectConfig = {| modulePathIgnorePatterns: Array, modulePaths: Array, name: string, + preserveSymlinks: boolean, prettierPath: string, resetMocks: boolean, resetModules: boolean,