helium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'helium' do |p|
7
+ p.developer('James Coglan', 'james.coglan@othermedia.com')
8
+ p.extra_deps = [['grit','>= 0'], ['jake','>= 1.0.1'], ['packr','>= 3.1'],
9
+ ['oyster','>= 0.9.3'], ['sinatra','>= 0.9.4'], ['rack','>= 1.0']]
10
+ end
11
+
12
+ # vim: syntax=ruby
data/bin/he ADDED
@@ -0,0 +1,126 @@
1
+ #! /usr/bin/env ruby
2
+ $VERBOSE = nil
3
+ home = File.join(File.dirname(__FILE__), '..')
4
+
5
+ require 'fileutils'
6
+ require 'rubygems'
7
+ require 'oyster'
8
+ require 'jake'
9
+ require 'rack'
10
+
11
+ require File.join(home, 'lib', 'helium')
12
+
13
+ spec = Oyster.spec do
14
+ name 'helium -- the Git-backed JavaScript package server'
15
+ author 'James Coglan <jcoglan@googlemail.com>'
16
+
17
+ description <<-EOS
18
+ Helium is a web application for deploying and serving versioned copies of
19
+ JavaScript libraries from Git repositories. It uses Jake to build libraries
20
+ and extract dependency data, generating a manifest for JS.Packages so that
21
+ client sites can load objects from Helium on demand.
22
+
23
+ The following command line tools are available; run each one with `--help`
24
+ for more information.
25
+
26
+ `he install` creates a copy of the web frontend that you can serve using
27
+ Rack and Passenger.
28
+
29
+ `he create` generates a stub JavaScript project with all the files required
30
+ for Helium to deploy it.
31
+
32
+ `he serve` starts a web server to serve static files from any directory,
33
+ useful for testing code that requires a domain name to function.
34
+ EOS
35
+
36
+ subcommand :install do
37
+ synopsis 'he install DIRECTORY'
38
+
39
+ description <<-EOS
40
+ Installs a copy of the Helium web application in the given directory.
41
+ Set up an Apache VHost with the DocumentRoot pointing to DIRECTORY/public
42
+ to serve the app through Phusion Passenger.
43
+ EOS
44
+ end
45
+
46
+ subcommand :create do
47
+ synopsis 'he create NAME'
48
+
49
+ description <<-EOS
50
+ Creates a new JavaScript project with the given name, containing a stub
51
+ library file, a jake.yml config file and support code for generating
52
+ local package listings and running tests.
53
+ EOS
54
+ end
55
+
56
+ subcommand :serve do
57
+ synopsis 'he serve DIRECTORY'
58
+
59
+ description <<-EOS
60
+ Starts a webserver on port 8000 that serves the given directory over HTTP.
61
+ Useful for testing pages that require Ajax or services with domain specific
62
+ API keys like Google Maps.
63
+ EOS
64
+ end
65
+ end
66
+
67
+ begin; options = spec.parse
68
+ rescue; exit
69
+ end
70
+
71
+ if opts = options[:install]
72
+ dir = opts[:unclaimed].first
73
+ if dir.nil?
74
+ puts "Installation directory required -- type `he install --help` for more info"
75
+ exit
76
+ end
77
+
78
+ dir = File.expand_path(dir)
79
+ d = File.basename(dir)
80
+
81
+ puts "\nInstalling Helium app in #{dir}..."
82
+ Helium.generate('web', dir)
83
+
84
+ puts "\n... done, now set up your webserver to serve the app:\n\n"
85
+ puts " * Point Apache at #{d}/public to serve using Passenger"
86
+ puts " * Run `rackup #{d}/config.ru` to run it locally\n\n"
87
+
88
+ elsif opts = options[:create]
89
+ name = opts[:unclaimed].first
90
+ if name.nil?
91
+ puts "Project name required -- type `he create --help` for more info"
92
+ exit
93
+ end
94
+
95
+ dir = File.expand_path(name)
96
+ d = File.basename(dir)
97
+
98
+ puts "\nGenerating JavaScript project in #{dir}..."
99
+ Helium.generate('project', dir, :name => name)
100
+
101
+ puts "\nBuilding project using Jake..."
102
+ jake_hook(:file_created) do |build, package, type, path|
103
+ puts "create #{d}#{ path.sub(dir, '') }"
104
+ end
105
+ Jake.build!(dir)
106
+
107
+ puts "\n... done, now your new JavaScript project is ready.\n\n"
108
+ puts " * Build your project by running `jake` in the root directory"
109
+ puts " * We've added generated files to your .gitignore"
110
+ puts " * Keep your dependencies up-to-date in jake.yml"
111
+ puts " * Point test/index.html at your Helium server and write some tests!\n\n"
112
+
113
+ elsif opts = options[:serve]
114
+ dir = File.expand_path(opts[:unclaimed].first || '.')
115
+ app = Rack::File.new(dir)
116
+
117
+ server = %w[thin mongrel webrick].map { |name|
118
+ begin; Rack::Handler.get(name)
119
+ rescue LoadError; nil
120
+ end
121
+ }.compact.first
122
+
123
+ puts "Serving contents of #{dir} on port 8000..."
124
+ server.run(app, :Port => 8000)
125
+ end
126
+
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'fileutils'
5
+ require 'find'
6
+
7
+ require 'rubygems'
8
+ require 'grit'
9
+ require 'jake'
10
+ require 'packr'
11
+ require 'oyster'
12
+
13
+ module Helium
14
+
15
+ VERSION = '0.1.0'
16
+
17
+ ROOT = File.expand_path(File.dirname(__FILE__))
18
+ TEMPLATES = File.join(ROOT, '..', 'templates')
19
+ ERB_EXT = '.erb'
20
+ JS_CONFIG_TEMPLATE = File.join(TEMPLATES, 'packages.js.erb')
21
+
22
+ CONFIG_FILE = 'deploy.yml'
23
+ REPOS = 'repos'
24
+ STATIC = 'static'
25
+ PACKAGES = 'helium-src.js'
26
+ PACKAGES_MIN = 'helium.js'
27
+ WEB_ROOT = 'js'
28
+
29
+ GIT = '.git'
30
+ HEAD = 'HEAD'
31
+
32
+ JS_CLASS = 'js.class'
33
+ LOADER_FILE = 'loader.js'
34
+ JAKE_FILE = Jake::CONFIG_FILE
35
+
36
+ SEP = File::SEPARATOR
37
+ BYTE = 1024.0
38
+
39
+ ERB_TRIM_MODE = '-'
40
+
41
+ %w[trie configurable deployer generator logger].each do |file|
42
+ require File.join(ROOT, 'helium', file)
43
+ end
44
+
45
+ def self.generate(template, dir, options = {})
46
+ Generator.new(template, dir, options).run!
47
+ end
48
+
49
+ end
50
+
@@ -0,0 +1,26 @@
1
+ module Helium
2
+ module Configurable
3
+
4
+ def configure
5
+ yield(configuration)
6
+ end
7
+
8
+ def configuration
9
+ @configuration ||= Configuration.new
10
+ end
11
+ alias :config :configuration
12
+
13
+ class Configuration
14
+ def initialize(hash = {})
15
+ @options = {}.merge(hash)
16
+ end
17
+
18
+ def method_missing(name, value = nil)
19
+ @options[name] = value unless value.nil?
20
+ @options[name]
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
@@ -0,0 +1,216 @@
1
+ module Helium
2
+ # The +Deployer+ class is responsible for performing all of Helium's central functions:
3
+ # downloading projects from Git, exporting static copies of branches, building projects
4
+ # using Jake and generating the dependency file.
5
+ #
6
+ # To run, the class requires a file called `deploy.yml` in the target directory, listing
7
+ # projects with their Git URLs. (See tests and README for examples.)
8
+ #
9
+ class Deployer
10
+ include Observable
11
+
12
+ # Initialize using the directory containing the `deploy.yml` file, and the name of
13
+ # the directory to export repositories and static files to.
14
+ #
15
+ # deployer = Helium::Deployer.new('path/to/app', 'js')
16
+ #
17
+ def initialize(path, output_dir = 'output', options = {})
18
+ @path = File.expand_path(path)
19
+ @config = join(@path, CONFIG_FILE)
20
+ raise "Expected config file at #{@config}" unless File.file?(@config)
21
+ @output_dir = join(@path, output_dir)
22
+ @options = options
23
+ end
24
+
25
+ # Returns a hash of projects names and their Git URLs.
26
+ def projects
27
+ return @projects if defined?(@projects)
28
+
29
+ data = YAML.load(File.read(@config))
30
+ raise "No configuration for JS.Class" unless js_class = data[JS_CLASS]
31
+ @jsclass_version = js_class['version']
32
+
33
+ @projects = data['projects'] || {}
34
+ @projects[JS_CLASS] = js_class['repository']
35
+ @projects
36
+ end
37
+
38
+ # Runs all the deploy actions. If given a project name, only checks out and builds
39
+ # the given project, otherwise builds all projects in `deploy.yml`.
40
+ def run!(project = nil)
41
+ return deploy!(project) if project
42
+ projects.each { |project, url| deploy!(project, false) }
43
+ run_builds!
44
+ end
45
+
46
+ # Deploys an individual project by checking it out from Git, exporting static copies
47
+ # of all its branches and building them using Jake.
48
+ def deploy!(project, build = true)
49
+ checkout(project)
50
+ export(project)
51
+ run_builds! if build
52
+ end
53
+
54
+ # Checks out (or updates if already checked out) a project by name from its Git
55
+ # repository. If the project is new, we use `git clone` to copy it, otherwise
56
+ # we use `git fetch` to update it.
57
+ def checkout(project)
58
+ dir = repo_dir(project)
59
+ if File.directory?(join(dir, GIT))
60
+ log :git_fetch, "Updating Git repo in #{ dir }"
61
+ cd(dir) { `git fetch origin` }
62
+ else
63
+ url = projects[project]
64
+ log :git_clone, "Cloning Git repo #{ url } into #{ dir }"
65
+ `git clone #{ url } "#{ dir }"`
66
+ end
67
+ end
68
+
69
+ # Exports static copies of a project from every branch and tag in its Git repository.
70
+ # Existing static copies on disk are destroyed and replaced.
71
+ def export(project)
72
+ repo_dir = repo_dir(project)
73
+ repo = Grit::Repo.new(repo_dir)
74
+ branches = repo.remotes + repo.tags
75
+
76
+ mkdir_p(static_dir(project))
77
+
78
+ branches.each do |branch|
79
+ name, commit = branch.name.split(SEP).last, branch.commit.id
80
+ next if HEAD == name
81
+
82
+ target = static_dir(project, commit)
83
+
84
+ log :export, "Exporting branch '#{ name }' of '#{ project }' into #{ target }"
85
+ rm_rf(target) if File.directory?(target)
86
+ cp_r(repo_dir, target)
87
+
88
+ cd(target) {
89
+ if repo.branches.map { |b| b.name }.include?(name)
90
+ `git checkout #{ name }`
91
+ `git merge #{ branch.name }`
92
+ else
93
+ `git checkout -b #{ name } #{ branch.name }`
94
+ end
95
+ }
96
+ end
97
+ end
98
+
99
+ # Scans all the checked-out projects for Jake build files and builds those projects
100
+ # that have such a file. As the build progresses we use Jake event hooks to collect
101
+ # dependencies and generated file paths, and when all builds are finished we generate
102
+ # a JS.Packages file listing all the files discovered. This file should be included
103
+ # in web pages to set up the the packages manager for loading our projects.
104
+ def run_builds!(options = nil)
105
+ options ||= @options
106
+
107
+ @tree = Trie.new
108
+ @custom = options[:custom]
109
+ @domain = options[:domain]
110
+ manifest = []
111
+
112
+ # Loop over checked-out projects. Skip directories with no Jake file.
113
+ Find.find(static_dir) do |path|
114
+ next unless File.directory?(path) and File.file?(join(path, JAKE_FILE))
115
+
116
+ project, commit = *path.split(SEP)[-2..-1]
117
+ branch = Grit::Repo.new(path).head.name
118
+ Jake.clear_hooks!
119
+
120
+ # Event listener to capture file information from Jake
121
+ hook = lambda do |build, package, build_type, file|
122
+ if build_type == :min
123
+ @js_loader = file if File.basename(file) == LOADER_FILE and
124
+ project == JS_CLASS and
125
+ branch == @jsclass_version
126
+
127
+ file = file.sub(path, '')
128
+ manifest << join(project, commit, file)
129
+ @tree[[project, branch]] = commit
130
+ @tree[[project, branch, file]] = package.meta
131
+ end
132
+ end
133
+ jake_hook(:file_created, &hook)
134
+ jake_hook(:file_not_changed, &hook)
135
+
136
+ log :jake_build, "Building branch '#{ branch }' of '#{ project }' from #{ join(path, JAKE_FILE) }"
137
+
138
+ begin; Jake.build!(path)
139
+ rescue; end
140
+ end
141
+
142
+ generate_manifest!
143
+ manifest + [PACKAGES, PACKAGES_MIN]
144
+ end
145
+
146
+ # Removes any repositories and static files for projects not listed in the the
147
+ # `deploy.yml` file.
148
+ def cleanup!
149
+ [repo_dir, static_dir].each do |dir|
150
+ next unless File.directory?(dir)
151
+ (Dir.entries(dir) - %w[. ..]).each do |entry|
152
+ path = join(dir, entry)
153
+ next unless File.directory?(path)
154
+ rm_rf(path) unless projects.has_key?(entry)
155
+ end
156
+ end
157
+ end
158
+
159
+ # Returns the path to the Git repository for a given project.
160
+ def repo_dir(project = nil)
161
+ path = [@output_dir, REPOS, project].compact
162
+ join(*path)
163
+ end
164
+
165
+ # Returns the path to the static export directory for a given project and branch.
166
+ def static_dir(project = nil, branch = nil)
167
+ path = [@output_dir, STATIC, project, branch].compact
168
+ join(*path)
169
+ end
170
+
171
+ private
172
+
173
+ # Generates JS.Packages dependency file from ERB template and compresses the result
174
+ def generate_manifest!
175
+ template = File.read(JS_CONFIG_TEMPLATE)
176
+ code = ERB.new(template, nil, ERB_TRIM_MODE).result(binding)
177
+ packed = Packr.pack(code, :shrink_vars => true)
178
+
179
+ mkdir_p(static_dir)
180
+ File.open(static_dir(PACKAGES), 'w') { |f| f.write(code) }
181
+ File.open(static_dir(PACKAGES_MIN), 'w') { |f| f.write(packed) }
182
+ end
183
+
184
+ # Returns +true+ iff the set of files contains any dependency data.
185
+ def has_manifest?(config)
186
+ case config
187
+ when Trie then config.any? { |path, conf| has_manifest?(conf) }
188
+ when Hash then config.has_key?(:provides)
189
+ else nil
190
+ end
191
+ end
192
+
193
+ # Notifies observers by sending a log message.
194
+ def log(*args)
195
+ changed(true)
196
+ notify_observers(*args)
197
+ end
198
+
199
+ # Shorthand for <tt>File.join</tt>
200
+ def join(*args)
201
+ File.join(*args)
202
+ end
203
+
204
+ # We use +method_missing+ to create shorthands for +FileUtils+ methods.
205
+ def method_missing(*args, &block)
206
+ FileUtils.__send__(*args, &block)
207
+ end
208
+
209
+ def `(command)
210
+ puts command
211
+ system(command)
212
+ end
213
+
214
+ end
215
+ end
216
+
@@ -0,0 +1,69 @@
1
+ module Helium
2
+ # The +Generator+ class is used by the command-line tools to create copies of
3
+ # the web app and the JavaScript project template. It copies the contents of
4
+ # one of the +template+ directories into a local directory, expanding any files
5
+ # with .erb extensions as ERB templates.
6
+ #
7
+ # For example, a file <tt>jake.yml.erb</tt> will be copied into the target dir as
8
+ # <tt>jake.yml</tt> after being evaluated using ERB.
9
+ #
10
+ # If a filename contains variable names enclosed in double-underscores, the
11
+ # resulting copy will have those replaced by the value of the named instance
12
+ # variable. For example, <tt>__name__.js</tt> will be copied to <tt>myproj.js</tt>
13
+ # if <tt>@name = 'myproj'</tt>.
14
+ #
15
+ class Generator
16
+
17
+ # Generators are initialized using the name of the template (a collection of
18
+ # files in the +templates+ directory, a target directory and an option hash.
19
+ # Keys in the option hash become instance variables accessible to ERB templates.
20
+ def initialize(template, dir, options = {})
21
+ options.each do |key, value|
22
+ instance_variable_set("@#{key}", value)
23
+ end
24
+ @_source = join(TEMPLATES, template)
25
+ @_directory = expand_path(dir)
26
+ end
27
+
28
+ # Runs the generator, copying all files as required. All ERB/name replacement
29
+ # is handled in this method.
30
+ def run!
31
+ Find.find(@_source) do |path|
32
+ next unless file?(path)
33
+ content = read(path)
34
+
35
+ # Replace variable names in file paths
36
+ path = path.sub(@_source, '').gsub(/__(.+?)__/) { instance_variable_get("@#{$1}") }
37
+ target = join(@_directory, path)
38
+
39
+ # Evaluate using ERB if required
40
+ if extname(path) == ERB_EXT
41
+ content = ERB.new(content).result(binding)
42
+ target = join(dirname(target), basename(target, ERB_EXT))
43
+ end
44
+
45
+ # Generate destination file
46
+ FileUtils.mkdir_p(dirname(target))
47
+ open(target, 'w') { |f| f.write(content) }
48
+ puts "create #{ basename(@_directory) }#{ target.sub(@_directory, '') }"
49
+ end
50
+ end
51
+
52
+ # Provide shorthand access to all +File+ methods.
53
+ def method_missing(*args, &block)
54
+ File.__send__(*args, &block)
55
+ end
56
+
57
+ # Returns a camelcased copy of the string, for example:
58
+ #
59
+ # camelize('my-project')
60
+ # #=> 'MyProject'
61
+ #
62
+ def camelize(string)
63
+ string.gsub(/^(.)/) { $1.upcase }.
64
+ gsub(/[\s\-\_](.)/) { $1.upcase }
65
+ end
66
+
67
+ end
68
+ end
69
+