helium 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/LICENCE +339 -0
- data/Manifest.txt +35 -0
- data/README.txt +266 -0
- data/Rakefile +12 -0
- data/bin/he +126 -0
- data/lib/helium.rb +50 -0
- data/lib/helium/configurable.rb +26 -0
- data/lib/helium/deployer.rb +216 -0
- data/lib/helium/generator.rb +69 -0
- data/lib/helium/jake.rb +73 -0
- data/lib/helium/logger.rb +15 -0
- data/lib/helium/trie.rb +59 -0
- data/lib/helium/views/deploy.erb +13 -0
- data/lib/helium/views/edit.erb +11 -0
- data/lib/helium/views/index.erb +67 -0
- data/lib/helium/views/layout.erb +60 -0
- data/lib/helium/views/missing.erb +6 -0
- data/lib/helium/web.rb +119 -0
- data/lib/helium/web_helpers.rb +65 -0
- data/templates/packages.js.erb +126 -0
- data/templates/project/.gitignore +4 -0
- data/templates/project/Jakefile +3 -0
- data/templates/project/jake.yml.erb +23 -0
- data/templates/project/source/__name__.js.erb +14 -0
- data/templates/project/test/index.html.erb +29 -0
- data/templates/web/config.ru +11 -0
- data/templates/web/custom.js +37 -0
- data/templates/web/deploy.yml +8 -0
- data/templates/web/public/prettify.css +6 -0
- data/templates/web/public/prettify.js +23 -0
- data/templates/web/public/style.css +40 -0
- data/test/deploy.yml +8 -0
- data/test/index.html +50 -0
- data/test/test_helium.rb +21 -0
- metadata +160 -0
data/lib/helium/jake.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# This file is loaded by Jake for projects created using the Helium command
|
2
|
+
# line tools. It copies build files into the test directory and generates a
|
3
|
+
# JS.Packages file for the project so that the user can test that their
|
4
|
+
# dependencies are correctly configured.
|
5
|
+
|
6
|
+
module Helium
|
7
|
+
module JakeBuildHelper
|
8
|
+
|
9
|
+
LIB = 'lib'
|
10
|
+
|
11
|
+
TEST_DIR = File.join(PROJECT_DIR, 'test')
|
12
|
+
PUBLIC_DIR = File.join(TEST_DIR, 'public')
|
13
|
+
|
14
|
+
# If test/public exists, we generate files there. Otherwise
|
15
|
+
# we just put generated files in the test directory.
|
16
|
+
TARGET_DIR = File.directory?(PUBLIC_DIR) ? PUBLIC_DIR : TEST_DIR
|
17
|
+
LIB_DIR = File.join(TARGET_DIR, LIB)
|
18
|
+
|
19
|
+
PACKAGES = ERB.new(<<-EOS, nil, '-')
|
20
|
+
// This file is automatically generated when you run `jake`. You should
|
21
|
+
// keep it out of your version control system.
|
22
|
+
|
23
|
+
// Maintain your project's dependencies in jake.yml; they will be
|
24
|
+
// reflected here when you run a build.
|
25
|
+
|
26
|
+
JS.Packages(function() { with(this) {
|
27
|
+
<% files.each do |path, meta| %>
|
28
|
+
file('./<%= LIB %><%= path %>')
|
29
|
+
.provides(<%= list(meta[:provides]) %>)
|
30
|
+
.requires(<%= list(meta[:requires]) %>)
|
31
|
+
.uses(<%= list(meta[:uses]) %>);
|
32
|
+
<% end -%>
|
33
|
+
}});
|
34
|
+
EOS
|
35
|
+
|
36
|
+
def self.list(array)
|
37
|
+
(array || []).map { |s| s.inspect } * ', '
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.short_path(path)
|
41
|
+
path.sub(PROJECT_DIR, '.')
|
42
|
+
end
|
43
|
+
|
44
|
+
files = {}
|
45
|
+
|
46
|
+
update = lambda do |build, package, build_type, path|
|
47
|
+
puts "created: #{path}"
|
48
|
+
files[path.sub(build.build_directory, '')] = package.meta if build_type == :min
|
49
|
+
end
|
50
|
+
|
51
|
+
jake_hook(:file_created, &update)
|
52
|
+
jake_hook(:file_not_changed, &update)
|
53
|
+
|
54
|
+
jake_hook :build_complete do |build|
|
55
|
+
FileUtils.rm_rf(LIB_DIR) if File.exists?(LIB_DIR)
|
56
|
+
|
57
|
+
files.each do |path, meta|
|
58
|
+
source = File.join(build.build_directory, path)
|
59
|
+
target = File.join(LIB_DIR, path)
|
60
|
+
FileUtils.mkdir_p(File.dirname(target))
|
61
|
+
puts "Copying #{ short_path(source) } --> #{ short_path(target) }"
|
62
|
+
FileUtils.cp(source, target)
|
63
|
+
end
|
64
|
+
|
65
|
+
packages = PACKAGES.result(binding)
|
66
|
+
pkg_file = File.join(TARGET_DIR, 'packages.js')
|
67
|
+
puts "Writing package listing to #{ short_path(pkg_file) }"
|
68
|
+
File.open(pkg_file, 'w') { |f| f.write(packages) }
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Helium
|
2
|
+
# Class to pick up log messages from the build process so we can display
|
3
|
+
# them elsewhere, e.g. in web pages after deploy requests.
|
4
|
+
class Logger
|
5
|
+
attr_reader :messages
|
6
|
+
def initialize
|
7
|
+
@messages = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(type, msg)
|
11
|
+
@messages << msg
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/lib/helium/trie.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Helium
|
2
|
+
class Trie
|
3
|
+
include Enumerable
|
4
|
+
attr_accessor :value
|
5
|
+
|
6
|
+
def initialize(value = nil)
|
7
|
+
@value = value
|
8
|
+
@children = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def each_child(&block)
|
12
|
+
@children.keys.sort.each { |key| block.call(key, @children[key]) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(key = [], &block)
|
16
|
+
each_child { |prefix, trie| trie.each(key + [prefix], &block) }
|
17
|
+
block.call(key, @value) unless @value.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_key?(key)
|
21
|
+
trie = traverse(key)
|
22
|
+
trie and not trie.value.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
trie = traverse(key)
|
27
|
+
trie ? trie.value : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
traverse(key, true).value = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def traverse(key, create_if_absent = false)
|
35
|
+
key = convert_key(key)
|
36
|
+
return self if key.empty?
|
37
|
+
trie = @children[key.first]
|
38
|
+
return nil if trie.nil? and not create_if_absent
|
39
|
+
trie = @children[key.first] = Trie.new if trie.nil?
|
40
|
+
trie.traverse(key[1..-1], create_if_absent)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
@children.inspect
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def convert_key(key)
|
50
|
+
case key
|
51
|
+
when Array then key
|
52
|
+
when Symbol then key.to_s.split('')
|
53
|
+
when String then key.split('')
|
54
|
+
when Enumerable then key.entries
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<h2>Edit: <%=h File.basename(@file) %></h2>
|
2
|
+
|
3
|
+
<% if @error %>
|
4
|
+
<p class="error"><%=h @error %></p>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<form action="/app/<%=h @action %>" method="post">
|
8
|
+
<textarea name="contents" <%= disabled? %>><%=h @contents %></textarea>
|
9
|
+
<input type="submit" value="Save" <%= disabled? %>>
|
10
|
+
</form>
|
11
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
<h2>Helium, the JavaScript package server</h2>
|
2
|
+
|
3
|
+
<p>Helium hosts versioned copies of JavaScript libraries checked out from Git,
|
4
|
+
and lets you load objects from these libraries on demand. It uses
|
5
|
+
<a href="http://github.com/jcoglan/jake">Jake</a> to build each library and extract
|
6
|
+
dependency data, and <a href="http://jsclass.jcoglan.com/packages.html">JS.Packages</a>
|
7
|
+
to resolve dependencies and load files at runtime. The
|
8
|
+
<a href="/<%= Helium::WEB_ROOT %>/<%= Helium::PACKAGES %>">package manifest</a> is
|
9
|
+
generated using data extracted during the build process.</p>
|
10
|
+
|
11
|
+
<p>To use libraries from this server, drop the following in your <code>HEAD</code>:</p>
|
12
|
+
|
13
|
+
<pre class="prettyprint"><script src="http://<%= @location %>/<%= Helium::PACKAGES_MIN %>"
|
14
|
+
type="text/javascript"></script>
|
15
|
+
|
16
|
+
<script type="text/javascript">
|
17
|
+
// Specify which branch/tag of each Git project to use, e.g.
|
18
|
+
// Helium.use('yui', '2.7.0');
|
19
|
+
// Helium.use('ojay', '0.4.1');
|
20
|
+
</script></pre>
|
21
|
+
|
22
|
+
<p>Use the <code>require()</code> function throughout your pages to load JavaScript
|
23
|
+
objects on demand. Dependencies are automatically handled for you:</p>
|
24
|
+
|
25
|
+
<pre class="prettyprint"><script type="text/javascript">
|
26
|
+
require('GMap2', 'YAHOO.util.Selector', function() {
|
27
|
+
|
28
|
+
var box = document.getElementById('mapview'),
|
29
|
+
map = new GMap2(box),
|
30
|
+
links = YAHOO.util.Selector.query('a');
|
31
|
+
});
|
32
|
+
</script></pre>
|
33
|
+
|
34
|
+
<p>To set up Helium, you’ll need to <a href="/app/config">add projects to deploy</a>
|
35
|
+
using YAML format. Each entry should be the name of a project followed by its Git URL.
|
36
|
+
Helium relies on JS.Class, for which you need to set a Git URL and a version to use:</p>
|
37
|
+
|
38
|
+
<pre>---
|
39
|
+
js.class:
|
40
|
+
repository: git://github.com/jcoglan/js.class.git
|
41
|
+
version: 2.1.x
|
42
|
+
|
43
|
+
projects:
|
44
|
+
your-library: git://github.com/your/library.git</pre>
|
45
|
+
|
46
|
+
<p>You may also want to <a href="/app/custom">add some custom loaders</a> for libraries
|
47
|
+
not deployed using Helium. For example the following are loaders for Google Maps:</p>
|
48
|
+
|
49
|
+
<pre class="prettyprint">/**
|
50
|
+
* Loads the `google.load` function, required to load other
|
51
|
+
* parts of the Google API. Requires `Helium.GOOGLE_API_KEY`
|
52
|
+
* to be set beforehand.
|
53
|
+
**/
|
54
|
+
loader(function(cb) {
|
55
|
+
var url = 'http://www.google.com/jsapi?key=' + Helium.GOOGLE_API_KEY;
|
56
|
+
load(url, cb);
|
57
|
+
}) .provides('google.load');
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Loads the Google Maps API. Requires `Helium.GOOGLE_API_KEY`
|
61
|
+
* to be set beforehand.
|
62
|
+
**/
|
63
|
+
loader(function(cb) { google.load('maps', '2.x', {callback: cb}) })
|
64
|
+
.provides('GMap2', 'GClientGeocoder',
|
65
|
+
'GEvent', 'GLatLng', 'GMarker')
|
66
|
+
.requires('google.load');</pre>
|
67
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
|
3
|
+
<html>
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
6
|
+
<title>Helium</title>
|
7
|
+
<link rel="stylesheet" href="/style.css" type="text/css" media="screen">
|
8
|
+
<link rel="stylesheet" href="/prettify.css" type="text/css" media="screen">
|
9
|
+
<script src="/prettify.js" type="text/javascript"></script>
|
10
|
+
</head>
|
11
|
+
<body onload="prettyPrint()">
|
12
|
+
<div class="container">
|
13
|
+
|
14
|
+
<div class="header">
|
15
|
+
<h1><a href="/" class="logo"><%= logotype %></a>
|
16
|
+
<span class="tagline">Git-backed JavaScript deployment</span></h1>
|
17
|
+
|
18
|
+
<ul class="navigation">
|
19
|
+
<li>
|
20
|
+
<h3><a href="/app/config">Edit configuration</a></h3>
|
21
|
+
<p>Register libraries using their Git URL to have Helium deploy them.</p>
|
22
|
+
</li>
|
23
|
+
<li>
|
24
|
+
<h3><a href="/app/custom">Custom loaders</a></h3>
|
25
|
+
<p>Add custom loader code for libraries from outside Helium.</p>
|
26
|
+
</li>
|
27
|
+
</ul>
|
28
|
+
<div class="clear"></div>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<form action="/app/deploy" method="post">
|
32
|
+
<% unless @projects.empty? %>
|
33
|
+
<ul class="projects">
|
34
|
+
<% @projects.keys.sort.each_with_index do |name, i| %>
|
35
|
+
<li>
|
36
|
+
<input type="hidden" name="projects[<%=h name %>]" value="0">
|
37
|
+
<input type="checkbox" class="checkbox" name="projects[<%=h name %>]" id="projects_<%=h name %>" value="1">
|
38
|
+
<label for="projects_<%=h name %>"><%=h name %></label>
|
39
|
+
</li>
|
40
|
+
<% end %>
|
41
|
+
</ul>
|
42
|
+
<input type="submit" value="Deploy" <%= disabled? %>>
|
43
|
+
<% end %>
|
44
|
+
</form>
|
45
|
+
|
46
|
+
<div class="main">
|
47
|
+
<%= yield %>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<div class="clear"></div>
|
51
|
+
<div class="footer">
|
52
|
+
<p class="logo"><%= logotype %></p>
|
53
|
+
© 2009 <a href="http://othermedia.com">theOTHERmedia</a>. Source code is
|
54
|
+
<a href="http://github.com/othermedia/helium">hosted on GitHub</a>.
|
55
|
+
</div>
|
56
|
+
|
57
|
+
</div>
|
58
|
+
</body>
|
59
|
+
</html>
|
60
|
+
|
data/lib/helium/web.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Helium
|
7
|
+
class Web < Sinatra::Base
|
8
|
+
|
9
|
+
|
10
|
+
ROOT_DIR = File.dirname(__FILE__)
|
11
|
+
require File.join(ROOT_DIR, '..', 'helium')
|
12
|
+
require File.join(ROOT_DIR, 'web_helpers')
|
13
|
+
|
14
|
+
extend Configurable
|
15
|
+
|
16
|
+
LIB_DIR = 'lib'
|
17
|
+
|
18
|
+
CONFIG = File.join(APP_DIR, 'deploy.yml')
|
19
|
+
CUSTOM = File.join(APP_DIR, 'custom.js')
|
20
|
+
PUBLIC = File.join(APP_DIR, 'public', WEB_ROOT)
|
21
|
+
LOCK = File.join(APP_DIR, '.lock')
|
22
|
+
|
23
|
+
set :static, true
|
24
|
+
set :public, File.join(APP_DIR, 'public')
|
25
|
+
set :views, File.join(ROOT_DIR, 'views')
|
26
|
+
|
27
|
+
before do
|
28
|
+
@projects = project_config
|
29
|
+
@domain = env['HTTP_HOST']
|
30
|
+
@location = @domain + '/' + Helium::WEB_ROOT
|
31
|
+
end
|
32
|
+
|
33
|
+
## Home page -- just loads the project list and renders.
|
34
|
+
get('/') { erb :index }
|
35
|
+
|
36
|
+
## Rendered if a missing script file is requested.
|
37
|
+
get "/#{WEB_ROOT}/*" do
|
38
|
+
@path = params[:splat].first
|
39
|
+
halt 404, erb(:missing)
|
40
|
+
end
|
41
|
+
|
42
|
+
## Deploys all selected projects and renders a list of log messages.
|
43
|
+
post '/app/deploy' do
|
44
|
+
if not allow_write_access?(env)
|
45
|
+
@error = 'You are not authorized to run deployments'
|
46
|
+
elsif locked?
|
47
|
+
@error = 'Deployment already in progress'
|
48
|
+
end
|
49
|
+
|
50
|
+
halt(200, erb(:deploy)) if @error
|
51
|
+
|
52
|
+
with_lock do
|
53
|
+
deployer = Helium::Deployer.new(APP_DIR, LIB_DIR)
|
54
|
+
logger = Helium::Logger.new
|
55
|
+
deployer.add_observer(logger)
|
56
|
+
|
57
|
+
params[:projects].each do |name, value|
|
58
|
+
next unless value == '1'
|
59
|
+
deployer.deploy!(name, false)
|
60
|
+
end
|
61
|
+
|
62
|
+
deployer.cleanup!
|
63
|
+
|
64
|
+
custom = File.file?(CUSTOM) ? File.read(CUSTOM) : nil
|
65
|
+
files = deployer.run_builds!(:custom => custom, :domain => @domain)
|
66
|
+
|
67
|
+
FileUtils.rm_rf(PUBLIC) if File.exists?(PUBLIC)
|
68
|
+
|
69
|
+
files.each do |path|
|
70
|
+
source, dest = File.join(deployer.static_dir, path), File.join(PUBLIC, path)
|
71
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
72
|
+
FileUtils.cp(source, dest)
|
73
|
+
end
|
74
|
+
|
75
|
+
@log = logger.messages.map { |msg| msg.sub(File.join(APP_DIR, LIB_DIR), '') }
|
76
|
+
erb :deploy
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
get('/app/config') { view_file :config }
|
81
|
+
|
82
|
+
## Save changes to the configuration file, making sure it validates as YAML.
|
83
|
+
post '/app/config' do
|
84
|
+
@action = 'config'
|
85
|
+
@file = CONFIG
|
86
|
+
@contents = params[:contents]
|
87
|
+
if allow_write_access?(env)
|
88
|
+
begin
|
89
|
+
data = YAML.load(@contents)
|
90
|
+
raise 'invalid' unless Hash === data
|
91
|
+
File.open(@file, 'w') { |f| f.write(@contents) }
|
92
|
+
rescue
|
93
|
+
@error = 'File not saved: invalid YAML'
|
94
|
+
end
|
95
|
+
else
|
96
|
+
@error = 'You are not authorized to edit this file'
|
97
|
+
end
|
98
|
+
@projects = project_config
|
99
|
+
erb :edit
|
100
|
+
end
|
101
|
+
|
102
|
+
get('/app/custom') { view_file :custom }
|
103
|
+
|
104
|
+
## Save changes to the custom loaders file.
|
105
|
+
post '/app/custom' do
|
106
|
+
@action = 'custom'
|
107
|
+
@file = CUSTOM
|
108
|
+
@contents = params[:contents]
|
109
|
+
if allow_write_access?(env)
|
110
|
+
File.open(@file, 'w') { |f| f.write(@contents) }
|
111
|
+
else
|
112
|
+
@error = 'You are not authorized to edit this file'
|
113
|
+
end
|
114
|
+
erb :edit
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Helium
|
2
|
+
class Web
|
3
|
+
|
4
|
+
helpers do
|
5
|
+
# Returns the data structure contained in the app's deploy.yml file.
|
6
|
+
def project_config
|
7
|
+
Helium::Deployer.new(File.dirname(CONFIG)).projects
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the list of IP addresses that have write access to the app.
|
11
|
+
def allowed_ips
|
12
|
+
Helium::Web.config.allow_ips
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns +true+ iff the request should be allowed write access.
|
16
|
+
def allow_write_access?(env)
|
17
|
+
return true unless allowed_ips.is_a?(Array)
|
18
|
+
ip = (env['REMOTE_ADDR'] || '').scan(/(?:\d{1,3}\.){3}\d{1,3}/).flatten.first
|
19
|
+
allowed_ips.include?(ip)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns +true+ if a lock exists stopping other deploy processes running.
|
23
|
+
def locked?
|
24
|
+
File.file?(LOCK)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Places a lock in the filesystem while running a code block. This is
|
28
|
+
# used to make sure no more than one deploy process runs at once.
|
29
|
+
def with_lock(&block)
|
30
|
+
File.open(LOCK, 'w') { |f| f.write(Time.now.to_s) }
|
31
|
+
at_exit { File.delete(LOCK) if File.exists?(LOCK) }
|
32
|
+
result = block.call
|
33
|
+
File.delete(LOCK) if File.exists?(LOCK)
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generic handler for displaying editable files requested using GET.
|
38
|
+
def view_file(name)
|
39
|
+
@error = 'You are not authorized to edit this file' unless allow_write_access?(env)
|
40
|
+
@projects = project_config
|
41
|
+
@action = name.to_s
|
42
|
+
@file = Helium::Web.const_get(name.to_s.upcase)
|
43
|
+
@contents = File.file?(@file) ? File.read(@file) : ''
|
44
|
+
erb :edit
|
45
|
+
end
|
46
|
+
|
47
|
+
# Markup for the web UI's logo
|
48
|
+
def logotype
|
49
|
+
'<span class="symbol">He</span>lium'
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shorthand for ERB's HTML-escaping method
|
53
|
+
def h(string)
|
54
|
+
ERB::Util.h(string)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a disabled attribute if one is required
|
58
|
+
def disabled?
|
59
|
+
allow_write_access?(env) ? '' : 'disabled="disabled"'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|