diff --git a/.gitignore b/.gitignore index 3f3e5f72df46..8c68326f2f3b 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 3bc2c8d7bb91..644acf11b958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - `[jest-config]` Support config files exporting (`async`) `function`s ([#10001](https://github.com/facebook/jest/pull/10001)) - `[jest-cli, jest-core]` Add `--selectProjects` CLI argument to filter test suites by project name ([#8612](https://github.com/facebook/jest/pull/8612)) - `[jest-cli, jest-init]` Add `coverageProvider` to `jest --init` prompts ([#10044](https://github.com/facebook/jest/pull/10044)) +- `[*]` Add support for NODE_PRESERVE_SYMLINKS and --preserve-symlinks behavior ### Fixes diff --git a/e2e/__tests__/preserveSymlinks.ts b/e2e/__tests__/preserveSymlinks.ts new file mode 100644 index 000000000000..542c37225b77 --- /dev/null +++ b/e2e/__tests__/preserveSymlinks.ts @@ -0,0 +1,95 @@ +/** + * 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'; + +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('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 20c567f680d5..d706255fb085 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 @@ -78,8 +80,13 @@ function spawnJest( 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..a39800c4abd6 --- /dev/null +++ b/e2e/symlinked-source-dir/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/packages/jest-config/src/Defaults.ts b/packages/jest-config/src/Defaults.ts index f10ec309c835..5f8f247a3d07 100644 --- a/packages/jest-config/src/Defaults.ts +++ b/packages/jest-config/src/Defaults.ts @@ -7,6 +7,7 @@ import type {Config} from '@jest/types'; import {replacePathSepForRegex} from 'jest-regex-util'; +import {shouldPreserveSymlinks} from 'jest-util'; import {NODE_MODULES} from './constants'; import getCacheDirectory from './getCacheDirectory'; @@ -66,7 +67,7 @@ const defaultOptions: Config.DefaultOptions = { useStderr: false, watch: false, watchPathIgnorePatterns: [], - watchman: true, + watchman: !shouldPreserveSymlinks(), }; export default defaultOptions; diff --git a/packages/jest-haste-map/src/crawlers/node.ts b/packages/jest-haste-map/src/crawlers/node.ts index 0266816d91fb..19e6a4699e15 100644 --- a/packages/jest-haste-map/src/crawlers/node.ts +++ b/packages/jest-haste-map/src/crawlers/node.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import {spawn} from 'child_process'; import * as fs from 'graceful-fs'; +import {shouldPreserveSymlinks} from 'jest-util'; import which = require('which'); import H from '../constants'; import * as fastPath from '../lib/fast_path'; @@ -18,6 +19,8 @@ import type { InternalHasteMap, } from '../types'; +const preserveSymlinks = shouldPreserveSymlinks(); + type Result = Array<[/* id */ string, /* mtime */ number, /* size */ number]>; type Callback = (result: Result) => void; @@ -67,7 +70,7 @@ function find( } if (typeof entry !== 'string') { - if (entry.isSymbolicLink()) { + if (!preserveSymlinks && entry.isSymbolicLink()) { return; } @@ -84,7 +87,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 { @@ -121,7 +124,13 @@ function findNative( 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('('); } diff --git a/packages/jest-util/src/index.ts b/packages/jest-util/src/index.ts index 40d200b1930d..53f73e475b92 100644 --- a/packages/jest-util/src/index.ts +++ b/packages/jest-util/src/index.ts @@ -23,5 +23,6 @@ import * as preRunMessage from './preRunMessage'; export {default as pluralize} from './pluralize'; export {default as formatTime} from './formatTime'; export {default as tryRealpath} from './tryRealpath'; +export {default as shouldPreserveSymlinks} from './shouldPreserveSymlinks'; export {preRunMessage, specialChars}; diff --git a/packages/jest-util/src/shouldPreserveSymlinks.ts b/packages/jest-util/src/shouldPreserveSymlinks.ts new file mode 100644 index 000000000000..b18e01953b92 --- /dev/null +++ b/packages/jest-util/src/shouldPreserveSymlinks.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. + */ + +export default (): boolean => + Boolean(process.env.NODE_PRESERVE_SYMLINKS) || + process.execArgv.includes('--preserve-symlinks'); diff --git a/packages/jest-util/src/tryRealpath.ts b/packages/jest-util/src/tryRealpath.ts index ff14e377a963..5ba69259a07b 100644 --- a/packages/jest-util/src/tryRealpath.ts +++ b/packages/jest-util/src/tryRealpath.ts @@ -7,8 +7,14 @@ import {realpathSync} from 'graceful-fs'; import type {Config} from '@jest/types'; +import shouldPreserveSymlinks from './shouldPreserveSymlinks'; + +const preserveSymlinks = shouldPreserveSymlinks(); export default function tryRealpath(path: Config.Path): Config.Path { + if (preserveSymlinks) { + return path; + } try { path = realpathSync.native(path); } catch (error) {