├── targets ├── sass │ ├── .gitignore │ ├── README.md │ └── index.js ├── scss │ ├── .gitignore │ └── index.js ├── markdown │ └── index.js ├── to5 │ └── index.js ├── babel │ └── index.js ├── coffeescript │ └── index.js ├── clojurescript │ └── index.js ├── myth │ └── index.js ├── jade │ └── index.js ├── jsx │ └── index.js ├── livescript │ └── index.js ├── less │ └── index.js └── stylus │ └── index.js ├── test ├── targets │ ├── myth │ │ ├── broken.css │ │ ├── sample.css │ │ └── myth.test.js │ ├── clojurescript │ │ ├── sample.js │ │ ├── broken.js │ │ └── clojurescript.test.js │ ├── babel │ │ ├── broken.js │ │ ├── sample.js │ │ └── babel.test.js │ ├── sass │ │ ├── broken.sass │ │ ├── loop.sass │ │ ├── broken_import.sass │ │ ├── import.sass │ │ ├── sample_import.sass │ │ ├── sample_bourbon.sass │ │ ├── broken_bourbon.sass │ │ ├── sample.sass │ │ └── sass.test.js │ ├── scss │ │ ├── broken.scss │ │ ├── loop.scss │ │ ├── broken_import.scss │ │ ├── import.scss │ │ ├── sample_import.scss │ │ ├── broken_bourbon.scss │ │ ├── sample_bourbon.scss │ │ ├── sample.scss │ │ └── scss.test.js │ ├── livescript │ │ ├── broken.ls │ │ ├── sample.ls │ │ └── livescript.test.js │ ├── less │ │ ├── broken.less │ │ ├── sample.less │ │ └── less.test.js │ ├── coffeescript │ │ ├── broken.coffee │ │ ├── sample.coffee │ │ └── coffeescript.test.js │ ├── stylus │ │ ├── broken.styl │ │ ├── sample.styl │ │ └── stylus.test.js │ ├── markdown │ │ ├── broken.md │ │ ├── sample.md │ │ └── markdown.test.js │ ├── jsx │ │ ├── broken.jsx │ │ ├── sample.jsx │ │ └── jsx.test.js │ └── jade │ │ ├── broken.jade │ │ ├── sample.jade │ │ └── jade.test.js ├── mocha.opts └── lib │ ├── infinite.sass │ └── server.test.js ├── Procfile ├── vendor └── sass-frameworks │ └── .gitignore ├── nodemon.json ├── Gemfile ├── .travis.yml ├── lib ├── metrics.js ├── sass_config.rb ├── log.js ├── cors.js ├── importer.rb ├── processors.js ├── importer_http.rb └── server.js ├── public └── index.html ├── config.rb ├── bootstrap.sh ├── .gitignore ├── Gemfile.lock ├── package.json ├── bin └── pennyworth └── README.md /targets/sass/.gitignore: -------------------------------------------------------------------------------- 1 | output/* -------------------------------------------------------------------------------- /targets/scss/.gitignore: -------------------------------------------------------------------------------- 1 | output/* -------------------------------------------------------------------------------- /test/targets/myth/broken.css: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node lib/server.js 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd -------------------------------------------------------------------------------- /vendor/sass-frameworks/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /test/targets/clojurescript/sample.js: -------------------------------------------------------------------------------- 1 | (str "Hello World!") 2 | -------------------------------------------------------------------------------- /test/lib/infinite.sass: -------------------------------------------------------------------------------- 1 | $i: 6 2 | @while $i > 0 3 | $i: $i + 2 4 | -------------------------------------------------------------------------------- /test/targets/babel/broken.js: -------------------------------------------------------------------------------- 1 | const unique = array => [...Set(array) -------------------------------------------------------------------------------- /test/targets/babel/sample.js: -------------------------------------------------------------------------------- 1 | const unique = array => [...Set(array)] -------------------------------------------------------------------------------- /test/targets/sass/broken.sass: -------------------------------------------------------------------------------- 1 | $a: red; 2 | 3 | body 4 | color: $a -------------------------------------------------------------------------------- /test/targets/clojurescript/broken.js: -------------------------------------------------------------------------------- 1 | (str "Hello") 2 | (str "World!" 3 | -------------------------------------------------------------------------------- /test/targets/sass/loop.sass: -------------------------------------------------------------------------------- 1 | $i: 8 2 | @while $i > 0 3 | //$i: $i -1; -------------------------------------------------------------------------------- /test/targets/scss/broken.scss: -------------------------------------------------------------------------------- 1 | $a: red 2 | 3 | body { 4 | color: $a; 5 | } -------------------------------------------------------------------------------- /test/targets/scss/loop.scss: -------------------------------------------------------------------------------- 1 | $i: 8; 2 | @while $i > 0 { 3 | //$i: $i -1; 4 | } 5 | -------------------------------------------------------------------------------- /test/targets/sass/broken_import.sass: -------------------------------------------------------------------------------- 1 | @import 'import.0' 2 | 3 | body 4 | color: $foo -------------------------------------------------------------------------------- /test/targets/sass/import.sass: -------------------------------------------------------------------------------- 1 | $foo: red 2 | $bar: blue 3 | 4 | body 5 | color: $bar -------------------------------------------------------------------------------- /test/targets/scss/broken_import.scss: -------------------------------------------------------------------------------- 1 | @import 'import.0'; 2 | 3 | body { 4 | color: $foo; 5 | } -------------------------------------------------------------------------------- /test/targets/scss/import.scss: -------------------------------------------------------------------------------- 1 | $foo: red; 2 | $bar: blue; 3 | 4 | body { 5 | color: $bar; 6 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "VERBOSE": "true", 4 | "HOST": "localhost" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/targets/myth/sample.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --purple: #847AD1; 3 | } 4 | 5 | a { 6 | color: var(--purple); 7 | } -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem "compass", ">= 1.0.0.alpha.19" 5 | gem "bourbon" 6 | -------------------------------------------------------------------------------- /test/targets/livescript/broken.ls: -------------------------------------------------------------------------------- 1 | var test = 'hello'; 2 | 3 | var nums = { 4 | first: 1, 5 | second: 3, 6 | third: 5 7 | } 8 | -------------------------------------------------------------------------------- /test/targets/sass/sample_import.sass: -------------------------------------------------------------------------------- 1 | @import '_import/1.sass' 2 | @import '_import.1.sass' 3 | 4 | body 5 | background-color: $foo -------------------------------------------------------------------------------- /test/targets/less/broken.less: -------------------------------------------------------------------------------- 1 | body { 2 | color: hotpink 3 | p : { 4 | color: #bada55; 5 | font-weight:200; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/targets/less/sample.less: -------------------------------------------------------------------------------- 1 | body { 2 | color: hotpink; 3 | p { 4 | color: #bada55; 5 | font-weight:200; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/targets/coffeescript/broken.coffee: -------------------------------------------------------------------------------- 1 | var square = (x) -> x*x; 2 | 3 | nums = { 4 | first: 1, 5 | second: 3, 6 | third: 5 7 | }; 8 | -------------------------------------------------------------------------------- /test/targets/sass/sample_bourbon.sass: -------------------------------------------------------------------------------- 1 | @import 'bourbon/bourbon' 2 | 3 | h1 4 | font-family: $helvetica 5 | font-size: golden-ratio(14px, 1) -------------------------------------------------------------------------------- /test/targets/scss/sample_import.scss: -------------------------------------------------------------------------------- 1 | @import '_import/1.scss'; 2 | @import '_import.1.scss'; 3 | 4 | body { 5 | background-color: $foo; 6 | } -------------------------------------------------------------------------------- /test/targets/stylus/broken.styl: -------------------------------------------------------------------------------- 1 | fonts = helvetica, arial, sans-serif 2 | 3 | body 4 | padding: 50px 5 | font: 14px/1.4 fonts 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/targets/stylus/sample.styl: -------------------------------------------------------------------------------- 1 | fonts = helvetica, arial, sans-serif 2 | 3 | body 4 | padding: 50px 5 | font: 14px/1.4 fonts 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | before_install: 6 | - npm run-script gems 7 | - rvm use 1.9.3 8 | -------------------------------------------------------------------------------- /test/targets/sass/broken_bourbon.sass: -------------------------------------------------------------------------------- 1 | @import 'bourbon/bourbonx' 2 | 3 | h1 4 | font-family: $helvetica 5 | font-size: golden-ratio(14px, 1) -------------------------------------------------------------------------------- /test/targets/scss/broken_bourbon.scss: -------------------------------------------------------------------------------- 1 | @import 'bourbon/bourbonx'; 2 | 3 | h1 { 4 | font-family: $helvetica; 5 | font-size: golden-ratio(14px, 1); 6 | } -------------------------------------------------------------------------------- /test/targets/scss/sample_bourbon.scss: -------------------------------------------------------------------------------- 1 | @import 'bourbon/bourbon'; 2 | 3 | h1 { 4 | font-family: $helvetica; 5 | font-size: golden-ratio(14px, 1); 6 | } -------------------------------------------------------------------------------- /test/targets/markdown/broken.md: -------------------------------------------------------------------------------- 1 | k,degwlt4%$£Q5jkibq uiqyo 87777777@£ R;elarhuioq7 8bgfcy13 f9kefsahgiu44;'3qyuq9uifhlashbjhk222@@@λsasss````'````QW"ewklqwekb 2 | -------------------------------------------------------------------------------- /test/targets/coffeescript/sample.coffee: -------------------------------------------------------------------------------- 1 | square = (x) -> x*x 2 | 3 | nums = 4 | first: 1 5 | second: 3 6 | third: 5 7 | 8 | numsSquared = nums.map square 9 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | // Metrics client using lynx 2 | // git://github.com/dscape/lynx.git 3 | var lynx = require('lynx'); 4 | module.exports = new lynx('localhost', 8125, { scope: 'pennyworth' }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS Bin processors 6 | 7 | 8 | Welcome! 9 | 10 | -------------------------------------------------------------------------------- /test/targets/jsx/broken.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var HelloMessage = React.createClass({ 3 | render: function() { 4 | return
Hello {this.props.name}
; 5 | } 6 | }); 7 | 8 | React.renderComponent(, mountNode); 9 | -------------------------------------------------------------------------------- /test/targets/markdown/sample.md: -------------------------------------------------------------------------------- 1 | # Markdown 2 | 3 | this is just a para to make sure markdown is all bless, heres some things what markdown can do: 4 | 5 | - Look nice 6 | - Be easily readable 7 | - Compile to lots of different file types including html and docx! 8 | -------------------------------------------------------------------------------- /test/targets/jsx/sample.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var HelloMessage = React.createClass({ 3 | render: function() { 4 | return
Hello {this.props.name}
; 5 | } 6 | }); 7 | 8 | React.renderComponent(, mountNode); 9 | 10 | -------------------------------------------------------------------------------- /lib/sass_config.rb: -------------------------------------------------------------------------------- 1 | $url = "http://jsbin.com/" 2 | $timeout = 5 # in seconds 3 | 4 | require File.join(File.dirname(__FILE__), 'importer.rb') 5 | require File.join(File.dirname(__FILE__), 'importer_http.rb') 6 | Sass.load_paths << Sass::Importers::JSBin.new() 7 | Sass.load_paths << Sass::Importers::HTTP.new($url, $timeout) 8 | -------------------------------------------------------------------------------- /test/targets/jade/broken.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | 4 | base 5 | 6 | body 7 | h1 Jade - node template engine 8 | #container.col 9 | if youAreUsingJade 10 | p You are amazing 11 | else 12 | p Get on it! 13 | p. 14 | Jade is a terse and simple 15 | templating language with a 16 | strong focus on performance 17 | and powerful features. 18 | 19 | -------------------------------------------------------------------------------- /test/targets/livescript/sample.ls: -------------------------------------------------------------------------------- 1 | take = (n, [x, ...xs]:list) --> 2 | | n <= 0 => [] 3 | | empty list => [] 4 | | otherwise => [x] ++ take n - 1, xs 5 | 6 | 7 | take 2, [1 2 3 4 5] #=> [1, 2] 8 | 9 | take-three = take 3 10 | take-three [3 to 8] #=> [3, 4, 5] 11 | 12 | # Function composition, 'reverse' from prelude.ls 13 | last-three = reverse >> take-three >> reverse 14 | last-three [1 to 8] #=> [6, 7, 8] -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | require "../../../lib/sass_config.rb" 2 | add_import_path "../../../vendor/sass-frameworks" 3 | 4 | require 'compass/import-once/activate' 5 | # Require any additional compass plugins here. 6 | 7 | # Set this to the root of your project when deployed: 8 | http_path = "/" 9 | css_dir = "stylesheets" 10 | sass_dir = "sass" 11 | images_dir = "images" 12 | javascripts_dir = "javascripts" 13 | 14 | disable_warnings = true 15 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo apt-get install -y ruby 4 | # sudo apt-get install -y ruby1.9.3-dev 5 | sudo apt-get install -y ruby1.9.1-dev 6 | # sudo apt-get install -y make 7 | 8 | sudo gem install bundler 9 | bundle install 10 | 11 | # gem update --system 12 | # gem install --no-user-install --no-document --pre compass 13 | # gem install --no-user-install --no-document bourbon 14 | 15 | cd vendor/sass-frameworks 16 | bourbon install 17 | -------------------------------------------------------------------------------- /targets/markdown/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var marked = require('marked'); 4 | 5 | module.exports = function (resolve, reject, data) { 6 | try { 7 | var res = marked(data.source); 8 | resolve({ 9 | errors: null, 10 | result: res 11 | }); 12 | } catch (e) { 13 | var errors = { 14 | line: null, 15 | ch: null, 16 | msg: e 17 | }; 18 | resolve({ 19 | errors: [errors], 20 | result: null 21 | }); 22 | } 23 | }; -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var debug = !!process.env.VERBOSE; 4 | 5 | var noop = function () {}; 6 | 7 | var logWithPrefix = function (prefix) { 8 | return function (msg) { 9 | if (arguments.length > 1) { 10 | msg = [].slice.call(arguments).join(', '); 11 | } 12 | util.log(prefix + ' ' + msg); 13 | }; 14 | }; 15 | 16 | var log = debug ? logWithPrefix('Log::') : noop; 17 | log.error = debug ? logWithPrefix('Error::') : noop; 18 | 19 | module.exports = log; 20 | -------------------------------------------------------------------------------- /targets/scss/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Note: this expects that `compass` is installed and runnable 5 | */ 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var spawn = require('child_process').spawn; 10 | 11 | var output = path.join(__dirname, 'output'); 12 | 13 | var sass = require('../sass'); 14 | 15 | sass.makeProject(output); 16 | 17 | module.exports = function (resolve, reject, data) { 18 | data.ext = '.scss'; 19 | data.output = output; 20 | 21 | sass(resolve, reject, data); 22 | }; 23 | -------------------------------------------------------------------------------- /test/targets/jade/sample.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | script(type='text/javascript'). 6 | if (foo) { 7 | bar(1 + 5) 8 | } 9 | body 10 | h1 Jade - node template engine 11 | #container.col 12 | if youAreUsingJade 13 | p You are amazing 14 | else 15 | p Get on it! 16 | p. 17 | Jade is a terse and simple 18 | templating language with a 19 | strong focus on performance 20 | and powerful features. 21 | -------------------------------------------------------------------------------- /targets/to5/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var to5 = require('6to5'); 3 | 4 | module.exports = function convert6to5(resolve, reject, data) { 5 | try { 6 | var result = to5.transform(data.source); 7 | resolve({ 8 | errors: null, 9 | result: result.code 10 | }); 11 | } catch (e) { 12 | console.log('failed 6to5'); 13 | var errors = { 14 | line: e.loc.line - 1, 15 | ch: e.loc.column, 16 | msg: e.message 17 | }; 18 | resolve({ 19 | errors: [errors], 20 | result: null 21 | }); 22 | } 23 | }; -------------------------------------------------------------------------------- /targets/babel/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var babel = require('babel-core'); 3 | 4 | module.exports = function convertbabel(resolve, reject, data) { 5 | try { 6 | var result = babel.transform(data.source, { 7 | stage: 0 8 | }); 9 | resolve({ 10 | errors: null, 11 | result: result.code 12 | }); 13 | } catch (e) { 14 | var errors = { 15 | line: e.loc.line - 1, 16 | ch: e.loc.column, 17 | msg: e.message 18 | }; 19 | resolve({ 20 | errors: [errors], 21 | result: null 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /targets/coffeescript/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var coffeescript = require('coffee-script'); 3 | 4 | var convertToCoffeeScript = function (resolve, reject, data) { 5 | try { 6 | var res = coffeescript.compile(data.source); 7 | resolve({ 8 | errors: null, 9 | result: res 10 | }); 11 | } catch (e) { 12 | // index starts at 0 13 | var errors = { 14 | line: parseInt(e.location.first_line, 10) || 0, 15 | ch: parseInt(e.location.first_column, 10) || 0, 16 | msg: e.message 17 | }; 18 | resolve({ 19 | errors: [errors], 20 | result: null 21 | }); 22 | } 23 | }; 24 | 25 | module.exports = convertToCoffeeScript; 26 | -------------------------------------------------------------------------------- /targets/clojurescript/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var evalCLJS = require('./cljs'); 3 | 4 | module.exports = function cljsToJs(resolve, reject, data) { 5 | 6 | evalCLJS(data.source, function(err, result) { 7 | var error = {}; 8 | if (err) { 9 | var line = err.split('\n')[0]; 10 | line.replace(/^Error: (.*), starting at line (\d+) and column (\d+)/, function (all, message, line, ch) { 11 | error.message = message; 12 | error.line = line * 1; 13 | error.ch = ch * 1; 14 | }); 15 | } 16 | resolve({ 17 | errors: err ? [{ line: error.line, ch: error.ch, msg: error.message }] : null, 18 | result: result || null, 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /targets/myth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var myth = require('myth'); 3 | 4 | module.exports = function convertMyth(resolve, reject, data) { 5 | try { 6 | var res = myth(data.source); 7 | resolve({ 8 | errors: null, 9 | result: res 10 | }); 11 | } catch (e) { 12 | // index starts at 1 13 | var line = parseInt(e.line, 10) || 0; 14 | var ch = parseInt(e.column, 10) || 0; 15 | if (line > 0) { 16 | line = line - 1; 17 | } 18 | if (ch > 0) { 19 | ch = ch - 1; 20 | } 21 | var errors = { 22 | line: line, 23 | ch: ch, 24 | msg: e.message 25 | }; 26 | resolve({ 27 | errors: [errors], 28 | result: null 29 | }); 30 | } 31 | }; -------------------------------------------------------------------------------- /targets/jade/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var jade = require('jade'); 3 | 4 | module.exports = function (resolve, reject, data) { 5 | try { 6 | var res = jade.compile(data.source)(); 7 | resolve({ 8 | errors: null, 9 | result: res 10 | }); 11 | } catch (e) { 12 | // index starts at 1 13 | var lineMatch = e.message.match(/Jade:(\d+)/); 14 | var line = parseInt(lineMatch[1], 10) || 0; 15 | if (line > 0) { 16 | line = line - 1; 17 | } 18 | var msg = e.message.match(/\n\n(.+)$/); 19 | var errors = { 20 | line: line, 21 | ch: null, 22 | msg: msg[1] 23 | }; 24 | resolve({ 25 | errors: [errors], 26 | result: null 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /targets/jsx/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsx = require('./JSXTransformer'); 4 | 5 | module.exports = function (resolve, reject, data) { 6 | try { 7 | var res = jsx.transform(data.source).code; 8 | resolve({ 9 | errors: null, 10 | result: res 11 | }); 12 | } catch (e) { 13 | // index starts at 1 14 | var line = parseInt(e.lineNumber, 10) || 0; 15 | var ch = parseInt(e.column, 10) || 0; 16 | if (line > 0) { 17 | line = line - 1; 18 | } 19 | if (ch > 0) { 20 | ch = ch - 1; 21 | } 22 | var errors = { 23 | line: line, 24 | ch: ch, 25 | msg: e.description 26 | }; 27 | resolve({ 28 | errors: [errors], 29 | result: null 30 | }); 31 | } 32 | }; -------------------------------------------------------------------------------- /targets/livescript/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var livescript = require('LiveScript'); 3 | 4 | var convertToLiveScript = function (resolve, reject, data) { 5 | try { 6 | var res = livescript.compile(data.source); 7 | resolve({ 8 | errors: null, 9 | result: res 10 | }); 11 | } catch (e) { 12 | // index starts at 1 13 | var lineMatch = e.message.match(/on line (\d+)/) || [,]; 14 | var line = parseInt(lineMatch[1], 10) || 0; 15 | if (line > 0) { 16 | line = line - 1; 17 | } 18 | var msg = e.message.match(/(.+) on line (\d+)$/) || [,]; 19 | var errors = { 20 | line: line, 21 | ch: null, 22 | msg: msg[1] 23 | }; 24 | resolve({ 25 | errors: [errors], 26 | result: null 27 | }); 28 | } 29 | }; 30 | 31 | module.exports = convertToLiveScript; 32 | -------------------------------------------------------------------------------- /lib/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (origin) { 3 | if (!Array.isArray(origin)) { 4 | origin = [origin]; 5 | } 6 | return function (req, res, next) { 7 | var headers = req.header('Access-Control-Request-Headers'); 8 | 9 | // TODO should this check if the request is via the API? 10 | if (req.headers.origin && origin.indexOf(req.header.origin) !== -1) { 11 | res.header({ 12 | 'Access-Control-Allow-Origin': req.header.origin, 13 | 'Access-Control-Allow-Headers': headers, 14 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS' 15 | }); 16 | req.cors = true; 17 | } else if (req.header.origin) { 18 | res.send(401); 19 | } 20 | 21 | if (req.method === 'OPTIONS') { 22 | res.send(204); 23 | } else { 24 | next(); 25 | } 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /targets/less/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var less = require('less'); 3 | 4 | var convertToLess = function (resolve, reject, data) { 5 | less.render(data.source, function (error, css) { 6 | if (error) { 7 | // index starts at 1 8 | var line = parseInt(error.line, 10) || 0; 9 | var ch = parseInt(error.column, 10) || 0; 10 | if (line > 0) { 11 | line = line - 1; 12 | } 13 | if (ch > 0) { 14 | ch = ch - 1; 15 | } 16 | var errors = { 17 | line: line, 18 | ch: ch, 19 | msg: error.message 20 | }; 21 | resolve({ 22 | errors: [errors], 23 | result: null 24 | }); 25 | } 26 | var res = css.css; 27 | resolve({ 28 | errors: null, 29 | result: res 30 | }); 31 | }); 32 | }; 33 | 34 | module.exports = convertToLess; 35 | -------------------------------------------------------------------------------- /targets/stylus/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var stylus = require('stylus'); 3 | 4 | var convertToStylus = function (resolve, reject, data) { 5 | stylus.render(data.source, function (error, css) { 6 | if (error) { 7 | // index starts at 1 8 | var lineMatch = error.message.match(/stylus:(\d+)/); 9 | var line = parseInt(lineMatch[1], 10) || 0; 10 | var msg = error.message.match(/\n\n(.+)\n$/); 11 | if (line > 0) { 12 | line = line - 1; 13 | } 14 | var errors = { 15 | line: line, 16 | ch: null, 17 | msg: msg[1] 18 | }; 19 | resolve({ 20 | errors: [errors], 21 | result: null 22 | }); 23 | } 24 | var res = css; 25 | resolve({ 26 | errors: null, 27 | result: res 28 | }); 29 | }); 30 | }; 31 | 32 | module.exports = convertToStylus; 33 | 34 | -------------------------------------------------------------------------------- /test/lib/server.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../lib/server'); 12 | 13 | describe('Server', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://0.0.0.0:5555'); 18 | }); 19 | 20 | it('Should error if compilation time exceeds 10 seconds', function (done) { 21 | fs.readFile(__dirname + '/infinite.sass', function (error, file) { 22 | requester.send({ 23 | language: 'sass', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.not.be.true; 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | after(function () { 33 | requester.close(); 34 | server.stop(); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /lib/importer.rb: -------------------------------------------------------------------------------- 1 | module Sass 2 | module Importers 3 | class JSBin < Base 4 | def find(name, options) 5 | # if name == 'xyz/1.scss' 6 | m = name.match /([^\/\s]+)\/(\d+).(scss|sass)/ 7 | if m 8 | # options[:syntax] = :scss 9 | # options[:filename] = 'globals' 10 | # options[:importer] = self 11 | # return Sass::Engine.new("$foo: aaa; $bar: bbb;", options) 12 | f = Sass::Engine.new("@import '" + m[1] + "." + m[2] + "." + m[3] + "'", options) 13 | return f 14 | else 15 | return nil 16 | end 17 | end 18 | 19 | # def find_relative(uri, base, options) 20 | # nil 21 | # end 22 | 23 | def key(uri, options) 24 | [self.class.name, uri] 25 | end 26 | 27 | # def mtime(uri, options) 28 | # nil 29 | # end 30 | 31 | def to_s 32 | '[custom]' 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | bourbon (4.0.2) 5 | sass (~> 3.3) 6 | thor 7 | chunky_png (1.3.1) 8 | compass (1.0.0.alpha.19) 9 | chunky_png (~> 1.2) 10 | compass-core (~> 1.0.0.alpha.19) 11 | compass-import-once (~> 1.0.3) 12 | json 13 | listen (~> 1.1.0) 14 | sass (>= 3.3.0, < 3.5) 15 | compass-core (1.0.0.alpha.20) 16 | multi_json (~> 1.0) 17 | sass (>= 3.3.0, < 3.5) 18 | compass-import-once (1.0.4) 19 | sass (>= 3.2, < 3.5) 20 | ffi (1.9.3) 21 | json (1.8.1) 22 | listen (1.1.6) 23 | rb-fsevent (>= 0.9.3) 24 | rb-inotify (>= 0.9) 25 | rb-kqueue (>= 0.2) 26 | multi_json (1.10.1) 27 | rb-fsevent (0.9.4) 28 | rb-inotify (0.9.5) 29 | ffi (>= 0.5.0) 30 | rb-kqueue (0.2.3) 31 | ffi (>= 0.5.0) 32 | sass (3.3.8) 33 | thor (0.19.1) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | bourbon 40 | compass (>= 1.0.0.alpha.19) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsbin-processors", 3 | "version": "0.1.5", 4 | "description": "The server side processors server", 5 | "main": "lib/server.js", 6 | "scripts": { 7 | "start": "node lib/server.js", 8 | "test": "HOST=0.0.0.0 node_modules/mocha/bin/_mocha --timeout 15000 test/{**,**/*}/*.test.js", 9 | "gems": "sh bootstrap.sh" 10 | }, 11 | "author": "Remy Sharp", 12 | "license": "MIT", 13 | "dependencies": { 14 | "6to5": "^3.0.10", 15 | "LiveScript": "^1.2.0", 16 | "axon": "~2.0.0", 17 | "babel-core": "^5.8.23", 18 | "body-parser": "~1.0.2", 19 | "coffee-script": "~1.7.1", 20 | "express": "~4.1.1", 21 | "gaze": "~0.6.4", 22 | "jade": "^1.11.0", 23 | "less": "^2.5.1", 24 | "lynx": "0.0.11", 25 | "marked": "^0.3.3", 26 | "myth": "^1.4.0", 27 | "ps-tree": "~0.0.3", 28 | "rsvp": "~3.0.1", 29 | "stylus": "^0.51.1" 30 | }, 31 | "devDependencies": { 32 | "mocha": "~1.18.2", 33 | "should": "~4.0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bin/pennyworth: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // currently only reads from STDIN 6 | var source = ''; 7 | var fs = require('fs'); 8 | var processors = require('../lib/processors'); 9 | var language = process.argv[2]; 10 | 11 | if (!language) { 12 | console.error('Usage: cat file | pennyworth '); 13 | process.exit(1); 14 | } 15 | 16 | function run(source) { 17 | processors.run({ 18 | language: language, 19 | source: source 20 | }).then(function (result) { 21 | console.log(result); 22 | process.exit(0); 23 | }).catch(function (result) { 24 | console.error(result); 25 | process.exit(1); 26 | }); 27 | } 28 | 29 | processors.on('ready', function () { 30 | if (!processors.has(language)) { 31 | console.error('"' + language + '" processor not supported'); 32 | process.exit(1); 33 | } 34 | 35 | if (process.stdin.isTTY === true) { 36 | // read from file 37 | fs.readFile(process.argv[3], 'utf8', function (error, source) { 38 | if (error) { 39 | throw error; 40 | } 41 | 42 | run(source); 43 | }) 44 | } else { 45 | process.stdin.on('data', function (buffer) { 46 | source += buffer.toString(); 47 | }).on('end', function () { 48 | run(source); 49 | }); 50 | } 51 | }).on('error', function (error) { 52 | console.error(error.stack); 53 | process.exit(1); 54 | }); -------------------------------------------------------------------------------- /test/targets/markdown/markdown.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Markdown', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid Markdown and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.md', function (error, file) { 22 | requester.send({ 23 | language: 'markdown', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | res.output.should.exist; 28 | done(); 29 | }); 30 | }); 31 | }); 32 | 33 | it('Should process invalid Markdown and *not* give back an error', function (done) { 34 | fs.readFile(__dirname + '/broken.md', function (error, file) { 35 | requester.send({ 36 | language: 'markdown', 37 | source: file.toString() 38 | }, function (res) { 39 | (res.error === null).should.be.true; 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | after(function () { 46 | requester.close(); 47 | server.stop(); 48 | }); 49 | 50 | }); 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/targets/myth/myth.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var assert = require('assert'); 6 | 7 | var axon = require('axon'); 8 | var requester = axon.socket('req'); 9 | 10 | var server = require('../../../lib/server'); 11 | 12 | describe('myth', function () { 13 | 14 | before(function () { 15 | server.start(); 16 | requester.connect('tcp://localhost:5555'); 17 | }); 18 | 19 | it('Should process unprefixed CSS and pass back the compiled source', function (done) { 20 | fs.readFile(__dirname + '/sample.css', function (error, file) { 21 | requester.send({ 22 | language: 'myth', 23 | source: file.toString() 24 | }, function (res) { 25 | assert(res.error === null, 'error is null: ' + res.error); 26 | assert(!!res.output.result, 'result: ' + res.output.result); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | it('Should process invalid CSS and give back an error', function (done) { 33 | fs.readFile(__dirname + '/broken.css', function (error, file) { 34 | requester.send({ 35 | language: 'myth', 36 | source: file.toString() 37 | }, function (res) { 38 | assert(res.error === null, 'error is null: ' + res.error); 39 | assert(!!res.output.errors, 'result: ' + res.output.errors); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | after(function () { 46 | requester.close(); 47 | server.stop(); 48 | }); 49 | 50 | }); 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/targets/jsx/jsx.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('JSX', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid JSX and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.jsx', function (error, file) { 22 | requester.send({ 23 | language: 'jsx', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid JSX and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.jsx', function (error, file) { 36 | requester.send({ 37 | language: 'jsx', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/targets/babel/babel.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Babel', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid ES6 and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.js', function (error, file) { 22 | requester.send({ 23 | language: 'babel', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid ES6 and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.js', function (error, file) { 36 | requester.send({ 37 | language: 'babel', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/targets/jade/jade.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Jade', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid Jade and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.jade', function (error, file) { 22 | requester.send({ 23 | language: 'jade', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid Jade and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.jade', function (error, file) { 36 | requester.send({ 37 | language: 'jade', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/targets/less/less.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Less', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid Less and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.less', function (error, file) { 22 | requester.send({ 23 | language: 'less', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid Less and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.less', function (error, file) { 36 | requester.send({ 37 | language: 'less', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/targets/stylus/stylus.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Stylus', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid Stylus and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.styl', function (error, file) { 22 | requester.send({ 23 | language: 'stylus', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid Stylus and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.styl', function (error, file) { 36 | requester.send({ 37 | language: 'stylus', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/targets/livescript/livescript.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('LiveScript', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://0.0.0.0:5555'); 18 | }); 19 | 20 | it('Should process valid LiveScript and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.ls', function (error, file) { 22 | requester.send({ 23 | language: 'livescript', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid LiveScript and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.ls', function (error, file) { 36 | requester.send({ 37 | language: 'livescript', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /test/targets/clojurescript/clojurescript.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('ClojureScript', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://localhost:5555'); 18 | }); 19 | 20 | it('Should process valid ClojureScript and pass back evaluated value', function (done) { 21 | fs.readFile(__dirname + '/sample.js', function (error, file) { 22 | requester.send({ 23 | language: 'clojurescript', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid ClojureScript and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.js', function (error, file) { 36 | requester.send({ 37 | language: 'clojurescript', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/targets/coffeescript/coffeescript.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | 6 | var should = require('should'); 7 | 8 | var axon = require('axon'); 9 | var requester = axon.socket('req'); 10 | 11 | var server = require('../../../lib/server'); 12 | 13 | describe('Coffeescript', function () { 14 | 15 | before(function () { 16 | server.start(); 17 | requester.connect('tcp://0.0.0.0:5555'); 18 | }); 19 | 20 | it('Should process valid CoffeeScript and pass back the compiled source', function (done) { 21 | fs.readFile(__dirname + '/sample.coffee', function (error, file) { 22 | requester.send({ 23 | language: 'coffeescript', 24 | source: file.toString() 25 | }, function (res) { 26 | (res.error === null).should.be.true; 27 | (res.output.errors === null).should.be.true; 28 | res.output.result.should.exist; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | it('Should process invalid CoffeeScript and give back an error', function (done) { 35 | fs.readFile(__dirname + '/broken.coffee', function (error, file) { 36 | requester.send({ 37 | language: 'coffeescript', 38 | source: file.toString() 39 | }, function (res) { 40 | (res.error === null).should.be.true; 41 | (res.output.result === null).should.be.true; 42 | res.output.errors.should.exist; 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | after(function () { 49 | requester.close(); 50 | server.stop(); 51 | }); 52 | 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /lib/processors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var targets = path.resolve(__dirname + '/../targets'); 6 | var Promise = require('rsvp').Promise; // jshint ignore:line 7 | var EventEmitter = require('events').EventEmitter; 8 | var log = require('./log'); 9 | var available = []; 10 | 11 | // make our processors object eventy 12 | var processors = new EventEmitter(); 13 | 14 | processors.has = has; 15 | processors.run = run; 16 | 17 | // processors is our exported module 18 | module.exports = processors; 19 | 20 | function loadTargets() { 21 | return new Promise(function (resolve, reject) { 22 | fs.readdir(targets, function (error, files) { 23 | if (error) { 24 | log.error('failed to read targets directory', error); 25 | return reject(error); 26 | } 27 | 28 | files.forEach(function (file) { 29 | if (file.indexOf('.') !== 0) { 30 | try { 31 | require(path.join(targets, file)); 32 | available.push(path.basename(file)); 33 | } catch (error) { 34 | log.error('Problem with "' + file + '" module', error); 35 | } 36 | } 37 | }); 38 | 39 | log('Processors available: ' + available.sort().join(' ')); 40 | resolve([].slice.call(available)); 41 | }); 42 | }); 43 | } 44 | 45 | function has(language) { 46 | return available.indexOf(language) !== -1; 47 | } 48 | 49 | function run(event) { 50 | var lang = event.language; 51 | return new Promise(function (resolve, reject) { 52 | var module = require(path.join(targets, lang)); 53 | // FIXME ensure the url and revision are legit 54 | module(resolve, reject, {language: lang, source: event.source, file: event.url + '.' + event.revision}); 55 | }); 56 | } 57 | 58 | function send(data) { 59 | process.send(JSON.stringify(data)); 60 | } 61 | 62 | if (!module.parent) { 63 | processors.emit('ready'); 64 | process.on('message', function (event) { 65 | run(event).then(function (output) { 66 | send({ output: output, error: null }); 67 | }).catch(function (error) { 68 | send({ error: error }); 69 | }).then(function () { 70 | process.exit(0); 71 | }); 72 | }); 73 | } else { 74 | loadTargets().then(function () { 75 | processors.emit('ready'); 76 | }).catch(function (error) { 77 | processors.emit('error', error); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/targets/sass/sample.sass: -------------------------------------------------------------------------------- 1 | $n: 60 2 | $m: 36 3 | $r: 5em 4 | $R1: 3*$r 5 | 6 | html 7 | height: 100% 8 | perspective: 125em 9 | background: black 10 | 11 | 12 | .torus, .torus *, .torus *:before, .torus *:after 13 | box-sizing: border-box 14 | position: absolute 15 | top: 50% 16 | left: 50% 17 | transform-style: preserve-3d 18 | 19 | 20 | .torus 21 | transform: rotateX(-35deg) rotateY(360deg/$n/2) 22 | 23 | 24 | .circle, .circle:before, .circle:after 25 | border: solid .125em 26 | border-radius: 50% 27 | 28 | .circle:before, .circle:after 29 | margin: inherit 30 | width: inherit 31 | height: inherit 32 | content: '' 33 | 34 | 35 | .circleY 36 | margin: -$r 37 | width: 2*$r 38 | height: 2*$r 39 | color: tomato 40 | 41 | 42 | @for $i from 0 to $n 43 | .circleY:nth-child(#{$i + 1}) 44 | transform: rotateY($i*360deg/$n) translate($R1) 45 | 46 | 47 | 48 | .circleX 49 | color: rgba(yellow, .5) 50 | 51 | 52 | @for $i from 0 to $m 53 | .circleX:nth-child(#{$i + 1 + $n}) 54 | $a: $i*360deg/$m 55 | $d: 2*($R1 + $r*sin($a)) 56 | margin: -$d/2 57 | width: $d 58 | height: $d 59 | transform: rotateX(90deg) translateZ($r*cos($a)) 60 | 61 | 62 | //=== Sass 3.3 functionalities 63 | // Maps 64 | $themes: (mist: (header: #DCFAC0,text: #00968B,border: #85C79C),spring: (header: #F4FAC7,text: #C2454E,border: #FFB158),) 65 | 66 | @mixin themed-header($theme-name) 67 | body 68 | color: map-get(map-get($themes, $theme-name), text) 69 | 70 | h1 71 | color: map-get(map-get($themes, $theme-name), header) 72 | border-bottom: 1px solid map-get(map-get($themes, $theme-name), border) 73 | 74 | @include themed-header(spring) 75 | 76 | @each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) 77 | #{$header} 78 | font-size: $size 79 | 80 | // Suffix 81 | .test 82 | color: red 83 | 84 | &--title 85 | color: blue 86 | &__sub 87 | color: green 88 | 89 | // @at-root 90 | @media print 91 | .page 92 | width: 8in 93 | @at-root (without: media) 94 | color: red 95 | 96 | 97 | //=== Compass 98 | @import compass/reset.scss 99 | @import compass/layout.scss 100 | 101 | +sticky-footer(72px, "#layout", "#layout_footer", "#footer") 102 | 103 | #header 104 | :background #999 105 | :height 72px 106 | 107 | #footer 108 | :background #ccc 109 | 110 | .example 111 | height: 500px 112 | border: 3px solid red 113 | 114 | p 115 | margin: 1em 0.5em -------------------------------------------------------------------------------- /test/targets/scss/sample.scss: -------------------------------------------------------------------------------- 1 | $n: 60; 2 | $m: 36; 3 | $r: 5em; 4 | $R1: 3*$r; 5 | 6 | html { 7 | height: 100%; 8 | perspective: 125em; 9 | background: black; 10 | } 11 | 12 | .torus, .torus *, .torus *:before, .torus *:after { 13 | box-sizing: border-box; 14 | position: absolute; 15 | top: 50%; left: 50%; 16 | transform-style: preserve-3d; 17 | } 18 | 19 | .torus { 20 | transform: rotateX(-35deg) rotateY(360deg/$n/2); 21 | } 22 | 23 | .circle, .circle:before, .circle:after { 24 | border: solid .125em; 25 | border-radius: 50%; 26 | } 27 | .circle:before, .circle:after { 28 | margin: inherit; 29 | width: inherit; height: inherit; 30 | content: ''; 31 | } 32 | 33 | .circleY { 34 | margin: -$r; 35 | width: 2*$r; height: 2*$r; 36 | color: tomato; 37 | } 38 | 39 | @for $i from 0 to $n { 40 | .circleY:nth-child(#{$i + 1}) { 41 | transform: rotateY($i*360deg/$n) translate($R1) 42 | } 43 | } 44 | 45 | .circleX { 46 | color: rgba(yellow, .5); 47 | } 48 | 49 | @for $i from 0 to $m { 50 | .circleX:nth-child(#{$i + 1 + $n}) { 51 | $a: $i*360deg/$m; 52 | $d: 2*($R1 + $r*sin($a)); 53 | margin: -$d/2; 54 | width: $d; height: $d; 55 | transform: rotateX(90deg) translateZ($r*cos($a)); 56 | } 57 | } 58 | 59 | 60 | //=== Sass 3.3 functionalities 61 | // Maps 62 | $themes: ( 63 | mist: ( 64 | header: #DCFAC0, 65 | text: #00968B, 66 | border: #85C79C 67 | ), 68 | spring: ( 69 | header: #F4FAC7, 70 | text: #C2454E, 71 | border: #FFB158 72 | ), 73 | ); 74 | 75 | @mixin themed-header($theme-name) { 76 | body { 77 | color: map-get(map-get($themes, $theme-name), text); 78 | } 79 | 80 | h1 { 81 | color: map-get(map-get($themes, $theme-name), header); 82 | border-bottom: 1px solid map-get(map-get($themes, $theme-name), border); 83 | } 84 | } 85 | 86 | @include themed-header(spring); 87 | 88 | @each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) { 89 | #{$header} { 90 | font-size: $size; 91 | } 92 | } 93 | 94 | // Suffix 95 | .test { 96 | color: red; 97 | 98 | &--title { 99 | color: blue; 100 | } 101 | &__sub { 102 | color: green; 103 | } 104 | } 105 | 106 | // @at-root 107 | @media print { 108 | .page { 109 | width: 8in; 110 | @at-root (without: media) { 111 | color: red; 112 | } 113 | } 114 | } 115 | 116 | 117 | //=== Compass 118 | @import "compass/reset.scss"; 119 | @import "compass/layout.scss"; 120 | 121 | @include sticky-footer(72px, "#layout", "#layout_footer", "#footer"); 122 | 123 | #header { 124 | background: #999999; 125 | height: 72px; 126 | } 127 | 128 | #footer { 129 | background: #cccccc; 130 | } 131 | 132 | .example { 133 | height: 500px; 134 | border: 3px solid red; 135 | p { 136 | margin: 1em 0.5em; 137 | } 138 | } -------------------------------------------------------------------------------- /lib/importer_http.rb: -------------------------------------------------------------------------------- 1 | # https://github.com/joeellis/remote-sass 2 | 3 | require 'sass' 4 | require 'net/http' 5 | require 'time' 6 | 7 | module Sass 8 | module Importers 9 | class HTTP < Base 10 | def initialize root, timeout 11 | @root = URI.parse root 12 | @timeout = timeout 13 | 14 | unless scheme_allowed? @root 15 | raise ArgumentError, "Absolute HTTP URIs only" 16 | end 17 | end 18 | 19 | def find_relative uri, base, options 20 | _find @root + base + uri, options 21 | end 22 | 23 | def find uri, options 24 | _find @root + uri, options 25 | end 26 | 27 | def mtime uri, options 28 | uri = URI.parse uri 29 | return unless scheme_allowed? uri 30 | Net::HTTP.start(uri.host, uri.port) do |http| 31 | response = http.head uri.request_uri 32 | 33 | if response.is_a?(Net::HTTPOK) && response['Last-Modified'] 34 | Time.parse response['Last-Modified'] 35 | elsif response.is_a? Net::HTTPOK 36 | # we must assume that it just changed 37 | Time.now 38 | else 39 | nil 40 | end 41 | end 42 | end 43 | 44 | def key(uri, options) 45 | [self.class.name, uri] 46 | end 47 | 48 | def to_s 49 | @root.to_s 50 | end 51 | 52 | protected 53 | 54 | def extensions 55 | {'.sass' => :sass, '.scss' => :scss} 56 | end 57 | 58 | private 59 | 60 | def scheme_allowed? uri 61 | uri.absolute? && (uri.scheme == 'http' || uri.scheme == 'https') 62 | end 63 | 64 | def exists? uri 65 | begin 66 | Net::HTTP.start(uri.host, uri.port, :read_timeout => @timeout) do |http| 67 | http.head(uri.request_uri).is_a? Net::HTTPOK 68 | end 69 | rescue Timeout::Error => e 70 | return nil 71 | end 72 | end 73 | 74 | def get_syntax uri 75 | # determine the syntax being used 76 | ext = File.extname uri.path 77 | syntax = extensions[ext] 78 | 79 | # this must not be the full path: try another 80 | if syntax.nil? 81 | ext, syntax = extensions.find do |possible_ext, possible_syntax| 82 | new_uri = uri.dup 83 | new_uri.path += possible_ext 84 | exists? new_uri 85 | end 86 | return if syntax.nil? 87 | uri.path += ext 88 | end 89 | syntax 90 | end 91 | 92 | def _find uri, options 93 | raise ArgumentError, "Absolute HTTP URIs only" unless scheme_allowed? uri 94 | 95 | syntax = get_syntax uri 96 | 97 | # fetch the content 98 | if exists? uri 99 | Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http| 100 | response = http.get uri.request_uri 101 | response.value 102 | 103 | options[:importer] = self 104 | options[:filename] = uri.to_s 105 | options[:syntax] = syntax 106 | Sass::Engine.new response.body, options 107 | end 108 | else 109 | nil 110 | end 111 | # rescue 112 | # nil 113 | end 114 | end 115 | end 116 | end -------------------------------------------------------------------------------- /targets/sass/README.md: -------------------------------------------------------------------------------- 1 | # [SASS](http://sass-lang.com/) and [Compass](http://compass-style.org/) support 2 | 3 | ## How to install 4 | 5 | Installation procedure is declared in `bootstrap.sh`, which is run automatically by `npm install`. 6 | The ruby gems required are declared in `Gemfile` in the main pennyworth directory. 7 | 8 | ### Compass 9 | 10 | Compass requires at least version 1.0.0 as previous versions don't support Sass 3.3 (which have features highly requested by the users). 11 | 12 | It's not necessary to manually create a Compass project for the targets as the processor does it if it doesn't find the output folder. 13 | 14 | 15 | ## Install frameworks 16 | 17 | If it's possible to have the physical files (via download or installation), these should be located in the `vendor/sass-frameworks` directory. This directory is included in the main `config.rb` for Compass, therefore all the files within can be imported in Sass/SCSS with a simple `@import frameworkname`. 18 | 19 | If the framework requires the installation of a gem, this should be added in the main `Gemfile`. 20 | Also, check for its documentation for more eventual steps to follow to enable it. 21 | 22 | ### [Blueprint](http://compass-style.org/reference/blueprint/) 23 | 24 | **Not included** in the current version of pennyworth. The documentation is for reference if it will be requested by the users in the future. 25 | 26 | First please read [this note](http://compass-style.org/blog/2012/05/20/removing-blueprint/) about Blueprint being removed from Compass. 27 | 28 | # install 29 | sudo gem install --no-user-install --no-document compass-blueprint 30 | 31 | Add `require 'compass-blueprint'` to config.rb (at the end it's fine) 32 | 33 | ### [Bourbon](http://bourbon.io/) 34 | 35 | Installation is handled automatically by bundle, it just requires the gem to be listed in `Gemfile`. 36 | The extension is enabled in pennyworth by the `bootstrap.sh` script, which will create the files in the `vendor/sass-frameworks` directory and no further step is required. 37 | 38 | To use it, import the mixins 39 | 40 | @import 'bourbon/bourbon'; 41 | 42 | To update it 43 | 44 | sudo bourbon update 45 | 46 | 47 | ## Custom configurations 48 | 49 | In the main pennyworth directory we have the `config.rb` file. This is copied inside every target processor that runs Compass and eventually modified by the single processor according to its need (for example, Sass target adds the line `preferred_syntax = :sass` to it). 50 | 51 | The `lib/sass_config.rb` file includes all the common configurations between the Compass projects that can be declared outside the singles `config.rb`. 52 | Inside this file we declare custom importer that we use specifically for JS Bin. 53 | 54 | We need a custom Sass importer to translate all the `@import 'binname/1.scss'` to `binname.1.scss`. 55 | 56 | In `/lib/sass_config.rb` add these lines 57 | 58 | require File.join(File.dirname(__FILE__), 'importer.rb') 59 | Sass.load_paths << Sass::Importers::JSBin.new() 60 | 61 | We also need a custom importer to call bins via http, in the case for some reason the physical file is not avaiable or if the revision-less bin is requested (`@import binname.scss`). 62 | 63 | In `/lib/sass_config.rb` add these lines 64 | 65 | $url = "http://jsbin-dev.com/" # the absolute url from which we look for files 66 | $timeout = 5 # in seconds # after how many seconds the http request stops if it's still loading 67 | require File.join(File.dirname(__FILE__), 'importer_http.rb') 68 | Sass.load_paths << Sass::Importers::HTTP.new($url, $timeout) 69 | 70 | This `load_paths` must be after the previous one, to have the correct fallback (if that import doesn't work, fallback to this). 71 | 72 | -------------------------------------------------------------------------------- /targets/sass/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Note: this expects that `compass` is installed and runnable 5 | */ 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var spawn = require('child_process').spawn; 10 | 11 | var output = path.join(__dirname, 'output'); 12 | 13 | var makeProject = function(output, isSass) { 14 | var configFile = 'config.rb'; 15 | 16 | // make folder and create compass project 17 | fs.mkdir(output, function (error) { 18 | if (!error) { 19 | // copy config.rb 20 | var w = fs.createWriteStream(output + '/' + configFile); 21 | var r = fs.createReadStream(path.join(__dirname, '..', '..', configFile)).pipe(w); 22 | 23 | if (isSass) { 24 | r.on('finish', function(){ 25 | fs.createWriteStream(output + '/' + configFile, {flags: 'a'}).write('\npreferred_syntax = :sass'); 26 | }); 27 | } 28 | 29 | // FIXME we should actuall install compass to the travis box pre-test 30 | try { 31 | spawn('compass', ['init'], { 32 | cwd: output 33 | }); 34 | } catch (e) { 35 | console.error('Failed compass init'); 36 | console.error(e); 37 | } 38 | } else { 39 | // check for project files 40 | var projFiles = ['config.rb', 'sass', 'stylesheets']; 41 | projFiles.forEach(function (name) { 42 | var file = path.join(output, name); 43 | fs.exists(file, function (exists) { 44 | if (!exists) { 45 | console.log('Error: ' + file + ' not created'); 46 | } 47 | }); 48 | }); 49 | } 50 | }); 51 | }; 52 | 53 | makeProject(output, true); 54 | 55 | module.exports = function (resolve, reject, data) { 56 | var ext = data.ext || module.exports.ext; 57 | var output = data.output || module.exports.output; 58 | var targetFile = path.join(output, 'stylesheets', data.file + '.css'); 59 | var sourceFile = path.join('sass', data.file + ext); 60 | 61 | fs.writeFile(path.join(output, sourceFile), data.source, function () { 62 | var args = ['compile', sourceFile, '--no-line-comments', '--boring', '--quiet']; 63 | 64 | var compass = spawn('compass', args, { 65 | cwd: output 66 | }); 67 | 68 | compass.stderr.setEncoding('utf8'); 69 | compass.stdout.setEncoding('utf8'); 70 | 71 | var result = ''; 72 | var error = ''; 73 | 74 | compass.stdout.on('data', function (data) { 75 | result += data; 76 | }); 77 | 78 | compass.stderr.on('data', function (data) { 79 | error += data; 80 | }); 81 | 82 | compass.on('error', function (error) { 83 | reject(error); 84 | }); 85 | 86 | compass.on('close', function () { 87 | if (error) { 88 | return reject(error); 89 | } 90 | 91 | // this is because syntax errors are put on stdout... 92 | if (result.indexOf('error ' + sourceFile) !== -1) { 93 | var errors = []; 94 | var resultArr = result.split('\n'); 95 | resultArr.forEach(function (line) { 96 | // index starts at 1 97 | line.trim().replace(/\(Line\s+([\d]+):\s*(.*?)(\)|\.)$/g, function (a, n, e) { 98 | var l = parseInt(n, 10) || 0; 99 | if (l > 0) { 100 | l = l - 1; 101 | } 102 | errors.push({ 103 | line: l, 104 | ch: null, 105 | msg: e 106 | }); 107 | }); 108 | }); 109 | // send the errors so we can show them 110 | return resolve({ 111 | errors: errors, 112 | result: null 113 | }); 114 | } 115 | 116 | // if okay, then try to read the target 117 | fs.readFile(targetFile, 'utf8', function (error, data) { 118 | if (error) { 119 | reject(error); 120 | } else { 121 | resolve({ 122 | errors: null, 123 | result: data 124 | }); 125 | } 126 | }); 127 | }); 128 | 129 | //*/ 130 | }); 131 | }; 132 | 133 | module.exports.ext = '.sass'; 134 | module.exports.output = output; 135 | module.exports.makeProject = makeProject; 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis Status](https://travis-ci.org/jsbin/pennyworth.svg?branch=master)](https://travis-ci.org/jsbin/pennyworth) 2 | 3 | # Pennyworth: JS Bin Processors 4 | 5 | Pennyworth is the compliment to Jobsworth, handling tasks for Dave the JS Bin bot, and generally running around like a headless chicken turning gobbledegook in to sensible code! 6 | 7 | This is the server (and sample client) to handle processors. Though most of JS Bin's processors are handled both on the client side and server side, *some* processors need to be server side only (like Sass), but also they need to be (effectively) "thread safe". 8 | 9 | This server will respond to zeromq messages with appropriate source types, and respond with the translated output. 10 | 11 | ## Creating a processor target 12 | 13 | All processors live in the `targets` directory, and are structured as so: 14 | 15 | 1. Directory name for the target processor (such as `markdown`) 16 | 2. `index.js` will be loaded by the processor server 17 | 3. `module.exports` is a function that receives `resolve`, `reject` and `data` 18 | 4. The processor must handle *both* the resolve and the reject. 19 | 5. `data` is an object structured as: 20 | 21 | ```js 22 | { 23 | language: "", // maps to target processor 24 | source: "", // source text to be processed 25 | file: "", // optional filename to create tmp files from, should be unique 26 | } 27 | ``` 28 | 29 | ### Simple example with CoffeeScript 30 | 31 | The directory structure: 32 | 33 | ```text 34 | . 35 | └── targets 36 | └── coffeescript 37 | └── index.js 38 | ``` 39 | 40 | The `package.json` for *this* project includes the `coffee-script` npm module. 41 | 42 | `index.js` contains: 43 | 44 | ```js 45 | 'use strict'; 46 | var coffeescript = require('coffee-script'); 47 | 48 | module.exports = function (resolve, reject, data) { 49 | try { 50 | var res = coffeescript.compile(data.source); 51 | resolve({ 52 | errors: null, 53 | result: res 54 | }); 55 | } catch (e) { 56 | // index starts at 0 57 | var errors = { 58 | line: parseInt(e.location.first_line, 10) || 0, 59 | ch: parseInt(e.location.first_column, 10) || 0, 60 | msg: e.message 61 | }; 62 | resolve({ 63 | errors: [errors], 64 | result: null 65 | }); 66 | } 67 | }; 68 | ``` 69 | 70 | Now the processor server can handle requests for markdown conversion. 71 | 72 | Note that the actually processor won't need to `reject`, if the processor has errors, then these are considered runtime errors and they are sent back to the requester. 73 | 74 | ### Response object 75 | 76 | Pennyworth will return a single object with `output` (from the processor) and `error` (if there's any system level errors, like timeouts): 77 | 78 | ```js 79 | { 80 | output: { 81 | result: "", 82 | errors: null, // or: [{ line: x, ch: y, msg: string }, ... ] 83 | }, 84 | error: null, // or Error object 85 | } 86 | ``` 87 | 88 | The `output` property contains data if the processor successfully returned a result (be it intended result or otherwise). The `output` object contains `result` (a string representing processed code) and `errors` an *array* of compilation errors. 89 | 90 | The compilation `errors` array contains object structured as: 91 | 92 | ```js 93 | { 94 | line: x, // integer with index starting at 0 95 | ch: y, // integer with index starting at 0, or null 96 | msg: 'string' // error message 97 | } 98 | ``` 99 | 100 | ### Ruby dependencies 101 | 102 | Ideally node is used to run each processor, but some processors (like Sass and SCSS) run using Ruby. 103 | 104 | If the processor needs a ruby gem to run, add it to `./Gemfile` to be automatically installed by `npm run-script gems` 105 | 106 | ```ruby 107 | # Pennyworth Gemfile 108 | source "https://rubygems.org" 109 | 110 | gem "compass", ">= 1.0.0.alpha.19" 111 | gem "bourbon" 112 | gem "" 113 | ``` 114 | 115 | [More information about Gemfile and Bundler](http://bundler.io/v1.3/gemfile.html) 116 | 117 | ### Tests 118 | 119 | All processor specific tests live in `test/targets//*.test.js` and can be run with `npm test`. They use [Mocha](http://visionmedia.github.io/mocha/) and [should](https://github.com/visionmedia/should.js/). 120 | 121 | The following outline is our current process for processor tests (using markdown as the example, obviously change names and extensions as appropriate): 122 | 123 | 1. Directory name for the target processor in `test/targets/markdown` 124 | 2. `markdown.test.js` will contain the tests. 125 | 3. `broken.md` contains code that will return errors. 126 | 4. `sample.md` contains working code that will succesfully parse. 127 | 5. Tests should check for at least one positive and negative outcome. 128 | 129 | We welcome more tests and ideas on how to improve this process. 130 | 131 | ## License 132 | 133 | MIT / http://jsbin.mit-license.org 134 | 135 | 136 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Process 3 | * Take a string input, with language output, and spawn a process and send back 4 | * the result. 5 | */ 6 | 'use strict'; 7 | 8 | var childProcess = require('child_process'); 9 | var spawn = childProcess.spawn; 10 | var fork = childProcess.fork; 11 | var axon = require('axon'); 12 | var Promise = require('rsvp').Promise; // jshint ignore:line 13 | var processors = require('./processors'); 14 | var log = require('./log'); 15 | var metrics = require('./metrics'); 16 | var psTree = require('ps-tree'); 17 | 18 | var port = process.env.PORT || 5555; 19 | var host = process.env.HOST; 20 | var timeout = process.env.TIMEOUT || 10000; // 10 seconds default 21 | 22 | var queue = []; 23 | 24 | if (!host) { 25 | console.error('To use pennyworth, you must specify a host to pull from:\n\n$ HOST=mysite.com node .\n\n'); 26 | process.exit(1); 27 | } 28 | 29 | var server = { 30 | start: start, 31 | stop: function () {} // assigned when we start 32 | }; 33 | 34 | module.exports = server; 35 | 36 | function start() { 37 | var local = (host === '0.0.0.0'); 38 | var responder = axon.socket('rep'); 39 | // responder.bind(port, host); 40 | if (local) { 41 | // this is a fudge to allow tests to work 42 | responder.bind(port, host); 43 | responder.once('bind', function () { 44 | log('Ready on tcp://' + host + ':' + port + '...'); 45 | server.stop = responder.close.bind(responder); 46 | bind(); 47 | }).on('connect', function () { 48 | log('new connection'); 49 | }); 50 | } else { 51 | // production based, connect to the remote port 52 | log('Trying to connect to pull from tcp://' + host + ':' + port + '...'); 53 | responder.connect(port, host); 54 | responder.once('connect', function () { 55 | server.stop = responder.close.bind(responder); 56 | bind(); 57 | }).on('connect', function () { 58 | log('Ready pulling on tcp://' + host + ':' + port + '...'); 59 | }).on('bind', function () { 60 | log('new connection'); 61 | }); 62 | } 63 | 64 | var lastSocketError = null; 65 | 66 | responder.on('socket error', function (error) { 67 | if (!lastSocketError || lastSocketError.message !== error.message) { 68 | lastSocketError = error; 69 | console.error('socket error: ' + error.message); 70 | } 71 | }).on('error', log).on('close', function () { 72 | log('closed connection'); 73 | }); 74 | 75 | function bind() { 76 | responder.on('message', function (req, reply) { 77 | queue.push({ req: req, reply: reply }); 78 | processQueue(); 79 | }); 80 | } 81 | } 82 | 83 | function processQueue() { 84 | metrics.gauge('queue.size', queue.length); 85 | if (queue.length && !queue.pending) { 86 | var next = queue.pop(); 87 | queue.pending = true; 88 | 89 | processMessage(next.req, function () { 90 | // FIXME decide whether `this` is okay to use 91 | next.reply.apply(this, arguments); 92 | queue.pending = false; 93 | processQueue(); 94 | }); 95 | } 96 | } 97 | 98 | function processMessage(req, reply) { 99 | log('message in for: ' + req.language); 100 | if (processors.has(req.language)) { 101 | // send metric increment for event.language 102 | metrics.increment(req.language + '.run'); 103 | // start timer for metric 104 | var metricTimer = metrics.createTimer(req.language + '.timer'); 105 | 106 | var p = new Promise(function (resolve, reject) { 107 | var child = fork(__dirname + '/processors'); 108 | var output = ''; 109 | 110 | var timer = setTimeout(function () { 111 | log.error(req.language + ' processor timeout'); 112 | psTree(child.pid, function (err, children) { 113 | var pids = [child.pid].concat(children.map(function (p) { 114 | return p.PID; 115 | })); 116 | spawn('kill', ['-s', 'SIGTERM'].concat(pids)); 117 | }); 118 | metrics.increment(req.language + '.timeout'); 119 | reject({ error: 'timeout', data: null }); 120 | }, timeout); 121 | 122 | child.on('error', function (data) { 123 | reject({ error: 'errors', data: data }); 124 | }); 125 | 126 | child.on('message', function (message) { 127 | output += message; 128 | }); 129 | 130 | child.on('exit', function () { 131 | clearTimeout(timer); 132 | json(output).then(function (response) { 133 | if (response.error) { 134 | reject(response); 135 | } else { 136 | resolve(response); 137 | } 138 | }).catch(function () { 139 | log.error('corrupted result from ' + req.language); 140 | }); 141 | }); 142 | 143 | child.send(req); 144 | }); 145 | 146 | p.then(function (result) { 147 | reply(result); 148 | if (result.errors === null) { 149 | metrics.increment(req.language + '.run.successful'); 150 | } else { 151 | metrics.increment(req.language + '.run.error'); 152 | } 153 | }).catch(function (error) { 154 | reply(error); 155 | metrics.increment(req.language + '.error'); 156 | }).then(function () { 157 | metricTimer.stop(); 158 | }); 159 | } 160 | } 161 | 162 | function json(str) { 163 | return new Promise(function (resolve) { 164 | resolve(JSON.parse(str)); 165 | }); 166 | } 167 | /* 168 | expects req: 169 | { language: "markdown", source: "# Heading\n\nFoo *bar*", url: "abc", revsion: 12 } 170 | { language: "scss", source: "..." } 171 | { language: "scss-compass", source: "..." } 172 | */ 173 | 174 | if (!module.parent) { 175 | start(); 176 | } 177 | -------------------------------------------------------------------------------- /test/targets/sass/sass.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before, __dirname, require */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var Promise = require('rsvp').Promise; 6 | 7 | var should = require('should'); 8 | 9 | var axon = require('axon'); 10 | var requester = axon.socket('req'); 11 | 12 | var server = require('../../../lib/server'); 13 | 14 | var language = 'sass'; 15 | var ext = '.' + language; 16 | var sample = 'sample'; 17 | var broken = 'broken'; 18 | var imp = 'import'; 19 | var output = '/../../../targets/' + language + '/output/'; 20 | 21 | describe('Sass with Compass', function () { 22 | 23 | before(function () { 24 | server.start(); 25 | requester.connect('tcp://localhost:5555'); 26 | }); 27 | 28 | 29 | // Plain 30 | it('Should process valid Sass without errors', function (done) { 31 | var fileName = sample; 32 | var check = 'result'; 33 | var ncheck = 'errors'; 34 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 35 | requester.send({ 36 | language: language, 37 | source: file.toString(), 38 | url: '_' + fileName, 39 | revision: '_' 40 | }, function (res) { 41 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 42 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 43 | (res.error === null).should.be.true; 44 | res.output[check].should.exist; 45 | (res.output[ncheck] === null).should.be.true; 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | it('Should process invalid Sass and give back an error', function (done) { 52 | var fileName = broken; 53 | var check = 'errors'; 54 | var ncheck = 'result'; 55 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 56 | requester.send({ 57 | language: language, 58 | source: file.toString(), 59 | url: '_' + fileName, 60 | revision: '_' 61 | }, function (res) { 62 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 63 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 64 | (res.error === null).should.be.true; 65 | res.output[check].should.exist; 66 | (res.output[ncheck] === null).should.be.true; 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | 73 | // @import bin 74 | it('Should process valid @import of a bin without errors', function (done) { 75 | var fileName = sample + '_' + imp; 76 | var check = 'result'; 77 | var ncheck = 'errors'; 78 | var p = new Promise(function (resolve, reject) { 79 | fs.readFile(__dirname + '/' + imp + ext, function (error, file) { 80 | requester.send({ 81 | language: language, 82 | source: file.toString(), 83 | url: '_' + imp, 84 | revision: '1' 85 | }, function(res) { 86 | if (res.error === null && res.output.result !== null) { 87 | resolve(); 88 | } else { 89 | fs.unlink(__dirname + output + 'sass/_' + imp + '.1' + ext); 90 | fs.unlink(__dirname + output + 'stylesheets/_' + imp + '.1.css'); 91 | (res.error === null).should.be.true; 92 | res.output[ncheck].should.exist; 93 | (res.output[check] === null).should.be.true; 94 | done(); 95 | } 96 | }); 97 | }); 98 | }).then(function () { 99 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 100 | requester.send({ 101 | language: language, 102 | source: file.toString(), 103 | url: '_' + fileName, 104 | revision: '_' 105 | }, function(res) { 106 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 107 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 108 | fs.unlink(__dirname + output + 'sass/_' + imp + '.1' + ext); 109 | fs.unlink(__dirname + output + 'stylesheets/_' + imp + '.1.css'); 110 | (res.error === null).should.be.true; 111 | res.output[check].should.exist; 112 | (res.output[ncheck] === null).should.be.true; 113 | done(); 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | it('Should process invalid @import of a bin and give back an error', function (done) { 120 | var fileName = broken + '_' + imp; 121 | var check = 'errors'; 122 | var ncheck = 'result'; 123 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 124 | requester.send({ 125 | language: language, 126 | source: file.toString(), 127 | url: '_' + fileName, 128 | revision: '_' 129 | }, function(res) { 130 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 131 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 132 | (res.error === null).should.be.true; 133 | res.output[check].should.exist; 134 | (res.output[ncheck] === null).should.be.true; 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | 141 | // Bourbon 142 | it('Should import Bourbon without errors', function (done) { 143 | var fileName = sample + '_bourbon'; 144 | var check = 'result'; 145 | var ncheck = 'errors'; 146 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 147 | requester.send({ 148 | language: language, 149 | source: file.toString(), 150 | url: '_' + fileName, 151 | revision: '_' 152 | }, function (res) { 153 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 154 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 155 | (res.error === null).should.be.true; 156 | res.output[check].should.exist; 157 | (res.output[ncheck] === null).should.be.true; 158 | done(); 159 | }); 160 | }); 161 | }); 162 | 163 | it('Should fail importing Bourbon and give back an error', function (done) { 164 | var fileName = broken + '_bourbon'; 165 | var check = 'errors'; 166 | var ncheck = 'result'; 167 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 168 | requester.send({ 169 | language: language, 170 | source: file.toString(), 171 | url: '_' + fileName, 172 | revision: '_' 173 | }, function (res) { 174 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 175 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 176 | (res.error === null).should.be.true; 177 | res.output[check].should.exist; 178 | (res.output[ncheck] === null).should.be.true; 179 | done(); 180 | }); 181 | }); 182 | }); 183 | 184 | // Loop 185 | it('Should process infinite loop and give back "timeout" error', function (done) { 186 | var fileName = 'loop'; 187 | var check = 'result'; 188 | var ncheck = 'errors'; 189 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 190 | requester.send({ 191 | language: language, 192 | source: file.toString(), 193 | url: '_' + fileName, 194 | revision: '_' 195 | }, function (res) { 196 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 197 | (res.error === 'timeout').should.be.true; 198 | done(); 199 | }); 200 | }); 201 | }); 202 | 203 | 204 | after(function () { 205 | requester.close(); 206 | server.stop(); 207 | }); 208 | 209 | }); 210 | -------------------------------------------------------------------------------- /test/targets/scss/scss.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, after, before, __dirname, require */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var Promise = require('rsvp').Promise; 6 | 7 | var should = require('should'); 8 | 9 | var axon = require('axon'); 10 | var requester = axon.socket('req'); 11 | 12 | var server = require('../../../lib/server'); 13 | 14 | var language = 'scss'; 15 | var ext = '.' + language; 16 | var sample = 'sample'; 17 | var broken = 'broken'; 18 | var imp = 'import'; 19 | var output = '/../../../targets/' + language + '/output/'; 20 | 21 | describe('SCSS with Compass', function () { 22 | 23 | before(function () { 24 | server.start(); 25 | requester.connect('tcp://localhost:5555'); 26 | }); 27 | 28 | 29 | // Plain 30 | it('Should process valid SCSS without errors', function (done) { 31 | var fileName = sample; 32 | var check = 'result'; 33 | var ncheck = 'errors'; 34 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 35 | requester.send({ 36 | language: language, 37 | source: file.toString(), 38 | url: '_' + fileName, 39 | revision: '_' 40 | }, function (res) { 41 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 42 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 43 | (res.error === null).should.be.true; 44 | res.output[check].should.exist; 45 | (res.output[ncheck] === null).should.be.true; 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | it('Should process invalid SCSS and give back an error', function (done) { 52 | var fileName = broken; 53 | var check = 'errors'; 54 | var ncheck = 'result'; 55 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 56 | requester.send({ 57 | language: language, 58 | source: file.toString(), 59 | url: '_' + fileName, 60 | revision: '_' 61 | }, function (res) { 62 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 63 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 64 | (res.error === null).should.be.true; 65 | res.output[check].should.exist; 66 | (res.output[ncheck] === null).should.be.true; 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | 73 | // @import bin 74 | it('Should process valid @import of a bin without errors', function (done) { 75 | var fileName = sample + '_' + imp; 76 | var check = 'result'; 77 | var ncheck = 'errors'; 78 | var p = new Promise(function (resolve, reject) { 79 | fs.readFile(__dirname + '/' + imp + ext, function (error, file) { 80 | requester.send({ 81 | language: language, 82 | source: file.toString(), 83 | url: '_' + imp, 84 | revision: '1' 85 | }, function(res) { 86 | if (res.error === null && res.output.result !== null) { 87 | resolve(); 88 | } else { 89 | fs.unlink(__dirname + output + 'sass/_' + imp + '.1' + ext); 90 | fs.unlink(__dirname + output + 'stylesheets/_' + imp + '.1.css'); 91 | (res.error === null).should.be.true; 92 | res.output[ncheck].should.exist; 93 | (res.output[check] === null).should.be.true; 94 | done(); 95 | } 96 | }); 97 | }); 98 | }).then(function () { 99 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 100 | requester.send({ 101 | language: language, 102 | source: file.toString(), 103 | url: '_' + fileName, 104 | revision: '_' 105 | }, function(res) { 106 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 107 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 108 | fs.unlink(__dirname + output + 'sass/_' + imp + '.1' + ext); 109 | fs.unlink(__dirname + output + 'stylesheets/_' + imp + '.1.css'); 110 | (res.error === null).should.be.true; 111 | res.output[check].should.exist; 112 | (res.output[ncheck] === null).should.be.true; 113 | done(); 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | it('Should process invalid @import of a bin and give back an error', function (done) { 120 | var fileName = broken + '_' + imp; 121 | var check = 'errors'; 122 | var ncheck = 'result'; 123 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 124 | requester.send({ 125 | language: language, 126 | source: file.toString(), 127 | url: '_' + fileName, 128 | revision: '_' 129 | }, function(res) { 130 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 131 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 132 | (res.error === null).should.be.true; 133 | res.output[check].should.exist; 134 | (res.output[ncheck] === null).should.be.true; 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | 141 | // Bourbon 142 | it('Should import Bourbon without errors', function (done) { 143 | var fileName = sample + '_bourbon'; 144 | var check = 'result'; 145 | var ncheck = 'errors'; 146 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 147 | requester.send({ 148 | language: language, 149 | source: file.toString(), 150 | url: '_' + fileName, 151 | revision: '_' 152 | }, function (res) { 153 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 154 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 155 | (res.error === null).should.be.true; 156 | res.output[check].should.exist; 157 | (res.output[ncheck] === null).should.be.true; 158 | done(); 159 | }); 160 | }); 161 | }); 162 | 163 | it('Should fail importing Bourbon and give back an error', function (done) { 164 | var fileName = broken + '_bourbon'; 165 | var check = 'errors'; 166 | var ncheck = 'result'; 167 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 168 | requester.send({ 169 | language: language, 170 | source: file.toString(), 171 | url: '_' + fileName, 172 | revision: '_' 173 | }, function (res) { 174 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 175 | fs.unlink(__dirname + output + 'stylesheets/_' + fileName + '._.css'); 176 | (res.error === null).should.be.true; 177 | res.output[check].should.exist; 178 | (res.output[ncheck] === null).should.be.true; 179 | done(); 180 | }); 181 | }); 182 | }); 183 | 184 | // Loop 185 | it('Should process infinite loop and give back "timeout" error', function (done) { 186 | var fileName = 'loop'; 187 | var check = 'result'; 188 | var ncheck = 'errors'; 189 | fs.readFile(__dirname + '/' + fileName + ext, function (error, file) { 190 | requester.send({ 191 | language: language, 192 | source: file.toString(), 193 | url: '_' + fileName, 194 | revision: '_' 195 | }, function (res) { 196 | fs.unlink(__dirname + output + 'sass/_' + fileName + '._' + ext); 197 | (res.error === 'timeout').should.be.true; 198 | done(); 199 | }); 200 | }); 201 | }); 202 | 203 | 204 | after(function () { 205 | requester.close(); 206 | server.stop(); 207 | }); 208 | 209 | }); 210 | --------------------------------------------------------------------------------