From 185664ce794183ad6df5c6e64fbe0eca0de51915 Mon Sep 17 00:00:00 2001 From: Giancarlo Anemone Date: Tue, 5 May 2020 11:39:09 -0400 Subject: [PATCH] Add support for NODE_PRESERVE_SYMLINKS and --preserve-symlinks behavior --- .gitignore | 1 + CHANGELOG.md | 2 + e2e/__tests__/preserveSymlinks.ts | 95 +++++++++++++++++++ e2e/runJest.ts | 7 ++ e2e/symlinked-source-dir/__tests__/a.test.js | 12 +++ e2e/symlinked-source-dir/__tests__/ab.test.js | 12 +++ e2e/symlinked-source-dir/__tests__/b.test.js | 12 +++ e2e/symlinked-source-dir/a.js | 10 ++ e2e/symlinked-source-dir/ab.js | 13 +++ e2e/symlinked-source-dir/b.js | 10 ++ e2e/symlinked-source-dir/package.json | 5 + packages/jest-config/package.json | 3 +- .../jest-config/should-preserve-links.d.ts | 10 ++ packages/jest-config/src/Defaults.ts | 3 +- packages/jest-haste-map/package.json | 3 +- .../jest-haste-map/should-preserve-links.d.ts | 10 ++ packages/jest-haste-map/src/crawlers/node.ts | 15 ++- packages/jest-util/package.json | 1 + packages/jest-util/should-preserve-links.d.ts | 10 ++ packages/jest-util/src/tryRealpath.ts | 6 ++ yarn.lock | 5 + 21 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 e2e/__tests__/preserveSymlinks.ts create mode 100644 e2e/symlinked-source-dir/__tests__/a.test.js create mode 100644 e2e/symlinked-source-dir/__tests__/ab.test.js create mode 100644 e2e/symlinked-source-dir/__tests__/b.test.js create mode 100644 e2e/symlinked-source-dir/a.js create mode 100644 e2e/symlinked-source-dir/ab.js create mode 100644 e2e/symlinked-source-dir/b.js create mode 100644 e2e/symlinked-source-dir/package.json create mode 100644 packages/jest-config/should-preserve-links.d.ts create mode 100644 packages/jest-haste-map/should-preserve-links.d.ts create mode 100644 packages/jest-util/should-preserve-links.d.ts diff --git a/.gitignore b/.gitignore index c6b727d6dfa1..7ef7301f049d 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 244c1fa51cec..0b7562ea40c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[*]` Add support for NODE_PRESERVE_SYMLINKS and --preserve-symlinks behavior + ### Fixes ### Chore & Maintenance 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 28eda5785672..5d7db11c7e65 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 @@ -77,8 +79,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/package.json b/packages/jest-config/package.json index 93bf49c11e86..6ee43b726a21 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -27,7 +27,8 @@ "jest-util": "^26.0.1", "jest-validate": "^26.0.1", "micromatch": "^4.0.2", - "pretty-format": "^26.0.1" + "pretty-format": "^26.0.1", + "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/Defaults.ts b/packages/jest-config/src/Defaults.ts index f10ec309c835..11e0e2c112cc 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 'should-preserve-links'; 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/package.json b/packages/jest-haste-map/package.json index 27c83cc27d59..c10216af106c 100644 --- a/packages/jest-haste-map/package.json +++ b/packages/jest-haste-map/package.json @@ -21,7 +21,8 @@ "micromatch": "^4.0.2", "sane": "^4.0.3", "walker": "^1.0.7", - "which": "^2.0.2" + "which": "^2.0.2", + "should-preserve-links": "^1.0.4" }, "devDependencies": { "@jest/test-utils": "^26.0.0", 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..faebdd1d1e64 100644 --- a/packages/jest-haste-map/src/crawlers/node.ts +++ b/packages/jest-haste-map/src/crawlers/node.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import {spawn} from 'child_process'; import * as fs from 'graceful-fs'; import which = require('which'); +import shouldPreserveSymlinks from 'should-preserve-links'; import H from '../constants'; import * as fastPath from '../lib/fast_path'; import type { @@ -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/package.json b/packages/jest-util/package.json index 10cfaa9030df..0f32c0a5e433 100644 --- a/packages/jest-util/package.json +++ b/packages/jest-util/package.json @@ -11,6 +11,7 @@ "types": "build/index.d.ts", "dependencies": { "@jest/types": "^26.0.1", + "should-preserve-links": "^1.0.4", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "is-ci": "^2.0.0", diff --git a/packages/jest-util/should-preserve-links.d.ts b/packages/jest-util/should-preserve-links.d.ts new file mode 100644 index 000000000000..7fa8fb90fd2b --- /dev/null +++ b/packages/jest-util/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-util/src/tryRealpath.ts b/packages/jest-util/src/tryRealpath.ts index ff14e377a963..9d64e1deb485 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 'should-preserve-links'; + +const preserveSymlinks = shouldPreserveSymlinks(); export default function tryRealpath(path: Config.Path): Config.Path { + if (preserveSymlinks) { + return path; + } try { path = realpathSync.native(path); } catch (error) { diff --git a/yarn.lock b/yarn.lock index c40a326f6a5f..1005d5c11c80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12159,6 +12159,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"