Skip to content

Commit

Permalink
feat: automatically discover executable extensionless scripts
Browse files Browse the repository at this point in the history
Fixes #90
  • Loading branch information
alangpierce committed May 6, 2017
1 parent b19d9f4 commit 5eb2347
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 10 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -57,6 +57,7 @@
"dependencies": {
"babel-polyfill": "^6.13.0",
"commander": "^2.9.0",
"executable": "^4.1.0",
"fs-promise": "^1.0.0",
"mz": "^2.4.0",
"opn": "^4.0.2",
Expand Down
6 changes: 3 additions & 3 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, isExtensionless, jsPathFor } from '../util/FilePaths';
import { shouldConvertFile, isExtensionless, jsPathFor } from '../util/FilePaths';
import CLIError from '../util/CLIError';

export default async function getFilesToProcess(config) {
Expand All @@ -22,9 +22,9 @@ async function resolveFilesToProcess(config) {
return await getFilesFromPathFile(pathFile);
}
if (searchDirectory) {
return await getFilesUnderPath(searchDirectory, coffeePathPredicate);
return await getFilesUnderPath(searchDirectory, shouldConvertFile);
}
return await getFilesUnderPath('.', coffeePathPredicate);
return await getFilesUnderPath('.', shouldConvertFile);
}

function resolveFileFilter(filesToProcess, config) {
Expand Down
18 changes: 15 additions & 3 deletions src/util/FilePaths.js
@@ -1,3 +1,5 @@
import executable from 'executable';
import { readFile } from 'mz/fs';
import { basename, dirname, extname, join } from 'path';

const COFFEE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'];
Expand All @@ -14,9 +16,19 @@ function basePathFor(path) {
return join(dirname(path), basename(path, extension));
}

export function coffeePathPredicate(path) {
return COFFEE_EXTENSIONS.some(ext =>
path.endsWith(ext) && !path.endsWith(`.original${ext}`));
export async function shouldConvertFile(path) {
if (COFFEE_EXTENSIONS.some(ext =>
path.endsWith(ext) && !path.endsWith(`.original${ext}`))) {
return true;
}
if (isExtensionless(path) && await executable(path)) {
let contents = await readFile(path);
let firstLine = contents.toString().split('\n')[0];
if (firstLine.startsWith('#!') && firstLine.includes('coffee')) {
return true;
}
}
return false;
}

export function isExtensionless(path) {
Expand Down
6 changes: 3 additions & 3 deletions src/util/getFilesUnderPath.js
Expand Up @@ -5,7 +5,7 @@ import { join } from 'path';
* Recursively discover any matching files in the current directory, ignoring
* things like node_modules and .git.
*/
export default async function getFilesUnderPath(dirPath, pathPredicate) {
export default async function getFilesUnderPath(dirPath, asyncPathPredicate) {
let resultFiles = [];
let children = await readdir(dirPath);
for (let child of children) {
Expand All @@ -14,9 +14,9 @@ export default async function getFilesUnderPath(dirPath, pathPredicate) {
}
let childPath = join(dirPath, child);
if ((await stat(childPath)).isDirectory()) {
let subdirCoffeeFiles = await getFilesUnderPath(childPath, pathPredicate);
let subdirCoffeeFiles = await getFilesUnderPath(childPath, asyncPathPredicate);
resultFiles.push(...subdirCoffeeFiles);
} else if (pathPredicate(child)) {
} else if (await asyncPathPredicate(child)) {
resultFiles.push(childPath);
}
}
Expand Down
9 changes: 9 additions & 0 deletions test/bulk-decaffeinate-test.js
Expand Up @@ -104,4 +104,13 @@ describe('check', () => {
assertIncludes(stdout, 'All checks succeeded');
});
});

it('automatically discovers executable scripts', async function() {
await runWithTemplateDir('executable-extensionless-scripts', async function() {
let {stdout, stderr} = await runCli('check');
assert.equal(stderr, '');
assertIncludes(stdout, 'Doing a dry run of decaffeinate on 1 file...');
assertIncludes(stdout, 'All checks succeeded');
});
});
});
24 changes: 23 additions & 1 deletion test/convert-test.js
@@ -1,7 +1,7 @@
/* eslint-env mocha */
import assert from 'assert';
import { exec } from 'mz/child_process';
import { exists, writeFile } from 'mz/fs';
import { exists, readFile, writeFile } from 'mz/fs';

import {
assertFileContents,
Expand Down Expand Up @@ -222,6 +222,28 @@ console.log('Ran the thing!');
});
});

it('automatically discovers and converts extensionless scripts', async function() {
await runWithTemplateDir('executable-extensionless-scripts', async function () {
let untouchedContents1 = (await readFile('./executableScriptWithoutShebang')).toString();
let untouchedContents2 = (await readFile('./executableScriptWithWrongShebang')).toString();
let untouchedContents3 = (await readFile('./nonExecutableScript')).toString();

await initGitRepo();
await runCliExpectSuccess('convert');

await assertFileContents('./executableScript', `\
#!/usr/bin/env node
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
console.log('This script is executable so it should be converted.');
`);
await assertFileContents('./executableScriptWithoutShebang', untouchedContents1);
await assertFileContents('./executableScriptWithWrongShebang', untouchedContents2);
await assertFileContents('./nonExecutableScript', untouchedContents3);
});
});

it('allows converting a directory with no files', async function() {
await runWithTemplateDir('empty-directory', async function () {
await initGitRepo();
Expand Down
@@ -0,0 +1,3 @@
#!/usr/bin/env coffee

console.log 'This script is executable so it should be converted.'
@@ -0,0 +1,3 @@
#!/bin/sh

echo 'This is not a CoffeeScript file, so should not be converted.'
@@ -0,0 +1 @@
console.log 'This script has no shebang line, so should not be converted.'
@@ -0,0 +1,3 @@
#!/usr/bin/env coffee

console.log 'This script is not executable so it should not be converted.'

0 comments on commit 5eb2347

Please sign in to comment.