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,
|
||||
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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue