diff --git a/react-packager/src/DependencyResolver/Module.js b/react-packager/src/DependencyResolver/Module.js index df6ed710..f9142611 100644 --- a/react-packager/src/DependencyResolver/Module.js +++ b/react-packager/src/DependencyResolver/Module.js @@ -43,7 +43,7 @@ class Module { return this._cache.get( this.path, 'isHaste', - () => this.read().then(data => !!data.id) + () => this._readDocBlock().then(data => !!data.id) ); } @@ -55,9 +55,9 @@ class Module { return this._cache.get( this.path, 'name', - () => this.read().then(data => { - if (data.id) { - return data.id; + () => this._readDocBlock().then(({id}) => { + if (id) { + return id; } const p = this.getPackage(); @@ -103,48 +103,69 @@ class Module { this._cache.invalidate(this.path); } - read() { - if (!this._reading) { - this._reading = this._fastfs.readFile(this.path).then(content => { - const data = {}; + _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; - // Set an id on 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(content); - if (!this._depGraphHelpers.isNodeModulesDir(this.path) && - (moduleDocBlock.providesModule || moduleDocBlock.provides)) { - data.id = /^(\S*)/.exec( - moduleDocBlock.providesModule || moduleDocBlock.provides - )[1]; - } + const id = provides && !this._depGraphHelpers.isNodeModulesDir(this.path) + ? /^\S+/.exec(provides)[0] + : undefined; + return [id, moduleDocBlock]; + } - // Ignore requires in JSON files or generated code. An example of this - // is prebuilt files like the SourceMap library. - if (this.isJSON() || 'extern' in moduleDocBlock) { - data.dependencies = []; - data.asyncDependencies = []; - data.code = content; - return data; - } else { - const transformCode = this._transformCode; - const codePromise = transformCode - ? transformCode(this, content) - : Promise.resolve({code: content}); - - return codePromise.then(({code, dependencies, asyncDependencies}) => { - const {deps} = this._extractor(code); - data.dependencies = dependencies || deps.sync; - data.asyncDependencies = asyncDependencies || deps.async; - data.code = code; - return data; - }); - } - }); + _readDocBlock() { + const reading = this._reading || this._docBlock; + if (reading) { + return reading; } + this._docBlock = this._fastfs.readWhile(this.path, whileInDocBlock) + .then(docBlock => { + const [id] = this._parseDocBlock(docBlock); + return {id}; + }); + return this._docBlock; + } + + read() { + if (this._reading) { + return this._reading; + } + + this._reading = this._fastfs.readFile(this.path).then(content => { + const [id, moduleDocBlock] = this._parseDocBlock(content); + + // Ignore requires in JSON files or generated code. An example of this + // is prebuilt files like the SourceMap library. + if (this.isJSON() || 'extern' in moduleDocBlock) { + return { + id, + dependencies: [], + asyncDependencies: [], + code: content, + }; + } else { + const transformCode = this._transformCode; + const codePromise = transformCode + ? transformCode(this, content) + : Promise.resolve({code: content}); + + return codePromise.then(({code, dependencies, asyncDependencies}) => { + const {deps} = this._extractor(code); + return { + id, + code, + dependencies: dependencies || deps.sync, + asyncDependencies: asyncDependencies || deps.async, + }; + }); + } + }); return this._reading; } @@ -181,4 +202,19 @@ class Module { } } +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); +} + module.exports = Module; diff --git a/react-packager/src/DependencyResolver/__tests__/Module-test.js b/react-packager/src/DependencyResolver/__tests__/Module-test.js index 947fa0b1..4ed5868b 100644 --- a/react-packager/src/DependencyResolver/__tests__/Module-test.js +++ b/react-packager/src/DependencyResolver/__tests__/Module-test.js @@ -69,6 +69,104 @@ describe('Module', () => { 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('Async Dependencies', () => { function expectAsyncDependenciesToEqual(expected) { const module = createModule(); diff --git a/react-packager/src/__mocks__/fs.js b/react-packager/src/__mocks__/fs.js index 1aca9250..64b0c9b1 100644 --- a/react-packager/src/__mocks__/fs.js +++ b/react-packager/src/__mocks__/fs.js @@ -112,6 +112,56 @@ fs.stat.mockImpl(function(filepath, callback) { } }); +const noop = () => {}; +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); +}); + var filesystem; fs.__setMockFilesystem = function(object) {