From 58c88b8b7c35299c07d17a23c094168014cc43a8 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 3 Jan 2023 03:06:33 -0800 Subject: [PATCH] Add symlink field to file metadata and cache Summary: This adds another entry to the `FileMetadata` tuple used throughout `metro-file-map`, to be used to compactly represent: a) Whether a file is regular or a symlink b) The symlink target, if known. In doing so, it changes the persisted cache format - so this diff also bumps the `CACHE_BREAKER` key. Note that because Metro only passes `enableSymlinks: false` to the `metro-file-map`, and this config isn't exposed to the user, *all* crawl results should currently have a `0` entry. ## Storing symlinks in the `files` map I considered storing symlinks in a separate `Map` alongside `files`, which is slightly more compact. Ultimately though this is more difficult to work with - almost every lookup becomes two lookups and it's rare that we'd need to iterate over one but not the other. We could revisit this if we ever separate the serialised structure from the working-copy structure. ## Type: `0 | 1 | string` There are a couple of considerations in this type, which aims to compactly capture both the type and (optionally) the symlink target: - As an intermediate state, it's useful to be able to record that a file is a symlink without necessarily knowing the target. In particular, this allows us to `readlink` downstream of non-Watchman crawlers and watchers that can't natively provide the target, e.g. in the multi-thread `processFile` phase, or indeed on demand. - We use `0 | 1` as opposed to `boolean` for serialised compactness, following the precedent of `H.VISITED`. (`null` would be ambiguous in this context.) NOTE: `metadata[H.SYMLINK] === 0` should be used to check for regular files at the low level - truthiness is unreliable because [POSIX says](https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html) the target is an arbitrary string (including an empty string) - (in practice - macOS allows empty symlinks, linux doesn't). Reviewed By: huntie Differential Revision: D42131590 fbshipit-source-id: 1fa39576988159887b88057f003de6018b85a583 --- .../src/__tests__/index-test.js | 49 ++++++++++++++++--- packages/metro-file-map/src/constants.js | 1 + .../crawlers/__tests__/integration-test.js | 21 ++++---- .../src/crawlers/__tests__/node-test.js | 40 +++++++-------- .../src/crawlers/__tests__/watchman-test.js | 32 ++++++------ .../metro-file-map/src/crawlers/node/index.js | 40 ++++++++++++--- .../src/crawlers/watchman/index.js | 14 +++++- packages/metro-file-map/src/flow-types.js | 2 + packages/metro-file-map/src/index.js | 4 +- 9 files changed, 141 insertions(+), 62 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index c4e4333223..dea74b72d7 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -54,7 +54,7 @@ jest.mock('../crawlers/watchman', () => const relativeFilePath = path.relative(rootDir, file); if (list[file]) { const hash = computeSha1 ? mockHashContents(list[file]) : null; - changedFiles.set(relativeFilePath, ['', 32, 42, 0, [], hash]); + changedFiles.set(relativeFilePath, ['', 32, 42, 0, [], hash, 0]); } else { const fileData = previousState.files.get(relativeFilePath); if (fileData) { @@ -402,6 +402,7 @@ describe('HasteMap', () => { 1, 'Strawberry', null, + 0, ], [path.join('fruits', 'Pear.js')]: [ 'Pear', @@ -410,6 +411,7 @@ describe('HasteMap', () => { 1, 'Banana\0Strawberry', null, + 0, ], [path.join('fruits', 'Strawberry.js')]: [ 'Strawberry', @@ -418,6 +420,7 @@ describe('HasteMap', () => { 1, '', null, + 0, ], [path.join('fruits', '__mocks__', 'Pear.js')]: [ '', @@ -426,8 +429,17 @@ describe('HasteMap', () => { 1, 'Melon', null, + 0, + ], + [path.join('vegetables', 'Melon.js')]: [ + 'Melon', + 32, + 42, + 1, + '', + null, + 0, ], - [path.join('vegetables', 'Melon.js')]: ['Melon', 32, 42, 1, '', null], }), ); @@ -512,6 +524,7 @@ describe('HasteMap', () => { 0, 'Strawberry', null, + 0, ], [path.join('fruits', 'Pear.js')]: [ 'Pear', @@ -520,6 +533,7 @@ describe('HasteMap', () => { 0, 'Banana\0Strawberry', null, + 0, ], [path.join('fruits', 'Strawberry.js')]: [ 'Strawberry', @@ -528,6 +542,7 @@ describe('HasteMap', () => { 0, '', null, + 0, ], [path.join('fruits', '__mocks__', 'Pear.js')]: [ '', @@ -536,8 +551,17 @@ describe('HasteMap', () => { 0, 'Melon', null, + 0, + ], + [path.join('vegetables', 'Melon.js')]: [ + 'Melon', + 32, + 42, + 0, + '', + null, + 0, ], - [path.join('vegetables', 'Melon.js')]: ['Melon', 32, 42, 0, '', null], }); return Promise.resolve({ @@ -564,6 +588,7 @@ describe('HasteMap', () => { 1, 'Strawberry', '7772b628e422e8cf59c526be4bb9f44c0898e3d1', + 0, ], [path.join('fruits', 'Pear.js')]: [ 'Pear', @@ -572,6 +597,7 @@ describe('HasteMap', () => { 1, 'Banana\0Strawberry', '89d0c2cc11dcc5e1df50b8af04ab1b597acfba2f', + 0, ], [path.join('fruits', 'Strawberry.js')]: [ 'Strawberry', @@ -580,6 +606,7 @@ describe('HasteMap', () => { 1, '', 'e8aa38e232b3795f062f1d777731d9240c0f8c25', + 0, ], [path.join('fruits', '__mocks__', 'Pear.js')]: [ '', @@ -588,6 +615,7 @@ describe('HasteMap', () => { 1, 'Melon', '8d40afbb6e2dc78e1ba383b6d02cafad35cceef2', + 0, ], [path.join('vegetables', 'Melon.js')]: [ 'Melon', @@ -596,6 +624,7 @@ describe('HasteMap', () => { 1, '', 'f16ccf6f2334ceff2ddb47628a2c5f2d748198ca', + 0, ], }), ); @@ -647,7 +676,7 @@ describe('HasteMap', () => { cacheContent.files.get( path.join('fruits', 'node_modules', 'fbjs', 'fbjs.js'), ), - ).toEqual(['', 32, 42, 0, [], null]); + ).toEqual(['', 32, 42, 0, [], null, 0]); expect(cacheContent.map.get('fbjs')).not.toBeDefined(); @@ -769,6 +798,7 @@ describe('HasteMap', () => { 1, 'Blackberry', null, + 0, ], [path.join('fruits', 'Strawberry.ios.js')]: [ 'Strawberry', @@ -777,6 +807,7 @@ describe('HasteMap', () => { 1, 'Raspberry', null, + 0, ], [path.join('fruits', 'Strawberry.js')]: [ 'Strawberry', @@ -785,6 +816,7 @@ describe('HasteMap', () => { 1, 'Banana', null, + 0, ], }), ); @@ -879,6 +911,7 @@ describe('HasteMap', () => { 1, 'Kiwi', null, + 0, ]); expect(deepNormalize(data.files)).toEqual(files); @@ -1119,7 +1152,7 @@ describe('HasteMap', () => { const invalidFilePath = path.join('fruits', 'invalid', 'file.js'); watchman.mockImplementation(async options => { const {changedFiles} = await mockImpl(options); - changedFiles.set(invalidFilePath, ['', 34, 44, 0, []]); + changedFiles.set(invalidFilePath, ['', 34, 44, 0, [], null, 0]); return { changedFiles, removedFiles: new Map(), @@ -1214,7 +1247,7 @@ describe('HasteMap', () => { node.mockImplementation(options => { return Promise.resolve({ changedFiles: createMap({ - [path.join('fruits', 'Banana.js')]: ['', 32, 42, 0, '', null], + [path.join('fruits', 'Banana.js')]: ['', 32, 42, 0, '', null, 0], }), removedFiles: new Map(), }); @@ -1233,6 +1266,7 @@ describe('HasteMap', () => { 1, 'Strawberry', null, + 0, ], }), ); @@ -1250,7 +1284,7 @@ describe('HasteMap', () => { node.mockImplementation(options => { return Promise.resolve({ changedFiles: createMap({ - [path.join('fruits', 'Banana.js')]: ['', 32, 42, 0, '', null], + [path.join('fruits', 'Banana.js')]: ['', 32, 42, 0, '', null, 0], }), removedFiles: new Map(), }); @@ -1270,6 +1304,7 @@ describe('HasteMap', () => { 1, 'Strawberry', null, + 0, ], }), ); diff --git a/packages/metro-file-map/src/constants.js b/packages/metro-file-map/src/constants.js index 8d7ad87259..f4536a364d 100644 --- a/packages/metro-file-map/src/constants.js +++ b/packages/metro-file-map/src/constants.js @@ -34,6 +34,7 @@ const constants/*: HType */ = { VISITED: 3, DEPENDENCIES: 4, SHA1: 5, + SYMLINK: 6, /* module map attributes */ PATH: 0, diff --git a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js index 535f5cd6b9..1664adc7f6 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js @@ -5,15 +5,16 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local * @oncall react_native */ import nodeCrawl from '../node'; import watchmanCrawl from '../watchman'; import {execSync} from 'child_process'; +import invariant from 'invariant'; import os from 'os'; import {join} from 'path'; -import type {CrawlerOptions, FileData} from '../../flow-types'; // At runtime we use a more sophisticated + robust Watchman capability check, // but this simple heuristic is fast to check, synchronous (we can't @@ -33,10 +34,7 @@ const isWatchmanOnPath = () => { const mockUseNativeFind = jest.fn(); jest.mock('../node/hasNativeFindSupport', () => () => mockUseNativeFind()); -type Crawler = (opts: CrawlerOptions) => Promise<{ - removedFiles: FileData, - changedFiles: FileData, -}>; +type Crawler = typeof nodeCrawl | typeof watchmanCrawl; const CRAWLERS: {[key: string]: ?Crawler} = { 'node-find': opts => { @@ -59,9 +57,10 @@ describe.each(Object.keys(CRAWLERS))( const maybeTest = crawl ? test : test.skip; maybeTest('Finds the expected files', async () => { + invariant(crawl, 'crawl should not be null within maybeTest'); const result = await crawl({ previousState: { - files: new Map([['removed.js', ['', 123, 234, 0, '', null]]]), + files: new Map([['removed.js', ['', 123, 234, 0, '', null, 0]]]), clocks: new Map(), }, enableSymlinks: false, @@ -69,6 +68,10 @@ describe.each(Object.keys(CRAWLERS))( ignore: path => path.includes('ignored'), roots: [FIXTURES_DIR], rootDir: FIXTURES_DIR, + abortSignal: null, + computeSha1: false, + forceNodeFilesystemAPI: false, + onStatus: () => {}, }); // Map comparison is unordered, which is what we want @@ -76,11 +79,11 @@ describe.each(Object.keys(CRAWLERS))( changedFiles: new Map([ [ join('directory', 'bar.js'), - ['', expect.any(Number), 245, 0, '', null], + ['', expect.any(Number), 245, 0, '', null, 0], ], - ['foo.js', ['', expect.any(Number), 245, 0, '', null]], + ['foo.js', ['', expect.any(Number), 245, 0, '', null, 0]], ]), - removedFiles: new Map([['removed.js', ['', 123, 234, 0, '', null]]]), + removedFiles: new Map([['removed.js', ['', 123, 234, 0, '', null, 0]]]), }); if (crawlerName === 'watchman') { expect(result.clocks).toBeInstanceOf(Map); diff --git a/packages/metro-file-map/src/crawlers/__tests__/node-test.js b/packages/metro-file-map/src/crawlers/__tests__/node-test.js index 2dfb4583fd..01eab39f9b 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/node-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/node-test.js @@ -181,9 +181,9 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': ['', 32, 42, 0, '', null], - 'fruits/tomato.js': ['', 33, 42, 0, '', null], - 'vegetables/melon.json': ['', 34, 42, 0, '', null], + 'fruits/strawberry.js': ['', 32, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 33, 42, 0, '', null, 0], + 'vegetables/melon.json': ['', 34, 42, 0, '', null, 0], }), ); @@ -194,9 +194,9 @@ describe('node crawler', () => { nodeCrawl = require('../node'); // In this test sample, strawberry is changed and tomato is unchanged - const tomato = ['', 33, 42, 1, '', null]; + const tomato = ['', 33, 42, 1, '', null, 0]; const files = createMap({ - 'fruits/strawberry.js': ['', 30, 40, 1, '', null], + 'fruits/strawberry.js': ['', 30, 40, 1, '', null, 0], 'fruits/tomato.js': tomato, }); @@ -211,7 +211,7 @@ describe('node crawler', () => { // Tomato is not included because it is unchanged expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': ['', 32, 42, 0, '', null], + 'fruits/strawberry.js': ['', 32, 42, 0, '', null, 0], }), ); @@ -224,9 +224,9 @@ describe('node crawler', () => { // In this test sample, previouslyExisted was present before and will not be // when crawling this directory. const files = createMap({ - 'fruits/previouslyExisted.js': ['', 30, 40, 1, '', null], - 'fruits/strawberry.js': ['', 33, 42, 0, '', null], - 'fruits/tomato.js': ['', 32, 42, 0, '', null], + 'fruits/previouslyExisted.js': ['', 30, 40, 1, '', null, 0], + 'fruits/strawberry.js': ['', 33, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 32, 42, 0, '', null, 0], }); const {changedFiles, removedFiles} = await nodeCrawl({ @@ -239,13 +239,13 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': ['', 32, 42, 0, '', null], - 'fruits/tomato.js': ['', 33, 42, 0, '', null], + 'fruits/strawberry.js': ['', 32, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 33, 42, 0, '', null, 0], }), ); expect(removedFiles).toEqual( createMap({ - 'fruits/previouslyExisted.js': ['', 30, 40, 1, '', null], + 'fruits/previouslyExisted.js': ['', 30, 40, 1, '', null, 0], }), ); }); @@ -274,8 +274,8 @@ describe('node crawler', () => { ); expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], - 'fruits/tomato.js': ['', 32, 42, 0, '', null], + 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 32, 42, 0, '', null, 0], }), ); expect(removedFiles).toEqual(new Map()); @@ -300,8 +300,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], - 'fruits/tomato.js': ['', 32, 42, 0, '', null], + 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 32, 42, 0, '', null, 0], }), ); expect(removedFiles).toEqual(new Map()); @@ -324,8 +324,8 @@ describe('node crawler', () => { expect(childProcess.spawn).toHaveBeenCalledTimes(0); expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], - 'fruits/tomato.js': ['', 32, 42, 0, '', null], + 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 32, 42, 0, '', null, 0], }), ); expect(removedFiles).toEqual(new Map()); @@ -381,8 +381,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], - 'fruits/tomato.js': ['', 32, 42, 0, '', null], + 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null, 0], + 'fruits/tomato.js': ['', 32, 42, 0, '', null, 0], }), ); expect(removedFiles).toEqual(new Map()); diff --git a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js index cf4c542332..ee181b0902 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js @@ -104,9 +104,9 @@ describe('watchman watch', () => { }; mockFiles = createMap({ - [MELON_RELATIVE]: ['', 33, 43, 0, '', null], - [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null], - [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null], + [MELON_RELATIVE]: ['', 33, 43, 0, '', null, 0], + [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null, 0], + [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null, 0], }); }); @@ -209,13 +209,13 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: ['', 42, 40, 0, '', null], + [KIWI_RELATIVE]: ['', 42, 40, 0, '', null, 0], }), ); expect(removedFiles).toEqual( createMap({ - [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null], + [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null, 0], }), ); }); @@ -255,9 +255,9 @@ describe('watchman watch', () => { 'watch-project': WATCH_PROJECT_MOCK, }; - const mockBananaMetadata = ['Banana', 41, 51, 1, ['Raspberry'], null]; + const mockBananaMetadata = ['Banana', 41, 51, 1, ['Raspberry'], null, 0]; mockFiles.set(BANANA_RELATIVE, mockBananaMetadata); - const mockTomatoMetadata = ['Tomato', 31, 41, 1, [], mockTomatoSha1]; + const mockTomatoMetadata = ['Tomato', 31, 41, 1, [], mockTomatoSha1, 0]; mockFiles.set(TOMATO_RELATIVE, mockTomatoMetadata); const {changedFiles, clocks, removedFiles} = await watchmanCrawl({ @@ -286,8 +286,8 @@ describe('watchman watch', () => { // banana is not included because it is unchanged expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: ['', 42, 52, 0, '', null], - [TOMATO_RELATIVE]: ['Tomato', 76, 41, 1, [], mockTomatoSha1], + [KIWI_RELATIVE]: ['', 42, 52, 0, '', null, 0], + [TOMATO_RELATIVE]: ['Tomato', 76, 41, 1, [], mockTomatoSha1, 0], }), ); @@ -296,8 +296,8 @@ describe('watchman watch', () => { expect(removedFiles).toEqual( createMap({ - [MELON_RELATIVE]: ['', 33, 43, 0, '', null], - [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null], + [MELON_RELATIVE]: ['', 33, 43, 0, '', null, 0], + [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null, 0], }), ); }); @@ -366,14 +366,14 @@ describe('watchman watch', () => { // Melon is not included because it is unchanged. expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: ['', 42, 52, 0, '', null], + [KIWI_RELATIVE]: ['', 42, 52, 0, '', null, 0], }), ); expect(removedFiles).toEqual( createMap({ - [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null], - [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null], + [STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null, 0], + [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null, 0], }), ); }); @@ -538,13 +538,13 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: ['', 42, 40, 0, '', null], + [KIWI_RELATIVE]: ['', 42, 40, 0, '', null, 0], }), ); expect(removedFiles).toEqual( createMap({ - [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null], + [TOMATO_RELATIVE]: ['', 31, 41, 0, '', null, 0], }), ); }); diff --git a/packages/metro-file-map/src/crawlers/node/index.js b/packages/metro-file-map/src/crawlers/node/index.js index 37b084ac2e..a5d22780ab 100644 --- a/packages/metro-file-map/src/crawlers/node/index.js +++ b/packages/metro-file-map/src/crawlers/node/index.js @@ -21,7 +21,14 @@ import * as path from 'path'; const debug = require('debug')('Metro:NodeCrawler'); -type Result = Array<[/* id */ string, /* mtime */ number, /* size */ number]>; +type Result = Array< + [ + string, // id + number, // mtime + number, // size + 0 | 1, // symlink + ], +>; type Callback = (result: Result) => void; @@ -70,7 +77,12 @@ function find( if (!err && stat) { const ext = path.extname(file).substr(1); if (extensions.indexOf(ext) !== -1) { - result.push([file, stat.mtime.getTime(), stat.size]); + result.push([ + file, + stat.mtime.getTime(), + stat.size, + entry.isSymbolicLink() ? 1 : 0, + ]); } } @@ -102,7 +114,11 @@ function findNative( ): void { const args = Array.from(roots); if (enableSymlinks) { - args.push('(', '-type', 'f', '-o', '-type', 'l', ')'); + // Temporarily(?) disable `enableSymlinks` because we can't satisfy it + // consistently with recursive crawl without calling *both* stat and lstat + // on every file. TODO: Change the definition of `enableSymlinks` to return + // the lstat-equivalent metadata and include links to directories. + throw new Error('enableSymlinks is not supported by native find'); } else { args.push('-type', 'f'); } @@ -145,7 +161,7 @@ function findNative( fs.stat(path, (err, stat) => { // Filter out symlinks that describe directories if (!err && stat && !stat.isDirectory()) { - result.push([path, stat.mtime.getTime(), stat.size]); + result.push([path, stat.mtime.getTime(), stat.size, 0]); } if (--count === 0) { callback(result); @@ -172,7 +188,9 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{ } = options; perfLogger?.point('nodeCrawl_start'); const useNativeFind = - !forceNodeFilesystemAPI && (await hasNativeFindSupport()); + !forceNodeFilesystemAPI && + !enableSymlinks && + (await hasNativeFindSupport()); debug('Using system find: %s', useNativeFind); @@ -181,13 +199,21 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{ const changedFiles = new Map(); const removedFiles = new Map(previousState.files); for (const fileData of list) { - const [filePath, mtime, size] = fileData; + const [filePath, mtime, size, symlink] = fileData; const relativeFilePath = fastPath.relative(rootDir, filePath); const existingFile = previousState.files.get(relativeFilePath); removedFiles.delete(relativeFilePath); if (existingFile == null || existingFile[H.MTIME] !== mtime) { // See ../constants.js; SHA-1 will always be null and fulfilled later. - changedFiles.set(relativeFilePath, ['', mtime, size, 0, '', null]); + changedFiles.set(relativeFilePath, [ + '', + mtime, + size, + 0, + '', + null, + symlink, + ]); } } diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 4fdd2b2ac5..69c1e2b69e 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -331,12 +331,21 @@ module.exports = async function watchmanCrawl({ sha1hex = undefined; } - let nextData: FileMetaData = ['', mtime, size, 0, '', sha1hex ?? null]; + let nextData: FileMetaData = [ + '', + mtime, + size, + 0, + '', + sha1hex ?? null, + fileData.type === 'l' ? 1 : 0, + ]; if ( existingFileData && sha1hex != null && - existingFileData[H.SHA1] === sha1hex + existingFileData[H.SHA1] === sha1hex && + (existingFileData[H.SYMLINK] !== 0) === (fileData.type === 'l') ) { // Special case - file touched but not modified, so we can reuse the // metadata and just update mtime. @@ -347,6 +356,7 @@ module.exports = async function watchmanCrawl({ existingFileData[3], existingFileData[4], existingFileData[5], + existingFileData[6], ]; } diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 55f288d67c..d38efc1c60 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -121,6 +121,7 @@ export type HType = { VISITED: 3, DEPENDENCIES: 4, SHA1: 5, + SYMLINK: 6, PATH: 0, TYPE: 1, MODULE: 0, @@ -151,6 +152,7 @@ export type FileMetaData = [ /* visited */ 0 | 1, /* dependencies */ string, /* sha1 */ ?string, + /* symlink */ 0 | 1 | string, // string specifies target, if known ]; export interface FileSystem { diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index d93a28a950..717fb4a967 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -137,7 +137,7 @@ export type { // This should be bumped whenever a code change to `metro-file-map` itself // would cause a change to the cache data structure and/or content (for a given // filesystem state and build parameters). -const CACHE_BREAKER = '2'; +const CACHE_BREAKER = '3'; const CHANGE_INTERVAL = 30; const NODE_MODULES = path.sep + 'node_modules' + path.sep; @@ -189,6 +189,7 @@ const WATCHMAN_REQUIRED_CAPABILITIES = [ * visited: boolean, // whether the file has been parsed or not. * dependencies: Array, // all relative dependencies of this file. * sha1: ?string, // SHA-1 of the file, if requested via options. + * symlink: ?(1 | 0 | string), // Truthy if symlink, string is target * }; * * // Modules can be targeted to a specific platform based on the file name. @@ -986,6 +987,7 @@ export default class HasteMap extends EventEmitter { 0, '', null, + metadata.type === 'l' ? 1 : 0, ]; data.files.set(relativeFilePath, fileMetadata); const promise = this._processFile(