diff --git a/index.js b/index.js index c7cc180..edf3f2a 100644 --- a/index.js +++ b/index.js @@ -1,264 +1,22 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var assert = require('assert'); -var Transform = require('stream').Transform; -var Parser = require('jade/lib/parser.js'); -var jade = require('jade/lib/runtime.js'); -var React = require('react'); -var staticModule = require('static-module'); -var resolve = require('resolve'); -var uglify = require('uglify-js'); -var acornTransform = require('./lib/acorn-transform.js'); -var Compiler = require('./lib/compiler.js'); -var JavaScriptCompressor = require('./lib/java-script-compressor.js'); - -var reactRuntimePath = require.resolve('react'); - -function isTemplateLiteral(str) { - return str && typeof str === 'object' && - str.raw && typeof str.raw === 'object' && - str.raw.length === 1 && typeof str.raw[0] === 'string'; -} +var isTemplateLiteral = require('./lib/utils/is-template-literal.js'); +var browserify = require('./lib/browserify'); +var compile = require('./lib/compile'); +var compileFile = require('./lib/compile-file'); +var compileClient = require('./lib/compile-client'); +var compileFileClient = require('./lib/compile-file-client'); exports = (module.exports = browserifySupport); function browserifySupport(options, extra) { if (isTemplateLiteral(options)) { return compile(options.raw[0]); - } - function transform(filename) { - function clientRequire(path) { - return require(clientRequire.resolve(path)); - } - clientRequire.resolve = function (path) { - return resolve.sync(path, { - basedir: path.dirname(filename) - }); - }; - var src = ''; - var stream = new Transform(); - stream._transform = function (chunk, encoding, callback) { - src += chunk; - callback(); - }; - stream._flush = function (callback) { - src = acornTransform(src, { - TaggedTemplateExpression: function (node) { - var quasi = '(function () {' + - 'var quasi = ' + acornTransform.stringify(node.quasi.quasis.map(function (q) { - return q.value.cooked; - })) + ';' + - 'quasi.raw = ' + acornTransform.stringify(node.quasi.quasis.map(function (q) { - return q.value.raw; - })) + ';' + - 'return quasi;}())'; - - var expressions = node.quasi.expressions.map(acornTransform.getSource); - acornTransform.setSource(node, acornTransform.getSource(node.tag) + '(' + - [quasi].concat(expressions).join(', ') + ')'); - } - }); - makeStatic.on('data', this.push.bind(this)); - makeStatic.on('error', callback); - makeStatic.on('end', callback.bind(null, null)); - makeStatic.end(src); - }; - - function staticCompileImplementation(jadeSrc, localOptions) { - localOptions = localOptions || {}; - for (var key in options) { - if ((key in options) && !(key in localOptions)) - localOptions[key] = options[key]; - } - localOptions.outputFile = filename; - return compileClient(jadeSrc, localOptions); - } - function staticCompileFileImplementation(jadeFile, localOptions) { - localOptions = localOptions || {}; - for (var key in options) { - if ((key in options) && !(key in localOptions)) - localOptions[key] = options[key]; - } - localOptions.outputFile = filename; - return compileFileClient(jadeFile, localOptions); - } - function staticImplementation(templateLiteral) { - if (isTemplateLiteral(templateLiteral)) { - return staticCompileImplementation(templateLiteral.raw[0]); - } else { - return 'throw new Error("Invalid client side argument to react-jade");'; - } - } - staticImplementation.compile = staticCompileImplementation; - staticImplementation.compileFile = staticCompileFileImplementation; - var makeStatic = staticModule({ 'react-jade': staticImplementation }, { - vars: { - __dirname: path.dirname(filename), - __filename: path.resolve(filename), - path: path, - require: clientRequire - } - }); - - return stream; - } - if (typeof options === 'string') { - var file = options; - options = extra || {}; - return transform(file); } else { - options = options || {}; - return transform; + return browserify.apply(this, arguments); } } -function parse(str, options) { - var options = options || {}; - var parser = new Parser(str, options.filename, options); - var tokens; - try { - // Parse - tokens = parser.parse(); - } catch (err) { - parser = parser.context(); - jade.rethrow(err, parser.filename, parser.lexer.lineno, parser.input); - } - var compiler = new Compiler(tokens); - - var js = 'var fn = function (locals) {' + - 'function jade_join_classes(val) {' + - 'return (Array.isArray(val) ? val.map(jade_join_classes) : ' + - '(val && typeof val === "object") ? Object.keys(val).filter(function (key) { return val[key]; }) :' + - '[val]).filter(function (val) { return val != null && val !== ""; }).join(" ");' + - '};' + - 'function jade_fix_style(style) {' + - 'return typeof style === "string" ? style.split(";").filter(function (str) {' + - 'return str.split(":").length > 1;' + - '}).reduce(function (obj, style) {' + - 'obj[style.split(":")[0]] = style.split(":").slice(1).join(":"); return obj;' + - '}, {}) : style;' + - '}' + - 'var jade_mixins = {};' + - 'var jade_interp;' + - 'jade_variables(locals);' + - compiler.compile() + - '}'; - - // Check that the compiled JavaScript code is valid thus far. - // uglify-js throws very cryptic errors when it fails to parse code. - try { - Function('', js); - } catch (ex) { - console.log(js); - throw ex; - } - - var ast = uglify.parse(js, {filename: options.filename}); - - ast.figure_out_scope(); - ast = ast.transform(uglify.Compressor({ - sequences: false, // join consecutive statemets with the “comma operator" - properties: true, // optimize property access: a["foo"] → a.foo - dead_code: true, // discard unreachable code - unsafe: true, // some unsafe optimizations (see below) - conditionals: true, // optimize if-s and conditional expressions - comparisons: true, // optimize comparisonsx - evaluate: true, // evaluate constant expressions - booleans: true, // optimize boolean expressions - loops: true, // optimize loops - unused: true, // drop unused variables/functions - hoist_funs: true, // hoist function declarations - hoist_vars: false, // hoist variable declarations - if_return: true, // optimize if-s followed by return/continue - join_vars: false, // join var declarations - cascade: true, // try to cascade `right` into `left` in sequences - side_effects: true, // drop side-effect-free statements - warnings: false, // warn about potentially dangerous optimizations/code - global_defs: {} // global definitions)); - })); - - ast = ast.transform(new JavaScriptCompressor()); - - ast.figure_out_scope(); - var globals = ast.globals.map(function (node, name) { - return name; - }).filter(function (name) { - return name !== 'jade_variables' && name !== 'exports' && name !== 'Array' && name !== 'Object' - && name !== 'React'; - }); - - js = ast.print_to_string({ - beautify: true, - comments: true, - indent_level: 2 - }); - assert(/jade_variables\(locals\)/.test(js)); - - js = js.replace(/\n? *jade_variables\(locals\);?/, globals.map(function (g) { - return ' var ' + g + ' = ' + JSON.stringify(g) + ' in locals ? locals.' + g + ' : jade_globals_' + g + ';'; - }).join('\n')); - return globals.map(function (g) { - return 'var jade_globals_' + g + ' = typeof ' + g + ' === "undefined" ? undefined : ' + g + ';\n'; - }).join('') + js + ';\nfn.locals = ' + setLocals.toString() + ';\nreturn fn;'; -} - -function parseFile(filename, options) { - var str = fs.readFileSync(filename, 'utf8').toString(); - var options = options || {}; - options.filename = path.resolve(filename); - return parse(str, options); -} - exports.compile = compile; -function compile(str, options){ - options = options || { filename: '' }; - return Function('React', parse(str, options))(React); -} - exports.compileFile = compileFile; -function compileFile(filename, options) { - return Function('React', parseFile(filename, options))(React); -} - exports.compileClient = compileClient; -function compileClient(str, options){ - options = options || { filename: '' }; - var react = options.outputFile ? path.relative(path.dirname(options.outputFile), reactRuntimePath) : reactRuntimePath; - - if (options.globalReact) { - return '(function (React) {\n ' + - parse(str, options).split('\n').join('\n ') + - '\n}(React))'; - } else { - return '(function (React) {\n ' + - parse(str, options).split('\n').join('\n ') + - '\n}(typeof React !== "undefined" ? React : require("' + react.replace(/^([^\.])/, './$1').replace(/\\/g, '/') + '")))'; - } -} - exports.compileFileClient = compileFileClient; -function compileFileClient(filename, options) { - var str = fs.readFileSync(filename, 'utf8').toString(); - var options = options || {}; - options.filename = path.resolve(filename); - return compileClient(str, options); -} - -function setLocals(locals) { - var render = this; - function newRender(additionalLocals) { - var newLocals = {}; - for (var key in locals) { - newLocals[key] = locals[key]; - } - if (additionalLocals) { - for (var key in additionalLocals) { - newLocals[key] = additionalLocals[key]; - } - } - return render.call(this, newLocals); - } - newRender.locals = setLocals; - return newRender; -} diff --git a/lib/browserify.js b/lib/browserify.js new file mode 100644 index 0000000..f730452 --- /dev/null +++ b/lib/browserify.js @@ -0,0 +1,124 @@ +'use strict'; + +var Transform = require('stream').Transform; +var staticModule = require('static-module'); +var resolve = require('resolve'); +var path = require('path'); +var isTemplateLiteral = require('./utils/is-template-literal.js'); +var acornTransform = require('./utils/acorn-transform.js'); +var compileClient = require('./compile-client.js'); +var compileFileClient = require('./compile-file-client.js'); + +module.exports = browserify; +function browserify(options, extra) { + if (typeof options === 'string') { + var filename = options; + options = extra || {}; + return makeStream(function (source) { + return transform(filename, source, options); + }); + } else { + options = options || {}; + return function (filename, extra) { + extra = extra || {}; + Object.keys(options).forEach(function (key) { + if (typeof extra[key] === 'undefined') { + extra[key] = options[key]; + } + }); + return makeStream(function (source) { + return transform(filename, source, options); + }); + }; + } +} + +function makeStream(fn) { + var src = ''; + var stream = new Transform(); + stream._transform = function (chunk, encoding, callback) { + src += chunk; + callback(); + }; + stream._flush = function (callback) { + var res = fn(src); + res.on('data', this.push.bind(this)); + res.on('error', callback); + res.on('end', callback.bind(null, null)); + }; + return stream; +} + +function makeClientRequire(filename) { + function cr(path) { + return require(cr.resolve(path)); + } + cr.resolve = function (path) { + return resolve.sync(path, { + basedir: path.dirname(filename) + }); + }; + return cr; +} + +function makeStaticImplementation(filename, options) { + function staticImplementation(templateLiteral) { + if (isTemplateLiteral(templateLiteral)) { + return staticCompileImplementation(templateLiteral.raw[0]); + } else { + return 'throw new Error("Invalid client side argument to react-jade");'; + } + } + function staticCompileImplementation(jadeSrc, localOptions) { + localOptions = localOptions || {}; + for (var key in options) { + if ((key in options) && !(key in localOptions)) + localOptions[key] = options[key]; + } + localOptions.filename = localOptions.filename || filename; + localOptions.outputFile = filename; + return compileClient(jadeSrc, localOptions); + } + function staticCompileFileImplementation(jadeFile, localOptions) { + localOptions = localOptions || {}; + for (var key in options) { + if ((key in options) && !(key in localOptions)) + localOptions[key] = options[key]; + } + localOptions.outputFile = filename; + return compileFileClient(jadeFile, localOptions); + } + staticImplementation.compile = staticCompileImplementation; + staticImplementation.compileFile = staticCompileFileImplementation; + return staticImplementation; +} + +// compile filename and return a readable stream +function transform(filename, source, options) { + source = acornTransform(source, { + TaggedTemplateExpression: function (node) { + var quasi = '(function () {' + + 'var quasi = ' + acornTransform.stringify(node.quasi.quasis.map(function (q) { + return q.value.cooked; + })) + ';' + + 'quasi.raw = ' + acornTransform.stringify(node.quasi.quasis.map(function (q) { + return q.value.raw; + })) + ';' + + 'return quasi;}())'; + + var expressions = node.quasi.expressions.map(acornTransform.getSource); + acornTransform.setSource(node, acornTransform.getSource(node.tag) + '(' + + [quasi].concat(expressions).join(', ') + ')'); + } + }); + var makeStatic = staticModule({ 'react-jade': makeStaticImplementation(filename, options) }, { + vars: { + __dirname: path.dirname(filename), + __filename: path.resolve(filename), + path: path, + require: makeClientRequire(filename) + } + }); + makeStatic.end(source); + return makeStatic; +} diff --git a/lib/compile-client.js b/lib/compile-client.js new file mode 100644 index 0000000..b282eac --- /dev/null +++ b/lib/compile-client.js @@ -0,0 +1,28 @@ +'use strict'; + +var path = require('path'); +var parse = require('./parse'); + +var reactRuntimePath; + +try { + reactRuntimePath = require.resolve('react'); +} catch (ex) { + reactRuntimePath = false; +} + +module.exports = compileClient; +function compileClient(str, options){ + options = options || { filename: '' }; + var react = options.outputFile ? path.relative(path.dirname(options.outputFile), reactRuntimePath) : reactRuntimePath; + + if (options.globalReact || !reactRuntimePath) { + return '(function (React) {\n ' + + parse(str, options).split('\n').join('\n ') + + '\n}(React))'; + } else { + return '(function (React) {\n ' + + parse(str, options).split('\n').join('\n ') + + '\n}(typeof React !== "undefined" ? React : require("' + react.replace(/^([^\.])/, './$1').replace(/\\/g, '/') + '")))'; + } +} diff --git a/lib/compile-file-client.js b/lib/compile-file-client.js new file mode 100644 index 0000000..4b82a50 --- /dev/null +++ b/lib/compile-file-client.js @@ -0,0 +1,13 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var compileClient = require('./compile-client'); + +module.exports = compileFileClient; +function compileFileClient(filename, options) { + var str = fs.readFileSync(filename, 'utf8').toString(); + var options = options || {}; + options.filename = path.resolve(filename); + return compileClient(str, options); +} diff --git a/lib/compile-file.js b/lib/compile-file.js new file mode 100644 index 0000000..09e5ec5 --- /dev/null +++ b/lib/compile-file.js @@ -0,0 +1,9 @@ +'use strict'; + +var React = require('react'); +var parseFile = require('./parse-file'); + +module.exports = compileFile; +function compileFile(filename, options) { + return Function('React', parseFile(filename, options))(React); +} diff --git a/lib/compile.js b/lib/compile.js new file mode 100644 index 0000000..fa22390 --- /dev/null +++ b/lib/compile.js @@ -0,0 +1,10 @@ +'use strict'; + +var React = require('react'); +var parse = require('./parse'); + +module.exports = compile; +function compile(str, options){ + options = options || { filename: '' }; + return Function('React', parse(str, options))(React); +} diff --git a/lib/parse-file.js b/lib/parse-file.js new file mode 100644 index 0000000..7753eb1 --- /dev/null +++ b/lib/parse-file.js @@ -0,0 +1,13 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var parse = require('./parse'); + +module.exports = parseFile; +function parseFile(filename, options) { + var str = fs.readFileSync(filename, 'utf8').toString(); + var options = options || {}; + options.filename = path.resolve(filename); + return parse(str, options); +} diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 0000000..5c8d0e5 --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,94 @@ +'use strict'; + +var fs = require('fs'); +var assert = require('assert'); +var uglify = require('uglify-js'); +var Parser = require('jade/lib/parser.js'); +var jade = require('jade/lib/runtime.js'); +var Compiler = require('./utils/compiler.js'); +var JavaScriptCompressor = require('./utils/java-script-compressor.js'); + +var jade_join_classes = fs.readFileSync(__dirname + '/utils/jade-join-classes.js', 'utf8'); +var jade_fix_style = fs.readFileSync(__dirname + '/utils/jade-fix-style.js', 'utf8'); +var setLocals = fs.readFileSync(__dirname + '/utils/set-locals.js', 'utf8'); + +module.exports = parse; +function parse(str, options) { + var options = options || {}; + var parser = new Parser(str, options.filename, options); + var tokens; + try { + // Parse + tokens = parser.parse(); + } catch (err) { + parser = parser.context(); + jade.rethrow(err, parser.filename, parser.lexer.lineno, parser.input); + } + var compiler = new Compiler(tokens); + + var js = 'var fn = function (locals) {' + + jade_join_classes + ';' + + jade_fix_style + ';' + + 'var jade_mixins = {};' + + 'var jade_interp;' + + 'jade_variables(locals);' + + compiler.compile() + + '}'; + + // Check that the compiled JavaScript code is valid thus far. + // uglify-js throws very cryptic errors when it fails to parse code. + try { + Function('', js); + } catch (ex) { + console.log(js); + throw ex; + } + + var ast = uglify.parse(js, {filename: options.filename}); + + ast.figure_out_scope(); + ast = ast.transform(uglify.Compressor({ + sequences: false, // join consecutive statemets with the “comma operator" + properties: true, // optimize property access: a["foo"] → a.foo + dead_code: true, // discard unreachable code + unsafe: true, // some unsafe optimizations (see below) + conditionals: true, // optimize if-s and conditional expressions + comparisons: true, // optimize comparisonsx + evaluate: true, // evaluate constant expressions + booleans: true, // optimize boolean expressions + loops: true, // optimize loops + unused: true, // drop unused variables/functions + hoist_funs: true, // hoist function declarations + hoist_vars: false, // hoist variable declarations + if_return: true, // optimize if-s followed by return/continue + join_vars: false, // join var declarations + cascade: true, // try to cascade `right` into `left` in sequences + side_effects: true, // drop side-effect-free statements + warnings: false, // warn about potentially dangerous optimizations/code + global_defs: {} // global definitions)); + })); + + ast = ast.transform(new JavaScriptCompressor()); + + ast.figure_out_scope(); + var globals = ast.globals.map(function (node, name) { + return name; + }).filter(function (name) { + return name !== 'jade_variables' && name !== 'exports' && name !== 'Array' && name !== 'Object' + && name !== 'React'; + }); + + js = ast.print_to_string({ + beautify: true, + comments: true, + indent_level: 2 + }); + assert(/jade_variables\(locals\)/.test(js)); + + js = js.replace(/\n? *jade_variables\(locals\);?/, globals.map(function (g) { + return ' var ' + g + ' = ' + JSON.stringify(g) + ' in locals ? locals.' + g + ' : jade_globals_' + g + ';'; + }).join('\n')); + return globals.map(function (g) { + return 'var jade_globals_' + g + ' = typeof ' + g + ' === "undefined" ? undefined : ' + g + ';\n'; + }).join('') + js + ';\nfn.locals = ' + setLocals + ';\nreturn fn;'; +} diff --git a/lib/acorn-transform.js b/lib/utils/acorn-transform.js similarity index 100% rename from lib/acorn-transform.js rename to lib/utils/acorn-transform.js diff --git a/lib/compiler.js b/lib/utils/compiler.js similarity index 95% rename from lib/compiler.js rename to lib/utils/compiler.js index 419510b..73ac94c 100644 --- a/lib/compiler.js +++ b/lib/utils/compiler.js @@ -1,11 +1,14 @@ 'use strict'; -var runtime = require('jade/lib/runtime.js'); +var fs = require('fs'); var constantinople = require('constantinople'); var ent = require('ent'); var uglify = require('uglify-js'); var React = require('react'); +var joinClasses = Function('', 'return ' + fs.readFileSync(__dirname + '/jade-join-classes.js', 'utf8'))(); +var fixStyle = Function('', 'return ' + fs.readFileSync(__dirname + '/jade-fix-style.js', 'utf8'))(); + function isConstant(str) { return constantinople(str); } @@ -292,16 +295,7 @@ function getAttributes(attrs){ } else if (key === 'style') { if (isConstant(attr.val)) { var val = toConstant(attr.val); - if (typeof val === 'string') { - var obj = {}; - val.split(';').filter(function (str) { - return str.split(':').length > 1; - }).forEach(function (style) { - obj[style.split(':')[0]] = style.split(':').slice(1).join(':'); - }); - val = obj; - } - buf.push(JSON.stringify(key) + ': ' + JSON.stringify(val)); + buf.push(JSON.stringify(key) + ': ' + JSON.stringify(fixStyle(val))); } else { buf.push(JSON.stringify(key) + ': jade_fix_style(' + attr.val + ')'); } @@ -314,7 +308,7 @@ function getAttributes(attrs){ }); if (classes.length) { if (classes.every(isConstant)) { - classes = JSON.stringify(runtime.joinClasses(classes.map(toConstant))); + classes = JSON.stringify(joinClasses(classes.map(toConstant))); } else { classes = 'jade_join_classes([' + classes.join(',') + '])'; } diff --git a/lib/utils/is-template-literal.js b/lib/utils/is-template-literal.js new file mode 100644 index 0000000..5375d7e --- /dev/null +++ b/lib/utils/is-template-literal.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = isTemplateLiteral; +function isTemplateLiteral(str) { + return str && typeof str === 'object' && + str.raw && typeof str.raw === 'object' && + str.raw.length === 1 && typeof str.raw[0] === 'string'; +} diff --git a/lib/utils/jade-fix-style.js b/lib/utils/jade-fix-style.js new file mode 100644 index 0000000..5f95e22 --- /dev/null +++ b/lib/utils/jade-fix-style.js @@ -0,0 +1,7 @@ +function jade_fix_style(style) { + return typeof style === "string" ? style.split(";").filter(function (str) { + return str.split(":").length > 1; + }).reduce(function (obj, style) { + obj[style.split(":")[0]] = style.split(":").slice(1).join(":"); return obj; + }, {}) : style; +} diff --git a/lib/utils/jade-join-classes.js b/lib/utils/jade-join-classes.js new file mode 100644 index 0000000..de665be --- /dev/null +++ b/lib/utils/jade-join-classes.js @@ -0,0 +1,6 @@ +function jade_join_classes(val) { + return (Array.isArray(val) ? val.map(jade_join_classes) : + (val && typeof val === "object") ? Object.keys(val).filter(function (key) { return val[key]; }) : + [val] + ).filter(function (val) { return val != null && val !== ""; }).join(" "); +} diff --git a/lib/java-script-compressor.js b/lib/utils/java-script-compressor.js similarity index 100% rename from lib/java-script-compressor.js rename to lib/utils/java-script-compressor.js diff --git a/lib/utils/set-locals.js b/lib/utils/set-locals.js new file mode 100644 index 0000000..face54b --- /dev/null +++ b/lib/utils/set-locals.js @@ -0,0 +1,17 @@ +function setLocals(locals) { + var render = this; + function newRender(additionalLocals) { + var newLocals = {}; + for (var key in locals) { + newLocals[key] = locals[key]; + } + if (additionalLocals) { + for (var key in additionalLocals) { + newLocals[key] = additionalLocals[key]; + } + } + return render.call(this, newLocals); + } + newRender.locals = setLocals; + return newRender; +}