helium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+