diff --git a/.gitignore b/.gitignore index 36a8de27bd81..3304980b27f3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /examples/*/node_modules/ /examples/mongodb/globalConfig.json +/e2e/preserve-symlinks/* /e2e/*/node_modules /e2e/*/.pnp /e2e/*/.pnp.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b9b4387f3f..ff39d4b2c1e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[*]` Respect NODE_PRESERVE_SYMLINKS environment variable and --preserve-symlinks flag when resolving file paths ([#9732](https://github.com/facebook/jest/pull/9732)) + ### Fixes - `[jest-jasmine2]` Don't run `beforeAll` / `afterAll` in skipped describe block ([#9931](https://github.com/facebook/jest/pull/9931)) diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index ed248a26f289..d65fa16fcb5d 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -36,7 +36,7 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:561:17) at Object.require (index.js:10:1) `; @@ -65,6 +65,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:561:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 062640b2406a..2eb8081795f7 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ FAIL __tests__/test.js | ^ 9 | - at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:299:11) + at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:315:11) at Object.require (index.js:8:18) `; diff --git a/e2e/__tests__/preserveSymlinks.ts b/e2e/__tests__/preserveSymlinks.ts new file mode 100644 index 000000000000..2d293aff754f --- /dev/null +++ b/e2e/__tests__/preserveSymlinks.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {join, resolve} from 'path'; +import { + existsSync, + mkdirSync, + rmdirSync, + symlinkSync, + unlinkSync, +} from 'graceful-fs'; + +import runJest from '../runJest'; +import {extractSummary} from '../Utils'; +import HasteMap = require('jest-haste-map'); + +const destRoot = resolve(__dirname, '../preserve-symlinks'); +const srcRoot = resolve(__dirname, '../symlinked-source-dir'); + +const files = [ + 'package.json', + 'a.js', + 'b.js', + 'ab.js', + '__tests__/a.test.js', + '__tests__/b.test.js', + '__tests__/ab.test.js', +]; + +function cleanup() { + files + .map(f => join(destRoot, f)) + .filter(f => existsSync(f)) + .forEach(f => { + unlinkSync(f); + }); + if (existsSync(join(destRoot, '__tests__'))) { + rmdirSync(join(destRoot, '__tests__')); + } + if (existsSync(destRoot)) { + rmdirSync(destRoot); + } +} + +beforeAll(() => { + cleanup(); + mkdirSync(destRoot); + mkdirSync(join(destRoot, '__tests__')); + files.forEach(f => { + symlinkSync(join(srcRoot, f), join(destRoot, f)); + }); +}); + +afterAll(() => { + cleanup(); +}); + +test('preserving symlinks with environment variable', () => { + const {stderr, exitCode} = runJest('preserve-symlinks', ['--no-watchman'], { + preserveSymlinks: '1', + }); + const {summary, rest} = extractSummary(stderr); + expect(exitCode).toEqual(0); + expect(rest.split('\n').length).toEqual(3); + expect(rest).toMatch('PASS __tests__/ab.test.js'); + expect(rest).toMatch('PASS __tests__/a.test.js'); + expect(rest).toMatch('PASS __tests__/b.test.js'); + expect(summary).toMatch('Test Suites: 3 passed, 3 total'); + expect(summary).toMatch('Tests: 3 passed, 3 total'); + expect(summary).toMatch('Snapshots: 0 total'); +}); + +test('preserving symlinks with --preserve-symlinks node flag', () => { + const {stderr, exitCode} = runJest('preserve-symlinks', ['--no-watchman'], { + nodeFlags: ['--preserve-symlinks'], + }); + const {summary, rest} = extractSummary(stderr); + expect(exitCode).toEqual(0); + expect(rest.split('\n').length).toEqual(3); + expect(rest).toMatch('PASS __tests__/ab.test.js'); + expect(rest).toMatch('PASS __tests__/a.test.js'); + expect(rest).toMatch('PASS __tests__/b.test.js'); + expect(summary).toMatch('Test Suites: 3 passed, 3 total'); + expect(summary).toMatch('Tests: 3 passed, 3 total'); + expect(summary).toMatch('Snapshots: 0 total'); +}); + +test('hasteMap finds symlinks correctly', async () => { + const options = { + extensions: ['js'], + forceNodeFilesystemAPI: true, + maxWorkers: 2, + mocksPattern: '', + name: 'tmp', + platforms: [], + preserveSymlinks: true, + retainAllFiles: true, + rootDir: resolve(__dirname, '../preserve-symlinks'), + roots: [resolve(__dirname, '../preserve-symlinks')], + useWatchman: false, + watch: false, + }; + const hasteMap = new HasteMap(options); + const result = await hasteMap.build(); + const files = result.hasteFS + .getAllFiles() + .map(f => f.split('preserve-symlinks').pop()); + + const expectedFiles = [ + join('/__tests__', 'a.test.js'), + join('/__tests__', 'ab.test.js'), + join('/__tests__', 'b.test.js'), + join('/', 'a.js'), + join('/', 'ab.js'), + join('/', 'b.js'), + ]; + + expectedFiles.forEach(f => { + expect(files).toContain(f); + }); +}); + +test('no preserve symlinks configuration', () => { + const {exitCode, stdout} = runJest('preserve-symlinks', ['--no-watchman']); + expect(exitCode).toEqual(1); + expect(stdout).toMatch('No tests found, exiting with code 1'); +}); diff --git a/e2e/runJest.ts b/e2e/runJest.ts index 28eda5785672..6811febbf237 100644 --- a/e2e/runJest.ts +++ b/e2e/runJest.ts @@ -18,6 +18,8 @@ import {normalizeIcons} from './Utils'; const JEST_PATH = path.resolve(__dirname, '../packages/jest-cli/bin/jest.js'); type RunJestOptions = { + preserveSymlinks?: string; + nodeFlags?: Array; nodeOptions?: string; nodePath?: string; skipPkgJsonCheck?: boolean; // don't complain if can't find package.json @@ -74,11 +76,15 @@ function spawnJest( ); } const env = Object.assign({}, process.env, {FORCE_COLOR: '0'}); - if (options.nodeOptions) env['NODE_OPTIONS'] = options.nodeOptions; if (options.nodePath) env['NODE_PATH'] = options.nodePath; + if (options.preserveSymlinks) + env['NODE_PRESERVE_SYMLINKS'] = options.preserveSymlinks; const spawnArgs = [JEST_PATH, ...args]; + if (options.nodeFlags) { + spawnArgs.unshift(...options.nodeFlags); + } const spawnOptions = { cwd: dir, env, diff --git a/e2e/symlinked-source-dir/__tests__/a.test.js b/e2e/symlinked-source-dir/__tests__/a.test.js new file mode 100644 index 000000000000..7ae15d959838 --- /dev/null +++ b/e2e/symlinked-source-dir/__tests__/a.test.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +const a = require('../a'); + +test('a', () => { + expect(a()).toEqual('a'); +}); diff --git a/e2e/symlinked-source-dir/__tests__/ab.test.js b/e2e/symlinked-source-dir/__tests__/ab.test.js new file mode 100644 index 000000000000..b312f6d57c34 --- /dev/null +++ b/e2e/symlinked-source-dir/__tests__/ab.test.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +const ab = require('../ab'); + +test('ab', () => { + expect(ab()).toEqual('ab'); +}); diff --git a/e2e/symlinked-source-dir/__tests__/b.test.js b/e2e/symlinked-source-dir/__tests__/b.test.js new file mode 100644 index 000000000000..4c95a97391eb --- /dev/null +++ b/e2e/symlinked-source-dir/__tests__/b.test.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +const b = require('../b'); + +test('b', () => { + expect(b()).toEqual('b'); +}); diff --git a/e2e/symlinked-source-dir/a.js b/e2e/symlinked-source-dir/a.js new file mode 100644 index 000000000000..c3e6f1d5f6cb --- /dev/null +++ b/e2e/symlinked-source-dir/a.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +module.exports = function a() { + return 'a'; +}; diff --git a/e2e/symlinked-source-dir/ab.js b/e2e/symlinked-source-dir/ab.js new file mode 100644 index 000000000000..30b92befb9c2 --- /dev/null +++ b/e2e/symlinked-source-dir/ab.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +const a = require('./a'); +const b = require('./b'); + +module.exports = function ab() { + return a() + b(); +}; diff --git a/e2e/symlinked-source-dir/b.js b/e2e/symlinked-source-dir/b.js new file mode 100644 index 000000000000..503a0820a921 --- /dev/null +++ b/e2e/symlinked-source-dir/b.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +module.exports = function b() { + return 'b'; +}; diff --git a/e2e/symlinked-source-dir/package.json b/e2e/symlinked-source-dir/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/symlinked-source-dir/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 81a534547e9a..0944373c4731 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -25,6 +25,7 @@ "jest-validate": "^25.5.0", "prompts": "^2.0.1", "realpath-native": "^2.0.0", + "should-preserve-links": "^1.0.4", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/packages/jest-cli/should-preserve-links.d.ts b/packages/jest-cli/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-cli/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-cli/src/cli/index.ts b/packages/jest-cli/src/cli/index.ts index 06fb3bf4d31a..6234676fd559 100644 --- a/packages/jest-cli/src/cli/index.ts +++ b/packages/jest-cli/src/cli/index.ts @@ -15,10 +15,16 @@ import {getVersion, runCLI} from '@jest/core'; import chalk = require('chalk'); import exit = require('exit'); import yargs = require('yargs'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; +import shouldPreserveSymlinks from 'should-preserve-links'; import init from '../init'; import * as args from './args'; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + export async function run( maybeArgv?: Array, project?: Config.Path, diff --git a/packages/jest-cli/src/init/index.ts b/packages/jest-cli/src/init/index.ts index b2503fc901f3..4e45f1016eb9 100644 --- a/packages/jest-cli/src/init/index.ts +++ b/packages/jest-cli/src/init/index.ts @@ -9,8 +9,9 @@ import * as path from 'path'; import * as fs from 'graceful-fs'; import chalk = require('chalk'); import prompts = require('prompts'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import {constants} from 'jest-config'; +import shouldPreserveSymlinks from 'should-preserve-links'; import defaultQuestions, {testScriptQuestion} from './questions'; import {MalformedPackageJsonError, NotFoundPackageJsonError} from './errors'; import generateConfigFile from './generate_config_file'; @@ -32,6 +33,11 @@ type PromptsResults = { scripts: boolean; }; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + const getConfigFilename = (ext: string) => JEST_CONFIG_BASE_NAME + ext; export default async ( diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index d18735426d05..db2b723a751f 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -35,7 +35,8 @@ "jest-validate": "^25.5.0", "micromatch": "^4.0.2", "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" + "realpath-native": "^2.0.0", + "should-preserve-links": "^1.0.4" }, "devDependencies": { "@types/babel__core": "^7.0.4", diff --git a/packages/jest-config/should-preserve-links.d.ts b/packages/jest-config/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-config/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-config/src/getCacheDirectory.ts b/packages/jest-config/src/getCacheDirectory.ts index 2871d1e5eaab..c42909c8b7f2 100644 --- a/packages/jest-config/src/getCacheDirectory.ts +++ b/packages/jest-config/src/getCacheDirectory.ts @@ -7,7 +7,14 @@ import * as path from 'path'; import {tmpdir} from 'os'; -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; + +import shouldPreserveSymlinks from 'should-preserve-links'; + +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} const getCacheDirectory = () => { const {getuid} = process; diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 0331c2d23379..7b4b895f23af 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -9,7 +9,8 @@ import * as path from 'path'; import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; import chalk = require('chalk'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; +import shouldPreserveSymlinks from 'should-preserve-links'; import {isJSONString, replaceRootDirInPath} from './utils'; import normalize from './normalize'; import resolveConfigPath from './resolveConfigPath'; @@ -23,6 +24,11 @@ export {default as descriptions} from './Descriptions'; import * as constants from './constants'; export {constants}; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + type ReadConfig = { configPath: Config.Path | null | undefined; globalConfig: Config.GlobalConfig; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 4cff07d83bc8..564031d0b5a6 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -14,10 +14,11 @@ import {ValidationError, validate} from 'jest-validate'; import {clearLine, replacePathSepForGlob} from 'jest-util'; import chalk = require('chalk'); import micromatch = require('micromatch'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import Resolver = require('jest-resolve'); import {replacePathSepForRegex} from 'jest-regex-util'; import merge = require('deepmerge'); +import shouldPreserveSymlinks from 'should-preserve-links'; import validatePattern from './validatePattern'; import getMaxWorkers from './getMaxWorkers'; import { @@ -45,6 +46,11 @@ const PRESET_NAME = 'jest-preset'; type AllOptions = Config.ProjectConfig & Config.GlobalConfig; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + const createConfigError = (message: string) => new ValidationError(ERROR, message, DOCUMENTATION_NOTE); diff --git a/packages/jest-core/package.json b/packages/jest-core/package.json index 3e049195ccb6..284f818e196a 100644 --- a/packages/jest-core/package.json +++ b/packages/jest-core/package.json @@ -38,6 +38,7 @@ "p-each-series": "^2.1.0", "realpath-native": "^2.0.0", "rimraf": "^3.0.0", + "should-preserve-links": "^1.0.4", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, diff --git a/packages/jest-core/should-preserve-links.d.ts b/packages/jest-core/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-core/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index bea930899c0d..676142907785 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import chalk = require('chalk'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import {CustomConsole} from '@jest/console'; import {interopRequireDefault} from 'jest-util'; import exit = require('exit'); @@ -23,6 +23,7 @@ import { } from '@jest/test-result'; import type TestSequencer from '@jest/test-sequencer'; import type {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files'; +import shouldPreserveSymlinks from 'should-preserve-links'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; import runGlobalHook from './runGlobalHook'; import SearchSource from './SearchSource'; @@ -32,6 +33,11 @@ import collectNodeHandles from './collectHandles'; import type TestWatcher from './TestWatcher'; import type {Filter, TestRunData} from './types'; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + const getTestPaths = async ( globalConfig: Config.GlobalConfig, source: SearchSource, diff --git a/packages/jest-haste-map/package.json b/packages/jest-haste-map/package.json index 7b6deebf45df..1e91b9874c63 100644 --- a/packages/jest-haste-map/package.json +++ b/packages/jest-haste-map/package.json @@ -27,6 +27,7 @@ "jest-worker": "^25.5.0", "micromatch": "^4.0.2", "sane": "^4.0.3", + "should-preserve-links": "^1.0.4", "walker": "^1.0.7", "which": "^2.0.2" }, diff --git a/packages/jest-haste-map/should-preserve-links.d.ts b/packages/jest-haste-map/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-haste-map/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-haste-map/src/crawlers/node.ts b/packages/jest-haste-map/src/crawlers/node.ts index 0266816d91fb..2b47ac25d6e9 100644 --- a/packages/jest-haste-map/src/crawlers/node.ts +++ b/packages/jest-haste-map/src/crawlers/node.ts @@ -41,6 +41,7 @@ function find( roots: Array, extensions: Array, ignore: IgnoreMatcher, + preserveSymlinks: boolean, callback: Callback, ): void { const result: Result = []; @@ -67,7 +68,7 @@ function find( } if (typeof entry !== 'string') { - if (entry.isSymbolicLink()) { + if (!preserveSymlinks && entry.isSymbolicLink()) { return; } @@ -84,7 +85,7 @@ function find( // This logic is unnecessary for node > v10.10, but leaving it in // since we need it for backwards-compatibility still. - if (!err && stat && !stat.isSymbolicLink()) { + if (!err && stat && (preserveSymlinks || !stat.isSymbolicLink())) { if (stat.isDirectory()) { search(file); } else { @@ -118,10 +119,17 @@ function findNative( roots: Array, extensions: Array, ignore: IgnoreMatcher, + preserveSymlinks: boolean, callback: Callback, ): void { const args = Array.from(roots); - args.push('-type', 'f'); + if (preserveSymlinks) { + // follow symlinks to determine file type + args.unshift('-L'); + args.push('( -not -type d )'); + } else { + args.push('-type', 'f'); + } if (extensions.length) { args.push('('); } @@ -181,6 +189,7 @@ export = async function nodeCrawl( extensions, forceNodeFilesystemAPI, ignore, + preserveSymlinks, rootDir, roots, } = options; @@ -212,9 +221,9 @@ export = async function nodeCrawl( }; if (useNativeFind) { - findNative(roots, extensions, ignore, callback); + findNative(roots, extensions, ignore, preserveSymlinks, callback); } else { - find(roots, extensions, ignore, callback); + find(roots, extensions, ignore, preserveSymlinks, callback); } }); }; diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index 866a06a1b140..6a81fd2ed773 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -15,6 +15,7 @@ import {NodeWatcher, Watcher as SaneWatcher} from 'sane'; import type {Config} from '@jest/types'; import serializer from 'jest-serializer'; import Worker from 'jest-worker'; +import shouldPreserveSymlinks from 'should-preserve-links'; import {getSha1, worker} from './worker'; import getMockName from './getMockName'; import getPlatformExtension from './lib/getPlatformExtension'; @@ -45,6 +46,8 @@ import type { WorkerMetadata, } from './types'; +const preserveSymlinks = shouldPreserveSymlinks(); + type HType = typeof H; type Options = { @@ -61,6 +64,7 @@ type Options = { mocksPattern?: string; name: string; platforms: Array; + preserveSymlinks?: boolean; providesModuleNodeModules?: Array; resetCache?: boolean; retainAllFiles: boolean; @@ -85,6 +89,7 @@ type InternalOptions = { mocksPattern: RegExp | null; name: string; platforms: Array; + preserveSymlinks: boolean; resetCache?: boolean; retainAllFiles: boolean; rootDir: string; @@ -264,6 +269,7 @@ class HasteMap extends EventEmitter { : null, name: options.name, platforms: options.platforms, + preserveSymlinks: options.preserveSymlinks || preserveSymlinks, resetCache: options.resetCache, retainAllFiles: options.retainAllFiles, rootDir: options.rootDir, @@ -749,6 +755,7 @@ class HasteMap extends EventEmitter { extensions: options.extensions, forceNodeFilesystemAPI: options.forceNodeFilesystemAPI, ignore, + preserveSymlinks: options.preserveSymlinks, rootDir: options.rootDir, roots: options.roots, }; diff --git a/packages/jest-haste-map/src/types.ts b/packages/jest-haste-map/src/types.ts index 6d09b1fdc182..6b0c90f56e2b 100644 --- a/packages/jest-haste-map/src/types.ts +++ b/packages/jest-haste-map/src/types.ts @@ -35,6 +35,7 @@ export type CrawlerOptions = { forceNodeFilesystemAPI: boolean; ignore: IgnoreMatcher; rootDir: string; + preserveSymlinks: boolean; roots: Array; }; diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 4ad1b61c8f33..182f0c6108de 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -25,6 +25,7 @@ "read-pkg-up": "^7.0.1", "realpath-native": "^2.0.0", "resolve": "^1.17.0", + "should-preserve-links": "^1.0.4", "slash": "^3.0.0" }, "devDependencies": { diff --git a/packages/jest-resolve/should-preserve-links.d.ts b/packages/jest-resolve/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-resolve/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index e66a74036a3f..3af4cb9ee832 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -8,10 +8,17 @@ import * as fs from 'graceful-fs'; import {sync as resolveSync} from 'resolve'; import {sync as browserResolve} from 'browser-resolve'; -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import pnpResolver from 'jest-pnp-resolver'; import type {Config} from '@jest/types'; +import shouldPreserveSymlinks from 'should-preserve-links'; + +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + type ResolverOptions = { basedir: Config.Path; browser?: boolean; @@ -40,7 +47,7 @@ export default function defaultResolver( isFile, moduleDirectory: options.moduleDirectory, paths: options.paths, - preserveSymlinks: false, + preserveSymlinks, // @ts-ignore: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44137 realpathSync, }); diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 19937efafd7b..14dedecfa7ec 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -8,8 +8,9 @@ import * as path from 'path'; import type {Config} from '@jest/types'; import type {ModuleMap} from 'jest-haste-map'; -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import chalk = require('chalk'); +import shouldPreserveSymlinks from 'should-preserve-links'; import nodeModulesPaths from './nodeModulesPaths'; import isBuiltinModule from './isBuiltinModule'; import defaultResolver, {clearDefaultResolverCache} from './defaultResolver'; @@ -17,6 +18,11 @@ import type {ResolverConfig} from './types'; import ModuleNotFoundError from './ModuleNotFoundError'; import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm'; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + type FindNodeModuleConfig = { basedir: Config.Path; browser?: boolean; diff --git a/packages/jest-resolve/src/nodeModulesPaths.ts b/packages/jest-resolve/src/nodeModulesPaths.ts index dddf52229a13..542a03061f66 100644 --- a/packages/jest-resolve/src/nodeModulesPaths.ts +++ b/packages/jest-resolve/src/nodeModulesPaths.ts @@ -9,13 +9,20 @@ import * as path from 'path'; import type {Config} from '@jest/types'; -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; type NodeModulesPathsOptions = { moduleDirectory?: Array; paths?: Array; }; +import shouldPreserveSymlinks from 'should-preserve-links'; + +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + export default function nodeModulesPaths( basedir: Config.Path, options: NodeModulesPathsOptions, diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index d145df1016b5..666973adadbb 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -40,6 +40,7 @@ "jest-util": "^25.5.0", "jest-validate": "^25.5.0", "realpath-native": "^2.0.0", + "should-preserve-links": "^1.0.4", "slash": "^3.0.0", "strip-bom": "^4.0.0", "yargs": "^15.3.1" diff --git a/packages/jest-runtime/should-preserve-links.d.ts b/packages/jest-runtime/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-runtime/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-runtime/src/cli/index.ts b/packages/jest-runtime/src/cli/index.ts index 099373e2f67c..d360bf829bc0 100644 --- a/packages/jest-runtime/src/cli/index.ts +++ b/packages/jest-runtime/src/cli/index.ts @@ -8,7 +8,7 @@ import {cpus} from 'os'; import * as path from 'path'; import chalk = require('chalk'); -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import yargs = require('yargs'); import type {Config} from '@jest/types'; import type {JestEnvironment} from '@jest/environment'; @@ -16,10 +16,16 @@ import {CustomConsole} from '@jest/console'; import {setGlobal} from 'jest-util'; import {validateCLIOptions} from 'jest-validate'; import {deprecationEntries, readConfig} from 'jest-config'; +import shouldPreserveSymlinks from 'should-preserve-links'; import {VERSION} from '../version'; import type {Context} from '../types'; import * as args from './args'; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + export async function run( cliArgv?: Config.Argv, cliInfo?: Array, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 32c1c751b7f1..e171221bed3a 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -43,6 +43,7 @@ import { import type {V8CoverageResult} from '@jest/test-result'; import {CoverageInstrumenter, V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; +import shouldPreserveSymlinks from 'should-preserve-links'; import {run as cliRun} from './cli'; import {options as cliOptions} from './cli/args'; import {findSiblingsWithFileExtension} from './helpers'; @@ -86,6 +87,8 @@ type ResolveOptions = Parameters[1]; type StringMap = Map; type BooleanMap = Map; +const preserveSymlinks = shouldPreserveSymlinks(); + const fromEntries: typeof Object.fromEntries = Object.fromEntries ?? function fromEntries(iterable: Iterable<[string, T]>) { @@ -287,6 +290,7 @@ class Runtime { mocksPattern: escapePathForRegex(path.sep + '__mocks__' + path.sep), name: config.name, platforms: config.haste.platforms || ['ios', 'android'], + preserveSymlinks, providesModuleNodeModules: config.haste.providesModuleNodeModules, resetCache: options && options.resetCache, retainAllFiles: false, diff --git a/packages/jest-transform/package.json b/packages/jest-transform/package.json index a4bb71051a1d..89ac9b6b045c 100644 --- a/packages/jest-transform/package.json +++ b/packages/jest-transform/package.json @@ -30,6 +30,7 @@ "micromatch": "^4.0.2", "pirates": "^4.0.1", "realpath-native": "^2.0.0", + "should-preserve-links": "^1.0.4", "slash": "^3.0.0", "source-map": "^0.6.1", "write-file-atomic": "^3.0.0" diff --git a/packages/jest-transform/should-preserve-links.d.ts b/packages/jest-transform/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-transform/should-preserve-links.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'should-preserve-links' { + export default function shouldPreserveLinks(): boolean; +} diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index 46ae3afb049f..cfb382296b86 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -18,8 +18,9 @@ import HasteMap = require('jest-haste-map'); import stableStringify = require('fast-json-stable-stringify'); import slash = require('slash'); import {sync as writeFileAtomic} from 'write-file-atomic'; -import {sync as realpath} from 'realpath-native'; +import {sync as _realpath} from 'realpath-native'; import {addHook} from 'pirates'; +import shouldPreserveSymlinks from 'should-preserve-links'; import type { Options, TransformResult, @@ -29,6 +30,11 @@ import type { import shouldInstrument from './shouldInstrument'; import handlePotentialSyntaxError from './enhanceUnexpectedTokenMessage'; +const preserveSymlinks = shouldPreserveSymlinks(); +function realpath(p: string) { + return preserveSymlinks ? p : _realpath(p); +} + type ProjectCache = { configString: string; ignorePatternsRegExp?: RegExp; diff --git a/yarn.lock b/yarn.lock index 253db2b1ef97..4b6b277dab98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13165,6 +13165,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +should-preserve-links@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/should-preserve-links/-/should-preserve-links-1.0.4.tgz#7b2a0b44efb9f0edd94332a06af92d84cc988876" + integrity sha512-73GxeFbAj4PAj/q4lHvui1hd1X6TWRgG41znftUvwQa+lNRz+X73od8Z5N4CyD2xCFE2PDMUvIfkYSyhwJaI4A== + side-channel@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"