Add transform option to module / module cache

Summary:
public

This adds an option to `Module` (and its callers) that allows to transform code before extracting dependencies. The transform function has to return a promise that resolves to an object with `code`, and optionally `dependencies` and/or `asyncDependencies` properties, if standard dependency extraction cannot be applied

Reviewed By: cpojer

Differential Revision: D2870437

fb-gh-sync-id: 806d24ba16b1693d838a3fa747d82be9dc6ccf00
This commit is contained in:
David Aurelio 2016-01-28 16:28:37 -08:00 committed by facebook-github-bot-0
parent 370b7b25a3
commit 9fb1762784
4 changed files with 201 additions and 96 deletions

View File

@ -42,6 +42,7 @@ class DependencyGraph {
extensions,
mocksPattern,
extractRequires,
transformCode,
shouldThrowOnUnresolvedErrors = () => true,
}) {
this._opts = {
@ -54,13 +55,13 @@ class DependencyGraph {
providesModuleNodeModules,
platforms: platforms || [],
preferNativePlatform: preferNativePlatform || false,
cache,
extensions: extensions || ['js', 'json'],
mocksPattern,
extractRequires,
shouldThrowOnUnresolvedErrors,
transformCode
};
this._cache = this._opts.cache;
this._cache = cache;
this._helpers = new DependencyGraphHelpers(this._opts);
this.load().catch((err) => {
// This only happens at initialization. Live errors are easier to recover from.
@ -98,12 +99,13 @@ class DependencyGraph {
this._fastfs.on('change', this._processFileChange.bind(this));
this._moduleCache = new ModuleCache(
this._fastfs,
this._cache,
this._opts.extractRequires,
this._helpers
);
this._moduleCache = new ModuleCache({
fastfs: this._fastfs,
cache: this._cache,
extractRequires: this._opts.extractRequires,
transformCode: this._opts.transformCode,
depGraphHelpers: this._helpers,
});
this._hasteMap = new HasteMap({
fastfs: this._fastfs,

View File

@ -15,7 +15,15 @@ const extractRequires = require('./lib/extractRequires');
class Module {
constructor({ file, fastfs, moduleCache, cache, extractor, depGraphHelpers }) {
constructor({
file,
fastfs,
moduleCache,
cache,
extractor = extractRequires,
transformCode,
depGraphHelpers,
}) {
if (!isAbsolutePath(file)) {
throw new Error('Expected file to be absolute path but got ' + file);
}
@ -27,6 +35,7 @@ class Module {
this._moduleCache = moduleCache;
this._cache = cache;
this._extractor = extractor;
this._transformCode = transformCode;
this._depGraphHelpers = depGraphHelpers;
}
@ -38,6 +47,10 @@ class Module {
);
}
getCode() {
return this.read().then(({code}) => code);
}
getName() {
return this._cache.get(
this.path,
@ -114,13 +127,22 @@ class Module {
if (this.isJSON() || 'extern' in moduleDocBlock) {
data.dependencies = [];
data.asyncDependencies = [];
data.code = content;
return data;
} else {
var dependencies = (this._extractor || extractRequires)(content).deps;
data.dependencies = dependencies.sync;
data.asyncDependencies = dependencies.async;
}
const transformCode = this._transformCode;
const codePromise = transformCode
? transformCode(this, content)
: Promise.resolve({code: content});
return data;
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;
});
}
});
}

View File

@ -7,13 +7,21 @@ const path = require('path');
class ModuleCache {
constructor(fastfs, cache, extractRequires, depGraphHelpers) {
constructor({
fastfs,
cache,
extractRequires,
transformCode,
depGraphHelpers,
}) {
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;
fastfs.on('change', this._processFileChange.bind(this));
}
@ -26,6 +34,7 @@ class ModuleCache {
moduleCache: this,
cache: this._cache,
extractor: this._extractRequires,
transformCode: this._transformCode,
depGraphHelpers: this._depGraphHelpers,
});
}

View File

@ -26,74 +26,74 @@ const DependencyGraphHelpers = require('../DependencyGraph/DependencyGraphHelper
const Promise = require('promise');
const fs = require('graceful-fs');
function mockIndexFile(indexJs) {
fs.__setMockFilesystem({'root': {'index.js': indexJs}});
}
describe('Module', () => {
const fileWatcher = {
on: () => this,
isWatchman: () => Promise.resolve(false),
};
const fileName = '/root/index.js';
const Cache = jest.genMockFn();
Cache.prototype.get = jest.genMockFn().mockImplementation(
(filepath, field, cb) => cb(filepath)
);
Cache.prototype.invalidate = jest.genMockFn();
Cache.prototype.end = jest.genMockFn();
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({
cache,
fastfs,
file: fileName,
depGraphHelpers: new DependencyGraphHelpers(),
moduleCache: new ModuleCache({fastfs, cache}),
...options,
});
beforeEach(function(done) {
cache = createCache();
fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve([fileName]), ignore: []},
);
fastfs.build().then(done);
});
describe('Async Dependencies', () => {
function expectAsyncDependenciesToEqual(expected) {
const fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
const module = createModule();
return module.getAsyncDependencies().then(actual =>
expect(actual).toEqual(expected)
);
const cache = new Cache();
return fastfs.build().then(() => {
const module = new Module({
file: '/root/index.js',
fastfs,
moduleCache: new ModuleCache(fastfs, cache),
cache: cache,
depGraphHelpers: new DependencyGraphHelpers(),
});
return module.getAsyncDependencies().then(actual =>
expect(actual).toEqual(expected)
);
});
}
pit('should recognize single dependency', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.' + 'import("dep1")',
},
});
mockIndexFile('System.' + 'import("dep1")');
return expectAsyncDependenciesToEqual([['dep1']]);
});
pit('should parse single quoted dependencies', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.' + 'import(\'dep1\')',
},
});
mockIndexFile('System.' + 'import(\'dep1\')');
return expectAsyncDependenciesToEqual([['dep1']]);
});
pit('should parse multiple async dependencies on the same module', () => {
fs.__setMockFilesystem({
'root': {
'index.js': [
'System.' + 'import("dep1")',
'System.' + 'import("dep2")',
].join('\n'),
},
});
mockIndexFile([
'System.' + 'import("dep1")',
'System.' + 'import("dep2")',
].join('\n'));
return expectAsyncDependenciesToEqual([
['dep1'],
@ -102,53 +102,125 @@ describe('Module', () => {
});
pit('parse fine new lines', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.' + 'import(\n"dep1"\n)',
},
});
mockIndexFile('System.' + 'import(\n"dep1"\n)');
return expectAsyncDependenciesToEqual([['dep1']]);
});
});
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 contes via the `getCode()` method', () =>
createModule().getCode().then(code =>
expect(code).toBe(fileContents))
);
pit('does not save the code in the cache', () =>
createModule().getCode().then(() =>
expect(cache.get).not.toBeCalled()
)
);
});
describe('Extrators', () => {
function createModuleWithExtractor(extractor) {
const fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
);
const cache = new Cache();
return fastfs.build().then(() => {
return new Module({
file: '/root/index.js',
fastfs,
moduleCache: new ModuleCache(fastfs, cache),
cache,
extractor,
depGraphHelpers: new DependencyGraphHelpers(),
});
});
}
pit('uses custom require extractors if specified', () => {
fs.__setMockFilesystem({
'root': {
'index.js': '',
},
mockIndexFile('');
const module = createModule({
extractor: code => ({deps: {sync: ['foo', 'bar']}}),
});
return createModuleWithExtractor(
code => ({deps: {sync: ['foo', 'bar']}})
).then(module =>
module.getDependencies().then(actual =>
expect(actual).toEqual(['foo', 'bar'])
)
);
return module.getDependencies().then(actual =>
expect(actual).toEqual(['foo', 'bar']));
});
});
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);
});
});
pit('uses the code that `transformCode` resolves to to extract dependencies', () => {
transformCode.mockReturnValue(Promise.resolve({code: exampleCode}));
const module = createModule({transformCode});
return Promise.all([
module.getDependencies(),
module.getAsyncDependencies(),
]).then(([dependencies, asyncDependencies]) => {
expect(dependencies).toEqual(['a', 'c']);
expect(asyncDependencies).toEqual([['b']]);
});
});
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 Promise.all([
module.getDependencies(),
module.getAsyncDependencies(),
]).then(([dependencies, asyncDependencies]) => {
expect(dependencies).toEqual(mockedDependencies);
expect(asyncDependencies).toEqual([['b']]);
});
});
pit('uses async dependencies that `transformCode` resolves to, instead of extracting them', () => {
const mockedAsyncDependencies = [['foo', 'bar'], ['baz']];
transformCode.mockReturnValue(Promise.resolve({
code: exampleCode,
asyncDependencies: mockedAsyncDependencies,
}));
const module = createModule({transformCode});
return Promise.all([
module.getDependencies(),
module.getAsyncDependencies(),
]).then(([dependencies, asyncDependencies]) => {
expect(dependencies).toEqual(['a', 'c']);
expect(asyncDependencies).toEqual(mockedAsyncDependencies);
});
});
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);
});
});
});
});