Skip to content

Commit

Permalink
refactor: pull out various convert steps into helper functions (#125)
Browse files Browse the repository at this point in the history
This will be useful soon for a modernize-js command that uses some of those
steps.
  • Loading branch information
alangpierce authored May 16, 2017
1 parent 09af9e7 commit 41dd2de
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 153 deletions.
165 changes: 12 additions & 153 deletions src/convert.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { exec } from 'mz/child_process';
import { copy, move, readFile, unlink, writeFile } from 'fs-promise';
import { basename, join, relative, resolve } from 'path';
import { copy, move, unlink } from 'fs-promise';
import { basename } from 'path';
import git from 'simple-git/promise';
import zlib from 'zlib';

import getFilesToProcess from './config/getFilesToProcess';
import prependCodePrefix from './modernize/prependCodePrefix';
import prependMochaEnv from './modernize/prependMochaEnv';
import runEslintFix from './modernize/runEslintFix';
import runFixImports from './modernize/runFixImports';
import runJscodeshiftScripts from './modernize/runJscodeshiftScripts';
import makeCLIFn from './runner/makeCLIFn';
import makeDecaffeinateVerifyFn from './runner/makeDecaffeinateVerifyFn';
import runWithProgressBar from './runner/runWithProgressBar';
import CLIError from './util/CLIError';
import execLive from './util/execLive';
import { backupPathFor, decaffeinateOutPathFor, jsPathFor } from './util/FilePaths';
import getFilesUnderPath from './util/getFilesUnderPath';
import makeCommit from './util/makeCommit';
import pluralize from './util/pluralize';

Expand Down Expand Up @@ -103,65 +104,18 @@ Re-run with the "check" command for more details.`);
await makeCommit(decaffeinateCommitMsg);

if (config.jscodeshiftScripts) {
for (let scriptPath of config.jscodeshiftScripts) {
let resolvedPath = resolveJscodeshiftScriptPath(scriptPath);
console.log(`Running jscodeshift script ${resolvedPath}...`);
await execLive(`${config.jscodeshiftPath} --parser flow \
-t ${resolvedPath} ${jsFiles.map(p => relative('', p)).join(' ')}`);
}
await runJscodeshiftScripts(jsFiles, config);
}

if (config.mochaEnvFilePattern) {
let regex = new RegExp(config.mochaEnvFilePattern);
let testFiles = jsFiles.filter(f => regex.test(f));
await runWithProgressBar(
'Adding /* eslint-env mocha */ to test files...', testFiles, async function(path) {
await prependToFile(path, '/* eslint-env mocha */\n');
return {error: null};
});
await prependMochaEnv(jsFiles, config.mochaEnvFilePattern);
}

let thirdCommitModifiedFiles = jsFiles.slice();
if (config.fixImportsConfig) {
let {searchPath, absoluteImportPaths} = config.fixImportsConfig;
if (!absoluteImportPaths) {
absoluteImportPaths = [];
}
let scriptPath = join(__dirname, '../jscodeshift-scripts-dist/fix-imports.js');

let options = {
convertedFiles: jsFiles.map(p => resolve(p)),
absoluteImportPaths: absoluteImportPaths.map(p => resolve(p)),
};
let eligibleFixImportsFiles = await getEligibleFixImportsFiles(searchPath, jsFiles);
console.log('Fixing any imports across the whole codebase...');
if (eligibleFixImportsFiles.length > 0) {
// Note that the args can get really long, so we take reasonable steps to
// reduce the chance of hitting the system limit on arg length
// (256K by default on Mac).
let eligibleRelativePaths = eligibleFixImportsFiles.map(p => relative('', p));
thirdCommitModifiedFiles = eligibleFixImportsFiles;
let encodedOptions = zlib.deflateSync(JSON.stringify(options)).toString('base64');
await execLive(`\
${config.jscodeshiftPath} --parser flow -t ${scriptPath} \
${eligibleRelativePaths.join(' ')} --encoded-options=${encodedOptions}`);
}
}

let eslintResults = await runWithProgressBar(
'Running eslint --fix on all files...', jsFiles, makeEslintFixFn(config));
for (let result of eslintResults) {
for (let message of result.messages) {
console.log(message);
}
thirdCommitModifiedFiles = await runFixImports(jsFiles, config);
}

await runEslintFix(jsFiles, config);
if (config.codePrefix) {
await runWithProgressBar(
'Adding code prefix to converted files...', jsFiles, async function(path) {
await prependToFile(path, config.codePrefix);
return {error: null};
});
await prependCodePrefix(jsFiles, config.codePrefix);
}

let postProcessCommitMsg =
Expand Down Expand Up @@ -200,98 +154,3 @@ function getShortDescription(coffeeFiles) {
return `${firstFile} and ${pluralize(coffeeFiles.length - 1, 'other file')}`;
}
}

function resolveJscodeshiftScriptPath(scriptPath) {
if ([
'prefer-function-declarations.js',
'remove-coffee-from-imports.js',
'top-level-this-to-exports.js',
].includes(scriptPath)) {
return join(__dirname, `../jscodeshift-scripts-dist/${scriptPath}`);
}
return scriptPath;
}

async function getEligibleFixImportsFiles(searchPath, jsFiles) {
let jsBasenames = jsFiles.map(p => basename(p, '.js'));
let resolvedPaths = jsFiles.map(p => resolve(p));
let allJsFiles = await getFilesUnderPath(searchPath, p => p.endsWith('.js'));
await runWithProgressBar(
'Searching for files that may need to have updated imports...',
allJsFiles,
async function(p) {
let resolvedPath = resolve(p);
if (resolvedPaths.includes(resolvedPath)) {
return {error: null};
}
let contents = (await readFile(resolvedPath)).toString();
for (let jsBasename of jsBasenames) {
if (contents.includes(jsBasename)) {
resolvedPaths.push(resolvedPath);
return {error: null};
}
}
return {error: null};
});
return resolvedPaths;
}

function makeEslintFixFn(config) {
return async function runEslint(path) {
let messages = [];

// Ignore the eslint exit code; it gives useful stdout in the same format
// regardless of the exit code. Also keep a 10MB buffer since sometimes
// there can be a LOT of lint failures.
let eslintOutputStr = (await exec(
`${config.eslintPath} --fix --format json ${path}; :`,
{maxBuffer: 10000*1024}))[0];

let ruleIds;
if (eslintOutputStr.includes("ESLint couldn't find a configuration file")) {
messages.push(`Skipping "eslint --fix" on ${path} because there was no eslint config file.`);
ruleIds = [];
} else {
let eslintOutput;
try {
eslintOutput = JSON.parse(eslintOutputStr);
} catch (e) {
throw new CLIError(`Error while running eslint:\n${eslintOutputStr}`);
}
ruleIds = eslintOutput[0].messages
.map(message => message.ruleId).filter(ruleId => ruleId);
ruleIds = Array.from(new Set(ruleIds)).sort();
}

let suggestionLine;
if (ruleIds.length > 0) {
suggestionLine = 'Fix any style issues and re-enable lint.';
} else {
suggestionLine = 'Sanity-check the conversion and remove this comment.';
}

await prependToFile(`${path}`, `\
// TODO: This file was created by bulk-decaffeinate.
// ${suggestionLine}
`);
if (ruleIds.length > 0) {
await prependToFile(`${path}`, `\
/* eslint-disable
${ruleIds.map(ruleId => ` ${ruleId},`).join('\n')}
*/
`);
}
return {error: null, messages};
};
}

async function prependToFile(path, prependText) {
let contents = await readFile(path);
let lines = contents.toString().split('\n');
if (lines[0] && lines[0].startsWith('#!')) {
contents = lines[0] + '\n' + prependText + lines.slice(1).join('\n');
} else {
contents = prependText + contents;
}
await writeFile(path, contents);
}
10 changes: 10 additions & 0 deletions src/modernize/prependCodePrefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import prependToFile from '../util/prependToFile';
import runWithProgressBar from '../runner/runWithProgressBar';

export default async function prependCodePrefix(jsFiles, codePrefix) {
await runWithProgressBar(
'Adding code prefix to converted files...', jsFiles, async function(path) {
await prependToFile(path, codePrefix);
return {error: null};
});
}
12 changes: 12 additions & 0 deletions src/modernize/prependMochaEnv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import runWithProgressBar from '../runner/runWithProgressBar';
import prependToFile from '../util/prependToFile';

export default async function prependMochaEnv(jsFiles, mochaEnvFilePattern) {
let regex = new RegExp(mochaEnvFilePattern);
let testFiles = jsFiles.filter(f => regex.test(f));
await runWithProgressBar(
'Adding /* eslint-env mocha */ to test files...', testFiles, async function(path) {
await prependToFile(path, '/* eslint-env mocha */\n');
return {error: null};
});
}
63 changes: 63 additions & 0 deletions src/modernize/runEslintFix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { exec } from 'mz/child_process';

import runWithProgressBar from '../runner/runWithProgressBar';
import CLIError from '../util/CLIError';
import prependToFile from '../util/prependToFile';

export default async function runEslintFix(jsFiles, config) {
let eslintResults = await runWithProgressBar(
'Running eslint --fix on all files...', jsFiles, makeEslintFixFn(config));
for (let result of eslintResults) {
for (let message of result.messages) {
console.log(message);
}
}
}

export const HEADER_COMMENT_LINES = {
todo: '// TODO: This file was created by bulk-decaffeinate.',
fixIssues: '// Fix any style issues and re-enable lint.',
sanityCheck: '// Sanity-check the conversion and remove this comment.',
};

function makeEslintFixFn(config) {
return async function runEslint(path) {
let messages = [];

// Ignore the eslint exit code; it gives useful stdout in the same format
// regardless of the exit code. Also keep a 10MB buffer since sometimes
// there can be a LOT of lint failures.
let eslintOutputStr = (await exec(
`${config.eslintPath} --fix --format json ${path}; :`,
{maxBuffer: 10000*1024}))[0];

let ruleIds;
if (eslintOutputStr.includes("ESLint couldn't find a configuration file")) {
messages.push(`Skipping "eslint --fix" on ${path} because there was no eslint config file.`);
ruleIds = [];
} else {
let eslintOutput;
try {
eslintOutput = JSON.parse(eslintOutputStr);
} catch (e) {
throw new CLIError(`Error while running eslint:\n${eslintOutputStr}`);
}
ruleIds = eslintOutput[0].messages
.map(message => message.ruleId).filter(ruleId => ruleId);
ruleIds = Array.from(new Set(ruleIds)).sort();
}

await prependToFile(`${path}`, `\
${HEADER_COMMENT_LINES.todo}
${ruleIds.length > 0 ? HEADER_COMMENT_LINES.fixIssues : HEADER_COMMENT_LINES.sanityCheck}
`);
if (ruleIds.length > 0) {
await prependToFile(`${path}`, `\
/* eslint-disable
${ruleIds.map(ruleId => ` ${ruleId},`).join('\n')}
*/
`);
}
return {error: null, messages};
};
}
61 changes: 61 additions & 0 deletions src/modernize/runFixImports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Runs the fix-imports step on all specified JS files, and return an array of
* the files that changed.
*/
import { readFile } from 'fs-promise';
import { basename, join, relative, resolve } from 'path';
import zlib from 'zlib';

import runWithProgressBar from '../runner/runWithProgressBar';
import execLive from '../util/execLive';
import getFilesUnderPath from '../util/getFilesUnderPath';

export default async function runFixImports(jsFiles, config) {
let {searchPath, absoluteImportPaths} = config.fixImportsConfig;
if (!absoluteImportPaths) {
absoluteImportPaths = [];
}
let scriptPath = join(__dirname, '../jscodeshift-scripts-dist/fix-imports.js');

let options = {
convertedFiles: jsFiles.map(p => resolve(p)),
absoluteImportPaths: absoluteImportPaths.map(p => resolve(p)),
};
let eligibleFixImportsFiles = await getEligibleFixImportsFiles(searchPath, jsFiles);
console.log('Fixing any imports across the whole codebase...');
if (eligibleFixImportsFiles.length > 0) {
// Note that the args can get really long, so we take reasonable steps to
// reduce the chance of hitting the system limit on arg length
// (256K by default on Mac).
let eligibleRelativePaths = eligibleFixImportsFiles.map(p => relative('', p));
let encodedOptions = zlib.deflateSync(JSON.stringify(options)).toString('base64');
await execLive(`\
${config.jscodeshiftPath} --parser flow -t ${scriptPath} \
${eligibleRelativePaths.join(' ')} --encoded-options=${encodedOptions}`);
}
return eligibleFixImportsFiles;
}

async function getEligibleFixImportsFiles(searchPath, jsFiles) {
let jsBasenames = jsFiles.map(p => basename(p, '.js'));
let resolvedPaths = jsFiles.map(p => resolve(p));
let allJsFiles = await getFilesUnderPath(searchPath, p => p.endsWith('.js'));
await runWithProgressBar(
'Searching for files that may need to have updated imports...',
allJsFiles,
async function(p) {
let resolvedPath = resolve(p);
if (resolvedPaths.includes(resolvedPath)) {
return {error: null};
}
let contents = (await readFile(resolvedPath)).toString();
for (let jsBasename of jsBasenames) {
if (contents.includes(jsBasename)) {
resolvedPaths.push(resolvedPath);
return {error: null};
}
}
return {error: null};
});
return resolvedPaths;
}
23 changes: 23 additions & 0 deletions src/modernize/runJscodeshiftScripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { join, relative } from 'path';

import execLive from '../util/execLive';

export default async function runJscodeshiftScripts(jsFiles, config) {
for (let scriptPath of config.jscodeshiftScripts) {
let resolvedPath = resolveJscodeshiftScriptPath(scriptPath);
console.log(`Running jscodeshift script ${resolvedPath}...`);
await execLive(`${config.jscodeshiftPath} --parser flow \
-t ${resolvedPath} ${jsFiles.map(p => relative('', p)).join(' ')}`);
}
}

function resolveJscodeshiftScriptPath(scriptPath) {
if ([
'prefer-function-declarations.js',
'remove-coffee-from-imports.js',
'top-level-this-to-exports.js',
].includes(scriptPath)) {
return join(__dirname, `../jscodeshift-scripts-dist/${scriptPath}`);
}
return scriptPath;
}
12 changes: 12 additions & 0 deletions src/util/prependToFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { readFile, writeFile } from 'fs-promise';

export default async function prependToFile(path, prependText) {
let contents = await readFile(path);
let lines = contents.toString().split('\n');
if (lines[0] && lines[0].startsWith('#!')) {
contents = lines[0] + '\n' + prependText + lines.slice(1).join('\n');
} else {
contents = prependText + contents;
}
await writeFile(path, contents);
}

0 comments on commit 41dd2de

Please sign in to comment.