├── .gitignore ├── README.md ├── bin └── tocjs ├── index.js ├── lib ├── generators │ ├── cjsrequire.js │ ├── exports.js │ └── index.js ├── index.js ├── transformers │ ├── amdToCjs.js │ ├── identity.js │ └── index.js └── visitors │ └── findIdentifier.js ├── package.json └── test ├── cases ├── amdToCjs.js ├── amdToCjs.out.js ├── identity.js └── identity.out.js ├── generators.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMD to CJS conversion with Recast 2 | 3 | This repo is part of a tutorial at the Skookum.com/blog for converting a project 4 | from AMD to common.js. 5 | 6 | Follow along with the numerically ordered branches. 7 | 8 | ## Hacking 9 | 10 | * `git clone Skookum/recast-to-cjs` 11 | * `cd recast-to-cjs` 12 | * `npm link` 13 | * `tocjs -d test/cases/identity` 14 | 15 | ## License 16 | 17 | Copyright (c) 2014 Skookum Digital Works, Inc. 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining 20 | a copy of this software and associated documentation files (the 21 | "Software"), to deal in the Software without restriction, including 22 | without limitation the rights to use, copy, modify, merge, publish, 23 | distribute, sublicense, and/or sell copies of the Software, and to 24 | permit persons to whom the Software is furnished to do so, subject to 25 | the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be 28 | included in all copies or substantial portions of the Software. 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 30 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 31 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 32 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 33 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 34 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 35 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | -------------------------------------------------------------------------------- /bin/tocjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var program = require('commander'); 4 | var transformer = require('../'); 5 | 6 | program 7 | .version(require('../package.json').version) 8 | .usage('[options] ') 9 | .option('-d, --dryrun', 'Only log the transformation') 10 | .option('-t, --transformer ', 'Transformation to apply. [identity | amdToCjs]'); 11 | 12 | program.parse(process.argv); 13 | 14 | if (!program.args.length) program.help(); 15 | 16 | var files = program.args; 17 | if (!files) { 18 | console.error('file glob required'); 19 | process.exit(1); 20 | } 21 | 22 | // DEBUG 23 | 24 | transformer(files, { 25 | dryrun: program.dryrun, 26 | transformer: program.transformer 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/generators/cjsrequire.js: -------------------------------------------------------------------------------- 1 | var recast = require('recast'); 2 | var b = recast.types.builders; 3 | var n = recast.types.namedTypes; 4 | 5 | /** 6 | * Return the AST for `var variable = require(module);` or `require(module);` 7 | * @param {String} module 8 | * @param {String} [identifier] 9 | * @example: 10 | * recast.print(generateCJSExpr('React', 'react/addons').code; 11 | * -> var React = require('react/addons'); 12 | */ 13 | module.exports = function generateCJSExpr(module, identifier) { 14 | if (typeof identifier === 'string' && !n.Identifier.check(identifier)) { 15 | identifier = b.identifier(identifier); 16 | } 17 | 18 | if (!n.Literal.check(module)) { 19 | module = b.literal(module); 20 | } 21 | 22 | var requireExpression = b.callExpression( 23 | b.identifier('require'), [ 24 | module 25 | ] 26 | ); 27 | 28 | if (identifier) { 29 | return b.variableDeclaration("var", [ 30 | b.variableDeclarator( 31 | identifier, 32 | requireExpression 33 | ) 34 | ]); 35 | } 36 | 37 | // console.warn('Unnamed dependency: %s', module.value); 38 | return b.expressionStatement(requireExpression); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /lib/generators/exports.js: -------------------------------------------------------------------------------- 1 | var recast = require('recast'); 2 | var b = recast.types.builders; 3 | var n = recast.types.namedTypes; 4 | 5 | function assignment(value) { 6 | return b.assignmentExpression( 7 | '=', 8 | b.memberExpression( 9 | b.identifier('module'), 10 | b.identifier('exports'), 11 | false 12 | ), 13 | value 14 | ); 15 | } 16 | 17 | /** 18 | * Return a new module.exports expression with an object 19 | * 20 | * @param {Object} value a recast builder object or partial object 21 | * @example: 22 | * recast.print(generateExportsExpr({type: 'Identifier', name: 'React'})).code; 23 | * -> module.exports = React; 24 | */ 25 | module.exports = function generateExportsExpr(value) { 26 | // if it’s already an expression, just return the contents 27 | if (n.ObjectExpression.check(value)) { 28 | return assignment(value); 29 | }; 30 | 31 | return b.expressionStatement(assignment(value)); 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /lib/generators/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | exports: require('./exports'), 3 | require: require('./cjsrequire'), 4 | }; 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var fs = require('fs'); 3 | var read = fs.readFileSync; 4 | var transformers = require('./transformers'); 5 | 6 | var DEFAULT_WRITE_CALLBACK = function(err, file, output) { 7 | if (err) { 8 | return console.error(err); 9 | } 10 | fs.writeFileSync(file, output); 11 | }; 12 | 13 | var DEFAULT_LOG_CALLBACK = function(err, file, output) { 14 | console.log('%s\n%s\n', file, output); 15 | }; 16 | 17 | /** 18 | * @param {String|Array} g glob string or array of files to transform 19 | * @param {Object} [options] {dryrun: Boolean, transformer: String} 20 | * @param {Function} [callback] Callback to receive (err, filename, transformed) 21 | */ 22 | module.exports = function(g, options, callback) { 23 | if (typeof callback !== 'function') { 24 | callback = options.dryrun ? DEFAULT_LOG_CALLBACK : DEFAULT_WRITE_CALLBACK; 25 | } 26 | 27 | if (Array.isArray(g)) work(g); 28 | else { 29 | glob(g, function(error, files) { 30 | work(files); 31 | }); 32 | } 33 | 34 | function work(files) { 35 | return files.forEach(function(file) { 36 | var content = read(file).toString(); 37 | var output; 38 | 39 | try { 40 | console.log('Transforming %s', file); 41 | return respond(file, transform(content, options.transformer), callback); 42 | } 43 | catch(e) { 44 | console.error('Error transforming %s', file); 45 | return respond(file, e, callback); 46 | } 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * Helper function to transform code 53 | * 54 | * @param {String} code javascript string 55 | * @param {String} [method] transformer method from lib/transformers/index.js 56 | */ 57 | function transform(code, method) { 58 | return transformers[method || 'identity'](code); 59 | } 60 | 61 | /** 62 | * Helper function to handle dryrun response for CLI or node-based usage. 63 | * 64 | * @param {String} file 65 | * @param {String|Error} output 66 | * @param {Function} [callback] 67 | */ 68 | function respond(file, output, callback) { 69 | setImmediate(function() { 70 | if (typeof output === 'string') 71 | callback(null, file, output); 72 | else { 73 | // error case. file, error, callback 74 | callback(output, file); 75 | } 76 | }); 77 | } 78 | 79 | -------------------------------------------------------------------------------- /lib/transformers/amdToCjs.js: -------------------------------------------------------------------------------- 1 | var recast = require('recast'); 2 | var n = recast.types.namedTypes; 3 | 4 | var assert = require('assert'); 5 | var generators = require('../generators'); 6 | var generateRequire = generators.require; 7 | var generateExports = generators.exports; 8 | var b = recast.types.builders; 9 | var findIdentifier = require('../visitors/findIdentifier'); 10 | 11 | module.exports = function(code) { 12 | var ast = recast.parse(code); 13 | recast.visit(ast, module.exports.__visitors); 14 | return recast.print(ast).code; 15 | }; 16 | 17 | // keep this private and not mutatable 18 | Object.defineProperty(module.exports, '__visitors', { 19 | enumerable: false, 20 | configurable: false, 21 | value: Object.freeze({ 22 | visitCallExpression: function(path) { 23 | if (this.isAMDDefinition(path)) { 24 | return this.visitAMDModule(path); 25 | } 26 | 27 | this.traverse(path); 28 | }, 29 | 30 | visitReturnStatement: function(path) { 31 | if (this.shouldBeModuleExports(path)) { 32 | return generateExports(path.value.argument); 33 | } 34 | 35 | this.traverse(path); 36 | }, 37 | 38 | visitAMDModule: function(path) { 39 | var node = path.node; 40 | var dependencies = this.transformedDependencies(path); 41 | var moduleBody = this.transformedModuleBody(path); 42 | if (moduleBody) { 43 | var p = path.parent; 44 | 45 | if (!dependencies || dependencies.length === 0) { 46 | // define({obj: prop}); signature 47 | if (n.AssignmentExpression.check(moduleBody)) 48 | moduleBody = b.expressionStatement(moduleBody); 49 | 50 | p.replace(moduleBody); 51 | } 52 | else { 53 | p.replace.apply(p, dependencies.concat(moduleBody)); 54 | } 55 | } 56 | 57 | // replace the AMD CallExpression itself 58 | this.traverse(path); 59 | }, 60 | 61 | isAMDDefinition: function(path) { 62 | var node = path.node; 63 | return isCallExpressionNamed('require') || isCallExpressionNamed('define'); 64 | 65 | function isCallExpressionNamed(name) { 66 | return (n.CallExpression.check(node) && 67 | name === node.callee.name); 68 | } 69 | }, 70 | 71 | /** 72 | * @return [Array{CJSExpressions}|undefined] 73 | */ 74 | transformedDependencies: function(path) { 75 | var node = path.node; 76 | var dependencies = this.extractAMDDependencies(path) || []; 77 | 78 | return dependencies.map(function(a) { 79 | return generateRequire(a[0], a[1]); 80 | }); 81 | }, 82 | 83 | transformedModuleBody: function(path) { 84 | var node = path.node; 85 | var body = this.extractModuleBody(path); 86 | 87 | if (body) { 88 | if (n.ObjectExpression.check(body)) { 89 | return generateExports(body); 90 | } 91 | else if (n.FunctionExpression.check(body)) { 92 | this.traverse(path); 93 | return body.body; 94 | } 95 | } 96 | 97 | return path; 98 | }, 99 | 100 | hasAncestor: function(path, descriptor, maxDepth) { 101 | if (typeof descriptor.validator !== 'function') { 102 | assert.ok(descriptor.type); 103 | assert.ok(!!n[descriptor.type]); 104 | } 105 | 106 | function lookup(path, currentDepth) { 107 | if (currentDepth > maxDepth || typeof path === 'undefined') { 108 | return false; 109 | } 110 | 111 | if (descriptor.validator(path.parent)) { 112 | return true; 113 | } 114 | 115 | return lookup(path.parent, currentDepth + 1); 116 | } 117 | 118 | return lookup(path.parent, 0); 119 | }, 120 | 121 | shouldBeModuleExports: function(path) { 122 | var self = this; 123 | return this.hasAncestor(path, { 124 | validator: this.isAMDDefinition 125 | }, 3); 126 | }, 127 | 128 | /** 129 | * @param {NodePath} path AMD Call Expression 130 | * @return {Array} {value: Identifier|String, module: String} 131 | */ 132 | extractAMDDependencies: function(path) { 133 | assert.ok(this.isAMDDefinition(path)); 134 | 135 | var node = path.node; 136 | // TODO: http://requirejs.org/docs/whyamd.html#namedmodules 137 | if (node.arguments.length < 2) return null; 138 | 139 | var dependencies = node.arguments[0]; 140 | var factory = last(node.arguments); 141 | 142 | var args = node.arguments; 143 | 144 | if (n.ArrayExpression.check(dependencies)) { 145 | dependencies = dependencies.elements; 146 | } 147 | else { 148 | // resolve variable key 149 | // var REQUIREMENTS = ["dep1", "dep2"]; 150 | // require(REQUIREMENTS, (dep1, dep2) => {}); 151 | dependencies = findIdentifier(dependencies.name, this.getRoot(path)).elements; 152 | } 153 | 154 | return zip(dependencies, factory.params); 155 | }, 156 | 157 | /** 158 | * @param {NodePath} path AMD Call Expression 159 | * @return {Array} {value: Identifier|String, module: String} 160 | */ 161 | extractModuleBody: function(path) { 162 | assert.ok(this.isAMDDefinition(path)); 163 | var node = path.node; 164 | return last(node.arguments); 165 | }, 166 | 167 | getRoot: function(path) { 168 | var c = path; 169 | while (typeof c.parent !== 'undefined' && c.parent !== null) { 170 | c = c.parent; 171 | } 172 | return c; 173 | }, 174 | }), 175 | }); 176 | 177 | function last(a) { 178 | return a[a.length - 1]; 179 | } 180 | 181 | function zip(a, b) { 182 | return a.map(function(a, i) { 183 | return [a, b[i]]; 184 | }); 185 | } 186 | -------------------------------------------------------------------------------- /lib/transformers/identity.js: -------------------------------------------------------------------------------- 1 | var recast = require('recast'); 2 | 3 | module.exports = function identity(code) { 4 | var ast = recast.parse(code); 5 | return recast.print(ast).code; 6 | }; 7 | -------------------------------------------------------------------------------- /lib/transformers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | identity: require('./identity'), 3 | amdToCjs: require('./amdToCjs'), 4 | }; 5 | -------------------------------------------------------------------------------- /lib/visitors/findIdentifier.js: -------------------------------------------------------------------------------- 1 | var types = require('recast').types; 2 | var n = types.namedTypes; 3 | 4 | function findIdentifier(variable, ast) { 5 | var result = false; 6 | 7 | types.visit(ast, { 8 | visitVariableDeclarator: function(path) { 9 | if (path.value.id.name === variable) { 10 | result = path.value.init; 11 | return false; 12 | } 13 | this.traverse(path); 14 | }, 15 | }); 16 | 17 | return result; 18 | } 19 | 20 | module.exports = findIdentifier; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tocjs", 3 | "version": "1.0.0", 4 | "description": "Script to transfrom a file from AMD to CJS.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/*.js" 8 | }, 9 | "bin": { 10 | "tocjs": "bin/tocjs" 11 | }, 12 | "keywords": [ 13 | "amd", 14 | "cjs", 15 | "converter", 16 | "recast" 17 | ], 18 | "author": "Dustan Kasten ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "commander": "^2.5.1", 22 | "glob": "^4.3.1", 23 | "recast": "^0.9.11" 24 | }, 25 | "devDependencies": { 26 | "mocha": "^2.0.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/cases/amdToCjs.js: -------------------------------------------------------------------------------- 1 | /** Top level comments shouldn’t be duplicated. */ 2 | define(function() { 3 | return 'hello world'; 4 | }); 5 | 6 | define({ 7 | hello: 'world' 8 | }); 9 | 10 | require(['alphabet', 'novar'], function(soup) { 11 | window.init(); 12 | return soup.eatWith('spoon'); 13 | }); 14 | -------------------------------------------------------------------------------- /test/cases/amdToCjs.out.js: -------------------------------------------------------------------------------- 1 | /** Top level comments shouldn’t be duplicated. */ 2 | { 3 | module.exports = 'hello world'; 4 | } 5 | 6 | module.exports = { 7 | hello: 'world' 8 | }; 9 | 10 | var soup = require('alphabet'); 11 | require('novar'); 12 | { 13 | window.init(); 14 | module.exports = soup.eatWith('spoon'); 15 | } 16 | -------------------------------------------------------------------------------- /test/cases/identity.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | return 'Hello world'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/cases/identity.out.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | return 'Hello world'; 3 | }); 4 | 5 | -------------------------------------------------------------------------------- /test/generators.js: -------------------------------------------------------------------------------- 1 | /*global it*/ 2 | var assert = require('assert'); 3 | var tocjs = require('..'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var read = fs.readFileSync; 7 | var readdir = fs.readdirSync; 8 | var recast = require('recast'); 9 | var generators = require('../lib/generators'); 10 | 11 | describe('generating ASTs', function(){ 12 | describe('require', function() { 13 | it('should generate with a variable, module syntax', function() { 14 | var output = generators.require('react/addons', 'React'); 15 | var expected = 'var React = require("react/addons");'; 16 | assert.equal(recast.print(output).code, expected); 17 | }); 18 | 19 | it('should generate with a singular argument', function() { 20 | var output = generators.require('React'); 21 | var expected = 'require("React");'; 22 | assert.equal(recast.print(output).code, expected); 23 | }); 24 | }); 25 | 26 | describe('exports', function() { 27 | it('should module.exports', function() { 28 | var output = generators.exports({type: 'Identifier', name: 'myThing'}); 29 | var expected = 'module.exports = myThing;'; 30 | assert.equal(recast.print(output).code, expected); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*global it*/ 2 | var assert = require('assert'); 3 | var tocjs = require('..'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var read = fs.readFileSync; 7 | var readdir = fs.readdirSync; 8 | 9 | describe('should transform', function(){ 10 | readdir('test/cases').forEach(function(file){ 11 | if (file.indexOf('.out') > -1) return; 12 | 13 | var base = path.basename(file, '.js'); 14 | 15 | var f = 'test/cases/' + file; 16 | var input = read(f, 'utf8'); 17 | var output = read('test/cases/' + base + '.out.js', 'utf8'); 18 | it(base, function(done) { 19 | tocjs(f, {dryrun: true, transformer: base}, function(err, file, transformed) { 20 | assert.equal(transformed.trim(), output.trim()); 21 | done(); 22 | }); 23 | }) 24 | }); 25 | }); 26 | --------------------------------------------------------------------------------