diff --git a/.gitignore b/.gitignore index 0389698..a916152 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules *.trace +.env diff --git a/README.md b/README.md index 77e61d8..d6e4432 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,31 @@ codemod-cli new This will create a small project structure (`README.md`, `package.json`, etc) which is ready to help you manage your codemods. +You can also import from ast-explorer to create a codemod project via: + +``` +codemod-cli new --url --codemod +``` + +For example: + +``` +codemod-cli new my-awesome-codemod --url https://astexplorer.net/#/gist/cb7d2e7ce49741966e5e96a4b2eadc4d/d6b902bf639adc2bc6d31b35ba38aa45910b2413 --codemod reverse-string +``` + + Once you have a project, you can generate a new codemod: ``` codemod-cli generate codemod ``` +You can also import a codemod from ast-explorer url after you create a new project: + +``` +codemod-cli import +``` + This will setup a new jscodeshift codemod within your project at `transforms//index.js` along with a test harness, README, fixture directory, and an initial set of input/output fixtures. diff --git a/commands/global/new.js b/commands/global/new.js index 5d76899..b92e85a 100644 --- a/commands/global/new.js +++ b/commands/global/new.js @@ -4,62 +4,34 @@ module.exports.command = 'new '; module.exports.desc = 'Generate a new codemod project'; module.exports.builder = function builder(yargs) { - yargs.positional('project-name', { - describe: 'The name of the project to generate', - }); + yargs + .positional('project-name', { + describe: 'The name of the project to generate', + }) + .option('url', { + alias: 'u', + demandOption: false, + describe: 'ast-explorer gist url to import from', + type: 'string', + }) + .option('codemod', { + alias: 'c', + demandOption: false, + describe: 'name of the codemod to generate', + type: 'string', + }); }; module.exports.handler = async function handler(options) { - let { projectName } = options; + let { projectName, url, codemod: codemodName } = options; const fs = require('fs-extra'); const { stripIndent } = require('common-tags'); const latestVersion = require('latest-version'); const pkg = require('../../package.json'); + const { codemodReadme, projectReadme } = require('../../src/readme-support'); - fs.outputFileSync( - projectName + '/README.md', - stripIndent` - # ${projectName}\n - - A collection of codemod's for ${projectName}. - - ## Usage - - To run a specific codemod from this project, you would run the following: - - \`\`\` - npx ${projectName} path/of/files/ or/some**/*glob.js - - # or - - yarn global add ${projectName} - ${projectName} path/of/files/ or/some**/*glob.js - \`\`\` - - ## Transforms - - - - - ## Contributing - - ### Installation - - * clone the repo - * change into the repo directory - * \`yarn\` - - ### Running tests - - * \`yarn test\` - - ### Update Documentation - - * \`yarn update-docs\` - `, - 'utf8' - ); + fs.outputFileSync(projectName + '/README.md', projectReadme(projectName), 'utf8'); fs.outputJsonSync( projectName + '/package.json', { @@ -256,4 +228,52 @@ module.exports.handler = async function handler(options) { ); fs.outputFileSync(projectName + '/.gitignore', '/node_modules\n/.eslintcache'); fs.ensureFileSync(projectName + '/transforms/.gitkeep'); + + // If import options [ url && codemod] are present + if (url && codemodName) { + let regex = /https:\/\/astexplorer\.net\/#\/gist\/(\w+)\/(\w+)/; + let matches = regex.exec(url); + + let [, gist_id, gistRevision] = matches; + + let rawFile = `https://gist.githubusercontent.com/astexplorer/${gist_id}/raw/${gistRevision}/transform.js`; + + const request = require('request'); // eslint-disable-line + + request.get(rawFile, function(error, response, body) { + if (!error && response.statusCode == 200) { + // HACK: Replacing export default with module.exports + let _body = body.replace('export default', 'module.exports ='); + + // Formatting with prettier to avoid possible lint errors in new project + const prettier = require('prettier'); // eslint-disable-line + _body = prettier.format(_body, { parser: 'babel', singleQuote: true }); + fs.outputFileSync(`${codemodDir}/index.js`, _body, 'utf8'); + } + }); + + let codemodDir = `${process.cwd()}/${projectName}/transforms/${codemodName}`; + + fs.outputFileSync( + `${codemodDir}/test.js`, + stripIndent` + 'use strict'; + + const { runTransformTest } = require('codemod-cli'); + + runTransformTest({ + type: 'jscodeshift', + name: '${codemodName}', + });` + '\n', + 'utf8' + ); + + fs.outputFileSync(`${codemodDir}/README.md`, codemodReadme(projectName, codemodName), 'utf-8'); + + // Generate basic test fixtures + let fixturePath = `${codemodDir}/__testfixtures__/basic`; + + fs.outputFileSync(`${fixturePath}.input.js`, ''); + fs.outputFileSync(`${fixturePath}.output.js`, ''); + } }; diff --git a/commands/local/generate/codemod.js b/commands/local/generate/codemod.js index 5f6f83c..77f1f37 100644 --- a/commands/local/generate/codemod.js +++ b/commands/local/generate/codemod.js @@ -12,6 +12,7 @@ module.exports.handler = function handler(options) { const { stripIndent } = require('common-tags'); const importCwd = require('import-cwd'); const generateFixture = require('./fixture').handler; + const { codemodReadme } = require('../../../src/readme-support'); let { codemodName } = options; let projectName = importCwd('./package.json').name; @@ -54,32 +55,8 @@ module.exports.handler = function handler(options) { `, 'utf8' ); - fs.outputFileSync( - `${codemodDir}/README.md`, - stripIndent` - # ${codemodName}\n - - ## Usage - - \`\`\` - npx ${projectName} ${codemodName} path/of/files/ or/some**/*glob.js - - # or - - yarn global add ${projectName} - ${projectName} ${codemodName} path/of/files/ or/some**/*glob.js - \`\`\` - ## Input / Output - - - - - - - `, - 'utf8' - ); + fs.outputFileSync(`${codemodDir}/README.md`, codemodReadme(projectName, codemodName), 'utf-8'); generateFixture({ codemodName, fixtureName: 'basic' }); }; diff --git a/commands/local/import.js b/commands/local/import.js new file mode 100644 index 0000000..e7b9306 --- /dev/null +++ b/commands/local/import.js @@ -0,0 +1,55 @@ +module.exports.command = 'import '; +module.exports.desc = 'Generate a new codemod file from ast-explorer gist'; + +module.exports.builder = function builder(yargs) { + yargs.positional('codemod-name', { + describe: 'the name of the codemod to generate', + }); + yargs.positional('gist-url', { + describe: 'the url of the ast-explorer gist', + }); +}; + +module.exports.handler = function handler(options) { + const fs = require('fs-extra'); + const { stripIndent } = require('common-tags'); + const importCwd = require('import-cwd'); + const generateFixture = require('./generate/fixture').handler; + const { codemodReadme } = require('../../src/readme-support'); + + let { codemodName, gistUrl } = options; + let regex = /https:\/\/astexplorer\.net\/#\/gist\/(\w+)\/(\w+)/; + let matches = regex.exec(gistUrl); + + let [, gist_id, gistRevision] = matches; + + let rawFile = `https://gist.githubusercontent.com/astexplorer/${gist_id}/raw/${gistRevision}/transform.js`; + + const request = require('request'); // eslint-disable-line + request.get(rawFile, function(error, response, body) { + if (!error && response.statusCode == 200) { + fs.outputFileSync(`${codemodDir}/index.js`, body, 'utf8'); + } + }); + + let projectName = importCwd('./package.json').name; + let codemodDir = `${process.cwd()}/transforms/${codemodName}`; + + fs.outputFileSync( + `${codemodDir}/test.js`, + stripIndent` + 'use strict'; + + const { runTransformTest } = require('codemod-cli'); + + runTransformTest({ + type: 'jscodeshift', + name: '${codemodName}', + }); + `, + 'utf8' + ); + + fs.outputFileSync(`${codemodDir}/README.md`, codemodReadme(projectName, codemodName), 'utf-8'); + generateFixture({ codemodName, fixtureName: 'basic' }); +}; diff --git a/package.json b/package.json index 068ecb3..bc4dfe4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@babel/parser": "^7.6.0", "chalk": "^2.4.2", "common-tags": "^1.8.0", + "dotenv": "^8.2.0", "execa": "^2.0.4", "fs-extra": "^8.1.0", "globby": "^10.0.1", diff --git a/src/readme-support.js b/src/readme-support.js new file mode 100644 index 0000000..9b86be7 --- /dev/null +++ b/src/readme-support.js @@ -0,0 +1,75 @@ +'use strict'; + +const { stripIndent } = require('common-tags'); + +function projectReadme(projectName) { + return stripIndent` + # ${projectName}\n + + A collection of codemod's for ${projectName}. + + ## Usage + + To run a specific codemod from this project, you would run the following: + + \`\`\` + npx ${projectName} path/of/files/ or/some**/*glob.js + + # or + + yarn global add ${projectName} + ${projectName} path/of/files/ or/some**/*glob.js + \`\`\` + + ## Transforms + + + + + ## Contributing + + ### Installation + + * clone the repo + * change into the repo directory + * \`yarn\` + + ### Running tests + + * \`yarn test\` + + ### Update Documentation + + * \`yarn update-docs\` + `; +} + +function codemodReadme(projectName, codemodName) { + return stripIndent` + # ${codemodName}\n + + ## Usage + + \`\`\` + npx ${projectName} ${codemodName} path/of/files/ or/some**/*glob.js + + # or + + yarn global add ${projectName} + ${projectName} ${codemodName} path/of/files/ or/some**/*glob.js + \`\`\` + + ## Input / Output + + + + + + + `; +} + +module.exports = { + codemodReadme, + projectReadme, +}; diff --git a/tests/cli-test.js b/tests/cli-test.js index 2d5bbff..1947ee1 100644 --- a/tests/cli-test.js +++ b/tests/cli-test.js @@ -71,6 +71,47 @@ QUnit.module('codemod-cli', function(hooks) { }); }); + QUnit.module('new with import url', function() { + QUnit.test('should generate a basic project structure and one transform', async function( + assert + ) { + let result = await execa(EXECUTABLE_PATH, [ + 'new', + 'ember-qunit-codemod', + '--url', + 'https://astexplorer.net/#/gist/8cc5a4f80787283b994af842d8df5c38/58ceb9e4631064eac99f0cd7f8b35dbabe6b59a8', + '--codemod', + 'reverse-string', + ]); + + assert.equal(result.exitCode, 0, 'exited with zero'); + assert.deepEqual(walkSync(codemodProject.path()), [ + 'ember-qunit-codemod/', + 'ember-qunit-codemod/.eslintignore', + 'ember-qunit-codemod/.eslintrc.js', + 'ember-qunit-codemod/.github/', + 'ember-qunit-codemod/.github/workflows/', + 'ember-qunit-codemod/.github/workflows/ci.yml', + 'ember-qunit-codemod/.gitignore', + 'ember-qunit-codemod/.prettierrc', + 'ember-qunit-codemod/.travis.yml', + 'ember-qunit-codemod/README.md', + 'ember-qunit-codemod/bin/', + 'ember-qunit-codemod/bin/cli.js', + 'ember-qunit-codemod/package.json', + 'ember-qunit-codemod/transforms/', + 'ember-qunit-codemod/transforms/.gitkeep', + 'ember-qunit-codemod/transforms/reverse-string/', + 'ember-qunit-codemod/transforms/reverse-string/README.md', + 'ember-qunit-codemod/transforms/reverse-string/__testfixtures__/', + 'ember-qunit-codemod/transforms/reverse-string/__testfixtures__/basic.input.js', + 'ember-qunit-codemod/transforms/reverse-string/__testfixtures__/basic.output.js', + 'ember-qunit-codemod/transforms/reverse-string/index.js', + 'ember-qunit-codemod/transforms/reverse-string/test.js', + ]); + }); + }); + QUnit.module('linting', function(hooks) { setupProject(hooks); @@ -535,4 +576,29 @@ QUnit.module('codemod-cli', function(hooks) { }); }); }); + + // Local import command test + QUnit.module('import', function(hooks) { + setupProject(hooks); + + QUnit.test('should import one transform from astexplorer', async function(assert) { + let result = await execa(EXECUTABLE_PATH, [ + 'import', + 'https://astexplorer.net/#/gist/8cc5a4f80787283b994af842d8df5c38/58ceb9e4631064eac99f0cd7f8b35dbabe6b59a8', + 'reverse-string', + ]); + + assert.equal(result.exitCode, 0, 'exited with zero'); + assert.deepEqual(walkSync(codemodProject.path('transforms')), [ + '.gitkeep', + 'reverse-string/', + 'reverse-string/README.md', + 'reverse-string/__testfixtures__/', + 'reverse-string/__testfixtures__/basic.input.js', + 'reverse-string/__testfixtures__/basic.output.js', + 'reverse-string/index.js', + 'reverse-string/test.js', + ]); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 9ad5d5f..b4972ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2234,6 +2234,11 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"