mirror of https://github.com/status-im/metro.git
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:
parent
370b7b25a3
commit
9fb1762784
|
@ -42,6 +42,7 @@ class DependencyGraph {
|
||||||
extensions,
|
extensions,
|
||||||
mocksPattern,
|
mocksPattern,
|
||||||
extractRequires,
|
extractRequires,
|
||||||
|
transformCode,
|
||||||
shouldThrowOnUnresolvedErrors = () => true,
|
shouldThrowOnUnresolvedErrors = () => true,
|
||||||
}) {
|
}) {
|
||||||
this._opts = {
|
this._opts = {
|
||||||
|
@ -54,13 +55,13 @@ class DependencyGraph {
|
||||||
providesModuleNodeModules,
|
providesModuleNodeModules,
|
||||||
platforms: platforms || [],
|
platforms: platforms || [],
|
||||||
preferNativePlatform: preferNativePlatform || false,
|
preferNativePlatform: preferNativePlatform || false,
|
||||||
cache,
|
|
||||||
extensions: extensions || ['js', 'json'],
|
extensions: extensions || ['js', 'json'],
|
||||||
mocksPattern,
|
mocksPattern,
|
||||||
extractRequires,
|
extractRequires,
|
||||||
shouldThrowOnUnresolvedErrors,
|
shouldThrowOnUnresolvedErrors,
|
||||||
|
transformCode
|
||||||
};
|
};
|
||||||
this._cache = this._opts.cache;
|
this._cache = cache;
|
||||||
this._helpers = new DependencyGraphHelpers(this._opts);
|
this._helpers = new DependencyGraphHelpers(this._opts);
|
||||||
this.load().catch((err) => {
|
this.load().catch((err) => {
|
||||||
// This only happens at initialization. Live errors are easier to recover from.
|
// 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._fastfs.on('change', this._processFileChange.bind(this));
|
||||||
|
|
||||||
this._moduleCache = new ModuleCache(
|
this._moduleCache = new ModuleCache({
|
||||||
this._fastfs,
|
fastfs: this._fastfs,
|
||||||
this._cache,
|
cache: this._cache,
|
||||||
this._opts.extractRequires,
|
extractRequires: this._opts.extractRequires,
|
||||||
this._helpers
|
transformCode: this._opts.transformCode,
|
||||||
);
|
depGraphHelpers: this._helpers,
|
||||||
|
});
|
||||||
|
|
||||||
this._hasteMap = new HasteMap({
|
this._hasteMap = new HasteMap({
|
||||||
fastfs: this._fastfs,
|
fastfs: this._fastfs,
|
||||||
|
|
|
@ -15,7 +15,15 @@ const extractRequires = require('./lib/extractRequires');
|
||||||
|
|
||||||
class Module {
|
class Module {
|
||||||
|
|
||||||
constructor({ file, fastfs, moduleCache, cache, extractor, depGraphHelpers }) {
|
constructor({
|
||||||
|
file,
|
||||||
|
fastfs,
|
||||||
|
moduleCache,
|
||||||
|
cache,
|
||||||
|
extractor = extractRequires,
|
||||||
|
transformCode,
|
||||||
|
depGraphHelpers,
|
||||||
|
}) {
|
||||||
if (!isAbsolutePath(file)) {
|
if (!isAbsolutePath(file)) {
|
||||||
throw new Error('Expected file to be absolute path but got ' + file);
|
throw new Error('Expected file to be absolute path but got ' + file);
|
||||||
}
|
}
|
||||||
|
@ -27,6 +35,7 @@ class Module {
|
||||||
this._moduleCache = moduleCache;
|
this._moduleCache = moduleCache;
|
||||||
this._cache = cache;
|
this._cache = cache;
|
||||||
this._extractor = extractor;
|
this._extractor = extractor;
|
||||||
|
this._transformCode = transformCode;
|
||||||
this._depGraphHelpers = depGraphHelpers;
|
this._depGraphHelpers = depGraphHelpers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +47,10 @@ class Module {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return this.read().then(({code}) => code);
|
||||||
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return this._cache.get(
|
return this._cache.get(
|
||||||
this.path,
|
this.path,
|
||||||
|
@ -114,13 +127,22 @@ class Module {
|
||||||
if (this.isJSON() || 'extern' in moduleDocBlock) {
|
if (this.isJSON() || 'extern' in moduleDocBlock) {
|
||||||
data.dependencies = [];
|
data.dependencies = [];
|
||||||
data.asyncDependencies = [];
|
data.asyncDependencies = [];
|
||||||
|
data.code = content;
|
||||||
|
return data;
|
||||||
} else {
|
} else {
|
||||||
var dependencies = (this._extractor || extractRequires)(content).deps;
|
const transformCode = this._transformCode;
|
||||||
data.dependencies = dependencies.sync;
|
const codePromise = transformCode
|
||||||
data.asyncDependencies = dependencies.async;
|
? 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,21 @@ const path = require('path');
|
||||||
|
|
||||||
class ModuleCache {
|
class ModuleCache {
|
||||||
|
|
||||||
constructor(fastfs, cache, extractRequires, depGraphHelpers) {
|
constructor({
|
||||||
|
fastfs,
|
||||||
|
cache,
|
||||||
|
extractRequires,
|
||||||
|
transformCode,
|
||||||
|
depGraphHelpers,
|
||||||
|
}) {
|
||||||
this._moduleCache = Object.create(null);
|
this._moduleCache = Object.create(null);
|
||||||
this._packageCache = Object.create(null);
|
this._packageCache = Object.create(null);
|
||||||
this._fastfs = fastfs;
|
this._fastfs = fastfs;
|
||||||
this._cache = cache;
|
this._cache = cache;
|
||||||
this._extractRequires = extractRequires;
|
this._extractRequires = extractRequires;
|
||||||
|
this._transformCode = transformCode;
|
||||||
this._depGraphHelpers = depGraphHelpers;
|
this._depGraphHelpers = depGraphHelpers;
|
||||||
|
|
||||||
fastfs.on('change', this._processFileChange.bind(this));
|
fastfs.on('change', this._processFileChange.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +34,7 @@ class ModuleCache {
|
||||||
moduleCache: this,
|
moduleCache: this,
|
||||||
cache: this._cache,
|
cache: this._cache,
|
||||||
extractor: this._extractRequires,
|
extractor: this._extractRequires,
|
||||||
|
transformCode: this._transformCode,
|
||||||
depGraphHelpers: this._depGraphHelpers,
|
depGraphHelpers: this._depGraphHelpers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,74 +26,74 @@ const DependencyGraphHelpers = require('../DependencyGraph/DependencyGraphHelper
|
||||||
const Promise = require('promise');
|
const Promise = require('promise');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
|
|
||||||
|
function mockIndexFile(indexJs) {
|
||||||
|
fs.__setMockFilesystem({'root': {'index.js': indexJs}});
|
||||||
|
}
|
||||||
|
|
||||||
describe('Module', () => {
|
describe('Module', () => {
|
||||||
const fileWatcher = {
|
const fileWatcher = {
|
||||||
on: () => this,
|
on: () => this,
|
||||||
isWatchman: () => Promise.resolve(false),
|
isWatchman: () => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
|
const fileName = '/root/index.js';
|
||||||
|
|
||||||
const Cache = jest.genMockFn();
|
let cache, fastfs;
|
||||||
Cache.prototype.get = jest.genMockFn().mockImplementation(
|
|
||||||
(filepath, field, cb) => cb(filepath)
|
|
||||||
);
|
|
||||||
Cache.prototype.invalidate = jest.genMockFn();
|
|
||||||
Cache.prototype.end = jest.genMockFn();
|
|
||||||
|
|
||||||
|
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', () => {
|
describe('Async Dependencies', () => {
|
||||||
function expectAsyncDependenciesToEqual(expected) {
|
function expectAsyncDependenciesToEqual(expected) {
|
||||||
const fastfs = new Fastfs(
|
const module = createModule();
|
||||||
'test',
|
return module.getAsyncDependencies().then(actual =>
|
||||||
['/root'],
|
expect(actual).toEqual(expected)
|
||||||
fileWatcher,
|
|
||||||
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
|
|
||||||
);
|
);
|
||||||
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', () => {
|
pit('should recognize single dependency', () => {
|
||||||
fs.__setMockFilesystem({
|
mockIndexFile('System.' + 'import("dep1")');
|
||||||
'root': {
|
|
||||||
'index.js': 'System.' + 'import("dep1")',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return expectAsyncDependenciesToEqual([['dep1']]);
|
return expectAsyncDependenciesToEqual([['dep1']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
pit('should parse single quoted dependencies', () => {
|
pit('should parse single quoted dependencies', () => {
|
||||||
fs.__setMockFilesystem({
|
mockIndexFile('System.' + 'import(\'dep1\')');
|
||||||
'root': {
|
|
||||||
'index.js': 'System.' + 'import(\'dep1\')',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return expectAsyncDependenciesToEqual([['dep1']]);
|
return expectAsyncDependenciesToEqual([['dep1']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
pit('should parse multiple async dependencies on the same module', () => {
|
pit('should parse multiple async dependencies on the same module', () => {
|
||||||
fs.__setMockFilesystem({
|
mockIndexFile([
|
||||||
'root': {
|
'System.' + 'import("dep1")',
|
||||||
'index.js': [
|
'System.' + 'import("dep2")',
|
||||||
'System.' + 'import("dep1")',
|
].join('\n'));
|
||||||
'System.' + 'import("dep2")',
|
|
||||||
].join('\n'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return expectAsyncDependenciesToEqual([
|
return expectAsyncDependenciesToEqual([
|
||||||
['dep1'],
|
['dep1'],
|
||||||
|
@ -102,53 +102,125 @@ describe('Module', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
pit('parse fine new lines', () => {
|
pit('parse fine new lines', () => {
|
||||||
fs.__setMockFilesystem({
|
mockIndexFile('System.' + 'import(\n"dep1"\n)');
|
||||||
'root': {
|
|
||||||
'index.js': 'System.' + 'import(\n"dep1"\n)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return expectAsyncDependenciesToEqual([['dep1']]);
|
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', () => {
|
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', () => {
|
pit('uses custom require extractors if specified', () => {
|
||||||
fs.__setMockFilesystem({
|
mockIndexFile('');
|
||||||
'root': {
|
const module = createModule({
|
||||||
'index.js': '',
|
extractor: code => ({deps: {sync: ['foo', 'bar']}}),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return createModuleWithExtractor(
|
return module.getDependencies().then(actual =>
|
||||||
code => ({deps: {sync: ['foo', 'bar']}})
|
expect(actual).toEqual(['foo', 'bar']));
|
||||||
).then(module =>
|
});
|
||||||
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue