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,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
+
@@ -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,13 @@
1
+ <%if @log %>
2
+ <h2>Deploy logs</h2>
3
+ <ul class="logs">
4
+ <% @log.each do |msg| %>
5
+ <li><%=h msg %></li>
6
+ <% end %>
7
+ </ul>
8
+
9
+ <% elsif @error %>
10
+ <h2>Deployment error</h2>
11
+ <p class="error"><%=h @error %></p>
12
+ <% end %>
13
+
@@ -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">&lt;script src="http://<%= @location %>/<%= Helium::PACKAGES_MIN %>"
14
+ type="text/javascript">&lt;/script>
15
+
16
+ &lt;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
+ &lt;/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">&lt;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
+ &lt;/script></pre>
33
+
34
+ <p>To set up Helium, you&rsquo;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
+ &copy; 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
+
@@ -0,0 +1,6 @@
1
+ <h2>Missing file: <code><%=h @path %></code></h2>
2
+
3
+ <p>The file <code><%=h @path %></code> could not be found. If you expected this
4
+ file to exist, you probably need to run the <code>Deploy</code> script: try the
5
+ button over on the left.</p>
6
+
@@ -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
+