diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index 04cbaf4062e..38ba3b785ac 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -362,6 +362,8 @@ If everything is set up correctly, you should see your new app running in your A
+> If you hit a `ERROR Watcher took too long to load`, try increasing the timeout in [this file](https://github.com/facebook/react-native/blob/5fa33f3d07f8595a188f6fe04d6168a6ede1e721/packager/react-packager/src/DependencyResolver/FileWatcher/index.js#L16) (under your `node_modules/react-native/`).
+
> If you're targeting API level 23, the app might crash on first launch with an error smilar to `Unable to add window android.view.ViewRootImpl$W@c51fa6 -- permission denied for this window type`. To fix this, you need to go to `System settings > Apps > Configure apps > Draw over other apps` and grant the permission for the app.
NOTE: Many React Native modules haven't been tested on Marshmallow and might break. Please throughly test the app if you target API level 23 and file a bug report if you find that something is broken.
@@ -465,7 +467,7 @@ if (window.location.hash !== '' && window.location.hash !== 'content') { // cont
}
// We would have broken out if both targetPlatform and devOS hadn't been filled.
display('os', devOS);
- display('platform', targetPlatform);
+ display('platform', targetPlatform);
foundHash = true;
break;
}
diff --git a/local-cli/bundle/buildBundle.js b/local-cli/bundle/buildBundle.js
index d950ff80208..047ec2ac504 100644
--- a/local-cli/bundle/buildBundle.js
+++ b/local-cli/bundle/buildBundle.js
@@ -55,8 +55,8 @@ function buildBundle(args, config, output = outputBundle, packagerInstance) {
getTransformOptionsModulePath: config.getTransformOptionsModulePath,
transformModulePath: transformModulePath,
extraNodeModules: config.extraNodeModules,
+ nonPersistent: true,
resetCache: args.resetCache,
- watch: false,
};
packagerInstance = new Server(options);
diff --git a/local-cli/server/runServer.js b/local-cli/server/runServer.js
index 68e51d7f10c..5d2ae1730c0 100644
--- a/local-cli/server/runServer.js
+++ b/local-cli/server/runServer.js
@@ -8,14 +8,12 @@
*/
'use strict';
-const InspectorProxy = require('./util/inspectorProxy.js');
const ReactPackager = require('../../packager/react-packager');
const attachHMRServer = require('./util/attachHMRServer');
const connect = require('connect');
const copyToClipBoardMiddleware = require('./middleware/copyToClipBoardMiddleware');
const cpuProfilerMiddleware = require('./middleware/cpuProfilerMiddleware');
-const defaultAssetExts = require('../../packager/defaults').assetExts;
const getDevToolsMiddleware = require('./middleware/getDevToolsMiddleware');
const heapCaptureMiddleware = require('./middleware/heapCaptureMiddleware.js');
const http = require('http');
@@ -27,8 +25,10 @@ const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInE
const path = require('path');
const statusPageMiddleware = require('./middleware/statusPageMiddleware.js');
const systraceProfileMiddleware = require('./middleware/systraceProfileMiddleware.js');
-const unless = require('./middleware/unless');
const webSocketProxy = require('./util/webSocketProxy.js');
+const InspectorProxy = require('./util/inspectorProxy.js');
+const defaultAssetExts = require('../../packager/defaults').assetExts;
+const unless = require('./middleware/unless');
function runServer(args, config, readyCallback) {
var wsProxy = null;
@@ -86,17 +86,17 @@ function getPackagerServer(args, config) {
undefined;
return ReactPackager.createServer({
- assetExts: defaultAssetExts.concat(args.assetExts),
- assetRoots: args.assetRoots,
+ nonPersistent: args.nonPersistent,
+ projectRoots: args.projectRoots,
blacklistRE: config.getBlacklistRE(),
cacheVersion: '3',
- extraNodeModules: config.extraNodeModules,
getTransformOptionsModulePath: config.getTransformOptionsModulePath,
- projectRoots: args.projectRoots,
- resetCache: args.resetCache,
transformModulePath: transformModulePath,
+ extraNodeModules: config.extraNodeModules,
+ assetRoots: args.assetRoots,
+ assetExts: defaultAssetExts.concat(args.assetExts),
+ resetCache: args.resetCache,
verbose: args.verbose,
- watch: args.nonPersistent != null,
});
}
diff --git a/packager/package.json b/packager/package.json
index 126696df97b..700d9715c7a 100644
--- a/packager/package.json
+++ b/packager/package.json
@@ -5,5 +5,32 @@
"repository": {
"type": "git",
"url": "git@github.com:facebook/react-native.git"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "jest": {
+ "setupEnvScriptFile": "jest/setup.js",
+ "testPathIgnorePatterns": [
+ "/node_modules/"
+ ],
+ "testFileExtensions": [
+ "js"
+ ],
+ "unmockedModulePathPatterns": [
+ "source-map"
+ ]
+ },
+ "scripts": {
+ "test": "jest",
+ "lint": "node linter.js Examples/",
+ "start": "./packager.sh"
+ },
+ "dependencies": {
+ "wordwrap": "^1.0.0"
+ },
+ "devDependencies": {
+ "jest-cli": "git://github.com/facebook/jest#0.5.x",
+ "eslint": "0.9.2"
}
}
diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js
index a0f7c501c49..e8f31d07a04 100644
--- a/packager/react-packager/index.js
+++ b/packager/react-packager/index.js
@@ -45,14 +45,26 @@ function createServer(options) {
enableDebug();
}
- options = Object.assign({}, options);
- delete options.verbose;
var Server = require('./src/Server');
- return new Server(options);
+ return new Server(omit(options, ['verbose']));
}
function createNonPersistentServer(options) {
Logger.disablePrinting();
- options.watch = options.nonPersistent != null;
+ // Don't start the filewatcher or the cache.
+ if (options.nonPersistent == null) {
+ options.nonPersistent = true;
+ }
+
return createServer(options);
}
+
+function omit(obj, blacklistedKeys) {
+ return Object.keys(obj).reduce((clone, key) => {
+ if (blacklistedKeys.indexOf(key) === -1) {
+ clone[key] = obj[key];
+ }
+
+ return clone;
+ }, {});
+}
diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js
index 69b442bf7d6..7e70d6bc4b8 100644
--- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js
+++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js
@@ -15,15 +15,17 @@ jest.mock('fs');
const AssetServer = require('../');
const crypto = require('crypto');
+const {EventEmitter} = require('events');
const fs = require('fs');
const {objectContaining} = jasmine;
describe('AssetServer', () => {
+ let fileWatcher;
beforeEach(() => {
const NodeHaste = require('../../node-haste');
- NodeHaste.getAssetDataFromName =
- require.requireActual('../../node-haste/lib/getAssetDataFromName');
+ NodeHaste.getAssetDataFromName = require.requireActual('../../node-haste/lib/getAssetDataFromName');
+ fileWatcher = new EventEmitter();
});
describe('assetServer.get', () => {
@@ -31,6 +33,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -56,6 +59,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -93,6 +97,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpg'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -119,6 +124,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -141,6 +147,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -172,6 +179,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root', '/root2'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -200,6 +208,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -232,6 +241,7 @@ describe('AssetServer', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpeg'],
+ fileWatcher,
});
fs.__setMockFilesystem({
@@ -261,14 +271,15 @@ describe('AssetServer', () => {
});
describe('hash:', () => {
- let server, mockFS;
+ let server, fileSystem;
beforeEach(() => {
server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['jpg'],
+ fileWatcher,
});
- mockFS = {
+ fileSystem = {
'root': {
imgs: {
'b@1x.jpg': 'b1 image',
@@ -279,13 +290,13 @@ describe('AssetServer', () => {
}
};
- fs.__setMockFilesystem(mockFS);
+ fs.__setMockFilesystem(fileSystem);
});
it('uses the file contents to build the hash', () => {
const hash = crypto.createHash('md5');
- for (const name in mockFS.root.imgs) {
- hash.update(mockFS.root.imgs[name]);
+ for (const name in fileSystem.root.imgs) {
+ hash.update(fileSystem.root.imgs[name]);
}
return server.getAssetData('imgs/b.jpg').then(data =>
@@ -295,8 +306,8 @@ describe('AssetServer', () => {
it('changes the hash when the passed-in file watcher emits an `all` event', () => {
return server.getAssetData('imgs/b.jpg').then(initialData => {
- mockFS.root.imgs['b@4x.jpg'] = 'updated data';
- server.onFileChange('all', '/root/imgs/b@4x.jpg');
+ fileSystem.root.imgs['b@4x.jpg'] = 'updated data';
+ fileWatcher.emit('all', 'arbitrary', '/root', 'imgs/b@4x.jpg');
return server.getAssetData('imgs/b.jpg').then(data =>
expect(data.hash).not.toEqual(initialData.hash)
);
diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js
index a267eea6bd9..e1a02f65484 100644
--- a/packager/react-packager/src/AssetServer/index.js
+++ b/packager/react-packager/src/AssetServer/index.js
@@ -42,6 +42,10 @@ const validateOpts = declareOpts({
type: 'array',
required: true,
},
+ fileWatcher: {
+ type: 'object',
+ required: true,
+ }
});
class AssetServer {
@@ -51,6 +55,9 @@ class AssetServer {
this._assetExts = opts.assetExts;
this._hashes = new Map();
this._files = new Map();
+
+ opts.fileWatcher
+ .on('all', (type, root, file) => this._onFileChange(type, root, file));
}
get(assetPath, platform = null) {
@@ -77,6 +84,7 @@ class AssetServer {
data.scales = record.scales;
data.files = record.files;
+
if (this._hashes.has(assetPath)) {
data.hash = this._hashes.get(assetPath);
return data;
@@ -98,8 +106,9 @@ class AssetServer {
});
}
- onFileChange(type, filePath) {
- this._hashes.delete(this._files.get(filePath));
+ _onFileChange(type, root, file) {
+ const asset = this._files.get(path.join(root, file));
+ this._hashes.delete(asset);
}
/**
@@ -178,10 +187,7 @@ class AssetServer {
}
const rootsString = roots.map(s => `'${s}'`).join(', ');
- throw new Error(
- `'${debugInfoFile}' could not be found, because '${dir}' is not a ` +
- `subdirectory of any of the roots (${rootsString})`,
- );
+ throw new Error(`'${debugInfoFile}' could not be found, because '${dir}' is not a subdirectory of any of the roots (${rootsString})`);
});
}
diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js
index 26e9b6646d5..4165edcd9ea 100644
--- a/packager/react-packager/src/Bundler/index.js
+++ b/packager/react-packager/src/Bundler/index.js
@@ -79,6 +79,10 @@ const validateOpts = declareOpts({
type: 'object',
required: false,
},
+ nonPersistent: {
+ type: 'boolean',
+ default: false,
+ },
assetRoots: {
type: 'array',
required: false,
@@ -87,9 +91,9 @@ const validateOpts = declareOpts({
type: 'array',
default: ['png'],
},
- watch: {
- type: 'boolean',
- default: false,
+ fileWatcher: {
+ type: 'object',
+ required: true,
},
assetServer: {
type: 'object',
@@ -120,9 +124,10 @@ type Options = {
resetCache: boolean,
transformModulePath: string,
extraNodeModules: {},
+ nonPersistent: boolean,
assetRoots: Array,
assetExts: Array,
- watch: boolean,
+ fileWatcher: {},
assetServer: AssetServer,
transformTimeoutInterval: ?number,
allowBundleUpdates: boolean,
@@ -186,7 +191,7 @@ class Bundler {
blacklistRE: opts.blacklistRE,
cache: this._cache,
extraNodeModules: opts.extraNodeModules,
- watch: opts.watch,
+ fileWatcher: opts.fileWatcher,
minifyCode: this._transformer.minify,
moduleFormat: opts.moduleFormat,
polyfillModuleNames: opts.polyfillModuleNames,
@@ -212,12 +217,9 @@ class Bundler {
}
}
- end() {
+ kill() {
this._transformer.kill();
- return Promise.all([
- this._cache.end(),
- this.getResolver().getDependencyGraph().getWatcher().end(),
- ]);
+ return this._cache.end();
}
bundle(options: {
diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js
index 47c28123e19..7588d2384b3 100644
--- a/packager/react-packager/src/Resolver/index.js
+++ b/packager/react-packager/src/Resolver/index.js
@@ -35,9 +35,9 @@ const validateOpts = declareOpts({
type: 'array',
default: [],
},
- watch: {
- type: 'boolean',
- default: false,
+ fileWatcher: {
+ type: 'object',
+ required: true,
},
assetExts: {
type: 'array',
@@ -101,13 +101,14 @@ class Resolver {
providesModuleNodeModules: defaults.providesModuleNodeModules,
platforms: defaults.platforms,
preferNativePlatform: true,
- watch: opts.watch,
+ fileWatcher: opts.fileWatcher,
cache: opts.cache,
shouldThrowOnUnresolvedErrors: (_, platform) => platform !== 'android',
transformCode: opts.transformCode,
transformCacheKey: opts.transformCacheKey,
extraNodeModules: opts.extraNodeModules,
assetDependencies: ['react-native/Libraries/Image/AssetRegistry'],
+ // for jest-haste-map
resetCache: options.resetCache,
moduleOptions: {
cacheTransformResults: true,
@@ -259,7 +260,7 @@ class Resolver {
return this._minifyCode(path, code, map);
}
- getDependencyGraph() {
+ getDependecyGraph() {
return this._depGraph;
}
}
diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js
index 44f0cddf2c3..d7dfc2aa2b7 100644
--- a/packager/react-packager/src/Server/__tests__/Server-test.js
+++ b/packager/react-packager/src/Server/__tests__/Server-test.js
@@ -21,6 +21,8 @@ jest.setMock('worker-farm', function() { return () => {}; })
.mock('../../node-haste')
.mock('../../Logger');
+let FileWatcher;
+
describe('processRequest', () => {
let SourceMapConsumer, Bundler, Server, AssetServer, Promise;
beforeEach(() => {
@@ -60,9 +62,12 @@ describe('processRequest', () => {
);
const invalidatorFunc = jest.fn();
+ const watcherFunc = jest.fn();
let requestHandler;
+ let triggerFileChange;
beforeEach(() => {
+ FileWatcher = require('../../node-haste').FileWatcher;
Bundler.prototype.bundle = jest.fn(() =>
Promise.resolve({
getSource: () => 'this is the source',
@@ -70,10 +75,19 @@ describe('processRequest', () => {
getEtag: () => 'this is an etag',
}));
+ FileWatcher.prototype.on = function(eventType, callback) {
+ if (eventType !== 'all') {
+ throw new Error('Can only handle "all" event in watcher.');
+ }
+ watcherFunc.apply(this, arguments);
+ triggerFileChange = callback;
+ return this;
+ };
+
Bundler.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.getResolver =
jest.fn().mockReturnValue({
- getDependencyGraph: jest.fn().mockReturnValue({
+ getDependecyGraph: jest.fn().mockReturnValue({
getHasteMap: jest.fn().mockReturnValue({on: jest.fn()}),
load: jest.fn(() => Promise.resolve()),
}),
@@ -83,7 +97,7 @@ describe('processRequest', () => {
requestHandler = server.processRequest.bind(server);
});
- it('returns JS bundle source on request of *.bundle', () => {
+ pit('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
@@ -93,7 +107,7 @@ describe('processRequest', () => {
);
});
- it('returns JS bundle source on request of *.bundle (compat)', () => {
+ pit('returns JS bundle source on request of *.bundle (compat)', () => {
return makeRequest(
requestHandler,
'mybundle.runModule.bundle'
@@ -102,7 +116,7 @@ describe('processRequest', () => {
);
});
- it('returns ETag header on request of *.bundle', () => {
+ pit('returns ETag header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
@@ -111,7 +125,7 @@ describe('processRequest', () => {
});
});
- it('returns 304 on request of *.bundle when if-none-match equals the ETag', () => {
+ pit('returns 304 on request of *.bundle when if-none-match equals the ETag', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
@@ -121,7 +135,7 @@ describe('processRequest', () => {
});
});
- it('returns sourcemap on request of *.map', () => {
+ pit('returns sourcemap on request of *.map', () => {
return makeRequest(
requestHandler,
'mybundle.map?runModule=true'
@@ -130,7 +144,7 @@ describe('processRequest', () => {
);
});
- it('works with .ios.js extension', () => {
+ pit('works with .ios.js extension', () => {
return makeRequest(
requestHandler,
'index.ios.includeRequire.bundle'
@@ -155,7 +169,7 @@ describe('processRequest', () => {
});
});
- it('passes in the platform param', function() {
+ pit('passes in the platform param', function() {
return makeRequest(
requestHandler,
'index.bundle?platform=ios'
@@ -180,7 +194,7 @@ describe('processRequest', () => {
});
});
- it('passes in the assetPlugin param', function() {
+ pit('passes in the assetPlugin param', function() {
return makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2'
@@ -205,13 +219,24 @@ describe('processRequest', () => {
});
});
+ pit('watches all files in projectRoot', () => {
+ return makeRequest(
+ requestHandler,
+ 'mybundle.bundle?runModule=true'
+ ).then(() => {
+ expect(watcherFunc.mock.calls[0][0]).toEqual('all');
+ expect(watcherFunc.mock.calls[0][1]).not.toBe(null);
+ });
+ });
+
describe('file changes', () => {
- it('invalides files in bundle when file is updated', () => {
+ pit('invalides files in bundle when file is updated', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(() => {
- server.onFileChange('all', options.projectRoots[0] + '/path/file.js');
+ const onFileChange = watcherFunc.mock.calls[0][1];
+ onFileChange('all','path/file.js', options.projectRoots[0]);
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
});
});
@@ -248,7 +273,7 @@ describe('processRequest', () => {
jest.runAllTicks();
- server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
+ triggerFileChange('all','path/file.js', options.projectRoots[0]);
jest.runAllTimers();
jest.runAllTicks();
@@ -297,7 +322,7 @@ describe('processRequest', () => {
jest.runAllTicks();
- server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
+ triggerFileChange('all','path/file.js', options.projectRoots[0]);
jest.runAllTimers();
jest.runAllTicks();
@@ -330,7 +355,7 @@ describe('processRequest', () => {
it('should hold on to request and inform on change', () => {
server.processRequest(req, res);
- server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
+ triggerFileChange('all', 'path/file.js', options.projectRoots[0]);
jest.runAllTimers();
expect(res.end).toBeCalledWith(JSON.stringify({changed: true}));
});
@@ -339,7 +364,7 @@ describe('processRequest', () => {
server.processRequest(req, res);
req.emit('close');
jest.runAllTimers();
- server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
+ triggerFileChange('all', 'path/file.js', options.projectRoots[0]);
jest.runAllTimers();
expect(res.end).not.toBeCalled();
});
@@ -403,7 +428,7 @@ describe('processRequest', () => {
});
describe('buildbundle(options)', () => {
- it('Calls the bundler with the correct args', () => {
+ pit('Calls the bundler with the correct args', () => {
return server.buildBundle({
entryFile: 'foo file'
}).then(() =>
@@ -426,7 +451,7 @@ describe('processRequest', () => {
});
describe('buildBundleFromUrl(options)', () => {
- it('Calls the bundler with the correct args', () => {
+ pit('Calls the bundler with the correct args', () => {
return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false')
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
@@ -449,7 +474,7 @@ describe('processRequest', () => {
});
describe('/symbolicate endpoint', () => {
- it('should symbolicate given stack trace', () => {
+ pit('should symbolicate given stack trace', () => {
const body = JSON.stringify({stack: [{
file: 'http://foo.bundle?platform=ios',
lineNumber: 2100,
@@ -483,7 +508,7 @@ describe('processRequest', () => {
});
});
- it('ignores `/debuggerWorker.js` stack frames', () => {
+ pit('ignores `/debuggerWorker.js` stack frames', () => {
const body = JSON.stringify({stack: [{
file: 'http://localhost:8081/debuggerWorker.js',
lineNumber: 123,
@@ -507,7 +532,7 @@ describe('processRequest', () => {
});
describe('/symbolicate handles errors', () => {
- it('should symbolicate given stack trace', () => {
+ pit('should symbolicate given stack trace', () => {
const body = 'clearly-not-json';
console.error = jest.fn();
diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js
index 7fdcffa9212..cf63c47e3d4 100644
--- a/packager/react-packager/src/Server/index.js
+++ b/packager/react-packager/src/Server/index.js
@@ -9,6 +9,7 @@
'use strict';
const AssetServer = require('../AssetServer');
+const FileWatcher = require('../node-haste').FileWatcher;
const getPlatformExtension = require('../node-haste').getPlatformExtension;
const Bundler = require('../Bundler');
const MultipartResponse = require('./MultipartResponse');
@@ -75,7 +76,7 @@ const validateOpts = declareOpts({
type: 'object',
required: false,
},
- watch: {
+ nonPersistent: {
type: 'boolean',
default: false,
},
@@ -199,32 +200,57 @@ const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
class Server {
constructor(options) {
const opts = this._opts = validateOpts(options);
- const processFileChange =
- ({type, filePath, stat}) => this.onFileChange(type, filePath, stat);
this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
this._fileChangeListeners = [];
+ const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext);
+
+ let watchRootConfigs = opts.projectRoots.map(dir => {
+ return {
+ dir: dir,
+ globs: [
+ '**/*.js',
+ '**/*.json',
+ ].concat(assetGlobs),
+ };
+ });
+
+ if (opts.assetRoots != null) {
+ watchRootConfigs = watchRootConfigs.concat(
+ opts.assetRoots.map(dir => {
+ return {
+ dir: dir,
+ globs: assetGlobs,
+ };
+ })
+ );
+ }
+
+ this._fileWatcher = options.nonPersistent
+ ? FileWatcher.createDummyWatcher()
+ : new FileWatcher(watchRootConfigs, {useWatchman: true});
+
this._assetServer = new AssetServer({
assetExts: opts.assetExts,
+ fileWatcher: this._fileWatcher,
projectRoots: opts.projectRoots,
});
const bundlerOpts = Object.create(opts);
+ bundlerOpts.fileWatcher = this._fileWatcher;
bundlerOpts.assetServer = this._assetServer;
- bundlerOpts.allowBundleUpdates = options.watch;
- bundlerOpts.watch = options.watch;
+ bundlerOpts.allowBundleUpdates = !options.nonPersistent;
this._bundler = new Bundler(bundlerOpts);
+ this._fileWatcher.on('all', this._onFileChange.bind(this));
+
// changes to the haste map can affect resolution of files in the bundle
- const dependencyGraph = this._bundler.getResolver().getDependencyGraph();
+ const dependencyGraph = this._bundler.getResolver().getDependecyGraph();
+
dependencyGraph.load().then(() => {
- dependencyGraph.getWatcher().on(
- 'change',
- ({eventsQueue}) => eventsQueue.forEach(processFileChange),
- );
dependencyGraph.getHasteMap().on('change', () => {
debug('Clearing bundle cache due to haste map change');
this._clearBundles();
@@ -256,7 +282,10 @@ class Server {
}
end() {
- return this._bundler.end();
+ Promise.all([
+ this._fileWatcher.end(),
+ this._bundler.kill(),
+ ]);
}
setHMRFileChangeListener(listener) {
@@ -270,7 +299,7 @@ class Server {
}
buildBundle(options) {
- return this._bundler.getResolver().getDependencyGraph().load().then(() => {
+ return this._bundler.getResolver().getDependecyGraph().load().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
@@ -341,9 +370,9 @@ class Server {
});
}
- onFileChange(type, filePath, stat) {
- this._assetServer.onFileChange(type, filePath, stat);
- this._bundler.invalidateFile(filePath);
+ _onFileChange(type, filepath, root) {
+ const absPath = path.join(root, filepath);
+ this._bundler.invalidateFile(absPath);
// If Hot Loading is enabled avoid rebuilding bundles and sending live
// updates. Instead, send the HMR updates right away and clear the bundles
@@ -351,26 +380,26 @@ class Server {
if (this._hmrFileChangeListener) {
// Clear cached bundles in case user reloads
this._clearBundles();
- this._hmrFileChangeListener(filePath, this._bundler.stat(filePath));
+ this._hmrFileChangeListener(absPath, this._bundler.stat(absPath));
return;
- } else if (type !== 'change' && filePath.indexOf(NODE_MODULES) !== -1) {
+ } else if (type !== 'change' && absPath.indexOf(NODE_MODULES) !== -1) {
// node module resolution can be affected by added or removed files
debug('Clearing bundles due to potential node_modules resolution change');
this._clearBundles();
}
Promise.all(
- this._fileChangeListeners.map(listener => listener(filePath))
+ this._fileChangeListeners.map(listener => listener(absPath))
).then(
- () => this._onFileChangeComplete(filePath),
- () => this._onFileChangeComplete(filePath)
+ () => this._onFileChangeComplete(absPath),
+ () => this._onFileChangeComplete(absPath)
);
}
- _onFileChangeComplete(filePath) {
+ _onFileChangeComplete(absPath) {
// Make sure the file watcher event runs through the system before
// we rebuild the bundles.
- this._debouncedFileChangeHandler(filePath);
+ this._debouncedFileChangeHandler(absPath);
}
_clearBundles() {
diff --git a/packager/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js b/packager/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js
index e72829a0c55..fc24c994fa1 100644
--- a/packager/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js
+++ b/packager/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js
@@ -54,14 +54,14 @@ class DeprecatedAssetMap {
}
}
- processFileChange(type, filePath, fstat) {
+ processFileChange(type, filePath, root, fstat) {
const name = assetName(filePath);
if (type === 'change' || type === 'delete') {
delete this._map[name];
}
if (type === 'change' || type === 'add') {
- this._processAsset(filePath);
+ this._processAsset(path.join(root, filePath));
}
}
}
diff --git a/packager/react-packager/src/node-haste/FileWatcher/__mocks__/sane.js b/packager/react-packager/src/node-haste/FileWatcher/__mocks__/sane.js
new file mode 100644
index 00000000000..f59fa9104eb
--- /dev/null
+++ b/packager/react-packager/src/node-haste/FileWatcher/__mocks__/sane.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+module.exports = {
+ WatchmanWatcher: jest.genMockFromModule('sane/src/watchman_watcher'),
+ NodeWatcher: jest.genMockFromModule('sane/src/node_watcher'),
+};
diff --git a/packager/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js b/packager/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js
new file mode 100644
index 00000000000..03f2ad03611
--- /dev/null
+++ b/packager/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+jest
+ .dontMock('util')
+ .dontMock('events')
+ .dontMock('../')
+ .setMock('child_process', {
+ execSync: () => '/usr/bin/watchman',
+ });
+
+describe('FileWatcher', () => {
+ let WatchmanWatcher;
+ let FileWatcher;
+ let config;
+
+ beforeEach(() => {
+ jest.resetModules();
+ const sane = require('sane');
+ WatchmanWatcher = sane.WatchmanWatcher;
+ WatchmanWatcher.prototype.once.mockImplementation(
+ (type, callback) => callback()
+ );
+
+ FileWatcher = require('../');
+
+ config = [{
+ dir: 'rootDir',
+ globs: [
+ '**/*.js',
+ '**/*.json',
+ ],
+ }];
+ });
+
+ pit('gets the watcher instance when ready', () => {
+ const fileWatcher = new FileWatcher(config);
+ return fileWatcher.getWatchers().then(watchers => {
+ watchers.forEach(watcher => {
+ expect(watcher instanceof WatchmanWatcher).toBe(true);
+ });
+ });
+ });
+
+ pit('emits events', () => {
+ let cb;
+ WatchmanWatcher.prototype.on.mockImplementation((type, callback) => {
+ cb = callback;
+ });
+ const fileWatcher = new FileWatcher(config);
+ const handler = jest.genMockFn();
+ fileWatcher.on('all', handler);
+ return fileWatcher.getWatchers().then(watchers => {
+ cb(1, 2, 3, 4);
+ jest.runAllTimers();
+ expect(handler.mock.calls[0]).toEqual([1, 2, 3, 4]);
+ });
+ });
+
+ pit('ends the watcher', () => {
+ const fileWatcher = new FileWatcher(config);
+ WatchmanWatcher.prototype.close.mockImplementation(callback => callback());
+
+ return fileWatcher.end().then(() => {
+ expect(WatchmanWatcher.prototype.close).toBeCalled();
+ });
+ });
+});
diff --git a/packager/react-packager/src/node-haste/FileWatcher/index.js b/packager/react-packager/src/node-haste/FileWatcher/index.js
new file mode 100644
index 00000000000..782aed442a9
--- /dev/null
+++ b/packager/react-packager/src/node-haste/FileWatcher/index.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+const EventEmitter = require('events').EventEmitter;
+const denodeify = require('denodeify');
+const sane = require('sane');
+const execSync = require('child_process').execSync;
+
+const MAX_WAIT_TIME = 360000;
+
+const detectWatcherClass = () => {
+ try {
+ execSync('watchman version', {stdio: 'ignore'});
+ return sane.WatchmanWatcher;
+ } catch (e) {}
+ return sane.NodeWatcher;
+};
+
+const WatcherClass = detectWatcherClass();
+
+let inited = false;
+
+class FileWatcher extends EventEmitter {
+
+ constructor(rootConfigs) {
+ if (inited) {
+ throw new Error('FileWatcher can only be instantiated once');
+ }
+ inited = true;
+
+ super();
+ this._watcherByRoot = Object.create(null);
+
+ const watcherPromises = rootConfigs.map((rootConfig) => {
+ return this._createWatcher(rootConfig);
+ });
+
+ this._loading = Promise.all(watcherPromises).then(watchers => {
+ watchers.forEach((watcher, i) => {
+ this._watcherByRoot[rootConfigs[i].dir] = watcher;
+ watcher.on(
+ 'all',
+ // args = (type, filePath, root, stat)
+ (...args) => this.emit('all', ...args)
+ );
+ });
+ return watchers;
+ });
+ }
+
+ getWatchers() {
+ return this._loading;
+ }
+
+ getWatcherForRoot(root) {
+ return this._loading.then(() => this._watcherByRoot[root]);
+ }
+
+ isWatchman() {
+ return Promise.resolve(FileWatcher.canUseWatchman());
+ }
+
+ end() {
+ inited = false;
+ return this._loading.then(
+ (watchers) => watchers.map(
+ watcher => denodeify(watcher.close).call(watcher)
+ )
+ );
+ }
+
+ _createWatcher(rootConfig) {
+ const watcher = new WatcherClass(rootConfig.dir, {
+ glob: rootConfig.globs,
+ dot: false,
+ });
+
+ return new Promise((resolve, reject) => {
+ const rejectTimeout = setTimeout(
+ () => reject(new Error(timeoutMessage(WatcherClass))),
+ MAX_WAIT_TIME
+ );
+
+ watcher.once('ready', () => {
+ clearTimeout(rejectTimeout);
+ resolve(watcher);
+ });
+ });
+ }
+
+ static createDummyWatcher() {
+ return Object.assign(new EventEmitter(), {
+ isWatchman: () => Promise.resolve(false),
+ end: () => Promise.resolve(),
+ });
+ }
+
+ static canUseWatchman() {
+ return WatcherClass == sane.WatchmanWatcher;
+ }
+}
+
+function timeoutMessage(Watcher) {
+ const lines = [
+ 'Watcher took too long to load (' + Watcher.name + ')',
+ ];
+ if (Watcher === sane.WatchmanWatcher) {
+ lines.push(
+ 'Try running `watchman version` from your terminal',
+ 'https://facebook.github.io/watchman/docs/troubleshooting.html',
+ );
+ }
+ return lines.join('\n');
+}
+
+module.exports = FileWatcher;
diff --git a/packager/react-packager/src/node-haste/ModuleCache.js b/packager/react-packager/src/node-haste/ModuleCache.js
index a963c24af6b..655b01d15c0 100644
--- a/packager/react-packager/src/node-haste/ModuleCache.js
+++ b/packager/react-packager/src/node-haste/ModuleCache.js
@@ -16,6 +16,8 @@ const Module = require('./Module');
const Package = require('./Package');
const Polyfill = require('./Polyfill');
+const path = require('path');
+
import type Cache from './Cache';
import type {
DepGraphHelpers,
@@ -67,6 +69,8 @@ class ModuleCache {
this._assetDependencies = assetDependencies;
this._moduleOptions = moduleOptions;
this._packageModuleMap = new WeakMap();
+
+ fastfs.on('change', this._processFileChange.bind(this));
}
getModule(filePath: string) {
@@ -145,14 +149,16 @@ class ModuleCache {
});
}
- processFileChange(type: string, filePath: string) {
- if (this._moduleCache[filePath]) {
- this._moduleCache[filePath].invalidate();
- delete this._moduleCache[filePath];
+ _processFileChange(type, filePath, root) {
+ const absPath = path.join(root, filePath);
+
+ if (this._moduleCache[absPath]) {
+ this._moduleCache[absPath].invalidate();
+ delete this._moduleCache[absPath];
}
- if (this._packageCache[filePath]) {
- this._packageCache[filePath].invalidate();
- delete this._packageCache[filePath];
+ if (this._packageCache[absPath]) {
+ this._packageCache[absPath].invalidate();
+ delete this._packageCache[absPath];
}
}
}
diff --git a/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js b/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js
index be472c9a837..8e58f387519 100644
--- a/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js
+++ b/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js
@@ -61,6 +61,12 @@ describe('DependencyGraph', function() {
jest.resetModules();
Module = require('../Module');
+ const fileWatcher = {
+ on: function() {
+ return this;
+ },
+ isWatchman: () => Promise.resolve(false),
+ };
const Cache = jest.genMockFn().mockImplementation(function() {
this._maps = Object.create(null);
@@ -103,6 +109,7 @@ describe('DependencyGraph', function() {
defaults = {
assetExts: ['png', 'jpg'],
cache: new Cache(),
+ fileWatcher,
forceNodeFilesystemAPI: true,
providesModuleNodeModules: [
'haste-fbjs',
@@ -4818,6 +4825,7 @@ describe('DependencyGraph', function() {
});
describe('file watch updating', function() {
+ var triggerFileChange;
var mockStat = {
isDirectory: () => false,
};
@@ -4828,6 +4836,21 @@ describe('DependencyGraph', function() {
beforeEach(function() {
process.platform = 'linux';
DependencyGraph = require('../index');
+
+ var callbacks = [];
+ triggerFileChange = (...args) =>
+ callbacks.map(callback => callback(...args));
+
+ defaults.fileWatcher = {
+ on: function(eventType, callback) {
+ if (eventType !== 'all') {
+ throw new Error('Can only handle "all" event in watcher.');
+ }
+ callbacks.push(callback);
+ return this;
+ },
+ isWatchman: () => Promise.resolve(false),
+ };
});
afterEach(function() {
@@ -4868,7 +4891,7 @@ describe('DependencyGraph', function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', '');
- dgraph.processFileChange('change', root + '/index.js', mockStat);
+ triggerFileChange('change', 'index.js', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
@@ -4931,7 +4954,7 @@ describe('DependencyGraph', function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', '');
- dgraph.processFileChange('change', root + '/index.js', mockStat);
+ triggerFileChange('change', 'index.js', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
@@ -4993,7 +5016,7 @@ describe('DependencyGraph', function() {
});
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
delete filesystem.root.foo;
- dgraph.processFileChange('delete', root + '/foo.js');
+ triggerFileChange('delete', 'foo.js', root);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
@@ -5060,10 +5083,10 @@ describe('DependencyGraph', function() {
' */',
'require("foo")',
].join('\n');
- dgraph.processFileChange('add', root + '/bar.js', mockStat);
+ triggerFileChange('add', 'bar.js', root, mockStat);
filesystem.root.aPackage['main.js'] = 'require("bar")';
- dgraph.processFileChange('change', root + '/aPackage/main.js', mockStat);
+ triggerFileChange('change', 'aPackage/main.js', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
@@ -5152,7 +5175,7 @@ describe('DependencyGraph', function() {
]);
filesystem.root['foo.png'] = '';
- dgraph.processFileChange('add', root + '/foo.png', mockStat);
+ triggerFileChange('add', 'foo.png', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
@@ -5222,7 +5245,7 @@ describe('DependencyGraph', function() {
]);
filesystem.root['foo.png'] = '';
- dgraph.processFileChange('add', root + '/foo.png', mockStat);
+ triggerFileChange('add', 'foo.png', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
@@ -5254,6 +5277,171 @@ describe('DependencyGraph', function() {
});
});
+ it('runs changes through ignore filter', function() {
+ var root = '/root';
+ var filesystem = setMockFileSystem({
+ 'root': {
+ 'index.js': [
+ '/**',
+ ' * @providesModule index',
+ ' */',
+ 'require("aPackage")',
+ 'require("foo")',
+ ].join('\n'),
+ 'foo.js': [
+ '/**',
+ ' * @providesModule foo',
+ ' */',
+ 'require("aPackage")',
+ ].join('\n'),
+ 'aPackage': {
+ 'package.json': JSON.stringify({
+ name: 'aPackage',
+ main: 'main.js',
+ }),
+ 'main.js': 'main',
+ },
+ },
+ });
+
+ var dgraph = new DependencyGraph({
+ ...defaults,
+ roots: [root],
+ ignoreFilePath: function(filePath) {
+ if (filePath === '/root/bar.js') {
+ return true;
+ }
+ return false;
+ },
+ });
+ return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
+ filesystem.root['bar.js'] = [
+ '/**',
+ ' * @providesModule bar',
+ ' */',
+ 'require("foo")',
+ ].join('\n');
+ triggerFileChange('add', 'bar.js', root, mockStat);
+
+ filesystem.root.aPackage['main.js'] = 'require("bar")';
+ triggerFileChange('change', 'aPackage/main.js', root, mockStat);
+
+ return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
+ expect(deps)
+ .toEqual([
+ {
+ id: 'index',
+ path: '/root/index.js',
+ dependencies: ['aPackage', 'foo'],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ {
+ id: 'aPackage/main.js',
+ path: '/root/aPackage/main.js',
+ dependencies: ['bar'],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ {
+ id: 'foo',
+ path: '/root/foo.js',
+ dependencies: ['aPackage'],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ ]);
+ });
+ });
+ });
+
+ it('should ignore directory updates', function() {
+ var root = '/root';
+ setMockFileSystem({
+ 'root': {
+ 'index.js': [
+ '/**',
+ ' * @providesModule index',
+ ' */',
+ 'require("aPackage")',
+ 'require("foo")',
+ ].join('\n'),
+ 'foo.js': [
+ '/**',
+ ' * @providesModule foo',
+ ' */',
+ 'require("aPackage")',
+ ].join('\n'),
+ 'aPackage': {
+ 'package.json': JSON.stringify({
+ name: 'aPackage',
+ main: 'main.js',
+ }),
+ 'main.js': 'main',
+ },
+ },
+ });
+ var dgraph = new DependencyGraph({
+ ...defaults,
+ roots: [root],
+ });
+ return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
+ triggerFileChange('change', 'aPackage', '/root', {
+ isDirectory: () => true,
+ });
+ return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
+ expect(deps)
+ .toEqual([
+ {
+ id: 'index',
+ path: '/root/index.js',
+ dependencies: ['aPackage', 'foo'],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ {
+ id: 'aPackage/main.js',
+ path: '/root/aPackage/main.js',
+ dependencies: [],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ {
+ id: 'foo',
+ path: '/root/foo.js',
+ dependencies: ['aPackage'],
+ isAsset: false,
+ isAsset_DEPRECATED: false,
+ isJSON: false,
+ isPolyfill: false,
+ resolution: undefined,
+ resolveDependency: undefined,
+ },
+ ]);
+ });
+ });
+ });
+
it('changes to browser field', function() {
var root = '/root';
var filesystem = setMockFileSystem({
@@ -5285,7 +5473,7 @@ describe('DependencyGraph', function() {
main: 'main.js',
browser: 'browser.js',
});
- dgraph.processFileChange('change', root + '/aPackage/package.json', mockStat);
+ triggerFileChange('change', 'package.json', '/root/aPackage', mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
@@ -5347,7 +5535,7 @@ describe('DependencyGraph', function() {
name: 'bPackage',
main: 'main.js',
});
- dgraph.processFileChange('change', root + '/aPackage/package.json', mockStat);
+ triggerFileChange('change', 'package.json', '/root/aPackage', mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
@@ -5442,7 +5630,7 @@ describe('DependencyGraph', function() {
]);
filesystem.root.node_modules.foo['main.js'] = 'lol';
- dgraph.processFileChange('change', root + '/node_modules/foo/main.js', mockStat);
+ triggerFileChange('change', 'main.js', '/root/node_modules/foo', mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
@@ -5507,7 +5695,7 @@ describe('DependencyGraph', function() {
main: 'main.js',
browser: 'browser.js',
});
- dgraph.processFileChange('change', root + '/node_modules/foo/package.json', mockStat);
+ triggerFileChange('change', 'package.json', '/root/node_modules/foo', mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
@@ -5564,7 +5752,7 @@ describe('DependencyGraph', function() {
});
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
- dgraph.processFileChange('add', root + '/index.js', mockStat);
+ triggerFileChange('add', 'index.js', root, mockStat);
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js');
});
});
diff --git a/packager/react-packager/src/node-haste/__tests__/Module-test.js b/packager/react-packager/src/node-haste/__tests__/Module-test.js
index 53afd8a56bb..f7aad9a09a1 100644
--- a/packager/react-packager/src/node-haste/__tests__/Module-test.js
+++ b/packager/react-packager/src/node-haste/__tests__/Module-test.js
@@ -47,6 +47,10 @@ function mockIndexFile(indexJs) {
}
describe('Module', () => {
+ const fileWatcher = {
+ on: () => this,
+ isWatchman: () => Promise.resolve(false),
+ };
const fileName = '/root/index.js';
let cache, fastfs;
@@ -81,6 +85,7 @@ describe('Module', () => {
new Fastfs(
'test',
['/root'],
+ fileWatcher,
['/root/index.js', '/root/package.json'],
{ignore: []},
);
@@ -114,22 +119,22 @@ describe('Module', () => {
mockIndexFile(source);
});
- it('extracts the module name from the header', () =>
+ pit('extracts the module name from the header', () =>
module.getName().then(name => expect(name).toEqual(moduleId))
);
- it('identifies the module as haste module', () =>
+ pit('identifies the module as haste module', () =>
module.isHaste().then(isHaste => expect(isHaste).toBe(true))
);
- it('does not transform the file in order to access the name', () => {
+ pit('does not transform the file in order to access the name', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).getName()
.then(() => expect(transformCode).not.toBeCalled());
});
- it('does not transform the file in order to access the haste status', () => {
+ pit('does not transform the file in order to access the haste status', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).isHaste()
@@ -142,22 +147,22 @@ describe('Module', () => {
mockIndexFile(source.replace(/@providesModule/, '@provides'));
});
- it('extracts the module name from the header if it has a @provides annotation', () =>
+ pit('extracts the module name from the header if it has a @provides annotation', () =>
module.getName().then(name => expect(name).toEqual(moduleId))
);
- it('identifies the module as haste module', () =>
+ pit('identifies the module as haste module', () =>
module.isHaste().then(isHaste => expect(isHaste).toBe(true))
);
- it('does not transform the file in order to access the name', () => {
+ pit('does not transform the file in order to access the name', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).getName()
.then(() => expect(transformCode).not.toBeCalled());
});
- it('does not transform the file in order to access the haste status', () => {
+ pit('does not transform the file in order to access the haste status', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).isHaste()
@@ -170,22 +175,22 @@ describe('Module', () => {
mockIndexFile('arbitrary(code);');
});
- it('uses the file name as module name', () =>
+ pit('uses the file name as module name', () =>
module.getName().then(name => expect(name).toEqual(fileName))
);
- it('does not identify the module as haste module', () =>
+ pit('does not identify the module as haste module', () =>
module.isHaste().then(isHaste => expect(isHaste).toBe(false))
);
- it('does not transform the file in order to access the name', () => {
+ pit('does not transform the file in order to access the name', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).getName()
.then(() => expect(transformCode).not.toBeCalled());
});
- it('does not transform the file in order to access the haste status', () => {
+ pit('does not transform the file in order to access the haste status', () => {
const transformCode =
jest.genMockFn().mockReturnValue(Promise.resolve());
return createModule({transformCode}).isHaste()
@@ -200,12 +205,12 @@ describe('Module', () => {
mockIndexFile(fileContents);
});
- it('exposes file contents as `code` property on the data exposed by `read()`', () =>
+ pit('exposes file contents as `code` property on the data exposed by `read()`', () =>
createModule().read().then(({code}) =>
expect(code).toBe(fileContents))
);
- it('exposes file contents via the `getCode()` method', () =>
+ pit('exposes file contents via the `getCode()` method', () =>
createModule().getCode().then(code =>
expect(code).toBe(fileContents))
);
@@ -236,7 +241,7 @@ describe('Module', () => {
mockIndexFile(fileContents);
});
- it('passes the module and file contents to the transform function when reading', () => {
+ pit('passes the module and file contents to the transform function when reading', () => {
const module = createModule({transformCode});
return module.read()
.then(() => {
@@ -244,7 +249,7 @@ describe('Module', () => {
});
});
- it('passes any additional options to the transform function when reading', () => {
+ pit('passes any additional options to the transform function when reading', () => {
const module = createModule({transformCode});
const transformOptions = {arbitrary: Object()};
return module.read(transformOptions)
@@ -253,7 +258,7 @@ describe('Module', () => {
);
});
- it('passes module and file contents if the file is annotated with @extern', () => {
+ pit('passes the module and file contents to the transform if the file is annotated with @extern', () => {
const module = createModule({transformCode});
const customFileContents = `
/**
@@ -266,7 +271,7 @@ describe('Module', () => {
});
});
- it('passes the module and file contents to the transform for JSON files', () => {
+ pit('passes the module and file contents to the transform for JSON files', () => {
mockPackageFile();
const module = createJSONModule({transformCode});
return module.read().then(() => {
@@ -274,7 +279,7 @@ describe('Module', () => {
});
});
- it('does not extend the passed options object if the file is annotated with @extern', () => {
+ pit('does not extend the passed options object if the file is annotated with @extern', () => {
const module = createModule({transformCode});
const customFileContents = `
/**
@@ -290,7 +295,7 @@ describe('Module', () => {
});
});
- it('does not extend the passed options object for JSON files', () => {
+ pit('does not extend the passed options object for JSON files', () => {
mockPackageFile();
const module = createJSONModule({transformCode});
const options = {arbitrary: 'foo'};
@@ -301,7 +306,7 @@ describe('Module', () => {
});
});
- it('uses dependencies that `transformCode` resolves to, instead of extracting them', () => {
+ pit('uses dependencies that `transformCode` resolves to, instead of extracting them', () => {
const mockedDependencies = ['foo', 'bar'];
transformResult = {
code: exampleCode,
@@ -314,7 +319,7 @@ describe('Module', () => {
});
});
- it('forwards all additional properties of the result provided by `transformCode`', () => {
+ pit('forwards all additional properties of the result provided by `transformCode`', () => {
transformResult = {
code: exampleCode,
arbitrary: 'arbitrary',
@@ -329,7 +334,7 @@ describe('Module', () => {
});
});
- it('only stores dependencies if `cacheTransformResults` option is disabled', () => {
+ pit('does not store anything but dependencies if the `cacheTransformResults` option is disabled', () => {
transformResult = {
code: exampleCode,
arbitrary: 'arbitrary',
@@ -349,7 +354,7 @@ describe('Module', () => {
});
});
- it('stores all things if options is undefined', () => {
+ pit('stores all things if options is undefined', () => {
transformResult = {
code: exampleCode,
arbitrary: 'arbitrary',
@@ -365,7 +370,7 @@ describe('Module', () => {
});
});
- it('exposes the transformed code rather than the raw file contents', () => {
+ pit('exposes the transformed code rather than the raw file contents', () => {
transformResult = {code: exampleCode};
const module = createModule({transformCode});
return Promise.all([module.read(), module.getCode()])
@@ -375,13 +380,13 @@ describe('Module', () => {
});
});
- it('exposes the raw file contents as `source` property', () => {
+ pit('exposes the raw file contents as `source` property', () => {
const module = createModule({transformCode});
return module.read()
.then(data => expect(data.source).toBe(fileContents));
});
- it('exposes a source map returned by the transform', () => {
+ pit('exposes a source map returned by the transform', () => {
const map = {version: 3};
transformResult = {map, code: exampleCode};
const module = createModule({transformCode});
@@ -392,7 +397,7 @@ describe('Module', () => {
});
});
- it('caches the transform result for the same transform options', () => {
+ pit('caches the transform result for the same transform options', () => {
let module = createModule({transformCode});
return module.read()
.then(() => {
@@ -407,7 +412,7 @@ describe('Module', () => {
});
});
- it('triggers a new transform for different transform options', () => {
+ pit('triggers a new transform for different transform options', () => {
const module = createModule({transformCode});
return module.read({foo: 1})
.then(() => {
@@ -419,7 +424,7 @@ describe('Module', () => {
});
});
- it('triggers a new transform for different source code', () => {
+ pit('triggers a new transform for different source code', () => {
let module = createModule({transformCode});
return module.read()
.then(() => {
@@ -435,7 +440,7 @@ describe('Module', () => {
});
});
- it('triggers a new transform for different transform cache key', () => {
+ pit('triggers a new transform for different transform cache key', () => {
let module = createModule({transformCode});
return module.read()
.then(() => {
diff --git a/packager/react-packager/src/node-haste/fastfs.js b/packager/react-packager/src/node-haste/fastfs.js
index 58f381e2ade..e8e8220b5c2 100644
--- a/packager/react-packager/src/node-haste/fastfs.js
+++ b/packager/react-packager/src/node-haste/fastfs.js
@@ -18,6 +18,10 @@ const {EventEmitter} = require('events');
const NOT_FOUND_IN_ROOTS = 'NotFoundInRootsError';
+interface FileWatcher {
+ on(event: 'all', handler: (type: string, filePath: string, rootPath: string, fstat: fs.Stats) => void): void,
+}
+
const {
createActionStartEntry,
createActionEndEntry,
@@ -28,6 +32,7 @@ const {
class Fastfs extends EventEmitter {
_name: string;
+ _fileWatcher: FileWatcher;
_ignore: (filePath: string) => boolean;
_roots: Array;
_fastPaths: {[filePath: string]: File};
@@ -35,6 +40,7 @@ class Fastfs extends EventEmitter {
constructor(
name: string,
roots: Array,
+ fileWatcher: FileWatcher,
files: Array,
{ignore}: {
ignore: (filePath: string) => boolean,
@@ -42,6 +48,7 @@ class Fastfs extends EventEmitter {
) {
super();
this._name = name;
+ this._fileWatcher = fileWatcher;
this._ignore = ignore;
this._roots = roots.map(root => {
// If the path ends in a separator ("/"), remove it to make string
@@ -75,6 +82,10 @@ class Fastfs extends EventEmitter {
});
print(log(createActionEndEntry(buildingInMemoryFSLogEntry)));
+
+ if (this._fileWatcher) {
+ this._fileWatcher.on('all', this._processFileChange.bind(this));
+ }
}
stat(filePath: string) {
@@ -194,23 +205,33 @@ class Fastfs extends EventEmitter {
return this._fastPaths[filePath];
}
- processFileChange(type: string, filePath: string) {
+ _processFileChange(type, filePath, rootPath, fstat) {
+ const absPath = path.join(rootPath, filePath);
+ if (this._ignore(absPath) || (fstat && fstat.isDirectory())) {
+ return;
+ }
+
+ // Make sure this event belongs to one of our roots.
+ const root = this._getRoot(absPath);
+ if (!root) {
+ return;
+ }
+
if (type === 'delete' || type === 'change') {
- const file = this._getFile(filePath);
+ const file = this._getFile(absPath);
if (file) {
file.remove();
}
}
- delete this._fastPaths[path.resolve(filePath)];
+ delete this._fastPaths[path.resolve(absPath)];
if (type !== 'delete') {
- const file = new File(filePath, false);
- const root = this._getRoot(filePath);
- if (root) {
- root.addChild(file, this._fastPaths);
- }
+ const file = new File(absPath, false);
+ root.addChild(file, this._fastPaths);
}
+
+ this.emit('change', type, filePath, rootPath, fstat);
}
}
diff --git a/packager/react-packager/src/node-haste/index.js b/packager/react-packager/src/node-haste/index.js
index 1397723bdcf..9946e90aae3 100644
--- a/packager/react-packager/src/node-haste/index.js
+++ b/packager/react-packager/src/node-haste/index.js
@@ -15,6 +15,7 @@ const Cache = require('./Cache');
const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers');
const DeprecatedAssetMap = require('./DependencyGraph/DeprecatedAssetMap');
const Fastfs = require('./fastfs');
+const FileWatcher = require('./FileWatcher');
const HasteMap = require('./DependencyGraph/HasteMap');
const JestHasteMap = require('jest-haste-map');
const Module = require('./Module');
@@ -46,16 +47,12 @@ const {
print,
} = require('../Logger');
-const escapePath = (p: string) => {
- return (path.sep === '\\') ? p.replace(/(\/|\\(?!\.))/g, '\\\\') : p;
-};
-
class DependencyGraph {
_opts: {
roots: Array,
ignoreFilePath: (filePath: string) => boolean,
- watch: boolean,
+ fileWatcher: FileWatcher,
forceNodeFilesystemAPI: boolean,
assetRoots_DEPRECATED: Array,
assetExts: Array,
@@ -74,23 +71,21 @@ class DependencyGraph {
maxWorkers: number,
resetCache: boolean,
};
- _assetDependencies: mixed;
- _assetPattern: RegExp;
_cache: Cache;
- _deprecatedAssetMap: DeprecatedAssetMap;
- _fastfs: Fastfs;
- _haste: JestHasteMap;
- _hasteMap: HasteMap;
- _hasteMapError: ?Error;
+ _assetDependencies: mixed;
_helpers: DependencyGraphHelpers;
+ _fastfs: Fastfs;
_moduleCache: ModuleCache;
+ _hasteMap: HasteMap;
+ _deprecatedAssetMap: DeprecatedAssetMap;
+ _hasteMapError: ?Error;
- _loading: Promise;
+ _loading: ?Promise;
constructor({
roots,
ignoreFilePath,
- watch,
+ fileWatcher,
forceNodeFilesystemAPI,
assetRoots_DEPRECATED,
assetExts,
@@ -114,7 +109,7 @@ class DependencyGraph {
}: {
roots: Array,
ignoreFilePath: (filePath: string) => boolean,
- watch: boolean,
+ fileWatcher: FileWatcher,
forceNodeFilesystemAPI?: boolean,
assetRoots_DEPRECATED: Array,
assetExts: Array,
@@ -138,7 +133,7 @@ class DependencyGraph {
this._opts = {
roots,
ignoreFilePath: ignoreFilePath || (() => {}),
- watch: !!watch,
+ fileWatcher,
forceNodeFilesystemAPI: !!forceNodeFilesystemAPI,
assetRoots_DEPRECATED: assetRoots_DEPRECATED || [],
assetExts: assetExts || [],
@@ -160,9 +155,6 @@ class DependencyGraph {
maxWorkers,
resetCache,
};
- this._assetPattern =
- new RegExp('^' + this._opts.assetRoots_DEPRECATED.map(escapePath).join('|'));
-
this._cache = cache;
this._assetDependencies = assetDependencies;
this._helpers = new DependencyGraphHelpers(this._opts);
@@ -175,7 +167,7 @@ class DependencyGraph {
}
const mw = this._opts.maxWorkers;
- this._haste = new JestHasteMap({
+ const haste = new JestHasteMap({
extensions: this._opts.extensions.concat(this._opts.assetExts),
forceNodeFilesystemAPI: this._opts.forceNodeFilesystemAPI,
ignorePattern: {test: this._opts.ignoreFilePath},
@@ -188,10 +180,9 @@ class DependencyGraph {
retainAllFiles: true,
roots: this._opts.roots.concat(this._opts.assetRoots_DEPRECATED),
useWatchman: this._opts.useWatchman,
- watch: this._opts.watch,
});
- this._loading = this._haste.build().then(hasteMap => {
+ this._loading = haste.build().then(hasteMap => {
const initializingPackagerLogEntry =
print(log(createActionStartEntry('Initializing Packager')));
@@ -200,12 +191,15 @@ class DependencyGraph {
this._fastfs = new Fastfs(
'JavaScript',
this._opts.roots,
+ this._opts.fileWatcher,
hasteFSFiles,
{
ignore: this._opts.ignoreFilePath,
}
);
+ this._fastfs.on('change', this._processFileChange.bind(this));
+
this._moduleCache = new ModuleCache({
fastfs: this._fastfs,
cache: this._cache,
@@ -225,7 +219,14 @@ class DependencyGraph {
platforms: this._opts.platforms,
});
- const assetFiles = hasteMap.hasteFS.matchFiles(this._assetPattern);
+ const escapePath = (p: string) => {
+ return (path.sep === '\\') ? p.replace(/(\/|\\(?!\.))/g, '\\\\') : p;
+ };
+
+ const assetPattern =
+ new RegExp('^' + this._opts.assetRoots_DEPRECATED.map(escapePath).join('|'));
+
+ const assetFiles = hasteMap.hasteFS.matchFiles(assetPattern);
this._deprecatedAssetMap = new DeprecatedAssetMap({
helpers: this._helpers,
@@ -234,11 +235,11 @@ class DependencyGraph {
files: assetFiles,
});
- this._haste.on('change', ({eventsQueue}) =>
- eventsQueue.forEach(({type, filePath, stat}) =>
- this.processFileChange(type, filePath, stat)
- )
- );
+ this._fastfs.on('change', (type, filePath, root, fstat) => {
+ if (assetPattern.test(path.join(root, filePath))) {
+ this._deprecatedAssetMap.processFileChange(type, filePath, root, fstat);
+ }
+ });
const buildingHasteMapLogEntry =
print(log(createActionStartEntry('Building Haste Map')));
@@ -278,10 +279,6 @@ class DependencyGraph {
return this._fastfs;
}
- getWatcher() {
- return this._haste;
- }
-
/**
* Returns the module object for the given path.
*/
@@ -368,16 +365,21 @@ class DependencyGraph {
);
}
- processFileChange(type: string, filePath: string, stat: Object) {
- this._fastfs.processFileChange(type, filePath, stat);
- this._moduleCache.processFileChange(type, filePath, stat);
- if (this._assetPattern.test(filePath)) {
- this._deprecatedAssetMap.processFileChange(type, filePath, stat);
+ _processFileChange(type, filePath, root, fstat) {
+ const absPath = path.join(root, filePath);
+ if (fstat && fstat.isDirectory() ||
+ this._opts.ignoreFilePath(absPath) ||
+ this._helpers.isNodeModulesDir(absPath)) {
+ return;
}
- // This code reports failures but doesn't block recovery in the dev server
- // mode. When the hasteMap is left in an incorrect state, we'll rebuild when
- // the next file changes.
+ // Ok, this is some tricky promise code. Our requirements are:
+ // * we need to report back failures
+ // * failures shouldn't block recovery
+ // * Errors can leave `hasteMap` in an incorrect state, and we need to rebuild
+ // After we process a file change we record any errors which will also be
+ // reported via the next request. On the next file change, we'll see that
+ // we are in an error state and we should decide to do a full rebuild.
const resolve = () => {
if (this._hasteMapError) {
console.warn(
@@ -389,14 +391,13 @@ class DependencyGraph {
// Rebuild the entire map if last change resulted in an error.
this._loading = this._hasteMap.build();
} else {
- this._loading = this._hasteMap.processFileChange(type, filePath);
- this._loading.catch(error => {
- this._hasteMapError = error;
- });
+ this._loading = this._hasteMap.processFileChange(type, absPath);
+ this._loading.catch((e) => {this._hasteMapError = e;});
}
return this._loading;
};
-
+ /* $FlowFixMe: there is a risk this happen before we assign that
+ * variable in the load() function. */
this._loading = this._loading.then(resolve, resolve);
}
@@ -410,6 +411,7 @@ class DependencyGraph {
static Cache;
static Fastfs;
+ static FileWatcher;
static Module;
static Polyfill;
static getAssetDataFromName;
@@ -422,6 +424,7 @@ class DependencyGraph {
Object.assign(DependencyGraph, {
Cache,
Fastfs,
+ FileWatcher,
Module,
Polyfill,
getAssetDataFromName,