├── 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 | [](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 |
--------------------------------------------------------------------------------