├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── examples ├── app │ ├── controllers │ │ └── users.coffee │ ├── index.js │ └── models │ │ ├── orm.js │ │ └── user.js ├── compile.rb └── lib │ ├── jquery.js │ └── lib.coffee ├── lib ├── stitch-rb.rb ├── stitch.rb └── stitch │ ├── compiler.rb │ ├── compilers │ ├── coffeescript.rb │ ├── eco.rb │ ├── javascript.rb │ ├── mustache.rb │ └── tmpl.rb │ ├── package.rb │ ├── server.rb │ ├── source.rb │ ├── stitch.js.erb │ └── version.rb ├── stitch.gemspec └── test ├── fixtures ├── app │ ├── controllers │ │ └── users.coffee │ ├── index.js │ ├── models │ │ ├── orm.js │ │ ├── person.js │ │ └── user.js │ └── views │ │ └── index.mustache └── lib │ ├── jquery.js │ └── lib.coffee ├── jasmine ├── index.html ├── index.js └── specs.js ├── test_helper.rb ├── test_source.rb └── test_stitch.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .DS_Store 6 | test/jasmine/index.js 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in stitch.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'rake' 8 | gem 'json' 9 | gem 'coffee-script' 10 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Alex MacCaw (info@eribium.org) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stitch 2 | 3 | This is a port of Sam Stephenson's [Stitch](https://github.com/sstephenson/stitch) to Ruby. Stitch allows you to: 4 | 5 | > Develop and test your JavaScript applications as CommonJS modules in Node.js. Then __Stitch__ them together to run in the browser. 6 | 7 | In other words, this is a [CommonJS](http://dailyjs.com/2010/10/18/modules/) JavaScript package management solution. It's terribly simple, bundling up all your applications JavaScript files without intelligently resolving dependencies. However, unless your application is very modular, it turns out thats all you need. 8 | 9 | ##Usage 10 | 11 | Install the gem, or add it to your Gemfile: 12 | 13 | gem 'stitch-rb' 14 | 15 | You can compile your application like this: 16 | 17 | Stitch::Package.new(:paths => ["app"], :dependencies => ["lib/jquery.js"]).compile 18 | 19 | It returns a JavaScript file that you can serve to your users, or write to disk and cache. 20 | 21 | You should give `Stitch::Package` an array of `:paths`, the relative directories your JavaScript application is served from. You can also specify an array of `:dependencies`, JavaScript files which will be concatenated without being wrapped in CommonJS modules. 22 | 23 | ##Rails & Rack 24 | 25 | Stitch includes a basic Rack server, for example this is how you'd use it with Rails 3 routes: 26 | 27 | match '/application.js' => Stitch::Server.new(:paths => ["app/assets/javascripts"]) 28 | 29 | ##Adding compilers 30 | 31 | Compilers need to inherit from `Stitch::Compiler`. They're very simple, for example: 32 | 33 | class TmplCompiler < Stitch::Compiler 34 | # List of supported extensions 35 | extensions :tmpl 36 | 37 | # A compile method which takes a file path, 38 | # and returns a compiled string 39 | def compile(path) 40 | content = File.read(path) 41 | %{ 42 | var template = jQuery.template(#{content.to_json}); 43 | module.exports = (function(data){ return jQuery.tmpl(template, data); }); 44 | } 45 | end 46 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.test_files = FileList['test/test_helper.rb', 'test/test_*.rb'] 6 | t.libs << "." 7 | end 8 | 9 | task :default => :test -------------------------------------------------------------------------------- /examples/app/controllers/users.coffee: -------------------------------------------------------------------------------- 1 | someCoffeeScript = true -------------------------------------------------------------------------------- /examples/app/index.js: -------------------------------------------------------------------------------- 1 | require('models/user'); 2 | 3 | // Do some stuff -------------------------------------------------------------------------------- /examples/app/models/orm.js: -------------------------------------------------------------------------------- 1 | module.exports = "A ORM"; -------------------------------------------------------------------------------- /examples/app/models/user.js: -------------------------------------------------------------------------------- 1 | var ORM = require('models/orm'); 2 | 3 | var User = Dove.Class.sub(); 4 | User.extend(ORM); -------------------------------------------------------------------------------- /examples/compile.rb: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), *%w[.. lib]) 2 | 3 | require "stitch" 4 | 5 | # puts Stitch::Package.new(:paths => ["app"], :dependencies => ["lib"]).compile 6 | puts Stitch::Package.new(:files => ["./app/index.js"], :root => './app', :dependencies => ["lib"]).compile -------------------------------------------------------------------------------- /examples/lib/jquery.js: -------------------------------------------------------------------------------- 1 | // A dependency - jquery.js -------------------------------------------------------------------------------- /examples/lib/lib.coffee: -------------------------------------------------------------------------------- 1 | # Another dependency 2 | 3 | window.test = -> alert("test") -------------------------------------------------------------------------------- /lib/stitch-rb.rb: -------------------------------------------------------------------------------- 1 | # The Gem is called stitch-rb (due to a conflict), but the library is called Stitch 2 | require File.join(File.dirname(__FILE__), "stitch") -------------------------------------------------------------------------------- /lib/stitch.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "json" 3 | require "stitch/version" 4 | 5 | module Stitch 6 | autoload :Compiler, "stitch/compiler" 7 | autoload :Package, "stitch/package" 8 | autoload :Source, "stitch/source" 9 | autoload :Server, "stitch/server" 10 | end -------------------------------------------------------------------------------- /lib/stitch/compiler.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class Compiler 3 | class << self 4 | def compilers 5 | @compilers ||= [] 6 | end 7 | 8 | def inherited(child) 9 | Compiler.compilers.unshift(child) 10 | end 11 | 12 | def all 13 | Compiler.compilers.select {|c| c.enabled? } 14 | end 15 | 16 | def all_extensions 17 | Compiler.all.map {|c| c.extensions }.flatten 18 | end 19 | 20 | def for_extension(extension) 21 | extension.gsub!(/^\./, "") 22 | all.find do |item| 23 | item.extensions.include?(extension) 24 | end 25 | end 26 | 27 | # Child methods 28 | 29 | def extensions(*exts) 30 | @extensions ||= [] 31 | @extensions |= exts.map(&:to_s) 32 | end 33 | 34 | def enabled(value) 35 | @enabled = value 36 | end 37 | 38 | def enabled? 39 | @enabled != false 40 | end 41 | 42 | def source(value) 43 | @source = value 44 | end 45 | 46 | def source? 47 | @source || false 48 | end 49 | 50 | def compile(*args) 51 | self.new.compile(*args) 52 | end 53 | end 54 | 55 | def compile(filename) 56 | raise "Re-implement" 57 | end 58 | end 59 | end 60 | 61 | # Require default compilers 62 | require "stitch/compilers/javascript" 63 | require "stitch/compilers/coffeescript" 64 | require "stitch/compilers/tmpl" 65 | require "stitch/compilers/mustache" 66 | require "stitch/compilers/eco" 67 | -------------------------------------------------------------------------------- /lib/stitch/compilers/coffeescript.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class CoffeeScriptCompiler < Compiler 3 | extensions :cs, :coffee 4 | 5 | enabled begin 6 | require "coffee-script" 7 | true 8 | rescue LoadError 9 | false 10 | end 11 | 12 | source true 13 | 14 | def compile(path) 15 | source = File.read(path) 16 | CoffeeScript.compile(source, :filename => path.to_s) 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/stitch/compilers/eco.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class EcoCompiler < Compiler 3 | extensions :eco 4 | 5 | enabled begin 6 | require "eco" 7 | true 8 | rescue LoadError 9 | false 10 | end 11 | 12 | source true 13 | 14 | def compile(path) 15 | source = File.read(path) 16 | %{module.exports = #{Eco.compile(source)}} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/stitch/compilers/javascript.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class JavaScriptCompiler < Compiler 3 | extensions :js 4 | 5 | source true 6 | 7 | def compile(path) 8 | File.read(path) 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/stitch/compilers/mustache.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class MustacheCompiler < Compiler 3 | extensions :mustache 4 | 5 | def compile(path) 6 | content = File.read(path) 7 | %{var template = #{content.to_json}; 8 | module.exports = function(view){ 9 | return Mustache.to_html(template, view); 10 | };} 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/stitch/compilers/tmpl.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class TmplCompiler < Compiler 3 | extensions :tmpl 4 | 5 | def compile(path) 6 | content = File.read(path) 7 | %{var template = jQuery.template(#{content.to_json}); 8 | module.exports = function(data){ 9 | return jQuery.tmpl(template, data); 10 | };} 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/stitch/package.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | 3 | module Stitch 4 | class Package 5 | DEFAULTS = { 6 | :identifier => "require", 7 | :paths => [], 8 | :files => [], 9 | :dependencies => [] 10 | } 11 | 12 | def initialize(options = {}) 13 | options = DEFAULTS.merge(options) 14 | 15 | @identifier = options[:identifier] 16 | @paths = Array(options[:paths]) 17 | @files = Array(options[:files]) 18 | @root = options[:root] 19 | @dependencies = Array(options[:dependencies]) 20 | end 21 | 22 | def compile 23 | [compile_dependencies, compile_sources].join("\n") 24 | end 25 | 26 | protected 27 | def compile_dependencies 28 | @dependencies.map {|path| 29 | Source.from_path(path) 30 | }.flatten.map { |dep| 31 | dep.compile 32 | }.join("\n") 33 | end 34 | 35 | def compile_sources 36 | sources = @paths.map {|path| 37 | Source.from_path(path) 38 | }.flatten 39 | 40 | sources |= @files.map {|file| 41 | Source.from_file(@root, file) 42 | }.flatten 43 | 44 | sources.uniq! 45 | 46 | if sources.any? 47 | stitch(sources) 48 | end 49 | end 50 | 51 | def stitch(modules) 52 | # ERB binding variables 53 | identifier = @identifier 54 | modules = modules 55 | 56 | template = File.read(File.join(File.dirname(__FILE__), "stitch.js.erb")) 57 | template = ERB.new(template) 58 | template.result(binding) 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /lib/stitch/server.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | class Server 3 | def initialize(options = {}) 4 | @package = Package.new(options) 5 | end 6 | 7 | def call(env) 8 | [200, {"Content-Type" => "text/javascript"}, [@package.compile]] 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/stitch/source.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module Stitch 4 | class Source 5 | # Recursively load all the sources from a given directory 6 | # Usage: 7 | # sources = Source.from_path("./app") 8 | # 9 | def self.from_path(root, path = nil, result = []) 10 | path ||= root 11 | path = Pathname.new(path) 12 | 13 | if path.directory? 14 | path.children.each do |child| 15 | from_path(root, child, result) 16 | end 17 | else 18 | source = self.new(root, path) 19 | result << source if source.valid? 20 | end 21 | result 22 | end 23 | 24 | # Recursively resolve sources from a given file, 25 | # dynamically resolving its dependencies 26 | # Usage: 27 | # sources = Source.from_file("./app/index.js") 28 | # 29 | def self.from_file(root, path = nil, result = []) 30 | root = Pathname.new(root) 31 | 32 | unless path 33 | path = root 34 | root = root.dirname 35 | end 36 | 37 | source = self.new(root, path) 38 | source.requires.each do |child| 39 | from_file(root, child, result) 40 | end 41 | 42 | result << source 43 | end 44 | 45 | # Resolve a require call to an absolute path 46 | # Usage: 47 | # path = Source.resolve("../index.js", "/my/file.js") 48 | # 49 | def self.resolve(path, relative_to) 50 | path = Pathname.new(path) 51 | relative_to = Pathname.new(relative_to) 52 | 53 | unless path.absolute? 54 | path = path.expand_path(relative_to) 55 | end 56 | 57 | return path if path.exist? 58 | 59 | Compiler.all_extensions.each do |ext| 60 | candidate = Pathname.new(path.to_s + "." + ext) 61 | return candidate if candidate.exist? 62 | end 63 | 64 | raise "#{path} not found" 65 | end 66 | 67 | attr_reader :root, :path 68 | 69 | def initialize(root, path) 70 | @root = Pathname.new(root).expand_path 71 | @path = Pathname.new(path).expand_path 72 | end 73 | 74 | def name 75 | name = path.relative_path_from(root) 76 | name = name.dirname + name.basename(".*") 77 | name.to_s 78 | end 79 | 80 | def ext 81 | path.extname 82 | end 83 | 84 | def compile 85 | compiler.compile(path) 86 | end 87 | 88 | def valid? 89 | !!compiler 90 | end 91 | 92 | # Return an array of resolved paths 93 | # specifying this source's dependencies 94 | def requires 95 | return [] unless source? 96 | requires = path.read.scan(/require\(("|')(.+)\1\)/) 97 | requires.map {|(_, pn)| self.class.resolve(pn, root) } 98 | end 99 | 100 | def hash 101 | self.path.hash 102 | end 103 | 104 | def eql?(source) 105 | source.is_a?(Source) && 106 | source.path.to_s == self.path.to_s 107 | end 108 | 109 | protected 110 | def source? 111 | valid? && compiler.source? 112 | end 113 | 114 | def compiler 115 | @compiler ||= Compiler.for_extension(ext) 116 | end 117 | end 118 | end -------------------------------------------------------------------------------- /lib/stitch/stitch.js.erb: -------------------------------------------------------------------------------- 1 | (function(/*! Stitch !*/) { 2 | if (!this.<%= identifier %>) { 3 | var modules = {}, cache = {}; 4 | var require = function(name, root) { 5 | var path = expand(root, name), indexPath = expand(path, './index'), module, fn; 6 | module = cache[path] || cache[indexPath]; 7 | if (module) { 8 | return module; 9 | } else if (fn = modules[path] || modules[path = indexPath]) { 10 | module = {id: path, exports: {}}; 11 | cache[path] = module.exports; 12 | fn(module.exports, function(name) { 13 | return require(name, dirname(path)); 14 | }, module); 15 | return cache[path] = module.exports; 16 | } else { 17 | throw 'module ' + name + ' not found'; 18 | } 19 | }; 20 | var expand = function(root, name) { 21 | var results = [], parts, part; 22 | // If path is relative 23 | if (/^\.\.?(\/|$)/.test(name)) { 24 | parts = [root, name].join('/').split('/'); 25 | } else { 26 | parts = name.split('/'); 27 | } 28 | for (var i = 0, length = parts.length; i < length; i++) { 29 | part = parts[i]; 30 | if (part == '..') { 31 | results.pop(); 32 | } else if (part != '.' && part != '') { 33 | results.push(part); 34 | } 35 | } 36 | return results.join('/'); 37 | }; 38 | var dirname = function(path) { 39 | return path.split('/').slice(0, -1).join('/'); 40 | }; 41 | this.<%= identifier %> = function(name) { 42 | return require(name, ''); 43 | }; 44 | this.<%= identifier %>.define = function(bundle) { 45 | for (var key in bundle) { 46 | modules[key] = bundle[key]; 47 | } 48 | }; 49 | this.<%= identifier %>.modules = modules; 50 | this.<%= identifier %>.cache = cache; 51 | } 52 | return this.<%= identifier %>.define; 53 | }).call(this)({ 54 | <%= 55 | modules.map do |mod| 56 | "#{mod.name.to_json}: function(exports, require, module) {\n#{mod.compile}\n}" 57 | end.join(', ') 58 | %> 59 | }); -------------------------------------------------------------------------------- /lib/stitch/version.rb: -------------------------------------------------------------------------------- 1 | module Stitch 2 | VERSION = "0.0.8" 3 | end -------------------------------------------------------------------------------- /stitch.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "stitch/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "stitch-rb" 7 | s.version = Stitch::VERSION 8 | s.authors = ["Alex MacCaw"] 9 | s.email = ["maccman@gmail.com"] 10 | s.homepage = "" 11 | s.summary = %q{Stitch ported to Ruby} 12 | s.description = %q{A JavaScript package manager} 13 | 14 | s.files = `git ls-files`.split("\n") 15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 17 | s.require_paths = ["lib"] 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/app/controllers/users.coffee: -------------------------------------------------------------------------------- 1 | someCoffeeScript = true -------------------------------------------------------------------------------- /test/fixtures/app/index.js: -------------------------------------------------------------------------------- 1 | require('models/user'); 2 | require('models/person'); 3 | 4 | // Do some stuff -------------------------------------------------------------------------------- /test/fixtures/app/models/orm.js: -------------------------------------------------------------------------------- 1 | module.exports = {orm: true}; 2 | 3 | window.ormCount = window.ormCount || 0; 4 | window.ormCount += 1; -------------------------------------------------------------------------------- /test/fixtures/app/models/person.js: -------------------------------------------------------------------------------- 1 | var ORM = require('models/orm'); -------------------------------------------------------------------------------- /test/fixtures/app/models/user.js: -------------------------------------------------------------------------------- 1 | var ORM = require('models/orm'); 2 | 3 | var User = function(name){ 4 | this.name = name; 5 | }; 6 | 7 | User.ORM = ORM; 8 | 9 | module.exports = User; -------------------------------------------------------------------------------- /test/fixtures/app/views/index.mustache: -------------------------------------------------------------------------------- 1 | {{name}} -------------------------------------------------------------------------------- /test/fixtures/lib/jquery.js: -------------------------------------------------------------------------------- 1 | // A dependency - jquery.js -------------------------------------------------------------------------------- /test/fixtures/lib/lib.coffee: -------------------------------------------------------------------------------- 1 | # Another dependency 2 | 3 | window.test = -> alert("test") -------------------------------------------------------------------------------- /test/jasmine/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |