helium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+