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.
- 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
|
+
|