Skip to content

Commit

Permalink
feat: add support for explicitly-specified extensionless scripts
Browse files Browse the repository at this point in the history
Progress toward #90

We don't auto-discover them yet, but this adds all of the cases in terms of
doing the correct move operations. Also, we now detect shebang lines when
prepending the results comment and put them after the shebang line.
  • Loading branch information
alangpierce committed May 6, 2017
1 parent b10f60d commit 0bd5fe6
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 38 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -12,7 +12,7 @@
"build": "rollup -c && babel jscodeshift-scripts --out-dir jscodeshift-scripts-dist",
"lint": "eslint src test jscodeshift-scripts",
"pretest": "npm run build",
"test": "mocha",
"test": "mocha test test/util",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
},
"repository": {
Expand Down Expand Up @@ -44,7 +44,7 @@
"babel-preset-es2015": "^6.13.0",
"babel-register": "^6.11.6",
"babelrc-rollup": "^3.0.0",
"decaffeinate": "^2.54.0",
"decaffeinate": "^2.54.2",
"eslint": "^3.18.0",
"eslint-plugin-babel": "^4.0.0",
"jscodeshift": "^0.3.30",
Expand Down
12 changes: 8 additions & 4 deletions src/config/getFilesToProcess.js
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'path';

import getFilesFromPathFile from './getFilesFromPathFile';
import getFilesUnderPath from '../util/getFilesUnderPath';
import { coffeePathPredicate, jsPathFor } from '../util/FilePaths';
import { coffeePathPredicate, isExtensionless, jsPathFor } from '../util/FilePaths';
import CLIError from '../util/CLIError';

export default async function getFilesToProcess(config) {
Expand Down Expand Up @@ -35,9 +35,13 @@ function resolveFileFilter(filesToProcess, config) {
}

async function validateFilesToProcess(filesToProcess) {
for (let file of filesToProcess) {
if (await exists(jsPathFor(file))) {
throw new CLIError(`The file ${jsPathFor(file)} already exists.`);
for (let path of filesToProcess) {
if (isExtensionless(path)) {
continue;
}
let jsPath = jsPathFor(path);
if (await exists(jsPath)) {
throw new CLIError(`The file ${jsPath} already exists.`);
}
}
}
51 changes: 33 additions & 18 deletions src/convert.js
Expand Up @@ -10,7 +10,7 @@ import makeDecaffeinateVerifyFn from './runner/makeDecaffeinateVerifyFn';
import runWithProgressBar from './runner/runWithProgressBar';
import CLIError from './util/CLIError';
import execLive from './util/execLive';
import { backupPathFor, jsPathFor } from './util/FilePaths';
import { backupPathFor, decaffeinateOutPathFor, isExtensionless, jsPathFor } from './util/FilePaths';
import getFilesUnderPath from './util/getFilesUnderPath';
import isWorktreeEmpty from './util/isWorktreeEmpty';
import makeCommit from './util/makeCommit';
Expand All @@ -20,6 +20,10 @@ export default async function convert(config) {
await assertGitWorktreeClean();

let coffeeFiles = await getFilesToProcess(config);
let coffeeFilesWithExtension = coffeeFiles.filter(p => !isExtensionless(p));
// Extensionless files are special because they don't change their name, so
// handle them separately in some cases.
let coffeeFilesWithoutExtension = coffeeFiles.filter(p => isExtensionless(p));
let {decaffeinateArgs = [], decaffeinatePath} = config;

if (!config.skipVerify) {
Expand All @@ -34,36 +38,34 @@ Re-run with the "check" command for more details.`);
}
}

async function runAsync(description, asyncFn) {
await runWithProgressBar(
description, coffeeFiles, async function(path) {
await asyncFn(path);
return {path};
});
}

await runAsync(
await runWithProgressBar(
'Backing up files to .original.coffee...',
coffeeFiles,
async function(coffeePath) {
await copy(`${coffeePath}`, `${backupPathFor(coffeePath)}`);
});

await runAsync(
await runWithProgressBar(
'Renaming files from .coffee to .js...',
coffeeFilesWithExtension,
async function(coffeePath) {
await move(coffeePath, jsPathFor(coffeePath));
});

let shortDescription = getShortDescription(coffeeFiles);
let renameCommitMsg =
`decaffeinate: Rename ${shortDescription} from .coffee to .js`;
console.log(`Generating the first commit: "${renameCommitMsg}"...`);
await git().rm(coffeeFiles);
await git().raw(['add', '-f', ...coffeeFiles.map(p => jsPathFor(p))]);
await makeCommit(renameCommitMsg);

await runAsync(
if (coffeeFilesWithExtension.length > 0) {
console.log(`Generating the first commit: "${renameCommitMsg}"...`);
await git().rm(coffeeFilesWithExtension);
await git().raw(['add', '-f', ...coffeeFilesWithExtension.map(p => jsPathFor(p))]);
await makeCommit(renameCommitMsg);
}

await runWithProgressBar(
'Moving files back...',
coffeeFilesWithExtension,
async function(coffeePath) {
await move(jsPathFor(coffeePath), coffeePath);
});
Expand All @@ -74,12 +76,20 @@ Re-run with the "check" command for more details.`);
makeCLIFn(path => `${decaffeinatePath} ${decaffeinateArgs.join(' ')} ${path}`)
);

await runAsync(
await runWithProgressBar(
'Deleting old files...',
coffeeFiles,
async function(coffeePath) {
await unlink(coffeePath);
});

await runWithProgressBar(
'Setting proper extension for all files...',
coffeeFilesWithoutExtension,
async function(coffeePath) {
await move(decaffeinateOutPathFor(coffeePath), jsPathFor(coffeePath));
});

let decaffeinateCommitMsg =
`decaffeinate: Convert ${shortDescription} to JS`;
console.log(`Generating the second commit: ${decaffeinateCommitMsg}...`);
Expand Down Expand Up @@ -265,6 +275,11 @@ ${ruleIds.map(ruleId => ` ${ruleId},`).join('\n')}

async function prependToFile(path, prependText) {
let contents = await readFile(path);
contents = prependText + contents;
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);
}
2 changes: 1 addition & 1 deletion src/runner/runWithProgressBar.js
Expand Up @@ -21,7 +21,7 @@ export default async function runWithProgressBar(
let results;
try {
results = await runInParallel(files, asyncFn, numConcurrentProcesses, ({result}) => {
if (result.error) {
if (result && result.error) {
if (!allowFailures) {
throw new CLIError(`Error:\n${result.error}`);
}
Expand Down
50 changes: 38 additions & 12 deletions src/util/FilePaths.js
@@ -1,26 +1,52 @@
import { basename, dirname, extname, join } from 'path';

const COFFEE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'];

function extensionFor(path) {
if (path.endsWith('.coffee.md')) {
return '.coffee.md';
}
return extname(path);
}

function basePathFor(path) {
let extension = extensionFor(path);
return join(dirname(path), basename(path, extension));
}

export function coffeePathPredicate(path) {
return COFFEE_EXTENSIONS.some(ext =>
path.endsWith(ext) && !path.endsWith(`.original${ext}`));
path.endsWith(ext) && !path.endsWith(`.original${ext}`));
}

export function isExtensionless(path) {
return extensionFor(path) === '';
}

export function backupPathFor(path) {
for (let ext of COFFEE_EXTENSIONS) {
if (path.endsWith(ext)) {
return path.slice(0, path.length - ext.length) + '.original' + ext;
}
}
return path + '.original';
let extension = extensionFor(path);
let basePath = basePathFor(path);
return basePath + '.original' + extension;
}

/**
* The resulting path where we should send the given input file. Note that when
* the input file is an extensionless script, we prefer to keep it extensionless
* (and decaffeinate handles the shebang line).
*/
export function jsPathFor(path) {
for (let ext of COFFEE_EXTENSIONS) {
if (path.endsWith(ext)) {
return path.slice(0, path.length - ext.length) + '.js';
}
if (isExtensionless(path)) {
return path;
} else {
return basePathFor(path) + '.js';
}
return path + '.js';
}

/**
* The file generated by decaffeinate for the input file with this name.
*/
export function decaffeinateOutPathFor(path) {
return basePathFor(path) + '.js';
}

export function isLiterate(path) {
Expand Down
15 changes: 14 additions & 1 deletion test/convert-test.js
Expand Up @@ -191,7 +191,6 @@ console.log('This is a file');
});
});


it('respects decaffeinate args', async function() {
await runWithTemplateDir('decaffeinate-args-test', async function () {
await initGitRepo();
Expand All @@ -209,6 +208,20 @@ module.exports = c;
});
});

it('allows converting extensionless scripts', async function() {
await runWithTemplateDir('extensionless-script', async function () {
await initGitRepo();
await runCliExpectSuccess('convert');
await assertFileContents('./runThing', `\
#!/usr/bin/env node
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
console.log('Ran the thing!');
`);
});
});

it('runs eslint, applying fixes and disabling existing issues', async function() {
await runWithTemplateDir('eslint-fix-test', async function() {
await initGitRepo();
Expand Down
@@ -0,0 +1,3 @@
module.exports = {
filesToProcess: ['./runThing'],
};
3 changes: 3 additions & 0 deletions test/examples/extensionless-script/runThing
@@ -0,0 +1,3 @@
#!/usr/bin/env coffee

console.log 'Ran the thing!'
28 changes: 28 additions & 0 deletions test/util/FilePaths-test.js
@@ -0,0 +1,28 @@
/* eslint-env mocha */
import assert from 'assert';
import * as FilePaths from '../../src/util/FilePaths';

describe('FilePaths', () => {
it('generates correct backup paths', () => {
assert.equal(FilePaths.backupPathFor('./a/b/foo.coffee'), 'a/b/foo.original.coffee');
assert.equal(FilePaths.backupPathFor('foo.coffee'), 'foo.original.coffee');
assert.equal(FilePaths.backupPathFor('foo.coffee.md'), 'foo.original.coffee.md');
assert.equal(FilePaths.backupPathFor('foo.cjsx'), 'foo.original.cjsx');
assert.equal(FilePaths.backupPathFor('foo'), 'foo.original');
});

it('generates correct js paths', () => {
assert.equal(FilePaths.jsPathFor('./a/b/foo.coffee'), 'a/b/foo.js');
assert.equal(FilePaths.jsPathFor('foo.coffee'), 'foo.js');
assert.equal(FilePaths.jsPathFor('foo.coffee.md'), 'foo.js');
assert.equal(FilePaths.jsPathFor('foo.cjsx'), 'foo.js');
assert.equal(FilePaths.jsPathFor('foo'), 'foo');
});

it('generates correct decaffeinate out paths', () => {
assert.equal(FilePaths.decaffeinateOutPathFor('./a/b/foo.coffee'), 'a/b/foo.js');
assert.equal(FilePaths.decaffeinateOutPathFor('foo.coffee.md'), 'foo.js');
assert.equal(FilePaths.decaffeinateOutPathFor('foo.cjsx'), 'foo.js');
assert.equal(FilePaths.decaffeinateOutPathFor('foo'), 'foo.js');
});
});

0 comments on commit 0bd5fe6

Please sign in to comment.