linner-hc 1.0.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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG +129 -0
  6. data/Gemfile +14 -0
  7. data/LICENSE +22 -0
  8. data/README.md +74 -0
  9. data/Rakefile +8 -0
  10. data/bin/linner +11 -0
  11. data/docs/commands.md +29 -0
  12. data/docs/config.md +192 -0
  13. data/lib/linner.rb +261 -0
  14. data/lib/linner/archive.rb +53 -0
  15. data/lib/linner/asset.rb +102 -0
  16. data/lib/linner/bundler.rb +85 -0
  17. data/lib/linner/cache.rb +19 -0
  18. data/lib/linner/command.rb +133 -0
  19. data/lib/linner/compressor.rb +17 -0
  20. data/lib/linner/environment.rb +76 -0
  21. data/lib/linner/helper.rb +39 -0
  22. data/lib/linner/notifier.rb +24 -0
  23. data/lib/linner/reactor.rb +87 -0
  24. data/lib/linner/sprite.rb +127 -0
  25. data/lib/linner/template.rb +77 -0
  26. data/lib/linner/templates/app/images/.gitkeep +0 -0
  27. data/lib/linner/templates/app/images/logo.png +0 -0
  28. data/lib/linner/templates/app/scripts/app.coffee +3 -0
  29. data/lib/linner/templates/app/styles/app.scss +1 -0
  30. data/lib/linner/templates/app/templates/welcome.hbs +1 -0
  31. data/lib/linner/templates/app/views/index.html +21 -0
  32. data/lib/linner/templates/bin/server +3 -0
  33. data/lib/linner/templates/config.yml +54 -0
  34. data/lib/linner/templates/public/.gitkeep +1 -0
  35. data/lib/linner/templates/test/.gitkeep +0 -0
  36. data/lib/linner/templates/vendor/.gitkeep +1 -0
  37. data/lib/linner/version.rb +3 -0
  38. data/lib/linner/wrapper.rb +39 -0
  39. data/linner.gemspec +35 -0
  40. data/linner.gemspec.bak +34 -0
  41. data/spec/fixtures/app.js +1 -0
  42. data/spec/fixtures/config.yml +30 -0
  43. data/spec/linner/asset_spec.rb +33 -0
  44. data/spec/linner/bundler_spec.rb +26 -0
  45. data/spec/linner/environment_spec.rb +22 -0
  46. data/spec/linner/helper_spec.rb +26 -0
  47. data/spec/linner/sprites_spec.rb +23 -0
  48. data/spec/linner/template_spec.rb +16 -0
  49. data/spec/linner/wrapper_spec.rb +20 -0
  50. data/spec/spec_helper.rb +11 -0
  51. data/vendor/config.default.yml +13 -0
  52. data/vendor/livereload.js +1055 -0
  53. data/vendor/require_definition.js +60 -0
  54. metadata +289 -0
@@ -0,0 +1,261 @@
1
+ require "linner/version"
2
+ require "linner/command"
3
+ require "linner/asset"
4
+ require "linner/cache"
5
+ require "linner/helper"
6
+ require "linner/sprite"
7
+ require "linner/archive"
8
+ require "linner/bundler"
9
+ require "linner/reactor"
10
+ require "linner/wrapper"
11
+ require "linner/template"
12
+ require "linner/notifier"
13
+ require "linner/compressor"
14
+ require "linner/environment"
15
+
16
+ module Linner
17
+ extend self
18
+
19
+ attr_accessor :env, :compile, :strict
20
+
21
+ def root
22
+ @root ||= Pathname('.').realpath
23
+ end
24
+
25
+ def config_file
26
+ linner_file = root.join("Linnerfile")
27
+ config_file = root.join("config.yml")
28
+ File.exist?(linner_file) ? linner_file : config_file
29
+ end
30
+
31
+ def env
32
+ @env ||= Environment.new config_file
33
+ end
34
+
35
+ def cache
36
+ @cache ||= Cache.new
37
+ end
38
+
39
+ def manifest
40
+ @manifest ||= begin
41
+ hash = {}
42
+ copy_assets = []
43
+ concat_assets = []
44
+ template_assets = []
45
+ sprite_assets = []
46
+ cdn = env.revision["cdn"] || ""
47
+ prefix = env.revision["prefix"] || ""
48
+
49
+ env.groups.each do |config|
50
+ concat_assets << config["concat"].keys if config["concat"]
51
+ template_assets << config["template"].keys if config["template"]
52
+ sprite_assets << config["sprite"].keys if config["sprite"]
53
+ config["copy"].each do |dest, pattern|
54
+ copy_assets << Dir.glob(pattern).map do |path|
55
+ logical_path = Asset.new(path).logical_path
56
+ dest_path = File.join(dest, logical_path)
57
+ end
58
+ end if config["copy"]
59
+ end
60
+
61
+ # revision sprite assets
62
+ sprite_assets.flatten.each do |dest|
63
+ name = File.basename(dest).sub /[^.]+\z/, "png"
64
+ dest = File.join env.sprites["path"], name
65
+ asset = Asset.new(File.join env.public_folder, dest)
66
+ hash[prefix + dest] = cdn + prefix + asset.relative_digest_path
67
+ asset.revision!
68
+
69
+ (concat_assets + copy_assets).flatten.each do |file|
70
+ path = File.join env.public_folder, file
71
+ next unless Asset.new(path).stylesheet?
72
+ url = env.sprites["url"] || env.sprites["path"]
73
+ puts = File.read(path).gsub(File.join(url, File.basename(dest)), File.join(cdn, url, File.basename(asset.relative_digest_path)))
74
+ File.open(path, "w") { |file| file << puts }
75
+ end
76
+ end
77
+
78
+ # revision concat template and copy assets
79
+ (concat_assets + template_assets + copy_assets).flatten.each do |dest|
80
+ asset = Asset.new(File.join env.public_folder, dest)
81
+ next unless asset.revable?
82
+ hash[prefix + dest] = cdn + prefix + asset.relative_digest_path
83
+ asset.revision!
84
+ end
85
+
86
+ hash
87
+ end
88
+ end
89
+
90
+ def compile?
91
+ @compile
92
+ end
93
+
94
+ def strict?
95
+ @strict
96
+ end
97
+
98
+ def perform(*asset)
99
+ env.groups.each do |config|
100
+ precompile(config) if config["precompile"]
101
+ sprite(config) if config["sprite"]
102
+ end
103
+ env.groups.each do |config|
104
+ copy(config) if config["copy"]
105
+ compile(config) if config["compile"]
106
+ concat(config) if config["concat"]
107
+ end
108
+ env.groups.each do |config|
109
+ tar(config) if config["tar"]
110
+ end
111
+ revision if compile? and env.revision
112
+ end
113
+
114
+ private
115
+ def concat(config)
116
+ config["concat"].each_with_index do |pair, index|
117
+ dest, pattern, order = pair.first, pair.last, config["order"]||[]
118
+ matches = Dir.glob(pattern).sort_by(&:downcase).order_by(order)
119
+ next if matches.select {|path| cache.miss?(dest, path)}.empty?
120
+ write_asset(dest, matches)
121
+ end
122
+ end
123
+
124
+ def copy(config)
125
+ config["copy"].each do |dest, pattern|
126
+ Dir.glob(pattern).each do |path|
127
+ asset = Asset.new(path)
128
+ dest_path = File.join(env.public_folder, dest, asset.logical_path)
129
+ FileUtils.mkdir_p File.dirname(dest_path)
130
+ FileUtils.cp_r path, dest_path
131
+ end
132
+ end
133
+ end
134
+
135
+ def compile(config)
136
+ config["compile"].each do |dest, pattern|
137
+ Dir.glob(pattern).each do |path|
138
+ next if not cache.miss?(dest, path)
139
+ asset = Asset.new(path)
140
+ dest_path = File.join(env.public_folder, dest, asset.logical_path)
141
+ if asset.javascript? or asset.stylesheet?
142
+ asset.content
143
+ asset.compress if compile?
144
+ dest_path = dest_path.sub(/[^.]+\z/, "js") if asset.javascript?
145
+ dest_path = dest_path.sub(/[^.]+\z/, "css") if asset.stylesheet?
146
+ asset.path = dest_path
147
+ asset.write
148
+ elsif asset.eruby?
149
+ base, ext = path.split(".")
150
+ dest_path = if ext == "erb"
151
+ dest_path.sub(/[.]+\z/, "html")
152
+ else
153
+ dest_path.gsub(File.basename(dest_path), File.basename(dest_path, File.extname(dest_path)))
154
+ end
155
+ asset.content(config["context"])
156
+ asset.path = dest_path
157
+ asset.write
158
+ else
159
+ FileUtils.mkdir_p File.dirname(dest_path)
160
+ FileUtils.cp_r path, dest_path
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def precompile(config)
167
+ config["precompile"].each do |dest, pattern|
168
+ matches = Dir.glob(pattern).sort_by(&:downcase)
169
+ next if matches.select { |path| cache.miss?(dest, path) }.empty?
170
+ write_template(dest, matches)
171
+ end
172
+ end
173
+
174
+ def sprite(config)
175
+ config["sprite"].each do |dest, pattern|
176
+ matches = Dir.glob(pattern).sort_by(&:downcase)
177
+ next if matches.select { |path| cache.miss?(dest, path) }.empty?
178
+ paint_sprite(dest, matches)
179
+ end
180
+ end
181
+
182
+ def tar(config)
183
+ config["tar"].each do |dest, pattern|
184
+ path = File.join(env.public_folder, dest)
185
+ FileUtils.mkdir_p File.dirname(path)
186
+ Archive.tar(pattern, path)
187
+ end
188
+ end
189
+
190
+ def revision
191
+ dump_manifest
192
+ files = env.revision["files"] || []
193
+ files.flatten.each do |rev|
194
+ file = File.join env.public_folder, rev.to_s
195
+ next if not File.exist?(file)
196
+ replace_attributes file
197
+ end
198
+ end
199
+
200
+ def paint_sprite(dest, images)
201
+ images = images.map do |path|
202
+ ImageProxy.new(path, ChunkyPNG::Image.from_file(path))
203
+ end
204
+ sprite = Sprite.new(images).pack!
205
+ map = ChunkyPNG::Image.new(sprite.root[:w], sprite.root[:h], ChunkyPNG::Color::TRANSPARENT)
206
+
207
+ sprite.images.each do |image|
208
+ map.compose!(image.image, image.left, image.top)
209
+ end
210
+
211
+ name = File.basename(dest).sub(/[^.]+\z/, "png")
212
+ path = File.join(env.public_folder, env.sprites["path"], name)
213
+ FileUtils.mkdir_p File.dirname(path)
214
+ map.save path
215
+
216
+ asset = Asset.new(File.join env.public_folder, dest)
217
+ asset.content = sprite.generate_style(env.sprites, name)
218
+ asset.write
219
+ end
220
+
221
+ def write_template(dest, child_assets)
222
+ asset = Asset.new(File.join env.public_folder, dest)
223
+ content = child_assets.inject("") {|s, m| s << cache["#{dest}:#{m}"].content}
224
+ asset.content = Wrapper::Template.definition(content)
225
+ asset.compress if compile?
226
+ asset.write
227
+ end
228
+
229
+ def write_asset(dest, child_assets)
230
+ asset = Asset.new(File.join env.public_folder, dest)
231
+ definition = (asset.path == env.definition ? Wrapper::Module.definition : "")
232
+ asset.content = child_assets.inject(definition) {|s, m| s << cache["#{dest}:#{m}"].content}
233
+ asset.compress if compile?
234
+ asset.write
235
+ end
236
+
237
+ def replace_attributes file
238
+ doc = File.read file
239
+ if strict?
240
+ doc.gsub!(/(<script.+src=['"])([^"']+)(["'])/) do |m|
241
+ if p = manifest[$2] then $1 << p << $3 else m end
242
+ end
243
+
244
+ doc.gsub!(/(<link[^\>]+href=['"])([^"']+)(["'])/) do |m|
245
+ if p = manifest[$2] then $1 << p << $3 else m end
246
+ end
247
+ else
248
+ manifest.each do |k, v|
249
+ doc.gsub!(k, v)
250
+ end
251
+ end
252
+ File.open(file, "w") {|f| f.write doc}
253
+ end
254
+
255
+ def dump_manifest
256
+ manifest_file = env.revision["manifest"] || "manifest.yml"
257
+ File.open(File.join(env.public_folder, manifest_file), "w") do |f|
258
+ YAML.dump(manifest, f)
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,53 @@
1
+ require "rubygems/package"
2
+
3
+ module Linner
4
+ class Archive
5
+
6
+ class << self
7
+ def tar(glob, dest)
8
+ archived = StringIO.new
9
+ Gem::Package::TarWriter.new(archived) do |tar|
10
+ Dir[glob].each do |file|
11
+ paths = Linner.env.paths
12
+ mode = File.stat(file).mode
13
+ relative_file = file.gsub /^#{paths.join("|")}\/?/, ""
14
+ if File.directory?(file)
15
+ tar.mkdir relative_file, mode
16
+ else
17
+ tar.add_file relative_file, mode do |tf|
18
+ File.open(file, "rb") { |f| tf.write f.read }
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ archived.rewind
25
+
26
+ Zlib::GzipWriter.open(dest) do |gz|
27
+ gz.write archived.string
28
+ end
29
+ end
30
+
31
+ def untar(path, dest)
32
+ extracted = Gem::Package::TarReader.new Zlib::GzipReader.open(path)
33
+
34
+ extracted.rewind
35
+
36
+ extracted.each do |entry|
37
+ file = File.join dest, entry.full_name
38
+ if entry.directory?
39
+ FileUtils.mkdir_p file
40
+ else
41
+ directory = File.dirname(file)
42
+ FileUtils.mkdir_p directory unless File.directory?(directory)
43
+ File.open file, "wb" do |f|
44
+ f.print entry.read
45
+ end
46
+ end
47
+ end
48
+
49
+ extracted.close
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,102 @@
1
+ require "digest"
2
+
3
+ module Linner
4
+ class Asset
5
+ class RenderError < StandardError; end
6
+
7
+ attr_accessor :path, :content
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ @mtime = File.mtime(path).to_i if File.exist?(path)
12
+ end
13
+
14
+ def mtime
15
+ @mtime
16
+ end
17
+
18
+ def extname
19
+ @extname = File.extname path
20
+ end
21
+
22
+ def digest_path
23
+ digest = Digest::MD5.hexdigest content
24
+ path.chomp(extname) << "-#{digest}" << extname
25
+ end
26
+
27
+ def relative_digest_path
28
+ digest_path.gsub /#{Linner.env.public_folder}/, ""
29
+ end
30
+
31
+ def revable?
32
+ javascript? or stylesheet?
33
+ end
34
+
35
+ def revision!
36
+ File.rename path, digest_path
37
+ end
38
+
39
+ def content(context = nil)
40
+ return @content if @content
41
+ source = begin
42
+ File.exist?(path) ? Tilt.new(path, :default_encoding => "UTF-8").render(nil, context) : ""
43
+ rescue RuntimeError
44
+ File.read(path, mode: "rb")
45
+ rescue => e
46
+ raise RenderError, "#{e.message} in (#{path})"
47
+ end
48
+ if wrappable?
49
+ @content = wrap(source)
50
+ else
51
+ @content = source
52
+ end
53
+ end
54
+
55
+ def wrap(source)
56
+ if javascript?
57
+ Wrapper::Module.wrap(logical_path.chomp(File.extname logical_path), source)
58
+ elsif template?
59
+ if File.basename(path).start_with?("_")
60
+ Wrapper::Template.partial_wrap(logical_path.chomp(File.extname logical_path), source)
61
+ else
62
+ Wrapper::Template.wrap(logical_path.chomp(File.extname logical_path), source)
63
+ end
64
+ end
65
+ end
66
+
67
+ def javascript?
68
+ Tilt[path] and Tilt[path].default_mime_type == "application/javascript"
69
+ end
70
+
71
+ def stylesheet?
72
+ Tilt[path] and Tilt[path].default_mime_type == "text/css"
73
+ end
74
+
75
+ def template?
76
+ Tilt[path] and Tilt[path].default_mime_type == "text/template"
77
+ end
78
+
79
+ def eruby?
80
+ Tilt[path] and Tilt[path].default_mime_type = "application/x-eruby"
81
+ end
82
+
83
+ def wrappable?
84
+ !!(self.javascript? and !Linner.env.modules_ignored.include?(@path) or self.template?)
85
+ end
86
+
87
+ def write
88
+ FileUtils.mkdir_p File.dirname(@path)
89
+ File.open @path, "w" do |file|
90
+ file.write @content
91
+ end
92
+ end
93
+
94
+ def compress
95
+ @content = Compressor.compress(self)
96
+ end
97
+
98
+ def logical_path
99
+ @logical_path ||= @path.gsub(/^(#{Linner.env.paths.join("|")})\/?/, "")
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,85 @@
1
+ require "uri"
2
+ require "digest"
3
+ require "fileutils"
4
+ require "open-uri"
5
+
6
+ module Linner
7
+ class Bundler
8
+ REPOSITORY = File.expand_path "~/.linner/bundles"
9
+
10
+ Bundle = Struct.new(:name, :version, :url) do
11
+ def path
12
+ File.join(REPOSITORY, name, version, File.basename(url))
13
+ end
14
+ end
15
+
16
+ def initialize(env)
17
+ @bundles = []
18
+ env.bundles.each do |name, props|
19
+ @bundles << Bundle.new(name, props["version"], props["url"])
20
+ end
21
+ @vendor = Pathname(".").join env.vendor_folder
22
+ end
23
+
24
+ def check
25
+ return [false, "Bundles didn't exsit!"] unless File.exist? REPOSITORY
26
+ @bundles.each do |bundle|
27
+ unless File.exist?(bundle.path) and File.exist?(File.join(@vendor, bundle.name))
28
+ return [false, "Bundle #{bundle.name} v#{bundle.version} didn't match!"]
29
+ end
30
+ end
31
+ return [true, "Perfect bundled, ready to go!"]
32
+ end
33
+
34
+ def install
35
+ unless File.exist? REPOSITORY
36
+ FileUtils.mkdir_p(REPOSITORY)
37
+ end
38
+ @bundles.each do |bundle|
39
+ if bundle.version != "master"
40
+ next if File.exist?(bundle.path) and File.exist?(File.join(@vendor, bundle.name))
41
+ end
42
+ puts "Installing #{bundle.name} #{bundle.version}..."
43
+ install_to_repository bundle
44
+ if File.extname(bundle.path) == ".gz"
45
+ link_and_extract_to_vendor bundle.path, File.join(@vendor, ".pkg", bundle.name, File.basename(bundle.path)), File.join(@vendor, bundle.name)
46
+ else
47
+ link_to_vendor bundle.path, File.join(@vendor, bundle.name)
48
+ end
49
+ end
50
+ end
51
+
52
+ def perform
53
+ check and install
54
+ end
55
+
56
+ private
57
+ def install_to_repository(bundle)
58
+ FileUtils.mkdir_p File.dirname(bundle.path)
59
+ begin
60
+ File.open(bundle.path, "wb") do |dest|
61
+ if bundle.url =~ URI::regexp
62
+ open(bundle.url, "r:UTF-8") {|file| dest.write file.read}
63
+ else
64
+ dest.write(File.read Pathname(bundle.url).expand_path)
65
+ end
66
+ end
67
+ rescue
68
+ Notifier.error("Can't fetch bundle #{bundle.name} from #{bundle.url}")
69
+ Kernel::exit
70
+ end
71
+ end
72
+
73
+ def link_to_vendor(path, dest)
74
+ return if File.exist?(dest) and Digest::MD5.file(path).hexdigest == Digest::MD5.file(dest).hexdigest
75
+ FileUtils.mkdir_p File.dirname(dest)
76
+ FileUtils.cp path, dest
77
+ end
78
+
79
+ def link_and_extract_to_vendor(path, linked_path, dest)
80
+ link_to_vendor(path, linked_path)
81
+ FileUtils.rm_rf Dir.glob("#{dest}/*")
82
+ Archive.untar(path, dest)
83
+ end
84
+ end
85
+ end