diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a833a46b4b..4c862aebb7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-jasmine2/jest-circus/jest-cli]` Add test.todo ([#6996](https://github.com/facebook/jest/pull/6996)) - `[pretty-format]` Option to not escape strings in diff messages ([#5661](https://github.com/facebook/jest/pull/5661)) +- `[jest-haste-map]` Support for symlinks in `node_modules` ([#6993](https://github.com/facebook/jest/pull/6993)) - `[jest-haste-map]` Add `getFileIterator` to `HasteFS` for faster file iteration ([#7010](https://github.com/facebook/jest/pull/7010)). - `[jest-worker]` [**BREAKING**] Add functionality to call a `setup` method in the worker before the first call and a `teardown` method when ending the farm ([#7014](https://github.com/facebook/jest/pull/7014)). - `[jest-config]` [**BREAKING**] Set default `notifyMode` to `failure-change` ([#7024](https://github.com/facebook/jest/pull/7024)) diff --git a/packages/jest-haste-map/package.json b/packages/jest-haste-map/package.json index b0e5089cf015..2437c738a0a4 100644 --- a/packages/jest-haste-map/package.json +++ b/packages/jest-haste-map/package.json @@ -15,6 +15,7 @@ "jest-serializer": "^23.0.1", "jest-worker": "^23.2.0", "micromatch": "^2.3.11", + "realpath-native": "^1.0.0", "sane": "^3.0.0" } } diff --git a/packages/jest-haste-map/src/crawlers/node.js b/packages/jest-haste-map/src/crawlers/node.js index 8d8557457ec7..4e31f30aaf66 100644 --- a/packages/jest-haste-map/src/crawlers/node.js +++ b/packages/jest-haste-map/src/crawlers/node.js @@ -153,6 +153,7 @@ module.exports = function nodeCrawl( } }); data.files = files; + data.links = new Map(); resolve(data); }; diff --git a/packages/jest-haste-map/src/crawlers/watchman.js b/packages/jest-haste-map/src/crawlers/watchman.js index 027017afed3e..4e17672f547a 100644 --- a/packages/jest-haste-map/src/crawlers/watchman.js +++ b/packages/jest-haste-map/src/crawlers/watchman.js @@ -19,6 +19,21 @@ import H from '../constants'; const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting.html'; +// Matches symlinks in "node_modules" directories. +const nodeModules = ['**/node_modules/*', '**/node_modules/@*/*']; +const linkExpression = [ + 'allof', + ['type', 'l'], + ['anyof'].concat( + nodeModules.map(glob => [ + 'match', + glob, + 'wholename', + {includedotfiles: true}, + ]), + ), +]; + function WatchmanError(error: Error): Error { error.message = `Watchman error: ${error.message.trim()}. Make sure watchman ` + @@ -29,9 +44,9 @@ function WatchmanError(error: Error): Error { module.exports = async function watchmanCrawl( options: CrawlerOptions, ): Promise { - const fields = ['name', 'exists', 'mtime_ms']; + const fields = ['name', 'type', 'exists', 'mtime_ms']; const {data, extensions, ignore, rootDir, roots} = options; - const defaultWatchExpression = [ + const fileExpression = [ 'allof', ['type', 'f'], ['anyof'].concat(extensions.map(extension => ['suffix', extension])), @@ -93,21 +108,24 @@ module.exports = async function watchmanCrawl( await Promise.all( Array.from(rootProjectDirMappings).map( async ([root, directoryFilters]) => { - const expression = Array.from(defaultWatchExpression); + let expression = ['anyof', fileExpression, linkExpression]; const glob = []; if (directoryFilters.length > 0) { - expression.push([ - 'anyof', - ...directoryFilters.map(dir => ['dirname', dir]), - ]); + expression = [ + 'allof', + ['anyof'].concat(directoryFilters.map(dir => ['dirname', dir])), + expression, + ]; for (const directory of directoryFilters) { + glob.push(...nodeModules.map(glob => directory + '/' + glob)); for (const extension of extensions) { glob.push(`${directory}/**/*.${extension}`); } } } else { + glob.push(...nodeModules); for (const extension of extensions) { glob.push(`**/*.${extension}`); } @@ -118,7 +136,7 @@ module.exports = async function watchmanCrawl( ? // Use the `since` generator if we have a clock available {expression, fields, since: clocks.get(relativeRoot)} : // Otherwise use the `glob` filter - {expression, fields, glob}; + {expression, fields, glob, glob_includedotfiles: true}; const response = await cmd('query', root, query); @@ -139,6 +157,7 @@ module.exports = async function watchmanCrawl( } let files = data.files; + let links = data.links; let watchmanFiles; try { const watchmanRoots = await getWatchmanRoots(roots); @@ -148,6 +167,7 @@ module.exports = async function watchmanCrawl( // files. if (watchmanFileResults.isFresh) { files = new Map(); + links = new Map(); } watchmanFiles = watchmanFileResults.files; @@ -166,29 +186,38 @@ module.exports = async function watchmanCrawl( for (const fileData of response.files) { const filePath = fsRoot + path.sep + normalizePathSep(fileData.name); - const relativeFilePath = fastPath.relative(rootDir, filePath); + const fileName = fastPath.relative(rootDir, filePath); + const cache: Map = fileData.type === 'f' ? files : links; if (!fileData.exists) { - files.delete(relativeFilePath); + cache.delete(filePath); } else if (!ignore(filePath)) { const mtime = typeof fileData.mtime_ms === 'number' ? fileData.mtime_ms : fileData.mtime_ms.toNumber(); + let sha1hex = fileData['content.sha1hex']; if (typeof sha1hex !== 'string' || sha1hex.length !== 40) { sha1hex = null; } - const existingFileData = data.files.get(relativeFilePath); + const existingFileData: any = + fileData.type === 'f' + ? data.files.get(fileName) + : data.links.get(fileName); + if (existingFileData && existingFileData[H.MTIME] === mtime) { - files.set(relativeFilePath, existingFileData); + cache.set(fileName, existingFileData); + } else if (fileData.type !== 'f') { + // See ../constants.js + cache.set(fileName, [undefined, mtime]); } else if ( - existingFileData && sha1hex && + existingFileData && existingFileData[H.SHA1] === sha1hex ) { - files.set(relativeFilePath, [ + cache.set(fileName, [ existingFileData[0], mtime, existingFileData[2], @@ -197,12 +226,13 @@ module.exports = async function watchmanCrawl( ]); } else { // See ../constants.js - files.set(relativeFilePath, ['', mtime, 0, [], sha1hex]); + cache.set(fileName, ['', mtime, 0, [], sha1hex]); } } } } data.files = files; + data.links = links; return data; }; diff --git a/packages/jest-haste-map/src/haste_fs.js b/packages/jest-haste-map/src/haste_fs.js index 69df848a5975..beade8e31e95 100644 --- a/packages/jest-haste-map/src/haste_fs.js +++ b/packages/jest-haste-map/src/haste_fs.js @@ -8,19 +8,30 @@ */ import type {Glob, Path} from 'types/Config'; -import type {FileData} from 'types/HasteMap'; +import type {FileData, LinkData} from 'types/HasteMap'; import * as fastPath from './lib/fast_path'; import micromatch from 'micromatch'; +import {sync as realpath} from 'realpath-native'; import H from './constants'; export default class HasteFS { _rootDir: Path; _files: FileData; + _links: LinkData; - constructor({rootDir, files}: {rootDir: Path, files: FileData}) { + constructor({ + rootDir, + files, + links, + }: { + rootDir: Path, + files: FileData, + links: LinkData, + }) { this._rootDir = rootDir; this._files = files; + this._links = links; } getModuleName(file: Path): ?string { @@ -42,6 +53,17 @@ export default class HasteFS { return this._getFileData(file) != null; } + follow(file: Path): Path { + const link = this._links[file]; + if (link === undefined) { + return file; + } + if (link[0] === undefined) { + link[0] = realpath(file); + } + return link[0]; + } + getAllFiles(): Array { return Array.from(this.getFileIterator()); } diff --git a/packages/jest-haste-map/src/index.js b/packages/jest-haste-map/src/index.js index 31ac05fd4e49..d0fdd735eeb2 100644 --- a/packages/jest-haste-map/src/index.js +++ b/packages/jest-haste-map/src/index.js @@ -305,6 +305,7 @@ class HasteMap extends EventEmitter { const rootDir = this._options.rootDir; const hasteFS = new HasteFS({ files: hasteMap.files, + links: hasteMap.links, rootDir, }); const moduleMap = new HasteModuleMap({ @@ -760,6 +761,7 @@ class HasteMap extends EventEmitter { eventsQueue, hasteFS: new HasteFS({ files: hasteMap.files, + links: hasteMap.links, rootDir, }), moduleMap: new HasteModuleMap({ @@ -811,6 +813,7 @@ class HasteMap extends EventEmitter { clocks: new Map(hasteMap.clocks), duplicates: new Map(hasteMap.duplicates), files: new Map(hasteMap.files), + links: new Map(hasteMap.links), map: new Map(hasteMap.map), mocks: new Map(hasteMap.mocks), }; @@ -1006,6 +1009,7 @@ class HasteMap extends EventEmitter { clocks: new Map(), duplicates: new Map(), files: new Map(), + links: new Map(), map: new Map(), mocks: new Map(), }; diff --git a/types/HasteMap.js b/types/HasteMap.js index 01865ad57790..8c69e9f64d24 100644 --- a/types/HasteMap.js +++ b/types/HasteMap.js @@ -19,6 +19,7 @@ export type ModuleMap = _ModuleMap; export type SerializableModuleMap = _SerializableModuleMap; export type FileData = Map; +export type LinkData = Map; export type MockData = Map; export type ModuleMapData = Map; export type WatchmanClocks = Map; @@ -37,6 +38,7 @@ export type InternalHasteMap = {| clocks: WatchmanClocks, duplicates: DuplicatesIndex, files: FileData, + links: LinkData, map: ModuleMapData, mocks: MockData, |}; @@ -62,6 +64,8 @@ export type FileMetaData = [ /* sha1 */ ?string, ]; +export type LinkMetaData = [/* target */ ?string, /* mtime */ number]; + type ModuleMapItem = {[platform: string]: ModuleMetaData, __proto__: null}; export type ModuleMetaData = [Path, /* type */ number];