#!/usr/bin/env node
const babel = require('@babel/core');
const babelParser = require('@babel/parser');
const fs = require('fs');
const JestHasteMap = require('jest-haste-map');
const _ = require('lodash');
const path = require('path');
const mkdirp = require('mkdirp');
const MetroConfig = require('metro-config');
const metroDefaults = MetroConfig
.getDefaultConfig
.getDefaultValues(path.resolve(__dirname, '..'))
.resolver;
const ROOTS = [
path.resolve(__dirname, '..', 'Libraries'),
path.resolve(__dirname, '..', 'jest'),
path.resolve(__dirname, '..', 'integrationTests'),
path.resolve(__dirname, '..', 'RNTester'),
];
const OVERRIDES = {
'React': 'react',
};
const ignoreREs = [];
async function main() {
// console.log('Cleaning...');
// rimraf.sync(LibrariesDest);
const haste = createHasteMap();
console.log('Loading dependency graph...');
const { moduleMap } = await haste.build();
console.log('Loaded dependency graph.');
// await transformRequires({
// source: ROOTS[1],
// dest: ROOTS[1],
// moduleMap: moduleMap.getRawModuleMap().map,
// });
for (let rootDir of ROOTS) {
await transformRequires({
source: rootDir,
dest: rootDir,
moduleMap: moduleMap.getRawModuleMap().map,
});
}
}
async function transformRequires({ source, dest, moduleMap }) {
const sourceDir = fs.readdirSync(source);
for (let filename of sourceDir) {
const filePath = path.resolve(source, filename);
if (_.some(ignoreREs.map(r => filePath.match(r)))) {
continue;
}
const fileStats = fs.statSync(filePath);
if (fileStats.isDirectory()) {
await transformRequires({
source: filePath,
dest: path.join(dest, filename),
moduleMap,
});
} else {
await _transformRequiresInFile(
filePath,
path.join(dest, filename),
moduleMap
);
}
}
}
function _transformRequiresInFile(sourceFilePath, destFilePath, moduleMap) {
const dirname = path.dirname(destFilePath);
// Make the directory if it doesn't exist
mkdirp.sync(dirname);
// If not a JS file, just copy the file
if (!sourceFilePath.endsWith('.js')) {
// console.log(`Writing ${destFilePath}...`);
// fs
// .createReadStream(sourceFilePath)
// .pipe(fs.createWriteStream(destFilePath));
return;
}
// Get dependencies
const code = fs.readFileSync(sourceFilePath, 'utf8');
console.log(`Writing ${destFilePath}...`);
const { dependencyOffsets, dependencies } = extractDependencies(code);
const dependencyMap = dependencies.reduce((result, dep, i) => {
if (!moduleMap.has(dep)) {
return result;
}
let depPath;
if (OVERRIDES[dep]) {
depPath = OVERRIDES[dep];
} else {
const mod = moduleMap.get(dep);
let modulePath;
if (mod.g) {
modulePath = mod.g[0];
} else if (mod.ios) {
modulePath = mod.ios[0];
} else if (mod.android) {
modulePath = mod.android[0];
} else {
return result;
}
depPath = path.relative(path.dirname(sourceFilePath), modulePath);
if (!depPath.startsWith('.')) {
depPath = `./${depPath}`;
}
depPath = depPath.replace(/(.*)\.[^.]+$/, '$1'); // remove extension
depPath = depPath.replace(/(.*).(android|ios)/, '$1'); // remove platform ext
}
return Object.assign({}, result, {
[dep]: {
offset: dependencyOffsets[i],
replacement: depPath,
},
});
}, {});
const newCode = dependencyOffsets
.reduceRight(
([unhandled, handled], offset) => [
unhandled.slice(0, offset),
replaceDependency(unhandled.slice(offset) + handled, dependencyMap),
],
[code, '']
)
.join('');
fs.writeFileSync(destFilePath, newCode);
}
function createHasteMap() {
return new JestHasteMap({
extensions: metroDefaults.sourceExts.concat(metroDefaults.assetExts),
hasteImplModulePath: path.resolve(__dirname, '../jest/hasteImpl'),
maxWorkers: 1,
ignorePattern: /\/__tests__\//,
mocksPattern: '',
platforms: metroDefaults.platforms,
providesModuleNodeModules: [],
resetCache: true,
retainAllFiles: true,
rootDir: path.resolve(__dirname, '..'),
roots: ROOTS,
useWatchman: true,
watch: false,
});
}
const reDepencencyString = /^(['"])([^'"']*)\1/;
function replaceDependency(stringWithDependencyIDAtStart, dependencyMap) {
const match = reDepencencyString.exec(stringWithDependencyIDAtStart);
const dependencyName = match && match[2];
if (match != null && dependencyName in dependencyMap) {
const { length } = match[0];
const { replacement } = dependencyMap[dependencyName];
return `'${replacement}'` + stringWithDependencyIDAtStart.slice(length);
} else {
return stringWithDependencyIDAtStart;
}
}
/**
* Extracts dependencies (module IDs imported with the `require` function) from
* a string containing code. This walks the full AST for correctness (versus
* using, for example, regular expressions, that would be faster but inexact.)
*
* The result of the dependency extraction is an de-duplicated array of
* dependencies, and an array of offsets to the string literals with module IDs.
* The index points to the opening quote.
*/
function extractDependencies(code) {
const ast = babelParser.parse(code, {
sourceType: 'module',
plugins: [
'classProperties',
'jsx',
'flow',
'exportExtensions',
'asyncGenerators',
'objectRestSpread',
'optionalChaining',
'nullishCoalescingOperator',
],
});
const dependencies = new Set();
const dependencyOffsets = [];
const types = require('@babel/types');
const transformedFunctions = [
'require',
'require.resolve',
'jest.requireActual',
// 'jest.requireMock',
'System.import',
'mockComponent',
];
const isJest = node => {
try {
let callee;
if (node.isCallExpression()) {
callee = node.get('callee');
} else if (node.isMemberExpression()) {
callee = node;
}
return (
callee.get('object').isIdentifier({ name: 'jest' }) ||
(callee.isMemberExpression() && isJest(callee.get('object')))
);
} catch (e) {
return false;
}
};
const isValidJestFunc = node => {
return (
node.isIdentifier({ name: 'mock' }) ||
node.isIdentifier({ name: 'unmock' }) ||
node.isIdentifier({ name: 'doMock' }) ||
node.isIdentifier({ name: 'dontMock' }) ||
node.isIdentifier({ name: 'setMock' }) ||
node.isIdentifier({ name: 'genMockFromModule' })
);
};
const transformCall = nodePath => {
if (isJest(nodePath)) {
const calleeProperty = nodePath.get('callee.property');
if (isValidJestFunc(calleeProperty)) {
const arg = nodePath.get('arguments.0');
if (!arg || arg.type !== 'StringLiteral') {
return;
}
dependencyOffsets.push(parseInt(arg.node.start, 10));
dependencies.add(arg.node.value);
}
} else {
const calleePath = nodePath.get('callee');
const isNormalCall = transformedFunctions.some(pattern =>
_matchesPattern(types, calleePath, pattern)
);
if (isNormalCall) {
const arg = nodePath.get('arguments.0');
if (!arg || arg.type !== 'StringLiteral') {
return;
}
dependencyOffsets.push(parseInt(arg.node.start, 10));
dependencies.add(arg.node.value);
}
}
};
babel.traverse(ast, {
ImportDeclaration: (nodePath) => {
const sourceNode = nodePath.get('source').node;
dependencyOffsets.push(parseInt(sourceNode.start, 10));
dependencies.add(sourceNode.value);
},
CallExpression: transformCall,
});
return {
dependencyOffsets: [...dependencyOffsets].sort((a, b) => a - b),
dependencies: Array.from(dependencies),
};
}
function _matchesPattern(types, calleePath, pattern) {
const { node } = calleePath;
if (types.isMemberExpression(node)) {
return calleePath.matchesPattern(pattern);
}
if (!types.isIdentifier(node) || pattern.includes('.')) {
return false;
}
const name = pattern.split('.')[0];
return node.name === name;
}
main().catch(e => {
console.trace(e.message);
throw e;
});