diff --git a/react-packager/index.js b/react-packager/index.js index ef143264..240c5fab 100644 --- a/react-packager/index.js +++ b/react-packager/index.js @@ -10,7 +10,7 @@ require('../babelRegisterOnly')([/react-packager\/src/]); -require('node-haste/lib/fastpath').replace(); +require('./src/node-haste/fastpath').replace(); useGracefulFs(); var debug = require('debug'); diff --git a/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 7f59253c..9ee5a1e9 100644 --- a/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -23,8 +23,8 @@ var fs = require('fs'); describe('AssetServer', () => { beforeEach(() => { - const NodeHaste = require('node-haste'); - NodeHaste.getAssetDataFromName = require.requireActual('node-haste/lib/lib/getAssetDataFromName'); + const NodeHaste = require('../../node-haste'); + NodeHaste.getAssetDataFromName = require.requireActual('../../node-haste/lib/getAssetDataFromName'); }); describe('assetServer.get', () => { diff --git a/react-packager/src/AssetServer/index.js b/react-packager/src/AssetServer/index.js index 88a7bebe..f03196cc 100644 --- a/react-packager/src/AssetServer/index.js +++ b/react-packager/src/AssetServer/index.js @@ -13,7 +13,7 @@ const Promise = require('promise'); const crypto = require('crypto'); const declareOpts = require('../lib/declareOpts'); const fs = require('fs'); -const getAssetDataFromName = require('node-haste').getAssetDataFromName; +const getAssetDataFromName = require('../node-haste').getAssetDataFromName; const path = require('path'); const createTimeoutPromise = (timeout) => new Promise((resolve, reject) => { diff --git a/react-packager/src/Bundler/__tests__/Bundler-test.js b/react-packager/src/Bundler/__tests__/Bundler-test.js index eef2dab5..bbf71a30 100644 --- a/react-packager/src/Bundler/__tests__/Bundler-test.js +++ b/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -17,7 +17,7 @@ jest .mock('fs') .mock('assert') .mock('progress') - .mock('node-haste') + .mock('../../node-haste') .mock('../../JSTransformer') .mock('../../lib/declareOpts') .mock('../../Resolver') diff --git a/react-packager/src/Bundler/index.js b/react-packager/src/Bundler/index.js index 8e7f21cd..404503bd 100644 --- a/react-packager/src/Bundler/index.js +++ b/react-packager/src/Bundler/index.js @@ -13,7 +13,7 @@ const fs = require('fs'); const path = require('path'); const Promise = require('promise'); const ProgressBar = require('progress'); -const Cache = require('node-haste').Cache; +const Cache = require('../node-haste').Cache; const Transformer = require('../JSTransformer'); const Resolver = require('../Resolver'); const Bundle = require('./Bundle'); diff --git a/react-packager/src/Resolver/__tests__/Resolver-test.js b/react-packager/src/Resolver/__tests__/Resolver-test.js index c86d1853..c6166548 100644 --- a/react-packager/src/Resolver/__tests__/Resolver-test.js +++ b/react-packager/src/Resolver/__tests__/Resolver-test.js @@ -13,7 +13,7 @@ jest.mock('path'); const DependencyGraph = jest.fn(); -jest.setMock('node-haste', DependencyGraph); +jest.setMock('../../node-haste', DependencyGraph); let Module; let Polyfill; @@ -36,7 +36,7 @@ describe('Resolver', function() { return polyfill; }); - DependencyGraph.replacePatterns = require.requireActual('node-haste/lib/lib/replacePatterns'); + DependencyGraph.replacePatterns = require.requireActual('../../node-haste/lib/replacePatterns'); DependencyGraph.prototype.createPolyfill = jest.fn(); DependencyGraph.prototype.getDependencies = jest.fn(); diff --git a/react-packager/src/Resolver/index.js b/react-packager/src/Resolver/index.js index 5d577e3d..799dece2 100644 --- a/react-packager/src/Resolver/index.js +++ b/react-packager/src/Resolver/index.js @@ -11,7 +11,7 @@ const path = require('path'); const Activity = require('../Activity'); -const DependencyGraph = require('node-haste'); +const DependencyGraph = require('../node-haste'); const declareOpts = require('../lib/declareOpts'); const Promise = require('promise'); diff --git a/react-packager/src/Server/__tests__/Server-test.js b/react-packager/src/Server/__tests__/Server-test.js index 42e8760c..6d841d91 100644 --- a/react-packager/src/Server/__tests__/Server-test.js +++ b/react-packager/src/Server/__tests__/Server-test.js @@ -18,7 +18,7 @@ jest.setMock('worker-farm', function() { return () => {}; }) .mock('../../Bundler') .mock('../../AssetServer') .mock('../../lib/declareOpts') - .mock('node-haste') + .mock('../../node-haste') .mock('../../Activity'); let FileWatcher; @@ -64,7 +64,7 @@ describe('processRequest', () => { let triggerFileChange; beforeEach(() => { - FileWatcher = require('node-haste').FileWatcher; + FileWatcher = require('../../node-haste').FileWatcher; Bundler.prototype.bundle = jest.fn(() => Promise.resolve({ getSource: () => 'this is the source', diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index 41febf63..faf17b62 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -10,8 +10,8 @@ const Activity = require('../Activity'); const AssetServer = require('../AssetServer'); -const FileWatcher = require('node-haste').FileWatcher; -const getPlatformExtension = require('node-haste').getPlatformExtension; +const FileWatcher = require('../node-haste').FileWatcher; +const getPlatformExtension = require('../node-haste').getPlatformExtension; const Bundler = require('../Bundler'); const Promise = require('promise'); const SourceMapConsumer = require('source-map').SourceMapConsumer; diff --git a/react-packager/src/__mocks__/fs.js b/react-packager/src/__mocks__/fs.js index adcbf11a..7071b1da 100644 --- a/react-packager/src/__mocks__/fs.js +++ b/react-packager/src/__mocks__/fs.js @@ -8,4 +8,4 @@ */ 'use strict'; -module.exports = require.requireActual('node-haste/mocks/graceful-fs'); +module.exports = require.requireActual('../node-haste/__mocks__/graceful-fs'); diff --git a/react-packager/src/node-haste/AssetModule.js b/react-packager/src/node-haste/AssetModule.js new file mode 100644 index 00000000..c1b48cde --- /dev/null +++ b/react-packager/src/node-haste/AssetModule.js @@ -0,0 +1,47 @@ +'use strict'; + +const Module = require('./Module'); +const getAssetDataFromName = require('./lib/getAssetDataFromName'); + +class AssetModule extends Module { + constructor(args, platforms) { + super(args); + const { resolution, name, type } = getAssetDataFromName(this.path, platforms); + this.resolution = resolution; + this._name = name; + this._type = type; + this._dependencies = args.dependencies || []; + } + + isHaste() { + return Promise.resolve(false); + } + + getDependencies() { + return Promise.resolve(this._dependencies); + } + + read() { + return Promise.resolve({}); + } + + getName() { + return super.getName().then( + id => id.replace(/\/[^\/]+$/, `/${this._name}.${this._type}`) + ); + } + + hash() { + return `AssetModule : ${this.path}`; + } + + isJSON() { + return false; + } + + isAsset() { + return true; + } +} + +module.exports = AssetModule; diff --git a/react-packager/src/node-haste/AssetModule_DEPRECATED.js b/react-packager/src/node-haste/AssetModule_DEPRECATED.js new file mode 100644 index 00000000..cfbbd16d --- /dev/null +++ b/react-packager/src/node-haste/AssetModule_DEPRECATED.js @@ -0,0 +1,45 @@ +'use strict'; + +const Module = require('./Module'); +const getAssetDataFromName = require('./lib/getAssetDataFromName'); + +class AssetModule_DEPRECATED extends Module { + constructor(args, platforms) { + super(args); + const {resolution, name} = getAssetDataFromName(this.path, platforms); + this.resolution = resolution; + this.name = name; + this.platforms = platforms; + } + + isHaste() { + return Promise.resolve(false); + } + + getName() { + return Promise.resolve(`image!${this.name}`); + } + + getDependencies() { + return Promise.resolve([]); + } + + hash() { + return `AssetModule_DEPRECATED : ${this.path}`; + } + + isJSON() { + return false; + } + + isAsset_DEPRECATED() { + return true; + } + + resolution() { + return getAssetDataFromName(this.path, this.platforms).resolution; + } + +} + +module.exports = AssetModule_DEPRECATED; diff --git a/react-packager/src/node-haste/Cache/__mocks__/index.js b/react-packager/src/node-haste/Cache/__mocks__/index.js new file mode 100644 index 00000000..6f7632f6 --- /dev/null +++ b/react-packager/src/node-haste/Cache/__mocks__/index.js @@ -0,0 +1,20 @@ +/** + * 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'; + +class Cache { + get(filepath, field, cb) { + return cb(filepath); + } + + invalidate(filepath) { } + end() { } +} + +module.exports = Cache; diff --git a/react-packager/src/node-haste/Cache/__tests__/Cache-test.js b/react-packager/src/node-haste/Cache/__tests__/Cache-test.js new file mode 100644 index 00000000..2c8b0f6f --- /dev/null +++ b/react-packager/src/node-haste/Cache/__tests__/Cache-test.js @@ -0,0 +1,336 @@ +/** + * 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('absolute-path') + .dontMock('../'); + +jest + .mock('fs') + .setMock('os', { + tmpDir() { return 'tmpDir'; }, + }); + +jest.useRealTimers(); + + +describe('Cache', () => { + let Cache, fs; + beforeEach(() => { + Cache = require('../'); + fs = require('graceful-fs'); + }); + + describe('getting/setting', () => { + pit('calls loader callback for uncached file', () => { + fs.stat.mockImpl((file, callback) => { + callback(null, { + mtime: { + getTime: () => {}, + }, + }); + }); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => Promise.resolve()); + + return cache + .get('/rootDir/someFile', 'field', loaderCb) + .then($ => + expect(loaderCb).toBeCalledWith('/rootDir/someFile') + ); + }); + + pit('supports storing multiple fields', () => { + fs.stat.mockImpl((file, callback) => { + callback(null, { + mtime: { + getTime: () => {}, + }, + }); + }); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var index = 0; + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve(index++) + ); + + return cache + .get('/rootDir/someFile', 'field1', loaderCb) + .then(value => { + expect(value).toBe(0); + return cache + .get('/rootDir/someFile', 'field2', loaderCb) + .then(value2 => expect(value2).toBe(1)); + }); + }); + + pit('gets the value from the loader callback', () => { + fs.stat.mockImpl((file, callback) => + callback(null, { + mtime: { + getTime: () => {}, + }, + }) + ); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('lol') + ); + + return cache + .get('/rootDir/someFile', 'field', loaderCb) + .then(value => expect(value).toBe('lol')); + }); + + pit('caches the value after the first call', () => { + fs.stat.mockImpl((file, callback) => { + callback(null, { + mtime: { + getTime: () => {}, + }, + }); + }); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('lol') + ); + + return cache + .get('/rootDir/someFile', 'field', loaderCb) + .then(() => { + var shouldNotBeCalled = jest.genMockFn(); + return cache.get('/rootDir/someFile', 'field', shouldNotBeCalled) + .then(value => { + expect(shouldNotBeCalled).not.toBeCalled(); + expect(value).toBe('lol'); + }); + }); + }); + + pit('clears old field when getting new field and mtime changed', () => { + var mtime = 0; + fs.stat.mockImpl((file, callback) => { + callback(null, { + mtime: { + getTime: () => mtime++, + }, + }); + }); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('lol' + mtime) + ); + + return cache + .get('/rootDir/someFile', 'field1', loaderCb) + .then(value => cache + .get('/rootDir/someFile', 'field2', loaderCb) + .then(value2 => cache + .get('/rootDir/someFile', 'field1', loaderCb) + .then(value3 => expect(value3).toBe('lol2')) + ) + ); + }); + }); + + describe('loading cache from disk', () => { + var fileStats; + + beforeEach(() => { + fileStats = { + '/rootDir/someFile': { + mtime: { + getTime: () => 22, + }, + }, + '/rootDir/foo': { + mtime: { + getTime: () => 11, + }, + }, + }; + + fs.existsSync.mockImpl(() => true); + + fs.statSync.mockImpl(filePath => fileStats[filePath]); + + fs.readFileSync.mockImpl(() => JSON.stringify({ + '/rootDir/someFile': { + metadata: {mtime: 22}, + data: {field: 'oh hai'}, + }, + '/rootDir/foo': { + metadata: {mtime: 11}, + data: {field: 'lol wat'}, + }, + })); + }); + + pit('should load cache from disk', () => { + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn(); + + return cache + .get('/rootDir/someFile', 'field', loaderCb) + .then(value => { + expect(loaderCb).not.toBeCalled(); + expect(value).toBe('oh hai'); + + return cache + .get('/rootDir/foo', 'field', loaderCb) + .then(val => { + expect(loaderCb).not.toBeCalled(); + expect(val).toBe('lol wat'); + }); + }); + }); + + pit('should not load outdated cache', () => { + fs.stat.mockImpl((file, callback) => + callback(null, { + mtime: { + getTime: () => {}, + }, + }) + ); + + fileStats['/rootDir/foo'].mtime.getTime = () => 123; + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('new value') + ); + + return cache + .get('/rootDir/someFile', 'field', loaderCb) + .then(value => { + expect(loaderCb).not.toBeCalled(); + expect(value).toBe('oh hai'); + + return cache + .get('/rootDir/foo', 'field', loaderCb) + .then(val => { + expect(loaderCb).toBeCalled(); + expect(val).toBe('new value'); + }); + }); + }); + }); + + describe('writing cache to disk', () => { + it('should write cache to disk', (done) => { + var index = 0; + var mtimes = [10, 20, 30]; + + fs.stat.mockImpl((file, callback) => + callback(null, { + mtime: { + getTime: () => mtimes[index++], + }, + }) + ); + + var cache = new Cache({ + cacheKey: 'cache', + }); + + cache.get('/rootDir/bar', 'field', () => + Promise.resolve('bar value') + ); + cache.get('/rootDir/foo', 'field', () => + Promise.resolve('foo value') + ); + cache.get('/rootDir/baz', 'field', () => + Promise.resolve('baz value') + ); + + setTimeout(() => { + expect(fs.writeFile).toBeCalled(); + done(); + }, 2020); + }); + }); + + describe('check for cache presence', () => { + it('synchronously resolves cache presence', () => { + fs.stat.mockImpl((file, callback) => + callback(null, { + mtime: { + getTime: () => {}, + }, + }) + ); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('banana') + ); + var file = '/rootDir/someFile'; + + return cache + .get(file, 'field', loaderCb) + .then(() => { + expect(cache.has(file)).toBe(true); + expect(cache.has(file, 'field')).toBe(true); + expect(cache.has(file, 'foo')).toBe(false); + }); + }); + }); + + describe('invalidate', () => { + it('invalidates the cache per file or per-field', () => { + fs.stat.mockImpl((file, callback) => + callback(null, { + mtime: { + getTime: () => {}, + }, + }) + ); + + var cache = new Cache({ + cacheKey: 'cache', + }); + var loaderCb = jest.genMockFn().mockImpl(() => + Promise.resolve('banana') + ); + var file = '/rootDir/someFile'; + + return cache.get(file, 'field', loaderCb).then(() => { + expect(cache.has(file)).toBe(true); + cache.invalidate(file, 'field'); + expect(cache.has(file)).toBe(true); + expect(cache.has(file, 'field')).toBe(false); + cache.invalidate(file); + expect(cache.has(file)).toBe(false); + }); + }); + }); +}); diff --git a/react-packager/src/node-haste/Cache/index.js b/react-packager/src/node-haste/Cache/index.js new file mode 100644 index 00000000..4a0349ab --- /dev/null +++ b/react-packager/src/node-haste/Cache/index.js @@ -0,0 +1,217 @@ +/** + * 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 denodeify = require('denodeify'); +const crypto = require('crypto'); +const fs = require('graceful-fs'); +const isAbsolutePath = require('absolute-path'); +const path = require('../fastpath'); +const tmpDir = require('os').tmpDir(); + +function getObjectValues(object) { + return Object.keys(object).map(key => object[key]); +} + +function debounce(fn, delay) { + var timeout; + return () => { + clearTimeout(timeout); + timeout = setTimeout(fn, delay); + }; +} + +class Cache { + constructor({ + resetCache, + cacheKey, + cacheDirectory = tmpDir, + }) { + this._cacheFilePath = Cache.getCacheFilePath(cacheDirectory, cacheKey); + if (!resetCache) { + this._data = this._loadCacheSync(this._cacheFilePath); + } else { + this._data = Object.create(null); + } + + this._persistEventually = debounce(this._persistCache.bind(this), 2000); + } + + static getCacheFilePath(tmpdir, ...args) { + const hash = crypto.createHash('md5'); + args.forEach(arg => hash.update(arg)); + return path.join(tmpdir, hash.digest('hex')); + } + + get(filepath, field, loaderCb) { + if (!isAbsolutePath(filepath)) { + throw new Error('Use absolute paths'); + } + + return this.has(filepath, field) + ? this._data[filepath].data[field] + : this.set(filepath, field, loaderCb(filepath)); + } + + invalidate(filepath, field) { + if (this.has(filepath, field)) { + if (field == null) { + delete this._data[filepath]; + } else { + delete this._data[filepath].data[field]; + } + } + } + + end() { + return this._persistCache(); + } + + has(filepath, field) { + return Object.prototype.hasOwnProperty.call(this._data, filepath) && + (field == null || Object.prototype.hasOwnProperty.call(this._data[filepath].data, field)); + } + + set(filepath, field, loaderPromise) { + let record = this._data[filepath]; + if (!record) { + record = Object.create(null); + this._data[filepath] = record; + this._data[filepath].data = Object.create(null); + this._data[filepath].metadata = Object.create(null); + } + + record.data[field] = loaderPromise + .then(data => Promise.all([ + data, + denodeify(fs.stat)(filepath), + ])) + .then(([data, stat]) => { + this._persistEventually(); + + // Evict all existing field data from the cache if we're putting new + // more up to date data + var mtime = stat.mtime.getTime(); + if (record.metadata.mtime !== mtime) { + record.data = Object.create(null); + } + record.metadata.mtime = mtime; + + return data; + }); + + return record.data[field]; + } + + _persistCache() { + if (this._persisting != null) { + return this._persisting; + } + + const data = this._data; + const cacheFilepath = this._cacheFilePath; + + const allPromises = getObjectValues(data) + .map(record => { + const fieldNames = Object.keys(record.data); + const fieldValues = getObjectValues(record.data); + + return Promise + .all(fieldValues) + .then(ref => { + const ret = Object.create(null); + ret.metadata = record.metadata; + ret.data = Object.create(null); + fieldNames.forEach((field, index) => + ret.data[field] = ref[index] + ); + + return ret; + }); + } + ); + + this._persisting = Promise.all(allPromises) + .then(values => { + const json = Object.create(null); + Object.keys(data).forEach((key, i) => { + // make sure the key wasn't added nor removed after we started + // persisting the cache + const value = values[i]; + if (!value) { + return; + } + + json[key] = Object.create(null); + json[key].metadata = data[key].metadata; + json[key].data = value.data; + }); + return denodeify(fs.writeFile)(cacheFilepath, JSON.stringify(json)); + }) + .catch(e => console.error( + '[node-haste] Encountered an error while persisting cache:\n%s', + e.stack.split('\n').map(line => '> ' + line).join('\n') + )) + .then(() => { + this._persisting = null; + return true; + }); + + return this._persisting; + } + + _loadCacheSync(cachePath) { + var ret = Object.create(null); + var cacheOnDisk = loadCacheSync(cachePath); + + // Filter outdated cache and convert to promises. + Object.keys(cacheOnDisk).forEach(key => { + if (!fs.existsSync(key)) { + return; + } + var record = cacheOnDisk[key]; + var stat = fs.statSync(key); + if (stat.mtime.getTime() === record.metadata.mtime) { + ret[key] = Object.create(null); + ret[key].metadata = Object.create(null); + ret[key].data = Object.create(null); + ret[key].metadata.mtime = record.metadata.mtime; + + Object.keys(record.data).forEach(field => { + ret[key].data[field] = Promise.resolve(record.data[field]); + }); + } + }); + + return ret; + } +} + +function loadCacheSync(cachePath) { + if (!fs.existsSync(cachePath)) { + return Object.create(null); + } + + try { + return JSON.parse(fs.readFileSync(cachePath)); + } catch (e) { + if (e instanceof SyntaxError) { + console.warn('Unable to parse cache file. Will clear and continue.'); + try { + fs.unlinkSync(cachePath); + } catch (err) { + // Someone else might've deleted it. + } + return Object.create(null); + } + throw e; + } +} + +module.exports = Cache; diff --git a/react-packager/src/node-haste/DependencyGraph/DependencyGraphHelpers.js b/react-packager/src/node-haste/DependencyGraph/DependencyGraphHelpers.js new file mode 100644 index 00000000..8674bb52 --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/DependencyGraphHelpers.js @@ -0,0 +1,47 @@ + /** + * 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 path = require('../fastpath'); + +const NODE_MODULES = path.sep + 'node_modules' + path.sep; + +class DependencyGraphHelpers { + constructor({ providesModuleNodeModules, assetExts }) { + this._providesModuleNodeModules = providesModuleNodeModules; + this._assetExts = assetExts; + } + + isNodeModulesDir(file) { + const index = file.lastIndexOf(NODE_MODULES); + if (index === -1) { + return false; + } + + const parts = file.substr(index + 14).split(path.sep); + const dirs = this._providesModuleNodeModules; + for (let i = 0; i < dirs.length; i++) { + if (parts.indexOf(dirs[i]) > -1) { + return false; + } + } + + return true; + } + + isAssetFile(file) { + return this._assetExts.indexOf(this.extname(file)) !== -1; + } + + extname(name) { + return path.extname(name).substr(1); + } +} + +module.exports = DependencyGraphHelpers; diff --git a/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js b/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js new file mode 100644 index 00000000..0512bf1e --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/DeprecatedAssetMap.js @@ -0,0 +1,120 @@ + /** + * 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 AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); +const Fastfs = require('../fastfs'); +const debug = require('debug')('ReactNativePackager:DependencyGraph'); +const path = require('../fastpath'); + +class DeprecatedAssetMap { + constructor({ + fsCrawl, + roots, + assetExts, + fileWatcher, + ignoreFilePath, + helpers, + activity, + enabled, + platforms, + }) { + if (roots == null || roots.length === 0 || !enabled) { + this._disabled = true; + return; + } + + this._helpers = helpers; + this._map = Object.create(null); + this._assetExts = assetExts; + this._activity = activity; + this._platforms = platforms; + + if (!this._disabled) { + this._fastfs = new Fastfs( + 'Assets', + roots, + fileWatcher, + { ignore: ignoreFilePath, crawling: fsCrawl, activity } + ); + + this._fastfs.on('change', this._processFileChange.bind(this)); + } + } + + build() { + if (this._disabled) { + return Promise.resolve(); + } + + return this._fastfs.build().then( + () => { + const activity = this._activity; + let processAsset_DEPRECATEDActivity; + if (activity) { + processAsset_DEPRECATEDActivity = activity.startEvent( + 'Building (deprecated) Asset Map', + ); + } + + this._fastfs.findFilesByExts(this._assetExts).forEach( + file => this._processAsset(file) + ); + + if (activity) { + activity.endEvent(processAsset_DEPRECATEDActivity); + } + } + ); + } + + resolve(fromModule, toModuleName) { + if (this._disabled) { + return null; + } + + const assetMatch = toModuleName.match(/^image!(.+)/); + if (assetMatch && assetMatch[1]) { + if (!this._map[assetMatch[1]]) { + debug('WARINING: Cannot find asset:', assetMatch[1]); + return null; + } + return this._map[assetMatch[1]]; + } + } + + _processAsset(file) { + const ext = this._helpers.extname(file); + if (this._assetExts.indexOf(ext) !== -1) { + const name = assetName(file, ext); + if (this._map[name] != null) { + debug('Conflicting assets', name); + } + + this._map[name] = new AssetModule_DEPRECATED({ file }, this._platforms); + } + } + + _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(path.join(root, filePath)); + } + } +} + +function assetName(file, ext) { + return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, ''); +} + +module.exports = DeprecatedAssetMap; diff --git a/react-packager/src/node-haste/DependencyGraph/HasteMap.js b/react-packager/src/node-haste/DependencyGraph/HasteMap.js new file mode 100644 index 00000000..ac3060d6 --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/HasteMap.js @@ -0,0 +1,147 @@ + /** + * 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 path = require('../fastpath'); +const getPlatformExtension = require('../lib/getPlatformExtension'); + +const GENERIC_PLATFORM = 'generic'; +const NATIVE_PLATFORM = 'native'; +const PACKAGE_JSON = path.sep + 'package.json'; + +class HasteMap { + constructor({ + extensions, + fastfs, + moduleCache, + preferNativePlatform, + helpers, + platforms, + }) { + this._extensions = extensions; + this._fastfs = fastfs; + this._moduleCache = moduleCache; + this._preferNativePlatform = preferNativePlatform; + this._helpers = helpers; + this._platforms = platforms; + } + + build() { + this._map = Object.create(null); + const promises = []; + this._fastfs.getAllFiles().forEach(filePath => { + if (!this._helpers.isNodeModulesDir(filePath)) { + if (this._extensions.indexOf(path.extname(filePath).substr(1)) !== -1) { + promises.push(this._processHasteModule(filePath)); + } + if (filePath.endsWith(PACKAGE_JSON)) { + promises.push(this._processHastePackage(filePath)); + } + } + }); + return Promise.all(promises).then(() => this._map); + } + + processFileChange(type, absPath) { + return Promise.resolve().then(() => { + /*eslint no-labels: 0 */ + if (type === 'delete' || type === 'change') { + loop: for (const name in this._map) { + const modulesMap = this._map[name]; + for (const platform in modulesMap) { + const module = modulesMap[platform]; + if (module.path === absPath) { + delete modulesMap[platform]; + break loop; + } + } + } + + if (type === 'delete') { + return null; + } + } + + if (this._extensions.indexOf(this._helpers.extname(absPath)) !== -1) { + if (path.basename(absPath) === 'package.json') { + return this._processHastePackage(absPath); + } else { + return this._processHasteModule(absPath); + } + } + }); + } + + getModule(name, platform = null) { + const modulesMap = this._map[name]; + if (modulesMap == null) { + return null; + } + + // If platform is 'ios', we prefer .ios.js to .native.js which we prefer to + // a plain .js file. + let module = undefined; + if (module == null && platform != null) { + module = modulesMap[platform]; + } + if (module == null && this._preferNativePlatform) { + module = modulesMap[NATIVE_PLATFORM]; + } + if (module == null) { + module = modulesMap[GENERIC_PLATFORM]; + } + return module; + } + + _processHasteModule(file) { + const module = this._moduleCache.getModule(file); + return module.isHaste().then( + isHaste => isHaste && module.getName() + .then(name => this._updateHasteMap(name, module)) + ); + } + + _processHastePackage(file) { + file = path.resolve(file); + const p = this._moduleCache.getPackage(file); + return p.isHaste() + .then(isHaste => isHaste && p.getName() + .then(name => this._updateHasteMap(name, p))) + .catch(e => { + if (e instanceof SyntaxError) { + // Malformed package.json. + return; + } + throw e; + }); + } + + _updateHasteMap(name, mod) { + if (this._map[name] == null) { + this._map[name] = Object.create(null); + } + + const moduleMap = this._map[name]; + const modulePlatform = getPlatformExtension(mod.path, this._platforms) || GENERIC_PLATFORM; + const existingModule = moduleMap[modulePlatform]; + + if (existingModule && existingModule.path !== mod.path) { + throw new Error( + `@providesModule naming collision:\n` + + ` Duplicate module name: ${name}\n` + + ` Paths: ${mod.path} collides with ${existingModule.path}\n\n` + + `This error is caused by a @providesModule declaration ` + + `with the same name across two different files.` + ); + } + + moduleMap[modulePlatform] = mod; + } +} + +module.exports = HasteMap; diff --git a/react-packager/src/node-haste/DependencyGraph/ResolutionRequest.js b/react-packager/src/node-haste/DependencyGraph/ResolutionRequest.js new file mode 100644 index 00000000..e7728d0b --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/ResolutionRequest.js @@ -0,0 +1,529 @@ + /** + * 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 AsyncTaskGroup = require('../lib/AsyncTaskGroup'); +const MapWithDefaults = require('../lib/MapWithDefaults'); +const debug = require('debug')('ReactNativePackager:DependencyGraph'); +const util = require('util'); +const path = require('../fastpath'); +const realPath = require('path'); +const isAbsolutePath = require('absolute-path'); +const getAssetDataFromName = require('../lib/getAssetDataFromName'); + +class ResolutionRequest { + constructor({ + platform, + platforms, + preferNativePlatform, + entryPath, + hasteMap, + deprecatedAssetMap, + helpers, + moduleCache, + fastfs, + shouldThrowOnUnresolvedErrors, + extraNodeModules, + }) { + this._platform = platform; + this._platforms = platforms; + this._preferNativePlatform = preferNativePlatform; + this._entryPath = entryPath; + this._hasteMap = hasteMap; + this._deprecatedAssetMap = deprecatedAssetMap; + this._helpers = helpers; + this._moduleCache = moduleCache; + this._fastfs = fastfs; + this._shouldThrowOnUnresolvedErrors = shouldThrowOnUnresolvedErrors; + this._extraNodeModules = extraNodeModules; + this._resetResolutionCache(); + } + + _tryResolve(action, secondaryAction) { + return action().catch((error) => { + if (error.type !== 'UnableToResolveError') { + throw error; + } + return secondaryAction(); + }); + } + + resolveDependency(fromModule, toModuleName) { + const resHash = resolutionHash(fromModule.path, toModuleName); + + if (this._immediateResolutionCache[resHash]) { + return Promise.resolve(this._immediateResolutionCache[resHash]); + } + + const asset_DEPRECATED = this._deprecatedAssetMap.resolve( + fromModule, + toModuleName + ); + if (asset_DEPRECATED) { + return Promise.resolve(asset_DEPRECATED); + } + + const cacheResult = (result) => { + this._immediateResolutionCache[resHash] = result; + return result; + }; + + const forgive = (error) => { + if ( + error.type !== 'UnableToResolveError' || + this._shouldThrowOnUnresolvedErrors(this._entryPath, this._platform) + ) { + throw error; + } + + debug( + 'Unable to resolve module %s from %s', + toModuleName, + fromModule.path + ); + return null; + }; + + if (!this._helpers.isNodeModulesDir(fromModule.path) + && !(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))) { + return this._tryResolve( + () => this._resolveHasteDependency(fromModule, toModuleName), + () => this._resolveNodeDependency(fromModule, toModuleName) + ).then( + cacheResult, + forgive, + ); + } + + return this._resolveNodeDependency(fromModule, toModuleName) + .then( + cacheResult, + forgive, + ); + } + + getOrderedDependencies({ + response, + mocksPattern, + transformOptions, + onProgress, + recursive = true, + }) { + return this._getAllMocks(mocksPattern).then(allMocks => { + const entry = this._moduleCache.getModule(this._entryPath); + const mocks = Object.create(null); + + response.pushDependency(entry); + let totalModules = 1; + let finishedModules = 0; + + const resolveDependencies = module => + module.getDependencies(transformOptions) + .then(dependencyNames => + Promise.all( + dependencyNames.map(name => this.resolveDependency(module, name)) + ).then(dependencies => [dependencyNames, dependencies]) + ); + + const addMockDependencies = !allMocks + ? (module, result) => result + : (module, [dependencyNames, dependencies]) => { + const list = [module.getName()]; + const pkg = module.getPackage(); + if (pkg) { + list.push(pkg.getName()); + } + return Promise.all(list).then(names => { + names.forEach(name => { + if (allMocks[name] && !mocks[name]) { + const mockModule = this._moduleCache.getModule(allMocks[name]); + dependencyNames.push(name); + dependencies.push(mockModule); + mocks[name] = allMocks[name]; + } + }); + return [dependencyNames, dependencies]; + }); + }; + + const collectedDependencies = new MapWithDefaults(module => collect(module)); + const crawlDependencies = (mod, [depNames, dependencies]) => { + const filteredPairs = []; + + dependencies.forEach((modDep, i) => { + const name = depNames[i]; + if (modDep == null) { + // It is possible to require mocks that don't have a real + // module backing them. If a dependency cannot be found but there + // exists a mock with the desired ID, resolve it and add it as + // a dependency. + if (allMocks && allMocks[name] && !mocks[name]) { + const mockModule = this._moduleCache.getModule(allMocks[name]); + mocks[name] = allMocks[name]; + return filteredPairs.push([name, mockModule]); + } + + debug( + 'WARNING: Cannot find required module `%s` from module `%s`', + name, + mod.path + ); + return false; + } + return filteredPairs.push([name, modDep]); + }); + + response.setResolvedDependencyPairs(mod, filteredPairs); + + const dependencyModules = filteredPairs.map(([, m]) => m); + const newDependencies = + dependencyModules.filter(m => !collectedDependencies.has(m)); + + if (onProgress) { + finishedModules += 1; + totalModules += newDependencies.length; + onProgress(finishedModules, totalModules); + } + + if (recursive) { + // doesn't block the return of this function invocation, but defers + // the resulution of collectionsInProgress.done.then(…) + dependencyModules + .forEach(dependency => collectedDependencies.get(dependency)); + } + return dependencyModules; + }; + + const collectionsInProgress = new AsyncTaskGroup(); + function collect(module) { + collectionsInProgress.start(module); + const result = resolveDependencies(module) + .then(result => addMockDependencies(module, result)) + .then(result => crawlDependencies(module, result)); + const end = () => collectionsInProgress.end(module); + result.then(end, end); + return result; + } + + return Promise.all([ + // kicks off recursive dependency discovery, but doesn't block until it's done + collectedDependencies.get(entry), + + // resolves when there are no more modules resolving dependencies + collectionsInProgress.done, + ]).then(([rootDependencies]) => { + return Promise.all( + Array.from(collectedDependencies, resolveKeyWithPromise) + ).then(moduleToDependenciesPairs => + [rootDependencies, new MapWithDefaults(() => [], moduleToDependenciesPairs)] + ); + }).then(([rootDependencies, moduleDependencies]) => { + // serialize dependencies, and make sure that every single one is only + // included once + const seen = new Set([entry]); + function traverse(dependencies) { + dependencies.forEach(dependency => { + if (seen.has(dependency)) { return; } + + seen.add(dependency); + response.pushDependency(dependency); + traverse(moduleDependencies.get(dependency)); + }); + } + + traverse(rootDependencies); + response.setMocks(mocks); + }); + }); + } + + _getAllMocks(pattern) { + // Take all mocks in all the roots into account. This is necessary + // because currently mocks are global: any module can be mocked by + // any mock in the system. + let mocks = null; + if (pattern) { + mocks = Object.create(null); + this._fastfs.matchFilesByPattern(pattern).forEach(file => + mocks[path.basename(file, path.extname(file))] = file + ); + } + return Promise.resolve(mocks); + } + + _resolveHasteDependency(fromModule, toModuleName) { + toModuleName = normalizePath(toModuleName); + + let p = fromModule.getPackage(); + if (p) { + p = p.redirectRequire(toModuleName); + } else { + p = Promise.resolve(toModuleName); + } + + return p.then((realModuleName) => { + let dep = this._hasteMap.getModule(realModuleName, this._platform); + if (dep && dep.type === 'Module') { + return dep; + } + + let packageName = realModuleName; + while (packageName && packageName !== '.') { + dep = this._hasteMap.getModule(packageName, this._platform); + if (dep && dep.type === 'Package') { + break; + } + packageName = path.dirname(packageName); + } + + if (dep && dep.type === 'Package') { + const potentialModulePath = path.join( + dep.root, + path.relative(packageName, realModuleName) + ); + return this._tryResolve( + () => this._loadAsFile( + potentialModulePath, + fromModule, + toModuleName, + ), + () => this._loadAsDir(potentialModulePath, fromModule, toModuleName), + ); + } + + throw new UnableToResolveError( + fromModule, + toModuleName, + 'Unable to resolve dependency', + ); + }); + } + + _redirectRequire(fromModule, modulePath) { + return Promise.resolve(fromModule.getPackage()).then(p => { + if (p) { + return p.redirectRequire(modulePath); + } + return modulePath; + }); + } + + _resolveFileOrDir(fromModule, toModuleName) { + const potentialModulePath = isAbsolutePath(toModuleName) ? + toModuleName : + path.join(path.dirname(fromModule.path), toModuleName); + + return this._redirectRequire(fromModule, potentialModulePath).then( + realModuleName => { + if (realModuleName === false) { + return null; + } + + return this._tryResolve( + () => this._loadAsFile(realModuleName, fromModule, toModuleName), + () => this._loadAsDir(realModuleName, fromModule, toModuleName) + ); + } + ); + } + + _resolveNodeDependency(fromModule, toModuleName) { + if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) { + return this._resolveFileOrDir(fromModule, toModuleName); + } else { + return this._redirectRequire(fromModule, toModuleName).then( + realModuleName => { + // exclude + if (realModuleName === false) { + return null; + } + + if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) { + // derive absolute path /.../node_modules/fromModuleDir/realModuleName + const fromModuleParentIdx = fromModule.path.lastIndexOf('node_modules/') + 13; + const fromModuleDir = fromModule.path.slice(0, fromModule.path.indexOf('/', fromModuleParentIdx)); + const absPath = path.join(fromModuleDir, realModuleName); + return this._resolveFileOrDir(fromModule, absPath); + } + + const searchQueue = []; + for (let currDir = path.dirname(fromModule.path); + currDir !== realPath.parse(fromModule.path).root; + currDir = path.dirname(currDir)) { + searchQueue.push( + path.join(currDir, 'node_modules', realModuleName) + ); + } + + if (this._extraNodeModules) { + const bits = toModuleName.split('/'); + const packageName = bits[0]; + if (this._extraNodeModules[packageName]) { + bits[0] = this._extraNodeModules[packageName]; + searchQueue.push(path.join.apply(path, bits)); + } + } + + let p = Promise.reject(new UnableToResolveError( + fromModule, + toModuleName, + 'Node module not found', + )); + searchQueue.forEach(potentialModulePath => { + p = this._tryResolve( + () => this._tryResolve( + () => p, + () => this._loadAsFile(potentialModulePath, fromModule, toModuleName), + ), + () => this._loadAsDir(potentialModulePath, fromModule, toModuleName) + ); + }); + + return p; + }); + } + } + + _loadAsFile(potentialModulePath, fromModule, toModule) { + return Promise.resolve().then(() => { + if (this._helpers.isAssetFile(potentialModulePath)) { + const dirname = path.dirname(potentialModulePath); + if (!this._fastfs.dirExists(dirname)) { + throw new UnableToResolveError( + fromModule, + toModule, + `Directory ${dirname} doesn't exist`, + ); + } + + const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms); + + let pattern = '^' + name + '(@[\\d\\.]+x)?'; + if (this._platform != null) { + pattern += '(\\.' + this._platform + ')?'; + } + pattern += '\\.' + type; + + // We arbitrarly grab the first one, because scale selection + // will happen somewhere + const [assetFile] = this._fastfs.matches( + dirname, + new RegExp(pattern) + ); + + if (assetFile) { + return this._moduleCache.getAssetModule(assetFile); + } + } + + let file; + if (this._fastfs.fileExists(potentialModulePath)) { + file = potentialModulePath; + } else if (this._platform != null && + this._fastfs.fileExists(potentialModulePath + '.' + this._platform + '.js')) { + file = potentialModulePath + '.' + this._platform + '.js'; + } else if (this._preferNativePlatform && + this._fastfs.fileExists(potentialModulePath + '.native.js')) { + file = potentialModulePath + '.native.js'; + } else if (this._fastfs.fileExists(potentialModulePath + '.js')) { + file = potentialModulePath + '.js'; + } else if (this._fastfs.fileExists(potentialModulePath + '.json')) { + file = potentialModulePath + '.json'; + } else { + throw new UnableToResolveError( + fromModule, + toModule, + `File ${potentialModulePath} doesnt exist`, + ); + } + + return this._moduleCache.getModule(file); + }); + } + + _loadAsDir(potentialDirPath, fromModule, toModule) { + return Promise.resolve().then(() => { + if (!this._fastfs.dirExists(potentialDirPath)) { + throw new UnableToResolveError( + fromModule, + toModule, +`Unable to find this module in its module map or any of the node_modules directories under ${potentialDirPath} and its parent directories + +This might be related to https://github.com/facebook/react-native/issues/4968 +To resolve try the following: + 1. Clear watchman watches: \`watchman watch-del-all\`. + 2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`. + 3. Reset packager cache: \`rm -fr $TMPDIR/react-*\` or \`npm start -- --reset-cache\`.`, + ); + } + + const packageJsonPath = path.join(potentialDirPath, 'package.json'); + if (this._fastfs.fileExists(packageJsonPath)) { + return this._moduleCache.getPackage(packageJsonPath) + .getMain().then( + (main) => this._tryResolve( + () => this._loadAsFile(main, fromModule, toModule), + () => this._loadAsDir(main, fromModule, toModule) + ) + ); + } + + return this._loadAsFile( + path.join(potentialDirPath, 'index'), + fromModule, + toModule, + ); + }); + } + + _resetResolutionCache() { + this._immediateResolutionCache = Object.create(null); + } + +} + + +function resolutionHash(modulePath, depName) { + return `${path.resolve(modulePath)}:${depName}`; +} + + +function UnableToResolveError(fromModule, toModule, message) { + Error.call(this); + Error.captureStackTrace(this, this.constructor); + this.message = util.format( + 'Unable to resolve module %s from %s: %s', + toModule, + fromModule.path, + message, + ); + this.type = this.name = 'UnableToResolveError'; +} + +util.inherits(UnableToResolveError, Error); + +function normalizePath(modulePath) { + if (path.sep === '/') { + modulePath = path.normalize(modulePath); + } else if (path.posix) { + modulePath = path.posix.normalize(modulePath); + } + + return modulePath.replace(/\/$/, ''); +} + +function resolveKeyWithPromise([key, promise]) { + return promise.then(value => [key, value]); +} + +function isRelativeImport(path) { + return /^[.][.]?[/]/.test(path); +} + +module.exports = ResolutionRequest; diff --git a/react-packager/src/node-haste/DependencyGraph/ResolutionResponse.js b/react-packager/src/node-haste/DependencyGraph/ResolutionResponse.js new file mode 100644 index 00000000..fdee65c7 --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/ResolutionResponse.js @@ -0,0 +1,97 @@ + /** + * 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'; + +class ResolutionResponse { + constructor({transformOptions}) { + this.transformOptions = transformOptions; + this.dependencies = []; + this.mainModuleId = null; + this.mocks = null; + this.numPrependedDependencies = 0; + this._mappings = Object.create(null); + this._finalized = false; + } + + copy(properties) { + const { + dependencies = this.dependencies, + mainModuleId = this.mainModuleId, + mocks = this.mocks, + } = properties; + + const numPrependedDependencies = dependencies === this.dependencies + ? this.numPrependedDependencies : 0; + + return Object.assign( + new this.constructor({transformOptions: this.transformOptions}), + this, + { + dependencies, + mainModuleId, + mocks, + numPrependedDependencies, + }, + ); + } + + _assertNotFinalized() { + if (this._finalized) { + throw new Error('Attempted to mutate finalized response.'); + } + } + + _assertFinalized() { + if (!this._finalized) { + throw new Error('Attempted to access unfinalized response.'); + } + } + + finalize() { + return this._mainModule.getName().then(id => { + this.mainModuleId = id; + this._finalized = true; + return this; + }); + } + + pushDependency(module) { + this._assertNotFinalized(); + if (this.dependencies.length === 0) { + this._mainModule = module; + } + + this.dependencies.push(module); + } + + prependDependency(module) { + this._assertNotFinalized(); + this.dependencies.unshift(module); + this.numPrependedDependencies += 1; + } + + setResolvedDependencyPairs(module, pairs) { + this._assertNotFinalized(); + const hash = module.hash(); + if (this._mappings[hash] == null) { + this._mappings[hash] = pairs; + } + } + + setMocks(mocks) { + this.mocks = mocks; + } + + getResolvedDependencyPairs(module) { + this._assertFinalized(); + return this._mappings[module.hash()]; + } +} + +module.exports = ResolutionResponse; diff --git a/react-packager/src/node-haste/DependencyGraph/docblock.js b/react-packager/src/node-haste/DependencyGraph/docblock.js new file mode 100644 index 00000000..d710112a --- /dev/null +++ b/react-packager/src/node-haste/DependencyGraph/docblock.js @@ -0,0 +1,83 @@ +/** + * 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'; + +var docblockRe = /^\s*(\/\*\*(.|\r?\n)*?\*\/)/; + +var ltrimRe = /^\s*/; +/** + * @param {String} contents + * @return {String} + */ +function extract(contents) { + var match = contents.match(docblockRe); + if (match) { + return match[0].replace(ltrimRe, '') || ''; + } + return ''; +} + + +var commentStartRe = /^\/\*\*/; +var commentEndRe = /\*\/$/; +var wsRe = /[\t ]+/g; +var stringStartRe = /(\r?\n|^) *\*/g; +var multilineRe = + /(?:^|\r?\n) *(@[^\r\n]*?) *\r?\n *([^@\r\n\s][^@\r\n]+?) *\r?\n/g; +var propertyRe = /(?:^|\r?\n) *@(\S+) *([^\r\n]*)/g; + +/** + * @param {String} contents + * @return {Array} + */ +function parse(docblock) { + docblock = docblock + .replace(commentStartRe, '') + .replace(commentEndRe, '') + .replace(wsRe, ' ') + .replace(stringStartRe, '$1'); + + // Normalize multi-line directives + var prev = ''; + while (prev !== docblock) { + prev = docblock; + docblock = docblock.replace(multilineRe, '\n$1 $2\n'); + } + docblock = docblock.trim(); + + var result = []; + var match; + while ((match = propertyRe.exec(docblock))) { + result.push([match[1], match[2]]); + } + + return result; +} + +/** + * Same as parse but returns an object of prop: value instead of array of paris + * If a property appers more than once the last one will be returned + * + * @param {String} contents + * @return {Object} + */ +function parseAsObject(docblock) { + var pairs = parse(docblock); + var result = {}; + for (var i = 0; i < pairs.length; i++) { + result[pairs[i][0]] = pairs[i][1]; + } + return result; +} + + +exports.extract = extract; +exports.parse = parse; +exports.parseAsObject = parseAsObject; diff --git a/react-packager/src/node-haste/FileWatcher/__mocks__/sane.js b/react-packager/src/node-haste/FileWatcher/__mocks__/sane.js new file mode 100644 index 00000000..f59fa910 --- /dev/null +++ b/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/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js b/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js new file mode 100644 index 00000000..a87a92a6 --- /dev/null +++ b/react-packager/src/node-haste/FileWatcher/__tests__/FileWatcher-test.js @@ -0,0 +1,74 @@ +/** + * 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(() => { + 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/react-packager/src/node-haste/FileWatcher/index.js b/react-packager/src/node-haste/FileWatcher/index.js new file mode 100644 index 00000000..0011adc3 --- /dev/null +++ b/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 = 120000; + +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/react-packager/src/node-haste/Module.js b/react-packager/src/node-haste/Module.js new file mode 100644 index 00000000..30ca0262 --- /dev/null +++ b/react-packager/src/node-haste/Module.js @@ -0,0 +1,232 @@ +/** + * 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 crypto = require('crypto'); +const docblock = require('./DependencyGraph/docblock'); +const isAbsolutePath = require('absolute-path'); +const jsonStableStringify = require('json-stable-stringify'); +const path = require('./fastpath'); +const extractRequires = require('./lib/extractRequires'); + +class Module { + + constructor({ + file, + fastfs, + moduleCache, + cache, + extractor = extractRequires, + transformCode, + depGraphHelpers, + options, + }) { + if (!isAbsolutePath(file)) { + throw new Error('Expected file to be absolute path but got ' + file); + } + + this.path = file; + this.type = 'Module'; + + this._fastfs = fastfs; + this._moduleCache = moduleCache; + this._cache = cache; + this._extractor = extractor; + this._transformCode = transformCode; + this._depGraphHelpers = depGraphHelpers; + this._options = options; + } + + isHaste() { + return this._cache.get( + this.path, + 'isHaste', + () => this._readDocBlock().then(({id}) => !!id) + ); + } + + getCode(transformOptions) { + return this.read(transformOptions).then(({code}) => code); + } + + getMap(transformOptions) { + return this.read(transformOptions).then(({map}) => map); + } + + getName() { + return this._cache.get( + this.path, + 'name', + () => this._readDocBlock().then(({id}) => { + if (id) { + return id; + } + + const p = this.getPackage(); + + if (!p) { + // Name is full path + return this.path; + } + + return p.getName() + .then(name => { + if (!name) { + return this.path; + } + + return path.join(name, path.relative(p.root, this.path)).replace(/\\/g, '/'); + }); + }) + ); + } + + getPackage() { + return this._moduleCache.getPackageForModule(this); + } + + getDependencies(transformOptions) { + return this.read(transformOptions).then(({dependencies}) => dependencies); + } + + invalidate() { + this._cache.invalidate(this.path); + } + + _parseDocBlock(docBlock) { + // Extract an id for the module if it's using @providesModule syntax + // and if it's NOT in node_modules (and not a whitelisted node_module). + // This handles the case where a project may have a dep that has @providesModule + // docblock comments, but doesn't want it to conflict with whitelisted @providesModule + // modules, such as react-haste, fbjs-haste, or react-native or with non-dependency, + // project-specific code that is using @providesModule. + const moduleDocBlock = docblock.parseAsObject(docBlock); + const provides = moduleDocBlock.providesModule || moduleDocBlock.provides; + + const id = provides && !this._depGraphHelpers.isNodeModulesDir(this.path) + ? /^\S+/.exec(provides)[0] + : undefined; + return {id, moduleDocBlock}; + } + + _readDocBlock(contentPromise) { + if (!this._docBlock) { + if (!contentPromise) { + contentPromise = this._fastfs.readWhile(this.path, whileInDocBlock); + } + this._docBlock = contentPromise + .then(docBlock => this._parseDocBlock(docBlock)); + } + return this._docBlock; + } + + read(transformOptions) { + return this._cache.get( + this.path, + cacheKey('moduleData', transformOptions), + () => { + const fileContentPromise = this._fastfs.readFile(this.path); + return Promise.all([ + fileContentPromise, + this._readDocBlock(fileContentPromise), + ]).then(([source, {id, moduleDocBlock}]) => { + // Ignore requires in JSON files or generated code. An example of this + // is prebuilt files like the SourceMap library. + const extern = this.isJSON() || 'extern' in moduleDocBlock; + if (extern) { + transformOptions = {...transformOptions, extern}; + } + const transformCode = this._transformCode; + const codePromise = transformCode + ? transformCode(this, source, transformOptions) + : Promise.resolve({code: source}); + return codePromise.then(result => { + const { + code, + dependencies = extern ? [] : this._extractor(code).deps.sync, + } = result; + if (this._options && this._options.cacheTransformResults === false) { + return {dependencies}; + } else { + return {...result, dependencies, id, source}; + } + }); + }); + } + ); + } + + hash() { + return `Module : ${this.path}`; + } + + isJSON() { + return path.extname(this.path) === '.json'; + } + + isAsset() { + return false; + } + + isPolyfill() { + return false; + } + + isAsset_DEPRECATED() { + return false; + } + + toJSON() { + return { + hash: this.hash(), + isJSON: this.isJSON(), + isAsset: this.isAsset(), + isAsset_DEPRECATED: this.isAsset_DEPRECATED(), + type: this.type, + path: this.path, + }; + } +} + +function whileInDocBlock(chunk, i, result) { + // consume leading whitespace + if (!/\S/.test(result)) { + return true; + } + + // check for start of doc block + if (!/^\s*\/(\*{2}|\*?$)/.test(result)) { + return false; + } + + // check for end of doc block + return !/\*\//.test(result); +} + +// use weak map to speed up hash creation of known objects +const knownHashes = new WeakMap(); +function stableObjectHash(object) { + let digest = knownHashes.get(object); + if (!digest) { + digest = crypto.createHash('md5') + .update(jsonStableStringify(object)) + .digest('base64'); + knownHashes.set(object, digest); + } + + return digest; +} + +function cacheKey(field, transformOptions) { + return transformOptions !== undefined + ? stableObjectHash(transformOptions) + '\0' + field + : field; +} + +module.exports = Module; diff --git a/react-packager/src/node-haste/ModuleCache.js b/react-packager/src/node-haste/ModuleCache.js new file mode 100644 index 00000000..b1b8ff6d --- /dev/null +++ b/react-packager/src/node-haste/ModuleCache.js @@ -0,0 +1,123 @@ +'use strict'; + +const AssetModule = require('./AssetModule'); +const Package = require('./Package'); +const Module = require('./Module'); +const Polyfill = require('./Polyfill'); +const path = require('./fastpath'); + +class ModuleCache { + + constructor({ + fastfs, + cache, + extractRequires, + transformCode, + depGraphHelpers, + assetDependencies, + moduleOptions, + }, platforms) { + this._moduleCache = Object.create(null); + this._packageCache = Object.create(null); + this._fastfs = fastfs; + this._cache = cache; + this._extractRequires = extractRequires; + this._transformCode = transformCode; + this._depGraphHelpers = depGraphHelpers; + this._platforms = platforms; + this._assetDependencies = assetDependencies; + this._moduleOptions = moduleOptions; + this._packageModuleMap = new WeakMap(); + + fastfs.on('change', this._processFileChange.bind(this)); + } + + getModule(filePath) { + if (!this._moduleCache[filePath]) { + this._moduleCache[filePath] = new Module({ + file: filePath, + fastfs: this._fastfs, + moduleCache: this, + cache: this._cache, + extractor: this._extractRequires, + transformCode: this._transformCode, + depGraphHelpers: this._depGraphHelpers, + options: this._moduleOptions, + }); + } + return this._moduleCache[filePath]; + } + + getAllModules() { + return this._moduleCache; + } + + getAssetModule(filePath) { + if (!this._moduleCache[filePath]) { + this._moduleCache[filePath] = new AssetModule({ + file: filePath, + fastfs: this._fastfs, + moduleCache: this, + cache: this._cache, + dependencies: this._assetDependencies, + }, this._platforms); + } + return this._moduleCache[filePath]; + } + + getPackage(filePath) { + if (!this._packageCache[filePath]) { + this._packageCache[filePath] = new Package({ + file: filePath, + fastfs: this._fastfs, + cache: this._cache, + }); + } + return this._packageCache[filePath]; + } + + getPackageForModule(module) { + if (this._packageModuleMap.has(module)) { + const packagePath = this._packageModuleMap.get(module); + if (this._packageCache[packagePath]) { + return this._packageCache[packagePath]; + } else { + this._packageModuleMap.delete(module); + } + } + + const packagePath = this._fastfs.closest(module.path, 'package.json'); + if (!packagePath) { + return null; + } + + this._packageModuleMap.set(module, packagePath); + return this.getPackage(packagePath); + } + + createPolyfill({file}) { + return new Polyfill({ + file, + cache: this._cache, + depGraphHelpers: this._depGraphHelpers, + fastfs: this._fastfs, + moduleCache: this, + transformCode: this._transformCode, + }); + } + + _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[absPath]) { + this._packageCache[absPath].invalidate(); + delete this._packageCache[absPath]; + } + } +} + +module.exports = ModuleCache; diff --git a/react-packager/src/node-haste/Package.js b/react-packager/src/node-haste/Package.js new file mode 100644 index 00000000..07ffa5de --- /dev/null +++ b/react-packager/src/node-haste/Package.js @@ -0,0 +1,134 @@ +'use strict'; + +const isAbsolutePath = require('absolute-path'); +const path = require('./fastpath'); + +class Package { + + constructor({ file, fastfs, cache }) { + this.path = path.resolve(file); + this.root = path.dirname(this.path); + this._fastfs = fastfs; + this.type = 'Package'; + this._cache = cache; + } + + getMain() { + return this.read().then(json => { + var replacements = getReplacements(json); + if (typeof replacements === 'string') { + return path.join(this.root, replacements); + } + + let main = json.main || 'index'; + + if (replacements && typeof replacements === 'object') { + main = replacements[main] || + replacements[main + '.js'] || + replacements[main + '.json'] || + replacements[main.replace(/(\.js|\.json)$/, '')] || + main; + } + + return path.join(this.root, main); + }); + } + + isHaste() { + return this._cache.get(this.path, 'package-haste', () => + this.read().then(json => !!json.name) + ); + } + + getName() { + return this._cache.get(this.path, 'package-name', () => + this.read().then(json => json.name) + ); + } + + invalidate() { + this._cache.invalidate(this.path); + } + + redirectRequire(name) { + return this.read().then(json => { + var replacements = getReplacements(json); + + if (!replacements || typeof replacements !== 'object') { + return name; + } + + if (name[0] !== '/') { + const replacement = replacements[name]; + // support exclude with "someDependency": false + return replacement === false + ? false + : replacement || name; + } + + if (!isAbsolutePath(name)) { + throw new Error(`Expected ${name} to be absolute path`); + } + + const relPath = './' + path.relative(this.root, name); + let redirect = replacements[relPath]; + + // false is a valid value + if (redirect == null) { + redirect = replacements[relPath + '.js']; + if (redirect == null) { + redirect = replacements[relPath + '.json']; + } + } + + // support exclude with "./someFile": false + if (redirect === false) { + return false; + } + + if (redirect) { + return path.join( + this.root, + redirect + ); + } + + return name; + }); + } + + read() { + if (!this._reading) { + this._reading = this._fastfs.readFile(this.path) + .then(jsonStr => JSON.parse(jsonStr)); + } + + return this._reading; + } +} + +function getReplacements(pkg) { + let rn = pkg['react-native']; + let browser = pkg.browser; + if (rn == null) { + return browser; + } + + if (browser == null) { + return rn; + } + + if (typeof rn === 'string') { + rn = { [pkg.main]: rn }; + } + + if (typeof browser === 'string') { + browser = { [pkg.main]: browser }; + } + + // merge with "browser" as default, + // "react-native" as override + return { ...browser, ...rn }; +} + +module.exports = Package; diff --git a/react-packager/src/node-haste/Polyfill.js b/react-packager/src/node-haste/Polyfill.js new file mode 100644 index 00000000..b2578f02 --- /dev/null +++ b/react-packager/src/node-haste/Polyfill.js @@ -0,0 +1,37 @@ +'use strict'; + +const Module = require('./Module'); + +class Polyfill extends Module { + constructor(options) { + super(options); + this._id = options.id; + this._dependencies = options.dependencies; + } + + isHaste() { + return Promise.resolve(false); + } + + getName() { + return Promise.resolve(this._id); + } + + getPackage() { + return null; + } + + getDependencies() { + return Promise.resolve(this._dependencies); + } + + isJSON() { + return false; + } + + isPolyfill() { + return true; + } +} + +module.exports = Polyfill; diff --git a/react-packager/src/node-haste/__mocks__/debug.js b/react-packager/src/node-haste/__mocks__/debug.js new file mode 100644 index 00000000..87390917 --- /dev/null +++ b/react-packager/src/node-haste/__mocks__/debug.js @@ -0,0 +1,11 @@ +/** + * 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 = () => () => {}; diff --git a/react-packager/src/node-haste/__mocks__/fs.js b/react-packager/src/node-haste/__mocks__/fs.js new file mode 100644 index 00000000..2036e6ea --- /dev/null +++ b/react-packager/src/node-haste/__mocks__/fs.js @@ -0,0 +1,11 @@ +/** + * 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 = require('graceful-fs'); diff --git a/react-packager/src/node-haste/__mocks__/graceful-fs.js b/react-packager/src/node-haste/__mocks__/graceful-fs.js new file mode 100644 index 00000000..24bd7712 --- /dev/null +++ b/react-packager/src/node-haste/__mocks__/graceful-fs.js @@ -0,0 +1,211 @@ +/** + * 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 fs = jest.genMockFromModule('fs'); +const noop = () => {}; + +function asyncCallback(cb) { + return function() { + setImmediate(() => cb.apply(this, arguments)); + }; +} + +const mtime = { + getTime: () => Math.ceil(Math.random() * 10000000), +}; + +fs.realpath.mockImpl((filepath, callback) => { + callback = asyncCallback(callback); + let node; + try { + node = getToNode(filepath); + } catch (e) { + return callback(e); + } + if (node && typeof node === 'object' && node.SYMLINK != null) { + return callback(null, node.SYMLINK); + } + callback(null, filepath); +}); + +fs.readdirSync.mockImpl((filepath) => Object.keys(getToNode(filepath))); + +fs.readdir.mockImpl((filepath, callback) => { + callback = asyncCallback(callback); + let node; + try { + node = getToNode(filepath); + if (node && typeof node === 'object' && node.SYMLINK != null) { + node = getToNode(node.SYMLINK); + } + } catch (e) { + return callback(e); + } + + if (!(node && typeof node === 'object' && node.SYMLINK == null)) { + return callback(new Error(filepath + ' is not a directory.')); + } + + callback(null, Object.keys(node)); +}); + +fs.readFile.mockImpl(function(filepath, encoding, callback) { + callback = asyncCallback(callback); + if (arguments.length === 2) { + callback = encoding; + encoding = null; + } + + let node; + try { + node = getToNode(filepath); + // dir check + if (node && typeof node === 'object' && node.SYMLINK == null) { + callback(new Error('Error readFile a dir: ' + filepath)); + } + return callback(null, node); + } catch (e) { + return callback(e); + } +}); + +fs.stat.mockImpl((filepath, callback) => { + callback = asyncCallback(callback); + let node; + try { + node = getToNode(filepath); + } catch (e) { + callback(e); + return; + } + + if (node.SYMLINK) { + fs.stat(node.SYMLINK, callback); + return; + } + + if (node && typeof node === 'object') { + callback(null, { + isDirectory: () => true, + isSymbolicLink: () => false, + mtime, + }); + } else { + callback(null, { + isDirectory: () => false, + isSymbolicLink: () => false, + mtime, + }); + } +}); + +fs.statSync.mockImpl((filepath) => { + const node = getToNode(filepath); + + if (node.SYMLINK) { + return fs.statSync(node.SYMLINK); + } + + return { + isDirectory: () => node && typeof node === 'object', + isSymbolicLink: () => false, + mtime, + }; +}); + +fs.lstatSync.mockImpl((filepath) => { + const node = getToNode(filepath); + + if (node.SYMLINK) { + return { + isDirectory: () => false, + isSymbolicLink: () => true, + mtime, + }; + } + + return { + isDirectory: () => node && typeof node === 'object', + isSymbolicLink: () => false, + mtime, + }; +}); + +fs.open.mockImpl(function(path) { + const callback = arguments[arguments.length - 1] || noop; + let data, error, fd; + try { + data = getToNode(path); + } catch (e) { + error = e; + } + + if (error || data == null) { + error = Error(`ENOENT: no such file or directory, open ${path}`); + } + if (data != null) { + /* global Buffer: true */ + fd = {buffer: new Buffer(data, 'utf8'), position: 0}; + } + + callback(error, fd); +}); + +fs.read.mockImpl((fd, buffer, writeOffset, length, position, callback = noop) => { + let bytesWritten; + try { + if (position == null || position < 0) { + ({position} = fd); + } + bytesWritten = fd.buffer.copy(buffer, writeOffset, position, position + length); + fd.position = position + bytesWritten; + } catch (e) { + callback(Error('invalid argument')); + return; + } + callback(null, bytesWritten, buffer); +}); + +fs.close.mockImpl((fd, callback = noop) => { + try { + fd.buffer = fs.position = undefined; + } catch (e) { + callback(Error('invalid argument')); + return; + } + callback(null); +}); + +let filesystem; + +fs.__setMockFilesystem = (object) => filesystem = object; + +function getToNode(filepath) { + // Ignore the drive for Windows paths. + if (filepath.match(/^[a-zA-Z]:\\/)) { + filepath = filepath.substring(2); + } + + const parts = filepath.split(/[\/\\]/); + if (parts[0] !== '') { + throw new Error('Make sure all paths are absolute.'); + } + let node = filesystem; + parts.slice(1).forEach((part) => { + if (node && node.SYMLINK) { + node = getToNode(node.SYMLINK); + } + node = node[part]; + }); + + return node; +} + +module.exports = fs; diff --git a/react-packager/src/node-haste/__tests__/AssetModule-test.js b/react-packager/src/node-haste/__tests__/AssetModule-test.js new file mode 100644 index 00000000..749351b9 --- /dev/null +++ b/react-packager/src/node-haste/__tests__/AssetModule-test.js @@ -0,0 +1,28 @@ +/** + * 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.autoMockOff(); + +const AssetModule = require('../AssetModule'); + +describe('AssetModule:', () => { + const defaults = {file: '/arbitrary'}; + + pit('has no dependencies by default', () => { + return new AssetModule(defaults).getDependencies() + .then(deps => expect(deps).toEqual([])); + }); + + pit('can be parametrized with dependencies', () => { + const dependencies = ['arbitrary', 'dependencies']; + return new AssetModule({...defaults, dependencies}).getDependencies() + .then(deps => expect(deps).toEqual(dependencies)); + }); +}); diff --git a/react-packager/src/node-haste/__tests__/DependencyGraph-test.js b/react-packager/src/node-haste/__tests__/DependencyGraph-test.js new file mode 100644 index 00000000..f5d1510d --- /dev/null +++ b/react-packager/src/node-haste/__tests__/DependencyGraph-test.js @@ -0,0 +1,6198 @@ +/** + * 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.autoMockOff(); +jest.useRealTimers(); +jest.mock('fs'); + +const mocksPattern = /(?:[\\/]|^)__mocks__[\\/]([^\/]+)\.js$/; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + +describe('DependencyGraph', function() { + let Module; + let defaults; + + function getOrderedDependenciesAsJSON(dgraph, entryPath, platform, recursive = true) { + return dgraph.getDependencies({entryPath, platform, recursive}) + .then(response => response.finalize()) + .then(({ dependencies }) => Promise.all(dependencies.map(dep => Promise.all([ + dep.getName(), + dep.getDependencies(), + ]).then(([name, moduleDependencies]) => ({ + path: dep.path, + isJSON: dep.isJSON(), + isAsset: dep.isAsset(), + isAsset_DEPRECATED: dep.isAsset_DEPRECATED(), + isPolyfill: dep.isPolyfill(), + resolution: dep.resolution, + id: name, + dependencies: moduleDependencies, + }))) + )); + } + + beforeEach(function() { + Module = require('../Module'); + const fileWatcher = { + on: function() { + return this; + }, + isWatchman: () => Promise.resolve(false), + }; + + const Cache = jest.genMockFn().mockImplementation(function() { + this._maps = Object.create(null); + }); + Cache.prototype.has = jest.genMockFn() + .mockImplementation(function(filepath, field) { + if (!(filepath in this._maps)) { + return false; + } + return !field || field in this._maps[filepath]; + }); + Cache.prototype.get = jest.genMockFn() + .mockImplementation(function(filepath, field, factory) { + let cacheForPath = this._maps[filepath]; + if (this.has(filepath, field)) { + return field ? cacheForPath[field] : cacheForPath; + } + + if (!cacheForPath) { + cacheForPath = this._maps[filepath] = Object.create(null); + } + const value = cacheForPath[field] = factory(); + return value; + }); + Cache.prototype.invalidate = jest.genMockFn() + .mockImplementation(function(filepath, field) { + if (!this.has(filepath, field)) { + return; + } + + if (field) { + delete this._maps[filepath][field]; + } else { + delete this._maps[filepath]; + } + }); + Cache.prototype.end = jest.genMockFn(); + + defaults = { + assetExts: ['png', 'jpg'], + cache: new Cache(), + fileWatcher, + providesModuleNodeModules: [ + 'haste-fbjs', + 'react-haste', + 'react-native', + // Parse requires AsyncStorage. They will + // change that to require('react-native') which + // should work after this release and we can + // remove it from here. + 'parse', + ], + platforms: ['ios', 'android'], + shouldThrowOnUnresolvedErrors: () => false, + }; + }); + + describe('get sync dependencies (posix)', function() { + let DependencyGraph; + const realPlatform = process.platform; + beforeEach(function() { + process.platform = 'linux'; + DependencyGraph = require('../index'); + }); + + afterEach(function() { + process.platform = realPlatform; + }); + + pit('should get dependencies', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("b")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: ['b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'b', + path: '/root/b.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should resolve relative entry path', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, 'index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get shallow dependencies', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("b")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js', null, false).then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: ['b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get dependencies with the correct extensions', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + 'a.js.orig': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get json dependencies', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'package', + }), + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./a.json")', + 'require("./b")', + ].join('\n'), + 'a.json': JSON.stringify({}), + 'b.json': JSON.stringify({}), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./a.json', './b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'package/a.json', + isJSON: true, + path: '/root/a.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'package/b.json', + isJSON: true, + path: '/root/b.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get package json as a dep', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'package', + }), + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./package.json")', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./package.json'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'package/package.json', + isJSON: true, + path: '/root/package.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get dependencies with deprecated assets', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("image!a")', + ].join('\n'), + 'imgs': { + 'a.png': '', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetRoots_DEPRECATED: ['/root/imgs'], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get dependencies with relative assets', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png")', + ].join('\n'), + 'imgs': { + 'a.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage', + }), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./imgs/a.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get dependencies with assets and resolution', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png");', + 'require("./imgs/b.png");', + 'require("./imgs/c.png");', + ].join('\n'), + 'imgs': { + 'a@1.5x.png': '', + 'b@.7x.png': '', + 'c.png': '', + 'c@2x.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage', + }), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [ + './imgs/a.png', + './imgs/b.png', + './imgs/c.png', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a@1.5x.png', + resolution: 1.5, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/b.png', + path: '/root/imgs/b@.7x.png', + resolution: 0.7, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/c.png', + path: '/root/imgs/c.png', + resolution: 1, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should respect platform extension in assets', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png");', + 'require("./imgs/b.png");', + 'require("./imgs/c.png");', + ].join('\n'), + 'imgs': { + 'a@1.5x.ios.png': '', + 'b@.7x.ios.png': '', + 'c.ios.png': '', + 'c@2x.ios.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage', + }), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js', 'ios').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [ + './imgs/a.png', + './imgs/b.png', + './imgs/c.png', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a@1.5x.ios.png', + resolution: 1.5, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/b.png', + path: '/root/imgs/b@.7x.ios.png', + resolution: 0.7, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/c.png', + path: '/root/imgs/c.ios.png', + resolution: 1, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('Deprecated and relative assets can live together', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png")', + 'require("image!a")', + ].join('\n'), + 'imgs': { + 'a.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage', + }), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetRoots_DEPRECATED: ['/root/imgs'], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./imgs/a.png', 'image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get recursive dependencies', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("index")', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: ['index'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages with a dot in the name', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("sha.js")', + 'require("x.y.z")', + ].join('\n'), + 'sha.js': { + 'package.json': JSON.stringify({ + name: 'sha.js', + main: 'main.js', + }), + 'main.js': 'lol', + }, + 'x.y.z': { + 'package.json': JSON.stringify({ + name: 'x.y.z', + main: 'main.js', + }), + 'main.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['sha.js', 'x.y.z'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'sha.js/main.js', + path: '/root/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'x.y.z/main.js', + path: '/root/x.y.z/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should default main package to index.js', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + 'index.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve using alternative ids', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + 'index.js': [ + '/**', + ' * @providesModule EpicModule', + ' */', + ].join('\n'), + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'EpicModule', + path: '/root/aPackage/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should default use index.js if main is a dir', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'lib', + }), + lib: { + 'index.js': 'lol', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/lib/index.js', + path: '/root/aPackage/lib/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve require to index if it is a dir', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'test', + }), + 'index.js': 'require("./lib/")', + lib: { + 'index.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'test/index.js', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'test/lib/index.js', + path: '/root/lib/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve require to main if it is a dir w/ a package.json', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'test', + }), + 'index.js': 'require("./lib/")', + lib: { + 'package.json': JSON.stringify({ + 'main': 'main.js', + }), + 'index.js': 'lol', + 'main.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'test/index.js', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/lib/main.js', + path: '/root/lib/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should ignore malformed packages', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + ].join('\n'), + 'aPackage': { + 'package.json': 'lol', + 'main.js': 'lol', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should fatal on multiple modules with the same name', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule index', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + + return dgraph.load().catch(err => { + expect(err.message).toEqual( + `Failed to build DependencyGraph: @providesModule naming collision:\n` + + ` Duplicate module name: index\n` + + ` Paths: /root/b.js collides with /root/index.js\n\n` + + `This error is caused by a @providesModule declaration ` + + `with the same name across two different files.` + ); + expect(err.type).toEqual('DependencyGraphError'); + }); + }); + + pit('should be forgiving with missing requires', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("lolomg")', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['lolomg'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with packages with subdirs', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with packages with symlinked subdirs', function() { + var root = '/root'; + setMockFileSystem({ + 'symlinkedPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot', + }, + }, + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { SYMLINK: '/symlinkedPackage' }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with relative modules in packages', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'require("./subdir/lolynot")', + 'subdir': { + 'lolynot.js': 'require("../other")', + }, + 'other.js': 'some code', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: ['./subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: ['../other'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/other.js', + path: '/root/aPackage/other.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + testBrowserField('browser'); + testBrowserField('react-native'); + + function replaceBrowserField(json, fieldName) { + if (fieldName !== 'browser') { + json[fieldName] = json.browser; + delete json.browser; + } + + return json; + } + + function testBrowserField(fieldName) { + pit('should support simple browser field in packages ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + main: 'main.js', + browser: 'client.js', + }, fieldName)), + 'main.js': 'some other code', + 'client.js': 'some code', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should support browser field in packages w/o .js ext ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + main: 'main.js', + browser: 'client', + }, fieldName)), + 'main.js': 'some other code', + 'client.js': 'some code', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support mapping main in browser field json ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + main: './main.js', + browser: { + './main.js': './client.js', + }, + }, fieldName)), + 'main.js': 'some other code', + 'client.js': 'some code', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetExts: ['png', 'jpg'], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work do correct browser mapping w/o js ext ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + }, + }, fieldName)), + 'main.js': 'some other code', + 'client.js': 'some code', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetExts: ['png', 'jpg'], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping of files ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + './node.js': './not-node.js', + './not-browser': './browser.js', + './dir/server.js': './dir/client', + './hello.js': './bye.js', + }, + }, fieldName)), + 'main.js': 'some other code', + 'client.js': 'require("./node")\nrequire("./dir/server.js")', + 'not-node.js': 'require("./not-browser")', + 'not-browser.js': 'require("./dir/server")', + 'browser.js': 'some browser code', + 'dir': { + 'server.js': 'some node code', + 'client.js': 'require("../hello")', + }, + 'hello.js': 'hello', + 'bye.js': 'bye', + }, + }, + }); + + const dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: ['./node', './dir/server.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/not-node.js', + path: '/root/aPackage/not-node.js', + dependencies: ['./not-browser'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/browser.js', + path: '/root/aPackage/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/dir/client.js', + path: '/root/aPackage/dir/client.js', + dependencies: ['../hello'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/bye.js', + path: '/root/aPackage/bye.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping for packages ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + browser: { + 'node-package': 'browser-package', + }, + }, fieldName)), + 'index.js': 'require("node-package")', + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + 'browser-package': { + 'package.json': JSON.stringify({ + 'name': 'browser-package', + }), + 'index.js': 'some browser code', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'browser-package/index.js', + path: '/root/aPackage/browser-package/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping of a package to a file ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + browser: { + 'node-package': './dir/browser.js', + }, + }, fieldName)), + 'index.js': 'require("./dir/ooga")', + 'dir': { + 'ooga.js': 'require("node-package")', + 'browser.js': 'some browser code', + }, + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + }, + }, + }); + + const dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['./dir/ooga'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/dir/ooga.js', + path: '/root/aPackage/dir/ooga.js', + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/dir/browser.js', + path: '/root/aPackage/dir/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping for packages ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + browser: { + 'node-package': 'browser-package', + }, + }, fieldName)), + 'index.js': 'require("node-package")', + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + 'browser-package': { + 'package.json': JSON.stringify({ + 'name': 'browser-package', + }), + 'index.js': 'some browser code', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'browser-package/index.js', + path: '/root/aPackage/browser-package/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser exclude of a package ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + browser: { + 'booga': false, + }, + }, fieldName)), + 'index.js': 'require("booga")', + 'booga': { + 'package.json': JSON.stringify({ + 'name': 'booga', + }), + 'index.js': 'some node code', + }, + }, + }, + }); + + const dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['booga'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser exclude of a file ("' + fieldName + '")', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify(replaceBrowserField({ + name: 'aPackage', + browser: { + './booga.js': false, + }, + }, fieldName)), + 'index.js': 'require("./booga")', + 'booga.js': 'some node code', + }, + }, + }); + + const dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['./booga'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + } + + pit('should fall back to browser mapping from react-native mapping', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + 'react-native': { + 'node-package': 'rn-package', + }, + }), + 'index.js': 'require("node-package")', + 'node_modules': { + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + 'rn-package': { + 'package.json': JSON.stringify({ + 'name': 'rn-package', + browser: { + 'nested-package': 'nested-browser-package', + }, + }), + 'index.js': 'require("nested-package")', + }, + 'nested-browser-package': { + 'package.json': JSON.stringify({ + 'name': 'nested-browser-package', + }), + 'index.js': 'some code', + }, + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'rn-package/index.js', + path: '/root/aPackage/node_modules/rn-package/index.js', + dependencies: ['nested-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'nested-browser-package/index.js', + path: '/root/aPackage/node_modules/nested-browser-package/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with absolute paths', () => { + const root = '/root'; + setMockFileSystem({ + [root.slice(1)]: { + 'index.js': 'require("/root/arbitrary.js");', + 'arbitrary.js': '', + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['/root/arbitrary.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/arbitrary.js', + path: '/root/arbitrary.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should merge browser mapping with react-native mapping', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + 'react-native': { + // should see this: + 'node-package-a': 'rn-package-a', + // should see this: + 'node-package-c': 'rn-package-d', + }, + 'browser': { + // should see this: + 'node-package-b': 'rn-package-b', + // should NOT see this: + 'node-package-c': 'rn-package-c', + }, + }), + 'index.js': 'require("node-package-a"); require("node-package-b"); require("node-package-c");', + 'node_modules': { + 'node-package-a': { + 'package.json': JSON.stringify({ + 'name': 'node-package-a', + }), + 'index.js': 'some node code', + }, + 'node-package-b': { + 'package.json': JSON.stringify({ + 'name': 'node-package-b', + }), + 'index.js': 'some node code', + }, + 'node-package-c': { + 'package.json': JSON.stringify({ + 'name': 'node-package-c', + }), + 'index.js': 'some node code', + }, + 'node-package-d': { + 'package.json': JSON.stringify({ + 'name': 'node-package-d', + }), + 'index.js': 'some node code', + }, + 'rn-package-a': { + 'package.json': JSON.stringify({ + 'name': 'rn-package-a', + }), + 'index.js': 'some rn code', + }, + 'rn-package-b': { + 'package.json': JSON.stringify({ + 'name': 'rn-package-b', + }), + 'index.js': 'some rn code', + }, + 'rn-package-c': { + 'package.json': JSON.stringify({ + 'name': 'rn-package-c', + }), + 'index.js': 'some rn code', + }, + 'rn-package-d': { + 'package.json': JSON.stringify({ + 'name': 'rn-package-d', + }), + 'index.js': 'some rn code', + }, + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['node-package-a', 'node-package-b', 'node-package-c'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'rn-package-a/index.js', + path: '/root/aPackage/node_modules/rn-package-a/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'rn-package-b/index.js', + path: '/root/aPackage/node_modules/rn-package-b/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'rn-package-d/index.js', + path: '/root/aPackage/node_modules/rn-package-d/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should fall back to `extraNodeModules`', () => { + const root = '/root'; + setMockFileSystem({ + [root.slice(1)]: { + 'index.js': 'require("./foo")', + 'foo': { + 'index.js': 'require("bar")', + }, + 'provides-bar': { + 'package.json': '{"main": "lib/bar.js"}', + 'lib': { + 'bar.js': '', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + extraNodeModules: { + 'bar': root + '/provides-bar', + }, + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['./foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/foo/index.js', + path: '/root/foo/index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/provides-bar/lib/bar.js', + path: '/root/provides-bar/lib/bar.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should only use `extraNodeModules` after checking all possible filesystem locations', () => { + const root = '/root'; + setMockFileSystem({ + [root.slice(1)]: { + 'index.js': 'require("bar")', + 'node_modules': { 'bar.js': '' }, + 'provides-bar': { 'index.js': '' }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + extraNodeModules: { + 'bar': root + '/provides-bar', + }, + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/node_modules/bar.js', + path: '/root/node_modules/bar.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should be able to resolve paths within `extraNodeModules`', () => { + const root = '/root'; + setMockFileSystem({ + [root.slice(1)]: { + 'index.js': 'require("bar/lib/foo")', + 'provides-bar': { + 'package.json': '{}', + 'lib': {'foo.js': ''}, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + extraNodeModules: { + 'bar': root + '/provides-bar', + }, + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['bar/lib/foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/provides-bar/lib/foo.js', + path: '/root/provides-bar/lib/foo.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('get sync dependencies (win32)', function() { + const realPlatform = process.platform; + let DependencyGraph; + beforeEach(function() { + process.platform = 'win32'; + DependencyGraph = require('../index'); // force reload with fastpath + }); + + afterEach(function() { + process.platform = realPlatform; + }); + + pit('should get dependencies', function() { + process.platform = 'win32'; + const root = 'C:\\root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("b")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, 'C:\\root\\index.js').then((deps) => { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'a', + path: 'C:\\root\\a.js', + dependencies: ['b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'b', + path: 'C:\\root\\b.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with absolute paths', () => { + const root = 'C:\\root'; + setMockFileSystem({ + 'root': { + 'index.js': 'require("C:\\root\\arbitrary.js");', + 'arbitrary.js': '', + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, 'C:\\root\\index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'C:\\root\\index.js', + path: 'C:\\root\\index.js', + dependencies: ['C:\\root\\arbitrary.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'C:\\root\\arbitrary.js', + path: 'C:\\root\\arbitrary.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('node_modules (posix)', function() { + const realPlatform = process.platform; + let DependencyGraph; + beforeEach(function() { + process.platform = 'linux'; + DependencyGraph = require('../index'); + }); + + afterEach(function() { + process.platform = realPlatform; + }); + + pit('should work with nested node_modules', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('platform should work with node_modules', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + }), + 'index.ios.js': '', + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main', + }), + 'main.ios.js': '', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.ios.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/index.ios.js', + path: '/root/node_modules/foo/index.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.ios.js', + path: '/root/node_modules/bar/main.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with specific paths', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar/");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: '/root/node_modules/foo/node_modules/bar/lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with browser field', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + browser: { + './lol': './wow', + }, + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + 'wow.js': '', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + browser: './main2', + }), + 'main2.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: '/root/node_modules/foo/node_modules/bar/lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main2.js', + path: '/root/node_modules/bar/main2.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('node_modules should support multi level', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': '', + }, + }, + 'path': { + 'to': { + 'bar.js': [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")', + ].join('\n'), + }, + 'node_modules': {}, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar', + path: '/root/path/to/bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should selectively ignore providesModule in node_modules', function() { + var root = '/root'; + var otherRoot = '/anotherRoot'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("shouldWork");', + 'require("dontWork");', + 'require("wontWork");', + 'require("ember");', + 'require("internalVendoredPackage");', + 'require("anotherIndex");', + ].join('\n'), + 'node_modules': { + 'react-haste': { + 'package.json': JSON.stringify({ + name: 'react-haste', + main: 'main.js', + }), + // @providesModule should not be ignored here, because react-haste is whitelisted + 'main.js': [ + '/**', + ' * @providesModule shouldWork', + ' */', + 'require("submodule");', + ].join('\n'), + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + // @providesModule should be ignored here, because it's not whitelisted + 'main.js':[ + '/**', + ' * @providesModule dontWork', + ' */', + 'hi();', + ].join('\n'), + }, + 'submodule': { + 'package.json': JSON.stringify({ + name: 'submodule', + main: 'main.js', + }), + 'main.js': 'log()', + }, + }, + }, + 'ember': { + 'package.json': JSON.stringify({ + name: 'ember', + main: 'main.js', + }), + // @providesModule should be ignored here, because it's not whitelisted, + // and also, the modules "id" should be ember/main.js, not it's haste name + 'main.js':[ + '/**', + ' * @providesModule wontWork', + ' */', + 'hi();', + ].join('\n'), + }, + }, + // This part of the dep graph is meant to emulate internal facebook infra. + // By whitelisting `vendored_modules`, haste should still work. + 'vendored_modules': { + 'a-vendored-package': { + 'package.json': JSON.stringify({ + name: 'a-vendored-package', + main: 'main.js', + }), + // @providesModule should _not_ be ignored here, because it's whitelisted. + 'main.js':[ + '/**', + ' * @providesModule internalVendoredPackage', + ' */', + 'hiFromInternalPackage();', + ].join('\n'), + }, + }, + }, + // we need to support multiple roots and using haste between them + 'anotherRoot': { + 'index.js': [ + '/**', + ' * @providesModule anotherIndex', + ' */', + 'wazup()', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root, otherRoot], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [ + 'shouldWork', + 'dontWork', + 'wontWork', + 'ember', + 'internalVendoredPackage', + 'anotherIndex', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'shouldWork', + path: '/root/node_modules/react-haste/main.js', + dependencies: ['submodule'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'submodule/main.js', + path: '/root/node_modules/react-haste/node_modules/submodule/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'ember/main.js', + path: '/root/node_modules/ember/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'internalVendoredPackage', + path: '/root/vendored_modules/a-vendored-package/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'anotherIndex', + path: '/anotherRoot/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should not be confused by prev occuring whitelisted names', function() { + var root = '/react-haste'; + setMockFileSystem({ + 'react-haste': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("shouldWork");', + ].join('\n'), + 'node_modules': { + 'react-haste': { + 'package.json': JSON.stringify({ + name: 'react-haste', + main: 'main.js', + }), + 'main.js': [ + '/**', + ' * @providesModule shouldWork', + ' */', + ].join('\n'), + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/react-haste/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/react-haste/index.js', + dependencies: ['shouldWork'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'shouldWork', + path: '/react-haste/node_modules/react-haste/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + + pit('should ignore modules it cant find (assumes own require system)', function() { + // For example SourceMap.js implements it's own require system. + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo/lol");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'foo module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with node packages with a .js in the name', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("sha.js")', + ].join('\n'), + 'node_modules': { + 'sha.js': { + 'package.json': JSON.stringify({ + name: 'sha.js', + main: 'main.js', + }), + 'main.js': 'lol', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['sha.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'sha.js/main.js', + path: '/root/node_modules/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with multiple platforms (haste)', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('a'); + `, + 'a.ios.js': ` + /** + * @providesModule a + */ + `, + 'a.android.js': ` + /** + * @providesModule a + */ + `, + 'a.js': ` + /** + * @providesModule a + */ + `, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.ios.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should pick the generic file', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('a'); + `, + 'a.android.js': ` + /** + * @providesModule a + */ + `, + 'a.js': ` + /** + * @providesModule a + */ + `, + 'a.web.js': ` + /** + * @providesModule a + */ + `, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + platforms: ['ios', 'android', 'web'], + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.ios.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with multiple platforms (node)', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('./a'); + `, + 'a.ios.js': '', + 'a.android.js': '', + 'a.js': '', + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.ios.js', + dependencies: ['./a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/a.ios.js', + path: '/root/a.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should require package.json', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo/package.json");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'require("./package.json")', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo/package.json', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/package.json', + path: '/root/node_modules/foo/package.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: true, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/bar/main.js', + dependencies: ['./package.json'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/package.json', + path: '/root/node_modules/bar/package.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: true, + isPolyfill: false, + resolution: undefined, + }, + + ]); + }); + }); + + pit('should work with one-character node_modules', () => { + const root = '/root'; + setMockFileSystem({ + [root.slice(1)]: { + 'index.js': 'require("a/index.js");', + 'node_modules': { + 'a': { + 'package.json': '{"name": "a", "version": "1.2.3"}', + 'index.js': '', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['a/index.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a/index.js', + path: '/root/node_modules/a/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('node_modules (win32)', function() { + const realPlatform = process.platform; + + // these tests will not work in a simulated way on linux testing VMs + // due to the drive letter expectation + if (realPlatform !== 'win32') { return; } + + const DependencyGraph = require('../index'); + + pit('should work with nested node_modules', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: 'C:\\root\\node_modules\\foo\\main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: 'C:\\root\\node_modules\\foo\\node_modules\\bar\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: 'C:\\root\\node_modules\\bar\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('platform should work with node_modules', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + }), + 'index.ios.js': '', + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main', + }), + 'main.ios.js': '', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.ios.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/index.ios.js', + path: 'C:\\root\\node_modules\\foo\\index.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.ios.js', + path: 'C:\\root\\node_modules\\bar\\main.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with specific paths', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar/");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['foo', 'bar/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: 'C:\\root\\node_modules\\foo\\main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: 'C:\\root\\node_modules\\foo\\node_modules\\bar\\lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: 'C:\\root\\node_modules\\bar\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with browser field', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + browser: { + './lol': './wow', + }, + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + 'wow.js': '', + }, + }, + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + browser: './main2', + }), + 'main2.js': 'bar 2 module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: 'C:\\root\\node_modules\\foo\\main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: 'C:\\root\\node_modules\\foo\\node_modules\\bar\\lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main2.js', + path: 'C:\\root\\node_modules\\bar\\main2.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('node_modules should support multi level', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': '', + }, + }, + 'path': { + 'to': { + 'bar.js': [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")', + ].join('\n'), + }, + 'node_modules': {}, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar', + path: 'C:\\root\\path\\to\\bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: 'C:\\root\\node_modules\\foo\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should selectively ignore providesModule in node_modules', function() { + var root = '/root'; + var otherRoot = '/anotherRoot'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("shouldWork");', + 'require("dontWork");', + 'require("wontWork");', + 'require("ember");', + 'require("internalVendoredPackage");', + 'require("anotherIndex");', + ].join('\n'), + 'node_modules': { + 'react-haste': { + 'package.json': JSON.stringify({ + name: 'react-haste', + main: 'main.js', + }), + // @providesModule should not be ignored here, because react-haste is whitelisted + 'main.js': [ + '/**', + ' * @providesModule shouldWork', + ' */', + 'require("submodule");', + ].join('\n'), + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + // @providesModule should be ignored here, because it's not whitelisted + 'main.js':[ + '/**', + ' * @providesModule dontWork', + ' */', + 'hi();', + ].join('\n'), + }, + 'submodule': { + 'package.json': JSON.stringify({ + name: 'submodule', + main: 'main.js', + }), + 'main.js': 'log()', + }, + }, + }, + 'ember': { + 'package.json': JSON.stringify({ + name: 'ember', + main: 'main.js', + }), + // @providesModule should be ignored here, because it's not whitelisted, + // and also, the modules "id" should be ember/main.js, not it's haste name + 'main.js':[ + '/**', + ' * @providesModule wontWork', + ' */', + 'hi();', + ].join('\n'), + }, + }, + // This part of the dep graph is meant to emulate internal facebook infra. + // By whitelisting `vendored_modules`, haste should still work. + 'vendored_modules': { + 'a-vendored-package': { + 'package.json': JSON.stringify({ + name: 'a-vendored-package', + main: 'main.js', + }), + // @providesModule should _not_ be ignored here, because it's whitelisted. + 'main.js':[ + '/**', + ' * @providesModule internalVendoredPackage', + ' */', + 'hiFromInternalPackage();', + ].join('\n'), + }, + }, + }, + // we need to support multiple roots and using haste between them + 'anotherRoot': { + 'index.js': [ + '/**', + ' * @providesModule anotherIndex', + ' */', + 'wazup()', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root, otherRoot], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: [ + 'shouldWork', + 'dontWork', + 'wontWork', + 'ember', + 'internalVendoredPackage', + 'anotherIndex', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'shouldWork', + path: 'C:\\root\\node_modules\\react-haste\\main.js', + dependencies: ['submodule'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'submodule/main.js', + path: 'C:\\root\\node_modules\\react-haste\\node_modules\\submodule\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'ember/main.js', + path: 'C:\\root\\node_modules\\ember\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'internalVendoredPackage', + path: 'C:\\root\\vendored_modules\\a-vendored-package\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'anotherIndex', + path: 'C:\\anotherRoot\\index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should not be confused by prev occuring whitelisted names', function() { + var root = '/react-haste'; + setMockFileSystem({ + 'react-haste': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("shouldWork");', + ].join('\n'), + 'node_modules': { + 'react-haste': { + 'package.json': JSON.stringify({ + name: 'react-haste', + main: 'main.js', + }), + 'main.js': [ + '/**', + ' * @providesModule shouldWork', + ' */', + ].join('\n'), + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/react-haste/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\react-haste\\index.js', + dependencies: ['shouldWork'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'shouldWork', + path: 'C:\\react-haste\\node_modules\\react-haste\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should ignore modules it cant find (assumes own require system)', function() { + // For example SourceMap.js implements it's own require system. + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo/lol");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'foo module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['foo/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with node packages with a .js in the name', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("sha.js")', + ].join('\n'), + 'node_modules': { + 'sha.js': { + 'package.json': JSON.stringify({ + name: 'sha.js', + main: 'main.js', + }), + 'main.js': 'lol', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['sha.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'sha.js/main.js', + path: 'C:\\root\\node_modules\\sha.js\\main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with multiple platforms (haste)', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('a'); + `, + 'a.ios.js': ` + /** + * @providesModule a + */ + `, + 'a.android.js': ` + /** + * @providesModule a + */ + `, + 'a.js': ` + /** + * @providesModule a + */ + `, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.ios.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: 'C:\\root\\a.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should pick the generic file', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('a'); + `, + 'a.android.js': ` + /** + * @providesModule a + */ + `, + 'a.js': ` + /** + * @providesModule a + */ + `, + 'a.web.js': ` + /** + * @providesModule a + */ + `, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.ios.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: 'C:\\root\\a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with multiple platforms (node)', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.ios.js': ` + /** + * @providesModule index + */ + require('./a'); + `, + 'a.ios.js': '', + 'a.android.js': '', + 'a.js': '', + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.ios.js', + dependencies: ['./a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'C:\\root\\a.ios.js', + path: 'C:\\root\\a.ios.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should require package.json', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo/package.json");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'require("./package.json")', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(deps => { + expect(deps) + .toEqual([ + { + id: 'index', + path: 'C:\\root\\index.js', + dependencies: ['foo/package.json', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/package.json', + path: 'C:\\root\\node_modules\\foo\\package.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: true, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: 'C:\\root\\node_modules\\bar\\main.js', + dependencies: ['./package.json'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/package.json', + path: 'C:\\root\\node_modules\\bar\\package.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: true, + isPolyfill: false, + resolution: undefined, + }, + + ]); + }); + }); + }); + + describe('file watch updating', function() { + var triggerFileChange; + var mockStat = { + isDirectory: () => false, + }; + + const realPlatform = process.platform; + let DependencyGraph; + + 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() { + process.platform = realPlatform; + }); + + pit('updates module dependencies', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")', + ].join('\n'), + 'foo': [ + '/**', + ' * @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() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root, mockStat); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file change', 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], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root, mockStat); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file delete', 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], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { + delete filesystem.root.foo; + triggerFileChange('delete', 'foo.js', root); + 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, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file add', 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], + }); + 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, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar', + path: '/root/bar.js', + dependencies: ['foo'], + 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, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on deprecated asset add', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("image!foo")', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetRoots_DEPRECATED: [root], + assetExts: ['png'], + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + + filesystem.root['foo.png'] = ''; + triggerFileChange('add', 'foo.png', root, mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'image!foo', + path: '/root/foo.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on relative asset add', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./foo.png")', + ].join('\n'), + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + assetExts: ['png'], + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + + filesystem.root['foo.png'] = ''; + triggerFileChange('add', 'foo.png', root, mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/foo.png', + path: '/root/foo.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('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, + }, + ]); + }); + }); + }); + + pit('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, + }, + ]); + }); + }); + }); + + pit('changes to browser field', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'main', + 'browser.js': 'browser', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'browser.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/browser.js', + path: '/root/aPackage/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('removes old package from cache', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': 'main', + 'browser.js': 'browser', + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'bPackage', + main: 'main.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should update node package changes', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + }, + }, + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + + filesystem.root.node_modules.foo['main.js'] = 'lol'; + triggerFileChange('change', 'main.js', '/root/node_modules/foo', mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should update node package main changes', function() { + var root = '/root'; + var filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'foo module', + 'browser.js': 'foo module', + }, + }, + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + filesystem.root.node_modules.foo['package.json'] = JSON.stringify({ + name: 'foo', + main: 'main.js', + browser: 'browser.js', + }); + triggerFileChange('change', 'package.json', '/root/node_modules/foo', mockStat); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/browser.js', + path: '/root/node_modules/foo/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should not error when the watcher reports a known file as added', function() { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'var b = require("b");', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + 'module.exports = function() {};', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { + triggerFileChange('add', 'index.js', root, mockStat); + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js'); + }); + }); + }); + + describe('Extensions', () => { + const realPlatform = process.platform; + let DependencyGraph; + beforeEach(function() { + process.platform = 'linux'; + DependencyGraph = require('../index'); + }); + + afterEach(function() { + process.platform = realPlatform; + }); + + pit('supports custom file extensions', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + 'index.jsx': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.coffee': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + 'X.js': '', + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + extensions: ['jsx', 'coffee'], + }); + + return dgraph.matchFilesByPattern('.*') + .then(files => { + expect(files).toEqual([ + '/root/index.jsx', '/root/a.coffee', + ]); + }) + .then(() => getOrderedDependenciesAsJSON(dgraph, '/root/index.jsx')) + .then(deps => { + expect(deps).toEqual([ + { + dependencies: ['a'], + id: 'index', + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + path: '/root/index.jsx', + resolution: undefined, + }, + { + dependencies: [], + id: 'a', + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + path: '/root/a.coffee', + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('Mocks', () => { + const realPlatform = process.platform; + let DependencyGraph; + beforeEach(function() { + process.platform = 'linux'; + DependencyGraph = require('../index'); + }); + + afterEach(function() { + process.platform = realPlatform; + }); + + pit('resolves to null if mocksPattern is not specified', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + '__mocks__': { + 'A.js': '', + }, + 'index.js': '', + }, + }); + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + }); + + return dgraph.getDependencies({entryPath: '/root/index.js'}) + .then(response => response.finalize()) + .then(response => { + expect(response.mocks).toEqual({}); + }); + }); + + pit('retrieves a list of all required mocks', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + '__mocks__': { + 'A.js': '', + 'b.js': '', + }, + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + 'require("A");', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + mocksPattern, + }); + + return dgraph.getDependencies({entryPath: '/root/b.js'}) + .then(response => response.finalize()) + .then(response => { + expect(response.mocks).toEqual({ + A: '/root/__mocks__/A.js', + b: '/root/__mocks__/b.js', + }); + }); + }); + + pit('adds mocks as a dependency of their actual module', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + '__mocks__': { + 'A.js': [ + 'require("b");', + ].join('\n'), + 'b.js': '', + }, + 'A.js': [ + '/**', + ' * @providesModule A', + ' */', + 'require("foo");', + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + mocksPattern, + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/A.js') + .then(deps => { + expect(deps).toEqual([ + { + path: '/root/A.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: 'A', + dependencies: ['foo', 'A'], + }, + { + path: '/root/foo.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: 'foo', + dependencies: [], + }, + { + path: '/root/__mocks__/A.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: '/root/__mocks__/A.js', + dependencies: ['b'], + }, + { + path: '/root/__mocks__/b.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: '/root/__mocks__/b.js', + dependencies: [], + }, + ]); + }); + }); + + pit('resolves mocks that do not have a real module associated with them', () => { + var root = '/root'; + setMockFileSystem({ + 'root': { + '__mocks__': { + 'foo.js': [ + 'require("b");', + ].join('\n'), + 'b.js': '', + }, + 'A.js': [ + '/**', + ' * @providesModule A', + ' */', + 'require("foo");', + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + ...defaults, + roots: [root], + mocksPattern, + }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/A.js') + .then(deps => { + expect(deps).toEqual([ + { + path: '/root/A.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: 'A', + dependencies: ['foo'], + }, + { + path: '/root/__mocks__/foo.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: '/root/__mocks__/foo.js', + dependencies: ['b'], + }, + { + path: '/root/__mocks__/b.js', + isJSON: false, + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + id: '/root/__mocks__/b.js', + dependencies: [], + }, + ]); + }); + }); + }); + + describe('Progress updates', () => { + let dependencyGraph, onProgress; + + function makeModule(id, dependencies = []) { + return ` + /** + * @providesModule ${id} + */\n` + + dependencies.map(d => `require(${JSON.stringify(d)});`).join('\n'); + } + + function getDependencies() { + return dependencyGraph.getDependencies({ + entryPath: '/root/index.js', + onProgress, + }); + } + + beforeEach(function() { + onProgress = jest.genMockFn(); + setMockFileSystem({ + 'root': { + 'index.js': makeModule('index', ['a', 'b']), + 'a.js': makeModule('a', ['c', 'd']), + 'b.js': makeModule('b', ['d', 'e']), + 'c.js': makeModule('c'), + 'd.js': makeModule('d', ['f']), + 'e.js': makeModule('e', ['f']), + 'f.js': makeModule('f', ['g']), + 'g.js': makeModule('g'), + }, + }); + const DependencyGraph = require('../'); + dependencyGraph = new DependencyGraph({ + ...defaults, + roots: ['/root'], + }); + }); + + pit('calls back for each finished module', () => { + return getDependencies().then(() => + expect(onProgress.mock.calls.length).toBe(8) + ); + }); + + pit('increases the number of finished modules in steps of one', () => { + return getDependencies().then(() => { + const increments = onProgress.mock.calls.map(([finished]) => finished); + expect(increments).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + }); + }); + + pit('adds the number of discovered modules to the number of total modules', () => { + return getDependencies().then(() => { + const increments = onProgress.mock.calls.map(([, total]) => total); + expect(increments).toEqual([3, 5, 6, 6, 7, 7, 8, 8]); + }); + }); + }); + + describe('Asset module dependencies', () => { + let DependencyGraph; + beforeEach(() => { + DependencyGraph = require('../index'); + }); + + pit('allows setting dependencies for asset modules', () => { + const assetDependencies = ['arbitrary', 'dependencies']; + + setMockFileSystem({ + 'root': { + 'index.js': 'require("./a.png")', + 'a.png' : '', + }, + }); + + const dependencyGraph = new DependencyGraph({ + ...defaults, + assetDependencies, + roots: ['/root'], + }); + + return dependencyGraph.getDependencies({ + entryPath: '/root/index.js', + }).then(({dependencies}) => { + const [, assetModule] = dependencies; + return assetModule.getDependencies() + .then(deps => expect(deps).toBe(assetDependencies)); + }); + }); + }); + + fdescribe('Deterministic order of dependencies', () => { + let callDeferreds, dependencyGraph, moduleReadDeferreds; + let moduleRead; + let DependencyGraph; + + beforeEach(() => { + moduleRead = Module.prototype.read; + DependencyGraph = require('../index'); + setMockFileSystem({ + 'root': { + 'index.js': ` + require('./a'); + require('./b'); + `, + 'a.js': ` + require('./c'); + require('./d'); + `, + 'b.js': ` + require('./c'); + require('./d'); + `, + 'c.js': 'require("./e");', + 'd.js': '', + 'e.js': 'require("./f");', + 'f.js': 'require("./c");', // circular dependency + }, + }); + dependencyGraph = new DependencyGraph({ + ...defaults, + roots: ['/root'], + }); + moduleReadDeferreds = {}; + callDeferreds = [defer()/* a.js */, defer()/* b.js */]; + + Module.prototype.read = jest.genMockFn().mockImplementation(function() { + const returnValue = moduleRead.apply(this, arguments); + if (/\/[ab]\.js$/.test(this.path)) { + let deferred = moduleReadDeferreds[this.path]; + if (!deferred) { + deferred = moduleReadDeferreds[this.path] = defer(returnValue); + const index = Number(this.path.endsWith('b.js')); // 0 or 1 + callDeferreds[index].resolve(); + } + return deferred.promise; + } + + return returnValue; + }); + }); + + afterEach(() => { + Module.prototype.read = moduleRead; + }); + + pit('produces a deterministic tree if the "a" module resolves first', () => { + const dependenciesPromise = getOrderedDependenciesAsJSON(dependencyGraph, 'index.js'); + + return Promise.all(callDeferreds.map(deferred => deferred.promise)) + .then(() => { + const main = moduleReadDeferreds['/root/a.js']; + main.promise.then(() => { + moduleReadDeferreds['/root/b.js'].resolve(); + }); + main.resolve(); + return dependenciesPromise; + }).then(result => { + const names = result.map(({path}) => path.split('/').pop()); + expect(names).toEqual([ + 'index.js', + 'a.js', + 'c.js', + 'e.js', + 'f.js', + 'd.js', + 'b.js', + ]); + }); + }); + + pit('produces a deterministic tree if the "b" module resolves first', () => { + const dependenciesPromise = getOrderedDependenciesAsJSON(dependencyGraph, 'index.js'); + + return Promise.all(callDeferreds.map(deferred => deferred.promise)) + .then(() => { + const main = moduleReadDeferreds['/root/b.js']; + main.promise.then(() => { + moduleReadDeferreds['/root/a.js'].resolve(); + }); + main.resolve(); + return dependenciesPromise; + }).then(result => { + const names = result.map(({path}) => path.split('/').pop()); + expect(names).toEqual([ + 'index.js', + 'a.js', + 'c.js', + 'e.js', + 'f.js', + 'd.js', + 'b.js', + ]); + }); + }); + }); + + function defer(value) { + let resolve; + const promise = new Promise(r => { resolve = r; }); + return {promise, resolve: () => resolve(value)}; + } + + function setMockFileSystem(object) { + return require('graceful-fs').__setMockFilesystem(object); + } +}); diff --git a/react-packager/src/node-haste/__tests__/Module-test.js b/react-packager/src/node-haste/__tests__/Module-test.js new file mode 100644 index 00000000..0e9c75c4 --- /dev/null +++ b/react-packager/src/node-haste/__tests__/Module-test.js @@ -0,0 +1,502 @@ +/** + * 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('absolute-path') + .dontMock('json-stable-stringify') + .dontMock('../fastfs') + .dontMock('../lib/extractRequires') + .dontMock('../lib/replacePatterns') + .dontMock('../DependencyGraph/docblock') + .dontMock('../Module'); + +jest + .mock('fs'); + +const Fastfs = require('../fastfs'); +const Module = require('../Module'); +const ModuleCache = require('../ModuleCache'); +const DependencyGraphHelpers = require('../DependencyGraph/DependencyGraphHelpers'); +const fs = require('graceful-fs'); + +const packageJson = + JSON.stringify({ + name: 'arbitrary', + version: '1.0.0', + description: "A require('foo') story", + }); + +function mockFS(rootChildren) { + fs.__setMockFilesystem({root: rootChildren}); +} + +function mockPackageFile() { + mockFS({'package.json': packageJson}); +} + +function mockIndexFile(indexJs) { + mockFS({'index.js': indexJs}); +} + +describe('Module', () => { + const fileWatcher = { + on: () => this, + isWatchman: () => Promise.resolve(false), + }; + const fileName = '/root/index.js'; + + let cache, fastfs; + + const createCache = () => ({ + get: jest.genMockFn().mockImplementation( + (filepath, field, cb) => cb(filepath) + ), + invalidate: jest.genMockFn(), + end: jest.genMockFn(), + }); + + const createModule = (options) => + new Module({ + options: { + cacheTransformResults: true, + }, + ...options, + cache, + fastfs, + file: options && options.file || fileName, + depGraphHelpers: new DependencyGraphHelpers(), + moduleCache: new ModuleCache({fastfs, cache}), + }); + + const createJSONModule = + (options) => createModule({...options, file: '/root/package.json'}); + + beforeEach(function(done) { + process.platform = 'linux'; + cache = createCache(); + fastfs = new Fastfs( + 'test', + ['/root'], + fileWatcher, + {crawling: Promise.resolve([fileName, '/root/package.json']), ignore: []}, + ); + + fastfs.build().then(done); + }); + + describe('Module ID', () => { + const moduleId = 'arbitraryModule'; + const source = + `/** + * @providesModule ${moduleId} + */ + `; + + let module; + beforeEach(() => { + module = createModule(); + }); + + describe('@providesModule annotations', () => { + beforeEach(() => { + mockIndexFile(source); + }); + + pit('extracts the module name from the header', () => + module.getName().then(name => expect(name).toEqual(moduleId)) + ); + + pit('identifies the module as haste module', () => + module.isHaste().then(isHaste => expect(isHaste).toBe(true)) + ); + + 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()); + }); + + pit('does not transform the file in order to access the haste status', () => { + const transformCode = + jest.genMockFn().mockReturnValue(Promise.resolve()); + return createModule({transformCode}).isHaste() + .then(() => expect(transformCode).not.toBeCalled()); + }); + }); + + describe('@provides annotations', () => { + beforeEach(() => { + mockIndexFile(source.replace(/@providesModule/, '@provides')); + }); + + pit('extracts the module name from the header if it has a @provides annotation', () => + module.getName().then(name => expect(name).toEqual(moduleId)) + ); + + pit('identifies the module as haste module', () => + module.isHaste().then(isHaste => expect(isHaste).toBe(true)) + ); + + 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()); + }); + + pit('does not transform the file in order to access the haste status', () => { + const transformCode = + jest.genMockFn().mockReturnValue(Promise.resolve()); + return createModule({transformCode}).isHaste() + .then(() => expect(transformCode).not.toBeCalled()); + }); + }); + + describe('no annotation', () => { + beforeEach(() => { + mockIndexFile('arbitrary(code);'); + }); + + pit('uses the file name as module name', () => + module.getName().then(name => expect(name).toEqual(fileName)) + ); + + pit('does not identify the module as haste module', () => + module.isHaste().then(isHaste => expect(isHaste).toBe(false)) + ); + + 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()); + }); + + pit('does not transform the file in order to access the haste status', () => { + const transformCode = + jest.genMockFn().mockReturnValue(Promise.resolve()); + return createModule({transformCode}).isHaste() + .then(() => expect(transformCode).not.toBeCalled()); + }); + }); + }); + + describe('Code', () => { + const fileContents = 'arbitrary(code)'; + beforeEach(function() { + mockIndexFile(fileContents); + }); + + pit('exposes file contents as `code` property on the data exposed by `read()`', () => + createModule().read().then(({code}) => + expect(code).toBe(fileContents)) + ); + + pit('exposes file contents via the `getCode()` method', () => + createModule().getCode().then(code => + expect(code).toBe(fileContents)) + ); + }); + + describe('Extractors', () => { + + pit('uses custom require extractors if specified', () => { + mockIndexFile(''); + const module = createModule({ + extractor: code => ({deps: {sync: ['foo', 'bar']}}), + }); + + return module.getDependencies().then(actual => + expect(actual).toEqual(['foo', 'bar'])); + }); + + pit('uses a default extractor to extract dependencies', () => { + mockIndexFile(` + require('dependency-a'); + import * as b from "dependency-b"; + export {something} from 'dependency-c'; + `); + + const module = createModule(); + return module.getDependencies().then(dependencies => + expect(dependencies.sort()) + .toEqual(['dependency-a', 'dependency-b', 'dependency-c']) + ); + }); + + pit('does not extract dependencies from files annotated with @extern', () => { + mockIndexFile(` + /** + * @extern + */ + require('dependency-a'); + import * as b from "dependency-b"; + export {something} from 'dependency-c'; + `); + + const module = createModule(); + return module.getDependencies().then(dependencies => + expect(dependencies).toEqual([]) + ); + }); + + pit('does not extract dependencies from JSON files', () => { + mockPackageFile(); + const module = createJSONModule(); + return module.getDependencies().then(dependencies => + expect(dependencies).toEqual([]) + ); + }); + }); + + describe('Custom Code Transform', () => { + let transformCode; + const fileContents = 'arbitrary(code);'; + const exampleCode = ` + ${'require'}('a'); + ${'System.import'}('b'); + ${'require'}('c');`; + + beforeEach(function() { + transformCode = jest.genMockFn(); + mockIndexFile(fileContents); + transformCode.mockReturnValue(Promise.resolve({code: ''})); + }); + + pit('passes the module and file contents to the transform function when reading', () => { + const module = createModule({transformCode}); + return module.read() + .then(() => { + expect(transformCode).toBeCalledWith(module, fileContents, undefined); + }); + }); + + pit('passes any additional options to the transform function when reading', () => { + const module = createModule({transformCode}); + const transformOptions = {arbitrary: Object()}; + return module.read(transformOptions) + .then(() => + expect(transformCode.mock.calls[0][2]).toBe(transformOptions) + ); + }); + + pit('passes the module and file contents to the transform if the file is annotated with @extern', () => { + const module = createModule({transformCode}); + const fileContents = ` + /** + * @extern + */ + `; + mockIndexFile(fileContents); + return module.read().then(() => { + expect(transformCode).toBeCalledWith(module, fileContents, {extern: true}); + }); + }); + + pit('passes the module and file contents to the transform for JSON files', () => { + mockPackageFile(); + const module = createJSONModule({transformCode}); + return module.read().then(() => { + expect(transformCode).toBeCalledWith(module, packageJson, {extern: true}); + }); + }); + + pit('does not extend the passed options object if the file is annotated with @extern', () => { + const module = createModule({transformCode}); + const fileContents = ` + /** + * @extern + */ + `; + mockIndexFile(fileContents); + const options = {arbitrary: 'foo'}; + return module.read(options).then(() => { + expect(options).not.toEqual(jasmine.objectContaining({extern: true})); + expect(transformCode) + .toBeCalledWith(module, fileContents, {...options, extern: true}); + }); + }); + + pit('does not extend the passed options object for JSON files', () => { + mockPackageFile(); + const module = createJSONModule({transformCode}); + const options = {arbitrary: 'foo'}; + return module.read(options).then(() => { + expect(options).not.toEqual(jasmine.objectContaining({extern: true})); + expect(transformCode) + .toBeCalledWith(module, packageJson, {...options, extern: true}); + }); + }); + + pit('uses the code that `transformCode` resolves to to extract dependencies', () => { + transformCode.mockReturnValue(Promise.resolve({code: exampleCode})); + const module = createModule({transformCode}); + + return module.getDependencies().then(dependencies => { + expect(dependencies).toEqual(['a', 'c']); + }); + }); + + pit('uses dependencies that `transformCode` resolves to, instead of extracting them', () => { + const mockedDependencies = ['foo', 'bar']; + transformCode.mockReturnValue(Promise.resolve({ + code: exampleCode, + dependencies: mockedDependencies, + })); + const module = createModule({transformCode}); + + return module.getDependencies().then(dependencies => { + expect(dependencies).toEqual(mockedDependencies); + }); + }); + + pit('forwards all additional properties of the result provided by `transformCode`', () => { + const mockedResult = { + code: exampleCode, + arbitrary: 'arbitrary', + dependencyOffsets: [12, 764], + map: {version: 3}, + subObject: {foo: 'bar'}, + }; + transformCode.mockReturnValue(Promise.resolve(mockedResult)); + const module = createModule({transformCode}); + + return module.read().then((result) => { + expect(result).toEqual(jasmine.objectContaining(mockedResult)); + }); + }); + + pit('does not store anything but dependencies if the `cacheTransformResults` option is disabled', () => { + const mockedResult = { + code: exampleCode, + arbitrary: 'arbitrary', + dependencies: ['foo', 'bar'], + dependencyOffsets: [12, 764], + map: {version: 3}, + subObject: {foo: 'bar'}, + }; + transformCode.mockReturnValue(Promise.resolve(mockedResult)); + const module = createModule({transformCode, options: { + cacheTransformResults: false, + }}); + + return module.read().then((result) => { + expect(result).toEqual({ + dependencies: ['foo', 'bar'], + }); + }); + }); + + pit('stores all things if options is undefined', () => { + const mockedResult = { + code: exampleCode, + arbitrary: 'arbitrary', + dependencies: ['foo', 'bar'], + dependencyOffsets: [12, 764], + map: {version: 3}, + subObject: {foo: 'bar'}, + }; + transformCode.mockReturnValue(Promise.resolve(mockedResult)); + const module = createModule({transformCode, options: undefined}); + + return module.read().then((result) => { + expect(result).toEqual({ ...mockedResult, source: 'arbitrary(code);'}); + }); + }); + + pit('exposes the transformed code rather than the raw file contents', () => { + transformCode.mockReturnValue(Promise.resolve({code: exampleCode})); + const module = createModule({transformCode}); + return Promise.all([module.read(), module.getCode()]) + .then(([data, code]) => { + expect(data.code).toBe(exampleCode); + expect(code).toBe(exampleCode); + }); + }); + + pit('exposes the raw file contents as `source` property', () => { + const module = createModule({transformCode}); + return module.read() + .then(data => expect(data.source).toBe(fileContents)); + }); + + pit('exposes a source map returned by the transform', () => { + const map = {version: 3}; + transformCode.mockReturnValue(Promise.resolve({map, code: exampleCode})); + const module = createModule({transformCode}); + return Promise.all([module.read(), module.getMap()]) + .then(([data, sourceMap]) => { + expect(data.map).toBe(map); + expect(sourceMap).toBe(map); + }); + }); + + describe('Caching based on options', () => { + let module; + beforeEach(function() { + module = createModule({transformCode}); + }); + + const callsEqual = ([path1, key1], [path2, key2]) => { + expect(path1).toEqual(path2); + expect(key1).toEqual(key2); + }; + + it('gets dependencies from the cache with the same cache key for the same transform options', () => { + const options = {some: 'options'}; + module.getDependencies(options); // first call + module.getDependencies(options); // second call + + const {calls} = cache.get.mock; + callsEqual(calls[0], calls[1]); + }); + + it('gets dependencies from the cache with the same cache key for the equivalent transform options', () => { + module.getDependencies({a: 'b', c: 'd'}); // first call + module.getDependencies({c: 'd', a: 'b'}); // second call + + const {calls} = cache.get.mock; + callsEqual(calls[0], calls[1]); + }); + + it('gets dependencies from the cache with different cache keys for different transform options', () => { + module.getDependencies({some: 'options'}); + module.getDependencies({other: 'arbitrary options'}); + const {calls} = cache.get.mock; + expect(calls[0][1]).not.toEqual(calls[1][1]); + }); + + it('gets code from the cache with the same cache key for the same transform options', () => { + const options = {some: 'options'}; + module.getCode(options); // first call + module.getCode(options); // second call + + const {calls} = cache.get.mock; + callsEqual(calls[0], calls[1]); + }); + + it('gets code from the cache with the same cache key for the equivalent transform options', () => { + module.getCode({a: 'b', c: 'd'}); // first call + module.getCode({c: 'd', a: 'b'}); // second call + + const {calls} = cache.get.mock; + callsEqual(calls[0], calls[1]); + }); + + it('gets code from the cache with different cache keys for different transform options', () => { + module.getCode({some: 'options'}); + module.getCode({other: 'arbitrary options'}); + const {calls} = cache.get.mock; + expect(calls[0][1]).not.toEqual(calls[1][1]); + }); + }); + }); +}); diff --git a/react-packager/src/node-haste/__tests__/fastfs-data b/react-packager/src/node-haste/__tests__/fastfs-data new file mode 100644 index 00000000..fe2c6388 --- /dev/null +++ b/react-packager/src/node-haste/__tests__/fastfs-data @@ -0,0 +1,39 @@ +/** + * 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. + * An arbitrary module header + * @providesModule + */ + + const some: string = 'arbitrary code'; + const containing: string = 'ūñïčødę'; + +/** + * An arbitrary class that extends some thing + * It exposes a random number, which may be reset at the callers discretion + */ +class Arbitrary extends Something { + constructor() { + this.reset(); + } + + /** + * Returns the random number + * @returns number + */ + get random(): number { + return this._random; + } + + /** + * Re-creates the internal random number + * @returns void + */ + reset(): void { + this._random = Math.random(); + } +} diff --git a/react-packager/src/node-haste/__tests__/fastfs-integrated-test.js b/react-packager/src/node-haste/__tests__/fastfs-integrated-test.js new file mode 100644 index 00000000..cbff314a --- /dev/null +++ b/react-packager/src/node-haste/__tests__/fastfs-integrated-test.js @@ -0,0 +1,87 @@ +/** + * 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.autoMockOff() + .dontMock('graceful-fs'); + +const Fastfs = require('../fastfs'); + +const {EventEmitter} = require('events'); +const fs = require('fs'); +const path = require('path'); + +const fileName = path.resolve(__dirname, 'fastfs-data'); +const contents = fs.readFileSync(fileName, 'utf-8'); + +describe('fastfs:', function() { + let fastfs; + const crawling = Promise.resolve([fileName]); + const roots = [__dirname]; + const watcher = new EventEmitter(); + + beforeEach(function(done) { + fastfs = new Fastfs('arbitrary', roots, watcher, {crawling}); + fastfs.build().then(done); + }); + + describe('partial reading', () => { + // these are integrated tests that read real files from disk + + pit('reads a file while a predicate returns true', function() { + return fastfs.readWhile(fileName, () => true).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + + pit('invokes the predicate with the new chunk, the invocation index, and the result collected so far', () => { + const predicate = jest.genMockFn().mockReturnValue(true); + return fastfs.readWhile(fileName, predicate).then(() => { + let aggregated = ''; + const {calls} = predicate.mock; + expect(calls).not.toEqual([]); + + calls.forEach((call, i) => { + const [chunk] = call; + aggregated += chunk; + expect(chunk).not.toBe(''); + expect(call).toEqual([chunk, i, aggregated]); + }); + + expect(aggregated).toEqual(contents); + }); + }); + + pit('stops reading when the predicate returns false', () => { + const predicate = jest.genMockFn().mockImpl((_, i) => i !== 0); + return fastfs.readWhile(fileName, predicate).then((readContent) => { + const {calls} = predicate.mock; + expect(calls.length).toBe(1); + expect(readContent).toBe(calls[0][2]); + }); + }); + + pit('after reading the whole file with `readWhile`, `read()` still works', () => { + // this test allows to reuse the results of `readWhile` for `readFile` + return fastfs.readWhile(fileName, () => true).then(() => { + fastfs.readFile(fileName).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + }); + + pit('after reading parts of the file with `readWhile`, `read()` still works', () => { + return fastfs.readWhile(fileName, () => false).then(() => { + fastfs.readFile(fileName).then(readContent => + expect(readContent).toEqual(contents) + ); + }); + }); + }); +}); diff --git a/react-packager/src/node-haste/__tests__/fastpath-test.js b/react-packager/src/node-haste/__tests__/fastpath-test.js new file mode 100644 index 00000000..b1a89e26 --- /dev/null +++ b/react-packager/src/node-haste/__tests__/fastpath-test.js @@ -0,0 +1,525 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* + * original author: dead_horse + * ported by: yaycmyk + */ + +jest.dontMock('../fastpath'); +const fp = () => require('../fastpath'); + +describe('fast-path', () => { + const actual_platform = process.platform; + const invalidInputTests = [ + true, + false, + 7, + null, + {}, + undefined, + [], + NaN, + () => {}, + ]; + + const getErrorMessage = (fnName, test, expected, actual) => { + return [ + `fastpath.${fnName}('${test}')`, + `expect=${JSON.stringify(expected)}`, + `actual=${JSON.stringify(actual)}`, + ].join('\n'); + }; + + beforeEach(function() { + process.platform = 'linux'; + }); + + afterEach(function() { + process.platform = actual_platform; + }); + + describe('basename', () => { + it('should resolve given paths', () => { + process.platform = actual_platform; + + expect(fp().basename(__filename)).toEqual('fastpath-test.js'); + expect(fp().basename(__filename, '.js')).toEqual('fastpath-test'); + expect(fp().basename('')).toEqual(''); + expect(fp().basename('/dir/basename.ext')).toEqual('basename.ext'); + expect(fp().basename('/basename.ext')).toEqual('basename.ext'); + expect(fp().basename('basename.ext')).toEqual('basename.ext'); + expect(fp().basename('basename.ext/')).toEqual('basename.ext'); + expect(fp().basename('basename.ext//')).toEqual('basename.ext'); + }); + + it('should handle backslashes properly (win32)', () => { + process.platform = 'win32'; + + expect(fp().basename('\\dir\\basename.ext')).toEqual('basename.ext'); + expect(fp().basename('\\basename.ext')).toEqual('basename.ext'); + expect(fp().basename('basename.ext')).toEqual('basename.ext'); + expect(fp().basename('basename.ext\\')).toEqual('basename.ext'); + expect(fp().basename('basename.ext\\\\')).toEqual('basename.ext'); + }); + + it('should handle backslashes properly (posix)', () => { + expect(fp().basename('\\dir\\basename.ext')).toEqual('\\dir\\basename.ext'); + expect(fp().basename('\\basename.ext')).toEqual('\\basename.ext'); + expect(fp().basename('basename.ext')).toEqual('basename.ext'); + expect(fp().basename('basename.ext\\')).toEqual('basename.ext\\'); + expect(fp().basename('basename.ext\\\\')).toEqual('basename.ext\\\\'); + }); + + it('should handle control characters (posix)', () => { + // POSIX filenames may include control characters + // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html + const name = 'Icon' + String.fromCharCode(13); + expect(fp().basename('/a/b/' + name)).toEqual(name); + }); + }); + + describe('extname', () => { + it('should extract the extension from a path', () => { + process.platform = actual_platform; + + expect(fp().extname(__filename)).toEqual('.js'); + expect(fp().extname('')).toEqual(''); + expect(fp().extname('/path/to/file')).toEqual(''); + expect(fp().extname('/path/to/file.ext')).toEqual('.ext'); + expect(fp().extname('/path.to/file.ext')).toEqual('.ext'); + expect(fp().extname('/path.to/file')).toEqual(''); + expect(fp().extname('/path.to/.file')).toEqual(''); + expect(fp().extname('/path.to/.file.ext')).toEqual('.ext'); + expect(fp().extname('/path/to/f.ext')).toEqual('.ext'); + expect(fp().extname('/path/to/..ext')).toEqual('.ext'); + expect(fp().extname('file')).toEqual(''); + expect(fp().extname('file.ext')).toEqual('.ext'); + expect(fp().extname('.file')).toEqual(''); + expect(fp().extname('.file.ext')).toEqual('.ext'); + expect(fp().extname('/file')).toEqual(''); + expect(fp().extname('/file.ext')).toEqual('.ext'); + expect(fp().extname('/.file')).toEqual(''); + expect(fp().extname('/.file.ext')).toEqual('.ext'); + expect(fp().extname('.path/file.ext')).toEqual('.ext'); + expect(fp().extname('file.ext.ext')).toEqual('.ext'); + expect(fp().extname('file.')).toEqual('.'); + expect(fp().extname('.')).toEqual(''); + expect(fp().extname('./')).toEqual(''); + expect(fp().extname('.file.ext')).toEqual('.ext'); + expect(fp().extname('.file')).toEqual(''); + expect(fp().extname('.file.')).toEqual('.'); + expect(fp().extname('.file..')).toEqual('.'); + expect(fp().extname('..')).toEqual(''); + expect(fp().extname('../')).toEqual(''); + expect(fp().extname('..file.ext')).toEqual('.ext'); + expect(fp().extname('..file')).toEqual('.file'); + expect(fp().extname('..file.')).toEqual('.'); + expect(fp().extname('..file..')).toEqual('.'); + expect(fp().extname('...')).toEqual('.'); + expect(fp().extname('...ext')).toEqual('.ext'); + expect(fp().extname('....')).toEqual('.'); + expect(fp().extname('file.ext/')).toEqual('.ext'); + expect(fp().extname('file.ext//')).toEqual('.ext'); + expect(fp().extname('file/')).toEqual(''); + expect(fp().extname('file//')).toEqual(''); + expect(fp().extname('file./')).toEqual('.'); + expect(fp().extname('file.//')).toEqual('.'); + }); + + it('should handle path backslashes (win32)', () => { + process.platform = 'win32'; + + // On windows, backspace is a path separator. + expect(fp().extname('.\\')).toEqual(''); + expect(fp().extname('..\\')).toEqual(''); + expect(fp().extname('file.ext\\')).toEqual('.ext'); + expect(fp().extname('file.ext\\\\')).toEqual('.ext'); + expect(fp().extname('file\\')).toEqual(''); + expect(fp().extname('file\\\\')).toEqual(''); + expect(fp().extname('file.\\')).toEqual('.'); + expect(fp().extname('file.\\\\')).toEqual('.'); + }); + + it('should handle path backslashes (posix)', () => { + // On unix, backspace is a valid name component like any other character. + expect(fp().extname('.\\')).toEqual(''); + expect(fp().extname('..\\')).toEqual('.\\'); + expect(fp().extname('file.ext\\')).toEqual('.ext\\'); + expect(fp().extname('file.ext\\\\')).toEqual('.ext\\\\'); + expect(fp().extname('file\\')).toEqual(''); + expect(fp().extname('file\\\\')).toEqual(''); + expect(fp().extname('file.\\')).toEqual('.\\'); + expect(fp().extname('file.\\\\')).toEqual('.\\\\'); + }); + }); + + describe('dirname', () => { + it('should isolate the directory name from a path (posix)', () => { + expect(fp().dirname('/a/b/')).toEqual('/a'); + expect(fp().dirname('/a/b')).toEqual('/a'); + expect(fp().dirname('/a')).toEqual('/'); + expect(fp().dirname('')).toEqual('.'); + expect(fp().dirname('/')).toEqual('/'); + expect(fp().dirname('////')).toEqual('/'); + }); + + it('should isolate the directory name from a path (win32)', () => { + process.platform = 'win32'; + + expect(fp().dirname('C:\\')).toEqual('C:\\'); + expect(fp().dirname('c:\\')).toEqual('c:\\'); + expect(fp().dirname('c:\\foo')).toEqual('c:\\'); + expect(fp().dirname('c:\\foo\\')).toEqual('c:\\'); + expect(fp().dirname('c:\\foo\\bar')).toEqual('c:\\foo'); + expect(fp().dirname('c:\\foo\\bar\\')).toEqual('c:\\foo'); + expect(fp().dirname('c:\\foo\\bar\\baz')).toEqual('c:\\foo\\bar'); + expect(fp().dirname('\\')).toEqual('\\'); + expect(fp().dirname('\\foo')).toEqual('\\'); + expect(fp().dirname('\\foo\\')).toEqual('\\'); + expect(fp().dirname('\\foo\\bar')).toEqual('\\foo'); + expect(fp().dirname('\\foo\\bar\\')).toEqual('\\foo'); + expect(fp().dirname('\\foo\\bar\\baz')).toEqual('\\foo\\bar'); + expect(fp().dirname('c:')).toEqual('c:'); + expect(fp().dirname('c:foo')).toEqual('c:'); + expect(fp().dirname('c:foo\\')).toEqual('c:'); + expect(fp().dirname('c:foo\\bar')).toEqual('c:foo'); + expect(fp().dirname('c:foo\\bar\\')).toEqual('c:foo'); + expect(fp().dirname('c:foo\\bar\\baz')).toEqual('c:foo\\bar'); + expect(fp().dirname('\\\\unc\\share')).toEqual('\\\\unc\\share'); + expect(fp().dirname('\\\\unc\\share\\foo')).toEqual('\\\\unc\\share\\'); + expect(fp().dirname('\\\\unc\\share\\foo\\')).toEqual('\\\\unc\\share\\'); + expect(fp().dirname('\\\\unc\\share\\foo\\bar')).toEqual('\\\\unc\\share\\foo'); + expect(fp().dirname('\\\\unc\\share\\foo\\bar\\')).toEqual('\\\\unc\\share\\foo'); + expect(fp().dirname('\\\\unc\\share\\foo\\bar\\baz')).toEqual('\\\\unc\\share\\foo\\bar'); + }); + }); + + describe('join', () => { + const posixJoinTests = [ + // arguments result + [['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'], + [['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'], + [['/foo', '../../../bar'], '/bar'], + [['foo', '../../../bar'], '../../bar'], + [['foo/', '../../../bar'], '../../bar'], + [['foo/x', '../../../bar'], '../bar'], + [['foo/x', './bar'], 'foo/x/bar'], + [['foo/x/', './bar'], 'foo/x/bar'], + [['foo/x/', '.', 'bar'], 'foo/x/bar'], + [['./'], './'], + [['.', './'], './'], + [['.', '.', '.'], '.'], + [['.', './', '.'], '.'], + [['.', '/./', '.'], '.'], + [['.', '/////./', '.'], '.'], + [['.'], '.'], + [['', '.'], '.'], + [['', 'foo'], 'foo'], + [['foo', '/bar'], 'foo/bar'], + [['', '/foo'], '/foo'], + [['', '', '/foo'], '/foo'], + [['', '', 'foo'], 'foo'], + [['foo', ''], 'foo'], + [['foo/', ''], 'foo/'], + [['foo', '', '/bar'], 'foo/bar'], + [['./', '..', '/foo'], '../foo'], + [['./', '..', '..', '/foo'], '../../foo'], + [['.', '..', '..', '/foo'], '../../foo'], + [['', '..', '..', '/foo'], '../../foo'], + [['/'], '/'], + [['/', '.'], '/'], + [['/', '..'], '/'], + [['/', '..', '..'], '/'], + [[''], '.'], + [['', ''], '.'], + [[' /foo'], ' /foo'], + [[' ', 'foo'], ' /foo'], + [[' ', '.'], ' '], + [[' ', '/'], ' /'], + [[' ', ''], ' '], + [['/', 'foo'], '/foo'], + [['/', '/foo'], '/foo'], + [['/', '//foo'], '/foo'], + [['/', '', '/foo'], '/foo'], + [['', '/', 'foo'], '/foo'], + [['', '/', '/foo'], '/foo'], + ]; + + const win32JoinTests = posixJoinTests.concat([ + // UNC path expected + [['//foo/bar'], '//foo/bar/'], + [['\\/foo/bar'], '//foo/bar/'], + [['\\\\foo/bar'], '//foo/bar/'], + // UNC path expected - server and share separate + [['//foo', 'bar'], '//foo/bar/'], + [['//foo/', 'bar'], '//foo/bar/'], + [['//foo', '/bar'], '//foo/bar/'], + // UNC path expected - questionable + [['//foo', '', 'bar'], '//foo/bar/'], + [['//foo/', '', 'bar'], '//foo/bar/'], + [['//foo/', '', '/bar'], '//foo/bar/'], + // UNC path expected - even more questionable + [['', '//foo', 'bar'], '//foo/bar/'], + [['', '//foo/', 'bar'], '//foo/bar/'], + [['', '//foo/', '/bar'], '//foo/bar/'], + // No UNC path expected (no double slash in first component) + [['\\', 'foo/bar'], '/foo/bar'], + [['\\', '/foo/bar'], '/foo/bar'], + [['', '/', '/foo/bar'], '/foo/bar'], + // No UNC path expected (no non-slashes in first component - questionable) + [['//', 'foo/bar'], '/foo/bar'], + [['//', '/foo/bar'], '/foo/bar'], + [['\\\\', '/', '/foo/bar'], '/foo/bar'], + [['//'], '/'], + // No UNC path expected (share name missing - questionable). + [['//foo'], '/foo'], + [['//foo/'], '/foo/'], + [['//foo', '/'], '/foo/'], + [['//foo', '', '/'], '/foo/'], + // No UNC path expected (too many leading slashes - questionable) + [['///foo/bar'], '/foo/bar'], + [['////foo', 'bar'], '/foo/bar'], + [['\\\\\\/foo/bar'], '/foo/bar'], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [['C:'], 'C:.'], + [['c:'], 'c:.'], + [['c:.'], 'c:.'], + [['c:', ''], 'c:.'], + [['', 'c:'], 'c:.'], + [['c:.', '/'], 'c:./'], + [['c:.', 'file'], 'c:file'], + [['c:', '/'], 'c:/'], + [['c:', 'file'], 'c:/file'], + ]); + + it('should join the paths correctly (posix)', () => { + posixJoinTests.forEach((test) => { + const actual = fp().join(...test[0]); + const expected = test[1]; + + expect(actual).toEqual(expected, getErrorMessage('join', + test[0].map(JSON.stringify).join(','), + expected, + actual, + )); + }); + }); + + it('should join the paths correctly (win32)', () => { + process.platform = 'win32'; + + win32JoinTests.forEach((test) => { + const actual = fp().join(...test[0]); + const expected = test[1].replace(/\//g, '\\'); + + expect(actual).toEqual(expected, getErrorMessage('join', + test[0].map(JSON.stringify).join(','), + expected, + actual, + )); + }); + }); + + it('should throw for invalid input', () => { + invalidInputTests.forEach((test) => { + expect(() => fp().join(test)).toThrow(); + }); + }); + }); + + describe('normalize', () => { + it('should return a valid path (posix)', () => { + expect(fp().normalize('./fixtures///b/../b/c.js')).toEqual('fixtures/b/c.js'); + expect(fp().normalize('/foo/../../../bar')).toEqual('/bar'); + expect(fp().normalize('a//b//../b')).toEqual('a/b'); + expect(fp().normalize('a//b//./c')).toEqual('a/b/c'); + expect(fp().normalize('a//b//.')).toEqual('a/b'); + }); + + it('should return a valid path (win32)', () => { + process.platform = 'win32'; + expect(fp().normalize('./fixtures///b/../b/c.js')).toEqual('fixtures\\b\\c.js'); + expect(fp().normalize('/foo/../../../bar')).toEqual('\\bar'); + expect(fp().normalize('a//b//../b')).toEqual('a\\b'); + expect(fp().normalize('a//b//./c')).toEqual('a\\b\\c'); + expect(fp().normalize('a//b//.')).toEqual('a\\b'); + expect(fp().normalize('//server/share/dir/file.ext')).toEqual('\\\\server\\share\\dir\\file.ext'); + }); + }); + + describe('resolve', () => { + const win32ResolveTests = [ + // arguments result + [['C:/blah\\blah', 'D:/games', 'C:../a'], 'C:\\blah\\a'], + [['c:/blah\\blah', 'd:/games', 'c:../a'], 'c:\\blah\\a'], + [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'], + [['c:/ignore', 'c:/some/file'], 'c:\\some\\file'], + [['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'], + [['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'], + [['c:/', '//'], 'c:\\'], + [['c:/', '//dir'], 'c:\\dir'], + [['c:/', '//server/share'], '\\\\server\\share\\'], + [['c:/', '//server//share'], '\\\\server\\share\\'], + [['c:/', '///some//dir'], 'c:\\some\\dir'], + ]; + + const posixResolveTests = [ + // arguments result + [['/var/lib', '../', 'file/'], '/var/file'], + [['/var/lib', '/../', 'file/'], '/file'], + [['/some/dir', '.', '/absolute/'], '/absolute'], + ]; + + it('should resolve the current working directory', () => { + process.platform = actual_platform; + + const actual = fp().resolve('.'); + const expected = process.cwd(); + + expect(actual).toEqual(expected, getErrorMessage('resolve', '.', expected, actual)); + }); + + it('should resolve paths (posix)', () => { + posixResolveTests.forEach((test) => { + const actual = fp().resolve(...test[0]); + const expected = test[1]; + + expect(actual).toEqual(expected, getErrorMessage('resolve', test, expected, actual)); + }); + }); + + it('should resolve paths (win32)', () => { + process.platform = 'win32'; + + win32ResolveTests.forEach((test) => { + const actual = fp().resolve(...test[0]); + const expected = test[1]; + + expect(actual).toEqual(expected, getErrorMessage('resolve', test, expected, actual)); + }); + }); + + it('should throw for invalid input', () => { + invalidInputTests.forEach((test) => { + expect(() => fp().resolve(test)).toThrow(); + }); + }); + }); + + describe('isAbsolute', () => { + it('should work (posix)', () => { + expect(fp().isAbsolute('/home/foo')).toEqual(true); + expect(fp().isAbsolute('/home/foo/..')).toEqual(true); + expect(fp().isAbsolute('bar/')).toEqual(false); + expect(fp().isAbsolute('./baz')).toEqual(false); + }); + + it('should work (win32)', () => { + process.platform = 'win32'; + expect(fp().isAbsolute('//server/file')).toEqual(true); + expect(fp().isAbsolute('\\\\server\\file')).toEqual(true); + expect(fp().isAbsolute('C:/Users/')).toEqual(true); + expect(fp().isAbsolute('C:\\Users\\')).toEqual(true); + expect(fp().isAbsolute('C:cwd/another')).toEqual(false); + expect(fp().isAbsolute('C:cwd\\another')).toEqual(false); + expect(fp().isAbsolute('directory/directory')).toEqual(false); + expect(fp().isAbsolute('directory\\directory')).toEqual(false); + }); + }); + + describe('relative', () => { + const win32RelativeTests = [ + // arguments result + ['C:/blah\\blah', 'D:/games', 'D:\\games'], + ['c:/blah\\blah', 'd:/games', 'd:\\games'], + ['c:/aaaa/bbbb', 'c:/aaaa', '..'], + ['c:/aaaa/bbbb', 'c:/cccc', '..\\..\\cccc'], + ['c:/aaaa/bbbb', 'c:/aaaa/bbbb', ''], + ['c:/aaaa/bbbb', 'c:/aaaa/cccc', '..\\cccc'], + ['c:/aaaa/', 'c:/aaaa/cccc', 'cccc'], + ['c:/', 'c:\\aaaa\\bbbb', 'aaaa\\bbbb'], + ['c:/aaaa/bbbb', 'd:\\', 'd:\\'], + ]; + + const posixRelativeTests = [ + // arguments result + ['/var/lib', '/var', '..'], + ['/var/lib', '/bin', '../../bin'], + ['/var/lib', '/var/lib', ''], + ['/var/lib', '/var/apache', '../apache'], + ['/var/', '/var/lib', 'lib'], + ['/', '/var/lib', 'var/lib'], + ]; + + it('should work (posix)', () => { + posixRelativeTests.forEach((test) => { + const actual = fp().relative(test[0], test[1]); + const expected = test[2]; + + expect(actual).toEqual(expected, getErrorMessage('relative', + test.slice(0, 2).map(JSON.stringify).join(','), + expected, + actual, + )); + }); + }); + + it('should work (win32)', () => { + process.platform = 'win32'; + + win32RelativeTests.forEach((test) => { + const actual = fp().relative(test[0], test[1]); + const expected = test[2]; + + expect(actual).toEqual(expected, getErrorMessage('relative', + test.slice(0, 2).map(JSON.stringify).join(','), + expected, + actual, + )); + }); + }); + }); + + describe('(static export) sep', () => { + it('should be a backslash (win32)', () => { + process.platform = 'win32'; + + expect(fp().sep).toEqual('\\'); + }); + + it('should be a forward slash (posix)', () => { + expect(fp().sep).toEqual('/'); + }); + }); + + describe('(static export) delimiter', () => { + it('should be a semicolon (win32)', () => { + process.platform = 'win32'; + expect(fp().delimiter).toEqual(';'); + }); + + it('should be a colon (posix)', () => { + expect(fp().delimiter).toEqual(':'); + }); + }); +}); diff --git a/react-packager/src/node-haste/crawlers/index.js b/react-packager/src/node-haste/crawlers/index.js new file mode 100644 index 00000000..16b73f27 --- /dev/null +++ b/react-packager/src/node-haste/crawlers/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const nodeCrawl = require('./node'); +const watchmanCrawl = require('./watchman'); + +function crawl(roots, options) { + const {fileWatcher} = options; + return (fileWatcher ? fileWatcher.isWatchman() : Promise.resolve(false)).then( + isWatchman => isWatchman ? watchmanCrawl(roots, options) : nodeCrawl(roots, options) + ); +} + +module.exports = crawl; diff --git a/react-packager/src/node-haste/crawlers/node.js b/react-packager/src/node-haste/crawlers/node.js new file mode 100644 index 00000000..cb018ded --- /dev/null +++ b/react-packager/src/node-haste/crawlers/node.js @@ -0,0 +1,61 @@ +'use strict'; + +const denodeify = require('denodeify'); +const debug = require('debug')('ReactNativePackager:DependencyGraph'); +const fs = require('graceful-fs'); +const path = require('../fastpath'); + +const readDir = denodeify(fs.readdir); +const stat = denodeify(fs.stat); + +function nodeRecReadDir(roots, {ignore, exts}) { + const queue = roots.slice(); + const retFiles = []; + const extPattern = new RegExp( + '\.(' + exts.join('|') + ')$' + ); + + function search() { + const currDir = queue.shift(); + if (!currDir) { + return Promise.resolve(); + } + + return readDir(currDir) + .then(files => files.map(f => path.join(currDir, f))) + .then(files => Promise.all( + files.map(f => stat(f).catch(handleBrokenLink)) + ).then(stats => [ + // Remove broken links. + files.filter((file, i) => !!stats[i]), + stats.filter(Boolean), + ])) + .then(([files, stats]) => { + files.forEach((filePath, i) => { + if (ignore(filePath)) { + return; + } + + if (stats[i].isDirectory()) { + queue.push(filePath); + return; + } + + if (filePath.match(extPattern)) { + retFiles.push(path.resolve(filePath)); + } + }); + + return search(); + }); + } + + return search().then(() => retFiles); +} + +function handleBrokenLink(e) { + debug('WARNING: error stating, possibly broken symlink', e.message); + return Promise.resolve(); +} + +module.exports = nodeRecReadDir; diff --git a/react-packager/src/node-haste/crawlers/watchman.js b/react-packager/src/node-haste/crawlers/watchman.js new file mode 100644 index 00000000..1b29634f --- /dev/null +++ b/react-packager/src/node-haste/crawlers/watchman.js @@ -0,0 +1,76 @@ +'use strict'; + +const denodeify = require('denodeify'); +const path = require('../fastpath'); + +const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting.html'; + +function watchmanRecReadDir(roots, {ignore, fileWatcher, exts}) { + const files = []; + return Promise.all( + roots.map( + root => fileWatcher.getWatcherForRoot(root) + ) + ).then( + watchers => { + // All watchman roots for all watches we have. + const watchmanRoots = watchers.map( + watcher => watcher.watchProjectInfo.root + ); + + // Actual unique watchers (because we use watch-project we may end up with + // duplicate "real" watches, and that's by design). + // TODO(amasad): push this functionality into the `FileWatcher`. + const uniqueWatchers = watchers.filter( + (watcher, i) => watchmanRoots.indexOf(watcher.watchProjectInfo.root) === i + ); + + return Promise.all( + uniqueWatchers.map(watcher => { + const watchedRoot = watcher.watchProjectInfo.root; + + // Build up an expression to filter the output by the relevant roots. + const dirExpr = ['anyof']; + for (let i = 0; i < roots.length; i++) { + const root = roots[i]; + if (isDescendant(watchedRoot, root)) { + dirExpr.push(['dirname', path.relative(watchedRoot, root)]); + } + } + + const cmd = denodeify(watcher.client.command.bind(watcher.client)); + return cmd(['query', watchedRoot, { + suffix: exts, + expression: ['allof', ['type', 'f'], 'exists', dirExpr], + fields: ['name'], + }]).then(resp => { + if ('warning' in resp) { + console.warn('watchman warning: ', resp.warning); + } + + resp.files.forEach(filePath => { + filePath = watchedRoot + path.sep + filePath; + if (!ignore(filePath)) { + files.push(filePath); + } + return false; + }); + }); + }) + ); + }).then( + () => files, + error => { + throw new Error( + `Watchman error: ${error.message.trim()}. Make sure watchman ` + + `is running for this project. See ${watchmanURL}.` + ); + } + ); +} + +function isDescendant(root, child) { + return child.startsWith(root); +} + +module.exports = watchmanRecReadDir; diff --git a/react-packager/src/node-haste/fastfs.js b/react-packager/src/node-haste/fastfs.js new file mode 100644 index 00000000..8d630200 --- /dev/null +++ b/react-packager/src/node-haste/fastfs.js @@ -0,0 +1,362 @@ +/** + * 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 denodeify = require('denodeify'); +const {EventEmitter} = require('events'); + +const fs = require('graceful-fs'); +const path = require('./fastpath'); + +const readFile = denodeify(fs.readFile); +const stat = denodeify(fs.stat); + +const NOT_FOUND_IN_ROOTS = 'NotFoundInRootsError'; + +class Fastfs extends EventEmitter { + constructor(name, roots, fileWatcher, {ignore, crawling, activity}) { + 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 + // operations on paths safer. + if (root.endsWith(path.sep)) { + root = root.substr(0, root.length - 1); + } + + root = path.resolve(root); + + return new File(root, true); + }); + this._fastPaths = Object.create(null); + this._crawling = crawling; + this._activity = activity; + } + + build() { + return this._crawling.then(files => { + let fastfsActivity; + const activity = this._activity; + if (activity) { + fastfsActivity = activity.startEvent('Building in-memory fs for ' + this._name); + } + files.forEach(filePath => { + const root = this._getRoot(filePath); + if (root) { + const newFile = new File(filePath, false); + const dirname = filePath.substr(0, filePath.lastIndexOf(path.sep)); + const parent = this._fastPaths[dirname]; + this._fastPaths[filePath] = newFile; + if (parent) { + parent.addChild(newFile, this._fastPaths); + } else { + root.addChild(newFile, this._fastPaths); + } + } + }); + if (activity) { + activity.endEvent(fastfsActivity); + } + + if (this._fileWatcher) { + this._fileWatcher.on('all', this._processFileChange.bind(this)); + } + }); + } + + stat(filePath) { + return Promise.resolve().then(() => this._getFile(filePath).stat()); + } + + getAllFiles() { + return Object.keys(this._fastPaths) + .filter(filePath => !this._fastPaths[filePath].isDir); + } + + findFilesByExts(exts, { ignore } = {}) { + return this.getAllFiles() + .filter(filePath => ( + exts.indexOf(path.extname(filePath).substr(1)) !== -1 && + (!ignore || !ignore(filePath)) + )); + } + + matchFilesByPattern(pattern) { + return this.getAllFiles().filter(file => file.match(pattern)); + } + + readFile(filePath) { + const file = this._getFile(filePath); + if (!file) { + throw new Error(`Unable to find file with path: ${filePath}`); + } + return file.read(); + } + + readWhile(filePath, predicate) { + const file = this._getFile(filePath); + if (!file) { + throw new Error(`Unable to find file with path: ${filePath}`); + } + return file.readWhile(predicate); + } + + closest(filePath, name) { + for (let file = this._getFile(filePath).parent; + file; + file = file.parent) { + if (file.children[name]) { + return file.children[name].path; + } + } + return null; + } + + fileExists(filePath) { + let file; + try { + file = this._getFile(filePath); + } catch (e) { + if (e.type === NOT_FOUND_IN_ROOTS) { + return false; + } + throw e; + } + + return file && !file.isDir; + } + + dirExists(filePath) { + let file; + try { + file = this._getFile(filePath); + } catch (e) { + if (e.type === NOT_FOUND_IN_ROOTS) { + return false; + } + throw e; + } + + return file && file.isDir; + } + + matches(dir, pattern) { + const dirFile = this._getFile(dir); + if (!dirFile.isDir) { + throw new Error(`Expected file ${dirFile.path} to be a directory`); + } + + return Object.keys(dirFile.children) + .filter(name => name.match(pattern)) + .map(name => path.join(dirFile.path, name)); + } + + _getRoot(filePath) { + for (let i = 0; i < this._roots.length; i++) { + const possibleRoot = this._roots[i]; + if (isDescendant(possibleRoot.path, filePath)) { + return possibleRoot; + } + } + return null; + } + + _getAndAssertRoot(filePath) { + const root = this._getRoot(filePath); + if (!root) { + const error = new Error(`File ${filePath} not found in any of the roots`); + error.type = NOT_FOUND_IN_ROOTS; + throw error; + } + return root; + } + + _getFile(filePath) { + filePath = path.resolve(filePath); + if (!this._fastPaths[filePath]) { + const file = this._getAndAssertRoot(filePath).getFileFromPath(filePath); + if (file) { + this._fastPaths[filePath] = file; + } + } + + return this._fastPaths[filePath]; + } + + _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(absPath); + if (file) { + file.remove(); + } + } + + delete this._fastPaths[path.resolve(absPath)]; + + if (type !== 'delete') { + const file = new File(absPath, false); + root.addChild(file, this._fastPaths); + } + + this.emit('change', type, filePath, rootPath, fstat); + } +} + +class File { + constructor(filePath, isDir) { + this.path = filePath; + this.isDir = isDir; + this.children = this.isDir ? Object.create(null) : null; + } + + read() { + if (!this._read) { + this._read = readFile(this.path, 'utf8'); + } + return this._read; + } + + readWhile(predicate) { + return readWhile(this.path, predicate).then(({result, completed}) => { + if (completed && !this._read) { + this._read = Promise.resolve(result); + } + return result; + }); + } + + stat() { + if (!this._stat) { + this._stat = stat(this.path); + } + + return this._stat; + } + + addChild(file, fileMap) { + const parts = file.path.substr(this.path.length + 1).split(path.sep); + if (parts.length === 1) { + this.children[parts[0]] = file; + file.parent = this; + } else if (this.children[parts[0]]) { + this.children[parts[0]].addChild(file, fileMap); + } else { + const dir = new File(this.path + path.sep + parts[0], true); + dir.parent = this; + this.children[parts[0]] = dir; + fileMap[dir.path] = dir; + dir.addChild(file, fileMap); + } + } + + getFileFromPath(filePath) { + const parts = path.relative(this.path, filePath).split(path.sep); + + /*eslint consistent-this:0*/ + let file = this; + for (let i = 0; i < parts.length; i++) { + const fileName = parts[i]; + if (!fileName) { + continue; + } + + if (!file || !file.isDir) { + // File not found. + return null; + } + + file = file.children[fileName]; + } + + return file; + } + + ext() { + return path.extname(this.path).substr(1); + } + + remove() { + if (!this.parent) { + throw new Error(`No parent to delete ${this.path} from`); + } + + delete this.parent.children[path.basename(this.path)]; + } +} + +function readWhile(filePath, predicate) { + return new Promise((resolve, reject) => { + fs.open(filePath, 'r', (openError, fd) => { + if (openError) { + reject(openError); + return; + } + + read( + fd, + /*global Buffer: true*/ + new Buffer(512), + makeReadCallback(fd, predicate, (readError, result, completed) => { + if (readError) { + reject(readError); + } else { + resolve({result, completed}); + } + }) + ); + }); + }); +} + +function read(fd, buffer, callback) { + fs.read(fd, buffer, 0, buffer.length, -1, callback); +} + +function close(fd, error, result, complete, callback) { + fs.close(fd, closeError => callback(error || closeError, result, complete)); +} + +function makeReadCallback(fd, predicate, callback) { + let result = ''; + let index = 0; + return function readCallback(error, bytesRead, buffer) { + if (error) { + close(fd, error, undefined, false, callback); + return; + } + + const completed = bytesRead === 0; + const chunk = completed ? '' : buffer.toString('utf8', 0, bytesRead); + result += chunk; + if (completed || !predicate(chunk, index++, result)) { + close(fd, null, result, completed, callback); + } else { + read(fd, buffer, readCallback); + } + }; +} + +function isDescendant(root, child) { + return child.startsWith(root); +} + +module.exports = Fastfs; diff --git a/react-packager/src/node-haste/fastpath.js b/react-packager/src/node-haste/fastpath.js new file mode 100644 index 00000000..f83102f4 --- /dev/null +++ b/react-packager/src/node-haste/fastpath.js @@ -0,0 +1,619 @@ +/*! + * fast-path - index.js + * MIT License + * + * Copyright Joyent, Inc. and other Node contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * original author: dead_horse + * ported by: yaycmyk + * + * VERSION 1.2.0 + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const all = Object.keys(exports).filter((name) => name !== 'replace'); +const IS_WINDOWS = process.platform === 'win32'; + +function isString(arg) { + return typeof arg === 'string'; +} + +const splitDeviceRe = + /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; + +function getDevice(filename) { + const result = splitDeviceRe.exec(filename); + + return (result[1] || '') + (result[2] || ''); +} + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + const nonEmptyParts = []; + let nonBack = true; + + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + + if (p && p !== '.') { + nonEmptyParts.push(p); + } + + if (p === '..') { + nonBack = false; + } + } + + parts = nonEmptyParts; + + // if the path does not contain .. + if (nonBack) { + return parts; + } + + // if the path tries to go ab ove the root, `up` ends up > 0 + let up = 0; + const res = []; + + for (let i = parts.length - 1; i >= 0; i--) { + if (parts[i] === '..') { + up++; + } else if (up) { + up--; + } else { + res.push(parts[i]); + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + res.push('..'); + } + } + + return res.reverse(); +} + +function trim(arr) { + let start = 0; + + for (; start < arr.length; start++) { + if (arr[start] !== '') { break; } + } + + let end = arr.length - 1; + + for (; end >= 0; end--) { + if (arr[end] !== '') { break; } + } + + return start <= end ? arr.slice(start, end + 1) : []; +} + +function normalizeUNCRoot(device) { + return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\'); +} + +exports.sep = IS_WINDOWS ? '\\' : '/'; +exports.delimiter = IS_WINDOWS ? ';' : ':'; + +if (IS_WINDOWS) { + // path.resolve([from ...], to) + // windows version + exports.resolve = function resolveWIN32() { + let resolvedDevice = ''; + let resolvedTail = ''; + let resolvedAbsolute = false; + + for (let i = arguments.length - 1; i >= -1; i--) { + let path; + + if (i >= 0) { + path = arguments[i]; + } else if (!resolvedDevice) { + path = process.cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive. We're sure the device is not + // an unc path at this points, because unc paths are always absolute. + path = process.env['=' + resolvedDevice]; + + // Verify that a drive-local cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if (!path || path.substr(0, 3).toLowerCase() !== + resolvedDevice.toLowerCase() + '\\') { + path = resolvedDevice + '\\'; + } + } + + // Skip empty and invalid entries + if (!isString(path)) { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + const result = splitDeviceRe.exec(path); + const device = result[1] || ''; + const isUnc = device && device.charAt(1) !== ':'; + const isAbsolute = exports.isAbsolute(path); + const tail = result[3]; + + if (device && + resolvedDevice && + device.toLowerCase() !== resolvedDevice.toLowerCase()) { + // This path points to another device so it is not applicable + continue; + } + + if (!resolvedDevice) { + resolvedDevice = device; + } + + if (!resolvedAbsolute) { + resolvedTail = tail + '\\' + resolvedTail; + resolvedAbsolute = isAbsolute; + } + + // Convert slashes to backslashes when `resolvedDevice` points to an UNC + // root. Also squash multiple slashes into a single one where appropriate. + if (isUnc) { + resolvedDevice = normalizeUNCRoot(resolvedDevice); + } + + if (resolvedDevice && resolvedAbsolute) { + break; + } + } + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + resolvedTail = normalizeArray( + resolvedTail.split(/[\\\/]+/), + !resolvedAbsolute + ).join('\\'); + + return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) + || '.'; + }; + + // windows version + exports.normalize = function normalizeWIN32(path) { + const result = splitDeviceRe.exec(path); + let device = result[1] || ''; + const isUnc = device && device.charAt(1) !== ':'; + const isAbsolute = exports.isAbsolute(path); + let tail = result[3]; + const trailingSlash = /[\\\/]$/.test(tail); + + // Normalize the tail path + tail = normalizeArray(tail.split(/[\\\/]+/), !isAbsolute).join('\\'); + + if (!tail && !isAbsolute) { + tail = '.'; + } + if (tail && trailingSlash) { + tail += '\\'; + } + + // Convert slashes to backslashes when `device` points to an UNC root. + // Also squash multiple slashes into a single one where appropriate. + if (isUnc) { + device = normalizeUNCRoot(device); + } + + return device + (isAbsolute ? '\\' : '') + tail; + }; + + // windows version + exports.isAbsolute = function isAbsoluteWIN32(path) { + const result = splitDeviceRe.exec(path); + const device = result[1] || ''; + const isUnc = !!device && device.charAt(1) !== ':'; + + // UNC paths are always absolute + return !!result[2] || isUnc; + }; + + // windows version + exports.join = function joinWIN32() { + function f(p) { + if (!isString(p)) { + throw new TypeError('Arguments to path.join must be strings'); + } + + return p; + } + + const paths = Array.prototype.filter.call(arguments, f); + let joined = paths.join('\\'); + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for an UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at an UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as an UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\') + if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) { + joined = joined.replace(/^[\\\/]{2,}/, '\\'); + } + + return exports.normalize(joined); + }; + + // path.relative(from, to) + // it will solve the relative path from 'from' to 'to', for instance: + // from = 'C:\\orandea\\test\\aaa' + // to = 'C:\\orandea\\impl\\bbb' + // The output of the function should be: '..\\..\\impl\\bbb' + // windows version + exports.relative = function relativeWIN32(from, to) { + from = exports.resolve(from); + to = exports.resolve(to); + + // windows is not case sensitive + const lowerFrom = from.toLowerCase(); + const lowerTo = to.toLowerCase(); + + const toParts = trim(to.split('\\')); + + const lowerFromParts = trim(lowerFrom.split('\\')); + const lowerToParts = trim(lowerTo.split('\\')); + + const length = Math.min(lowerFromParts.length, lowerToParts.length); + let samePartsLength = length; + + for (let i = 0; i < length; i++) { + if (lowerFromParts[i] !== lowerToParts[i]) { + samePartsLength = i; + break; + } + } + + if (samePartsLength == 0) { + return to; + } + + let outputParts = []; + + for (let i = samePartsLength; i < lowerFromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('\\'); + }; + +} else /* posix */ { + + // path.resolve([from ...], to) + // posix version + exports.resolve = function resolvePOSIX() { + let resolvedPath = ''; + let resolvedAbsolute = false; + + for (let i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + const path = i >= 0 ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (!isString(path)) { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray( + resolvedPath.split('/'), + !resolvedAbsolute + ).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }; + + // path.normalize(path) + // posix version + exports.normalize = function normalizePOSIX(path) { + const isAbsolute = exports.isAbsolute(path); + const trailingSlash = path[path.length - 1] === '/'; + + // normalize the path + path = normalizeArray(path.split('/'), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; + }; + + // posix version + exports.isAbsolute = function isAbsolutePOSIX(path) { + return path.charAt(0) === '/'; + }; + + // posix version + exports.join = function joinPOSIX() { + let path = ''; + + for (let i = 0; i < arguments.length; i++) { + const segment = arguments[i]; + + if (!isString(segment)) { + throw new TypeError('Arguments to path.join must be strings'); + } + + if (segment) { + path += `${path ? '/' : ''}${segment}`; + } + } + + return exports.normalize(path); + }; + + // path.relative(from, to) + // posix version + exports.relative = function relativePOSIX(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + const fromParts = trim(from.split('/')); + const toParts = trim(to.split('/')); + + const length = Math.min(fromParts.length, toParts.length); + let samePartsLength = length; + + for (let i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + let outputParts = []; + + for (let i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); + }; +} + +exports.exists = util.deprecate(function exists(path, callback) { + return fs.exists(path, callback); +}, 'path.exists is now called `fs.exists`.'); + +exports.existsSync = util.deprecate(function existsSync(path) { + return fs.existsSync(path); +}, 'path.existsSync is now called `fs.existsSync`.'); + +if (IS_WINDOWS) { + exports._makeLong = function _makeLongWIN32(path) { + // Note: this will *probably* throw somewhere. + if (!isString(path)) { return path; } + if (!path) { return ''; } + + const resolvedPath = exports.resolve(path); + + if (/^[a-zA-Z]\:\\/.test(resolvedPath)) { + // path is local filesystem path, which needs to be converted + // to long UNC path. + return '\\\\?\\' + resolvedPath; + } else if (/^\\\\[^?.]/.test(resolvedPath)) { + // path is network UNC path, which needs to be converted + // to long UNC path. + return '\\\\?\\UNC\\' + resolvedPath.substring(2); + } + + return path; + }; + +} else { + exports._makeLong = function _makeLongPOSIX(path) { + return path; + }; +} + +exports.extname = function _extname(filename) { + if (!filename) { return ''; } + + // /a.js/// + let end = filename.length; + let c = filename[end - 1]; + + while (c === exports.sep || c === '/') { + end--; + c = filename[end - 1]; + } + + let lastDot = -1; + let lastSep = -1; + + for (let i = end; i--;) { + const ch = filename[i]; + + if (lastDot === -1 && ch === '.') { + lastDot = i; + } else if (lastSep === -1 && ch === '/') { + lastSep = i; + } else if (IS_WINDOWS && lastSep === -1 && ch === '\\') { + lastSep = i; + } + + // /xxx + if (lastSep !== -1 && lastDot === -1) { return ''; } + // /*.js + if (lastDot !== -1 && i === lastDot - 2) { break; } + // /.js + if (lastSep !== -1 && lastDot !== -1) { break; } + } + + // ./js and /.js + if (lastDot < lastSep + 2) { return ''; } + + const extname = filename.slice(lastDot, end); + + if (extname === '.' && filename[lastDot - 1] === '.') { + // .. + if (lastDot === 1) { return ''; } + + const pre = filename[lastDot - 2]; + // [//\/].. + if (pre === '/' || pre === exports.sep) { return ''; } + } + + return extname; +}; + +exports.basename = function _basename(filename, ext) { + if (!filename) { return ''; } + + // /a.js/// + let end = filename.length; + let c = filename[end - 1]; + + while (c === exports.sep || c === '/') { + end--; + c = filename[end - 1]; + } + + let lastSep = -1; + + for (let i = end; i--;) { + const ch = filename[i]; + + if (lastSep === -1 && ch === '/') { + lastSep = i; + break; + } + + if (IS_WINDOWS && lastSep === -1 && ch === '\\') { + lastSep = i; + break; + } + } + + const basename = filename.slice(lastSep + 1, end); + + if (ext) { + const match = basename.lastIndexOf(ext); + + if (match === -1 || match !== basename.length - ext.length) { + return basename; + } + + return basename.slice(0, basename.length - ext.length); + } + + return basename; +}; + +exports.dirname = function _dirname(filename) { + if (!filename) { return '.'; } + + let start = 0; + let device = ''; + + if (IS_WINDOWS) { + // need to get device in windows + device = getDevice(filename); + + if (device) { start = device.length; } + } + + // /a.js/// + let end = filename.length; + let c = filename[end - 1]; + + while (end >= start && c === exports.sep || c === '/') { + end--; + c = filename[end - 1]; + } + + let lastSep = -1; + + for (let i = end; i-- > start;) { + const ch = filename[i]; + + if (lastSep === -1 && ch === '/') { + lastSep = i; + break; + } + + if (IS_WINDOWS && lastSep === -1 && ch === '\\') { + lastSep = i; + break; + } + } + + if (lastSep <= start) { + if (device) { return device; } + if (filename[0] === '/' || filename[0] === exports.sep) { return filename[0]; } + + return '.'; + } + + return device + filename.slice(start, lastSep); +}; + +exports.replace = function(props) { + if (!props) { props = all; } + if (!Array.isArray(props)) { props = [props]; } + + props.forEach(function(name) { + if (exports[name]) { path[name] = exports[name]; } + }); +}; diff --git a/react-packager/src/node-haste/index.js b/react-packager/src/node-haste/index.js new file mode 100644 index 00000000..5ddf93ee --- /dev/null +++ b/react-packager/src/node-haste/index.js @@ -0,0 +1,332 @@ + /** + * 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 Cache = require('./Cache'); +const Fastfs = require('./fastfs'); +const FileWatcher = require('./FileWatcher'); +const Module = require('./Module'); +const ModuleCache = require('./ModuleCache'); +const Polyfill = require('./Polyfill'); +const crawl = require('./crawlers'); +const extractRequires = require('./lib/extractRequires'); +const getAssetDataFromName = require('./lib/getAssetDataFromName'); +const getInverseDependencies = require('./lib/getInverseDependencies'); +const getPlatformExtension = require('./lib/getPlatformExtension'); +const isAbsolutePath = require('absolute-path'); +const replacePatterns = require('./lib/replacePatterns'); +const path = require('./fastpath'); +const util = require('util'); +const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers'); +const ResolutionRequest = require('./DependencyGraph/ResolutionRequest'); +const ResolutionResponse = require('./DependencyGraph/ResolutionResponse'); +const HasteMap = require('./DependencyGraph/HasteMap'); +const DeprecatedAssetMap = require('./DependencyGraph/DeprecatedAssetMap'); + +const ERROR_BUILDING_DEP_GRAPH = 'DependencyGraphError'; + +const defaultActivity = { + startEvent: () => {}, + endEvent: () => {}, +}; + +class DependencyGraph { + constructor({ + activity, + roots, + ignoreFilePath, + fileWatcher, + assetRoots_DEPRECATED, + assetExts, + providesModuleNodeModules, + platforms, + preferNativePlatform, + cache, + extensions, + mocksPattern, + extractRequires, + transformCode, + shouldThrowOnUnresolvedErrors = () => true, + enableAssetMap, + assetDependencies, + moduleOptions, + extraNodeModules, + }) { + this._opts = { + activity: activity || defaultActivity, + roots, + ignoreFilePath: ignoreFilePath || (() => {}), + fileWatcher, + assetRoots_DEPRECATED: assetRoots_DEPRECATED || [], + assetExts: assetExts || [], + providesModuleNodeModules, + platforms: new Set(platforms || []), + preferNativePlatform: preferNativePlatform || false, + extensions: extensions || ['js', 'json'], + mocksPattern, + extractRequires, + transformCode, + shouldThrowOnUnresolvedErrors, + enableAssetMap: enableAssetMap || true, + moduleOptions: moduleOptions || { + cacheTransformResults: true, + }, + extraNodeModules, + }; + this._cache = cache; + this._assetDependencies = assetDependencies; + this._helpers = new DependencyGraphHelpers(this._opts); + this.load(); + } + + load() { + if (this._loading) { + return this._loading; + } + + const {activity} = this._opts; + const depGraphActivity = activity.startEvent('Building Dependency Graph'); + const crawlActivity = activity.startEvent('Crawling File System'); + const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED); + this._crawling = crawl(allRoots, { + ignore: this._opts.ignoreFilePath, + exts: this._opts.extensions.concat(this._opts.assetExts), + fileWatcher: this._opts.fileWatcher, + }); + this._crawling.then((files) => activity.endEvent(crawlActivity)); + + this._fastfs = new Fastfs( + 'JavaScript', + this._opts.roots, + this._opts.fileWatcher, + { + ignore: this._opts.ignoreFilePath, + crawling: this._crawling, + activity: activity, + } + ); + + this._fastfs.on('change', this._processFileChange.bind(this)); + + this._moduleCache = new ModuleCache({ + fastfs: this._fastfs, + cache: this._cache, + extractRequires: this._opts.extractRequires, + transformCode: this._opts.transformCode, + depGraphHelpers: this._helpers, + assetDependencies: this._assetDependencies, + moduleOptions: this._opts.moduleOptions, + }, this._opts.platfomrs); + + this._hasteMap = new HasteMap({ + fastfs: this._fastfs, + extensions: this._opts.extensions, + moduleCache: this._moduleCache, + preferNativePlatform: this._opts.preferNativePlatform, + helpers: this._helpers, + platforms: this._opts.platforms, + }); + + this._deprecatedAssetMap = new DeprecatedAssetMap({ + fsCrawl: this._crawling, + roots: this._opts.assetRoots_DEPRECATED, + helpers: this._helpers, + fileWatcher: this._opts.fileWatcher, + ignoreFilePath: this._opts.ignoreFilePath, + assetExts: this._opts.assetExts, + activity: this._opts.activity, + enabled: this._opts.enableAssetMap, + platforms: this._opts.platforms, + }); + + this._loading = Promise.all([ + this._fastfs.build() + .then(() => { + const hasteActivity = activity.startEvent('Building Haste Map'); + return this._hasteMap.build().then(map => { + activity.endEvent(hasteActivity); + return map; + }); + }), + this._deprecatedAssetMap.build(), + ]).then( + response => { + activity.endEvent(depGraphActivity); + return response[0]; // Return the haste map + }, + err => { + const error = new Error( + `Failed to build DependencyGraph: ${err.message}` + ); + error.type = ERROR_BUILDING_DEP_GRAPH; + error.stack = err.stack; + throw error; + } + ); + + return this._loading; + } + + /** + * Returns a promise with the direct dependencies the module associated to + * the given entryPath has. + */ + getShallowDependencies(entryPath, transformOptions) { + return this._moduleCache + .getModule(entryPath) + .getDependencies(transformOptions); + } + + getFS() { + return this._fastfs; + } + + /** + * Returns the module object for the given path. + */ + getModuleForPath(entryFile) { + return this._moduleCache.getModule(entryFile); + } + + getAllModules() { + return this.load().then(() => this._moduleCache.getAllModules()); + } + + getDependencies({ + entryPath, + platform, + transformOptions, + onProgress, + recursive = true, + }) { + return this.load().then(() => { + platform = this._getRequestPlatform(entryPath, platform); + const absPath = this._getAbsolutePath(entryPath); + const req = new ResolutionRequest({ + platform, + platforms: this._opts.platforms, + preferNativePlatform: this._opts.preferNativePlatform, + entryPath: absPath, + deprecatedAssetMap: this._deprecatedAssetMap, + hasteMap: this._hasteMap, + helpers: this._helpers, + moduleCache: this._moduleCache, + fastfs: this._fastfs, + shouldThrowOnUnresolvedErrors: this._opts.shouldThrowOnUnresolvedErrors, + extraNodeModules: this._opts.extraNodeModules, + }); + + const response = new ResolutionResponse({transformOptions}); + + return req.getOrderedDependencies({ + response, + mocksPattern: this._opts.mocksPattern, + transformOptions, + onProgress, + recursive, + }).then(() => response); + }); + } + + matchFilesByPattern(pattern) { + return this.load().then(() => this._fastfs.matchFilesByPattern(pattern)); + } + + _getRequestPlatform(entryPath, platform) { + if (platform == null) { + platform = getPlatformExtension(entryPath, this._opts.platforms); + } else if (!this._opts.platforms.has(platform)) { + throw new Error('Unrecognized platform: ' + platform); + } + return platform; + } + + _getAbsolutePath(filePath) { + if (isAbsolutePath(filePath)) { + return path.resolve(filePath); + } + + for (let i = 0; i < this._opts.roots.length; i++) { + const root = this._opts.roots[i]; + const potentialAbsPath = path.join(root, filePath); + if (this._fastfs.fileExists(potentialAbsPath)) { + return path.resolve(potentialAbsPath); + } + } + + throw new NotFoundError( + 'Cannot find entry file %s in any of the roots: %j', + filePath, + this._opts.roots + ); + } + + _processFileChange(type, filePath, root, fstat) { + const absPath = path.join(root, filePath); + if (fstat && fstat.isDirectory() || + this._opts.ignoreFilePath(absPath) || + this._helpers.isNodeModulesDir(absPath)) { + return; + } + + // 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( + 'Rebuilding haste map to recover from error:\n' + + this._hasteMapError.stack + ); + this._hasteMapError = null; + + // Rebuild the entire map if last change resulted in an error. + this._loading = this._hasteMap.build(); + } else { + this._loading = this._hasteMap.processFileChange(type, absPath); + this._loading.catch((e) => this._hasteMapError = e); + } + return this._loading; + }; + this._loading = this._loading.then(resolve, resolve); + } + + createPolyfill(options) { + return this._moduleCache.createPolyfill(options); + } +} + +Object.assign(DependencyGraph, { + Cache, + Fastfs, + FileWatcher, + Module, + Polyfill, + extractRequires, + getAssetDataFromName, + getPlatformExtension, + replacePatterns, + getInverseDependencies, +}); + +function NotFoundError() { + Error.call(this); + Error.captureStackTrace(this, this.constructor); + var msg = util.format.apply(util, arguments); + this.message = msg; + this.type = this.name = 'NotFoundError'; + this.status = 404; +} +util.inherits(NotFoundError, Error); + +module.exports = DependencyGraph; diff --git a/react-packager/src/node-haste/lib/AsyncTaskGroup.js b/react-packager/src/node-haste/lib/AsyncTaskGroup.js new file mode 100644 index 00000000..7f99b25e --- /dev/null +++ b/react-packager/src/node-haste/lib/AsyncTaskGroup.js @@ -0,0 +1,28 @@ + /** + * Copyright (c) 2016-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 = class AsyncTaskGroup { + constructor() { + this._runningTasks = new Set(); + this._resolve = null; + this.done = new Promise(resolve => this._resolve = resolve); + } + + start(taskHandle) { + this._runningTasks.add(taskHandle); + } + + end(taskHandle) { + const runningTasks = this._runningTasks; + if (runningTasks.delete(taskHandle) && runningTasks.size === 0) { + this._resolve(); + } + } +}; diff --git a/react-packager/src/node-haste/lib/MapWithDefaults.js b/react-packager/src/node-haste/lib/MapWithDefaults.js new file mode 100644 index 00000000..a264f0c2 --- /dev/null +++ b/react-packager/src/node-haste/lib/MapWithDefaults.js @@ -0,0 +1,30 @@ + /** + * Copyright (c) 2016-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 = function MapWithDefaults(factory, iterable) { + // This can't be `MapWithDefaults extends Map`, b/c the way babel transforms + // super calls in constructors: Map.call(this, iterable) throws for native + // Map objects in node 4+. + // TODO(davidaurelio) switch to a transform that does not transform classes + // and super calls, and change this into a class + + const map = iterable ? new Map(iterable) : new Map(); + const {get} = map; + map.get = key => { + if (map.has(key)) { + return get.call(map, key); + } + + const value = factory(key); + map.set(key, value); + return value; + }; + return map; +}; diff --git a/react-packager/src/node-haste/lib/__tests__/extractRequires-test.js b/react-packager/src/node-haste/lib/__tests__/extractRequires-test.js new file mode 100644 index 00000000..28d3f72b --- /dev/null +++ b/react-packager/src/node-haste/lib/__tests__/extractRequires-test.js @@ -0,0 +1,106 @@ +/** + * 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('../extractRequires'); +jest.dontMock('../replacePatterns'); + +const extractRequires = require('../extractRequires'); + +describe('extractRequires', () => { + it('should extract both requires and imports from code', () => { + const code = ` + import module1 from 'module1'; + const module2 = require('module2'); + const module3 = require(\`module3\`); + `; + + expect(extractRequires(code)).toEqual({ + code, + deps: {sync: ['module1', 'module2', 'module3']}, + }); + }); + + it('should extract requires in order', () => { + const code = ` + const module1 = require('module1'); + const module2 = require('module2'); + const module3 = require('module3'); + `; + + expect(extractRequires(code)).toEqual({ + code, + deps: {sync: ['module1', 'module2', 'module3']}, + }); + }); + + it('should strip out comments from code', () => { + const code = '// comment'; + + expect(extractRequires(code)).toEqual({ + code: '', + deps: {sync: []}, + }); + }); + + it('should ignore requires in comments', () => { + const code = [ + '// const module1 = require("module1");', + '/**', + ' * const module2 = require("module2");', + ' */', + ].join('\n'); + + expect(extractRequires(code)).toEqual({ + code: '\n', + deps: {sync: []}, + }); + }); + + it('should ignore requires in comments with Windows line endings', () => { + const code = [ + '// const module1 = require("module1");', + '/**', + ' * const module2 = require("module2");', + ' */', + ].join('\r\n'); + + expect(extractRequires(code)).toEqual({ + code: '\r\n', + deps: {sync: []}, + }); + }); + + it('should ignore requires in comments with unicode line endings', () => { + const code = [ + '// const module1 = require("module1");\u2028', + '// const module1 = require("module2");\u2029', + '/*\u2028', + 'const module2 = require("module3");\u2029', + ' */', + ].join(''); + + expect(extractRequires(code)).toEqual({ + code: '\u2028\u2029', + deps: {sync: []}, + }); + }); + + it('should dedup duplicated requires', () => { + const code = ` + const module1 = require('module1'); + const module1Dup = require('module1'); + `; + + expect(extractRequires(code)).toEqual({ + code, + deps: {sync: ['module1']}, + }); + }); +}); diff --git a/react-packager/src/node-haste/lib/__tests__/getAssetDataFromName-test.js b/react-packager/src/node-haste/lib/__tests__/getAssetDataFromName-test.js new file mode 100644 index 00000000..ff61c405 --- /dev/null +++ b/react-packager/src/node-haste/lib/__tests__/getAssetDataFromName-test.js @@ -0,0 +1,119 @@ +/** + * 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('../getPlatformExtension') + .dontMock('../getAssetDataFromName'); + +var getAssetDataFromName = require('../getAssetDataFromName'); + +describe('getAssetDataFromName', () => { + it('should get data from name', () => { + expect(getAssetDataFromName('a/b/c.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c@1x.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c@2.5x.png')).toEqual({ + resolution: 2.5, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c.ios.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + + expect(getAssetDataFromName('a/b/c@1x.ios.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + + expect(getAssetDataFromName('a/b/c@2.5x.ios.png')).toEqual({ + resolution: 2.5, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + }); + + describe('resolution extraction', () => { + it('should extract resolution simple case', () => { + var data = getAssetDataFromName('test@2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 2, + type: 'png', + name: 'test', + platform: null, + }); + }); + + it('should default resolution to 1', () => { + var data = getAssetDataFromName('test.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1, + type: 'png', + name: 'test', + platform: null, + }); + }); + + it('should support float', () => { + var data = getAssetDataFromName('test@1.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1.1, + type: 'png', + name: 'test', + platform: null, + }); + + data = getAssetDataFromName('test@.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.1, + type: 'png', + name: 'test', + platform: null, + }); + + data = getAssetDataFromName('test@0.2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.2, + type: 'png', + name: 'test', + platform: null, + }); + }); + }); +}); diff --git a/react-packager/src/node-haste/lib/__tests__/getInverseDependencies-test.js b/react-packager/src/node-haste/lib/__tests__/getInverseDependencies-test.js new file mode 100644 index 00000000..cbb31a78 --- /dev/null +++ b/react-packager/src/node-haste/lib/__tests__/getInverseDependencies-test.js @@ -0,0 +1,55 @@ +/** + * 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('../getInverseDependencies'); + +const getInverseDependencies = require('../getInverseDependencies'); + +describe('getInverseDependencies', () => { + it('', () => { + const module1 = createModule('module1', ['module2', 'module3']); + const module2 = createModule('module2', ['module3', 'module4']); + const module3 = createModule('module3', ['module4']); + const module4 = createModule('module4', []); + + const modulePairs = { + 'module1': [['module2', module2], ['module3', module3]], + 'module2': [['module3', module3], ['module4', module4]], + 'module3': [['module4', module4]], + 'module4': [], + }; + + const resolutionResponse = { + dependencies: [module1, module2, module3, module4], + getResolvedDependencyPairs: (module) => { + return modulePairs[module.hash()]; + }, + }; + + const dependencies = getInverseDependencies(resolutionResponse); + const actual = // jest can't compare maps and sets + Array.from(dependencies.entries()) + .map(([key, value]) => [key, Array.from(value)]); + + expect(actual).toEqual([ + [module2, [module1]], + [module3, [module1, module2]], + [module4, [module2, module3]], + ]); + }); +}); + +function createModule(name, dependencies) { + return { + hash: () => name, + getName: () => Promise.resolve(name), + getDependencies: () => Promise.resolve(dependencies), + }; +} diff --git a/react-packager/src/node-haste/lib/__tests__/getPlatformExtension-test.js b/react-packager/src/node-haste/lib/__tests__/getPlatformExtension-test.js new file mode 100644 index 00000000..ac601236 --- /dev/null +++ b/react-packager/src/node-haste/lib/__tests__/getPlatformExtension-test.js @@ -0,0 +1,33 @@ +/** + * 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('../getPlatformExtension'); + +var getPlatformExtension = require('../getPlatformExtension'); + +describe('getPlatformExtension', function() { + it('should get platform ext', function() { + expect(getPlatformExtension('a.ios.js')).toBe('ios'); + expect(getPlatformExtension('a.android.js')).toBe('android'); + expect(getPlatformExtension('/b/c/a.ios.js')).toBe('ios'); + expect(getPlatformExtension('/b/c.android/a.ios.js')).toBe('ios'); + expect(getPlatformExtension('/b/c/a@1.5x.ios.png')).toBe('ios'); + expect(getPlatformExtension('/b/c/a@1.5x.lol.png')).toBe(null); + expect(getPlatformExtension('/b/c/a.lol.png')).toBe(null); + }); + + it('should optionally accept supported platforms', function() { + expect(getPlatformExtension('a.ios.js', new Set(['ios']))).toBe('ios'); + expect(getPlatformExtension('a.android.js', new Set(['android']))).toBe('android'); + expect(getPlatformExtension('/b/c/a.ios.js', new Set(['ios', 'android']))).toBe('ios'); + expect(getPlatformExtension('a.ios.js', new Set(['ubuntu']))).toBe(null); + expect(getPlatformExtension('a.ubuntu.js', new Set(['ubuntu']))).toBe('ubuntu'); + }); +}); diff --git a/react-packager/src/node-haste/lib/extractRequires.js b/react-packager/src/node-haste/lib/extractRequires.js new file mode 100644 index 00000000..1c5906c9 --- /dev/null +++ b/react-packager/src/node-haste/lib/extractRequires.js @@ -0,0 +1,55 @@ +/** + * 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 replacePatterns = require('./replacePatterns'); + +/** + * Extract all required modules from a `code` string. + */ +const blockCommentRe = /\/\*[^]*?\*\//g; +const lineCommentRe = /\/\/.*/g; +function extractRequires(code) { + const cache = Object.create(null); + var deps = { + sync: [], + }; + + const addDependency = (dep) => { + if (!cache[dep]) { + cache[dep] = true; + deps.sync.push(dep); + } + }; + + code = code + .replace(blockCommentRe, '') + .replace(lineCommentRe, '') + // Parse the sync dependencies this module has. When the module is + // required, all it's sync dependencies will be loaded into memory. + // Sync dependencies can be defined either using `require` or the ES6 + // `import` or `export` syntaxes: + // var dep1 = require('dep1'); + .replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => { + addDependency(dep); + return match; + }) + .replace(replacePatterns.EXPORT_RE, (match, pre, quot, dep, post) => { + addDependency(dep); + return match; + }) + .replace(replacePatterns.REQUIRE_RE, (match, pre, quot, dep, post) => { + addDependency(dep); + return match; + }); + + return {code, deps}; +} + +module.exports = extractRequires; diff --git a/react-packager/src/node-haste/lib/getAssetDataFromName.js b/react-packager/src/node-haste/lib/getAssetDataFromName.js new file mode 100644 index 00000000..ec0b4a22 --- /dev/null +++ b/react-packager/src/node-haste/lib/getAssetDataFromName.js @@ -0,0 +1,55 @@ + /** + * 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 path = require('../fastpath'); +const getPlatformExtension = require('./getPlatformExtension'); + +function getAssetDataFromName(filename, platforms) { + const ext = path.extname(filename); + const platformExt = getPlatformExtension(filename, platforms); + + let pattern = '@([\\d\\.]+)x'; + if (platformExt != null) { + pattern += '(\\.' + platformExt + ')?'; + } + pattern += '\\' + ext + '$'; + const re = new RegExp(pattern); + + const match = filename.match(re); + let resolution; + + if (!(match && match[1])) { + resolution = 1; + } else { + resolution = parseFloat(match[1], 10); + if (isNaN(resolution)) { + resolution = 1; + } + } + + let assetName; + if (match) { + assetName = filename.replace(re, ext); + } else if (platformExt != null) { + assetName = filename.replace(new RegExp(`\\.${platformExt}\\${ext}`), ext); + } else { + assetName = filename; + } + + return { + resolution: resolution, + assetName: assetName, + type: ext.slice(1), + name: path.basename(assetName, ext), + platform: platformExt, + }; +} + +module.exports = getAssetDataFromName; diff --git a/react-packager/src/node-haste/lib/getInverseDependencies.js b/react-packager/src/node-haste/lib/getInverseDependencies.js new file mode 100644 index 00000000..d2aa7517 --- /dev/null +++ b/react-packager/src/node-haste/lib/getInverseDependencies.js @@ -0,0 +1,42 @@ +/** + * 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'; + +function resolveModuleRequires(resolutionResponse, module) { + const pairs = resolutionResponse.getResolvedDependencyPairs(module); + return pairs + ? pairs.map(([, dependencyModule]) => dependencyModule) + : []; +} + +function getModuleDependents(cache, module) { + let dependents = cache.get(module); + if (!dependents) { + dependents = new Set(); + cache.set(module, dependents); + } + return dependents; +} + +/** + * Returns an object that indicates in which module each module is required. + */ +function getInverseDependencies(resolutionResponse) { + const cache = new Map(); + + resolutionResponse.dependencies.forEach(module => { + resolveModuleRequires(resolutionResponse, module).forEach(dependency => { + getModuleDependents(cache, dependency).add(module); + }); + }); + + return cache; +} + +module.exports = getInverseDependencies; diff --git a/react-packager/src/node-haste/lib/getPlatformExtension.js b/react-packager/src/node-haste/lib/getPlatformExtension.js new file mode 100644 index 00000000..6f505833 --- /dev/null +++ b/react-packager/src/node-haste/lib/getPlatformExtension.js @@ -0,0 +1,28 @@ +/** + * 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 SUPPORTED_PLATFORM_EXTS = new Set([ + 'android', + 'ios', + 'web', +]); + +// Extract platform extension: index.ios.js -> ios +function getPlatformExtension(file, platforms = SUPPORTED_PLATFORM_EXTS) { + const last = file.lastIndexOf('.'); + const secondToLast = file.lastIndexOf('.', last - 1); + if (secondToLast === -1) { + return null; + } + const platform = file.substring(secondToLast + 1, last); + return platforms.has(platform) ? platform : null; +} + +module.exports = getPlatformExtension; diff --git a/react-packager/src/node-haste/lib/replacePatterns.js b/react-packager/src/node-haste/lib/replacePatterns.js new file mode 100644 index 00000000..54f34b4d --- /dev/null +++ b/react-packager/src/node-haste/lib/replacePatterns.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'; + +exports.IMPORT_RE = /(\bimport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g; +exports.EXPORT_RE = /(\bexport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g; +exports.REQUIRE_RE = /(\brequire\s*?\(\s*?)(['"`])([^'"`]+)(\2\s*?\))/g;