Skip to content

Commit 009a4b1

Browse files
committedMar 23, 2015
Merge pull request #773 from opal/elia/wip-back-to-sprockets
Back to sprockets ✨
2 parents 19f3bd4 + 43aa499 commit 009a4b1

File tree

7 files changed

+305
-129
lines changed

7 files changed

+305
-129
lines changed
 

Diff for: ‎lib/opal/compiler.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def self.compiler_option(name, default_value, options = {})
5454
valid_values = options[:valid_values]
5555
define_method(mid || name) do
5656
value = @options.fetch(name) { default_value }
57-
raise ArgumentError if valid_values and not(valid_values.include?(value))
57+
if valid_values and not(valid_values.include?(value))
58+
raise ArgumentError, "invalid value #{value.inspect} for option #{name.inspect} "+
59+
"(valid values: #{valid_values.inspect})"
60+
end
5861
value
5962
end
6063
end

Diff for: ‎lib/opal/sprockets/processor.rb

+43-25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'opal/version'
55
require 'opal/builder'
66
require 'opal/sprockets/path_reader'
7+
require 'opal/sprockets/source_map_server'
78

89
$OPAL_SOURCE_MAPS = {}
910

@@ -50,8 +51,6 @@ class << self
5051
attr_accessor :source_map_enabled
5152
attr_accessor :irb_enabled
5253
attr_accessor :inline_operators_enabled
53-
54-
attr_accessor :source_map_register
5554
end
5655

5756
self.method_missing_enabled = true
@@ -62,32 +61,57 @@ class << self
6261
self.irb_enabled = false
6362
self.inline_operators_enabled = false
6463

65-
self.source_map_register = $OPAL_SOURCE_MAPS
66-
6764

6865
def evaluate(context, locals, &block)
69-
return Opal.compile data unless context.is_a? ::Sprockets::Context
66+
return Opal.compile data, file: file unless context.is_a? ::Sprockets::Context
67+
68+
sprockets = context.environment
69+
logical_path = context.logical_path
70+
compiler_options = self.class.compiler_options.merge(file: logical_path)
7071

71-
path = context.logical_path
72-
prerequired = []
72+
compiler = Compiler.new(data, compiler_options)
73+
result = compiler.compile
7374

74-
builder = self.class.new_builder(context)
75-
result = builder.build_str(data, path, :prerequired => prerequired)
75+
compiler.requires.each do |required|
76+
context.require_asset required unless stubbed_files.include? required
77+
end
7678

7779
if self.class.source_map_enabled
78-
register_source_map(context.logical_path, result.source_map.to_s)
79-
"#{result.to_s}\n//# sourceMappingURL=#{File.basename(context.logical_path)}.map\n"
80-
else
81-
result.to_s
80+
map_contents = compiler.source_map.as_json.to_json
81+
::Opal::SourceMapServer.set_map_cache(sprockets, logical_path, map_contents)
8282
end
83+
84+
result.to_s
8385
end
8486

85-
def register_source_map path, map_contents
86-
self.class.source_map_register[path] = map_contents
87+
def self.load_asset_code(sprockets, name)
88+
asset = sprockets[name.sub(/(\.js)?$/, '.js')]
89+
return '' if asset.nil?
90+
91+
module_name = -> asset { asset.logical_path.sub(/\.js$/, '') }
92+
mark_as_loaded = -> path { "Opal.mark_as_loaded(#{path.inspect});" }
93+
processed_by_opal = -> asset, sprockets {
94+
attributes = ::Sprockets::AssetAttributes.new(sprockets, asset.pathname)
95+
attributes.engines.any? { |engine| engine <= ::Opal::Processor }
96+
}
97+
98+
non_opal_assets = ([asset]+asset.dependencies)
99+
.select { |asset| not(processed_by_opal[asset, sprockets]) }
100+
.map { |asset| module_name[asset] }
101+
102+
mark_as_loaded = (non_opal_assets + stubbed_files.to_a)
103+
.map { |path| mark_as_loaded[path] }
104+
105+
<<-JS
106+
if (typeof(Opal) !== 'undefined') {
107+
#{mark_as_loaded.join("\n")}
108+
Opal.load(#{module_name[asset].inspect});
109+
}
110+
JS
87111
end
88112

89113
def self.stubbed_files
90-
@stubbed_files ||= []
114+
@stubbed_files ||= Set.new
91115
end
92116

93117
def self.stub_file(name)
@@ -98,22 +122,16 @@ def stubbed_files
98122
self.class.stubbed_files
99123
end
100124

101-
def self.new_builder(context)
102-
compiler_options = {
125+
def self.compiler_options
126+
{
103127
:method_missing => method_missing_enabled,
104128
:arity_check => arity_check_enabled,
105129
:const_missing => const_missing_enabled,
106130
:dynamic_require_severity => dynamic_require_severity,
107131
:irb => irb_enabled,
108132
:inline_operators => inline_operators_enabled,
133+
:requirable => true,
109134
}
110-
111-
path_reader = ::Opal::Sprockets::PathReader.new(context.environment, context)
112-
return Builder.new(
113-
compiler_options: compiler_options,
114-
stubs: stubbed_files,
115-
path_reader: path_reader
116-
)
117135
end
118136
end
119137
end

Diff for: ‎lib/opal/sprockets/server.rb

+41-78
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,12 @@
99
require 'sprockets'
1010
require 'sourcemap'
1111
require 'erb'
12+
require 'opal/sprockets/source_map_server'
13+
require 'opal/sprockets/source_map_header_patch'
1214

1315
module Opal
14-
15-
class SourceMapServer
16-
def initialize sprockets, prefix = '/'
17-
@sprockets = sprockets
18-
@prefix = prefix
19-
end
20-
21-
attr_reader :sprockets, :prefix
22-
23-
def inspect
24-
"#<#{self.class}:#{object_id}>"
25-
end
26-
27-
def call(env)
28-
prefix_regex = %r{^(?:#{prefix}/|/)}
29-
path_info = env['PATH_INFO'].to_s.sub(prefix_regex, '')
30-
31-
case path_info
32-
when %r{^(.*)\.map$}
33-
path = $1
34-
asset = sprockets[path]
35-
return not_found(path) if asset.nil?
36-
37-
# "logical_name" of a BundledAsset keeps the .js extension
38-
source = register[asset.logical_path.sub(/\.js$/, '')]
39-
return not_found(asset) if source.nil?
40-
41-
map = JSON.parse(source)
42-
map['sources'] = map['sources'].map {|s| "#{prefix}/#{s}"}
43-
source = map.to_json
44-
return not_found(asset) if source.nil?
45-
46-
return [200, {"Content-Type" => "text/json"}, [source.to_s]]
47-
when %r{^(.*)\.rb$}
48-
source = File.read(sprockets.resolve(path_info))
49-
return not_found(path_info) if source.nil?
50-
return [200, {"Content-Type" => "text/ruby"}, [source]]
51-
else
52-
not_found(path_info)
53-
end
54-
end
55-
56-
def not_found(*messages)
57-
not_found = [404, {}, [{not_found: messages, keys: register.keys}.inspect]]
58-
end
59-
60-
def register
61-
Opal::Processor.source_map_register
62-
end
63-
end
64-
6516
class Server
17+
SOURCE_MAPS_PREFIX_PATH = '/__OPAL_SOURCE_MAPS__'
6618

6719
attr_accessor :debug, :use_index, :index_path, :main, :public_root,
6820
:public_urls, :sprockets, :prefix
@@ -113,16 +65,21 @@ def use_gem gem_name
11365
def create_app
11466
server, sprockets, prefix = self, @sprockets, self.prefix
11567
sprockets.logger.level ||= Logger::DEBUG
68+
source_map_enabled = self.source_map_enabled
69+
if source_map_enabled
70+
maps_prefix = SOURCE_MAPS_PREFIX_PATH
71+
maps_app = SourceMapServer.new(sprockets, maps_prefix)
72+
::Opal::Sprockets::SourceMapHeaderPatch.inject!(maps_prefix)
73+
end
74+
11675
@app = Rack::Builder.app do
11776
not_found = lambda { |env| [404, {}, []] }
11877
use Rack::Deflater
11978
use Rack::ShowExceptions
12079
use Index, server if server.use_index
121-
assets = []
122-
assets << SourceMapServer.new(sprockets, prefix) if server.source_map_enabled
123-
assets << sprockets
124-
map(prefix) { run Rack::Cascade.new(assets) }
125-
run Rack::Static.new(not_found, :root => server.public_root, :urls => server.public_urls)
80+
map(maps_prefix) { run maps_app } if source_map_enabled
81+
map(prefix) { run sprockets }
82+
run Rack::Static.new(not_found, root: server.public_root, urls: server.public_urls)
12683
end
12784
end
12885

@@ -152,37 +109,43 @@ def html
152109
raise "index does not exist: #{@index_path}" unless File.exist?(@index_path)
153110
Tilt.new(@index_path).render(self)
154111
else
155-
::ERB.new(SOURCE).result binding
112+
source
156113
end
157114
end
158115

159-
def javascript_include_tag source
160-
if @server.debug
161-
assets = @server.sprockets[source].to_a
162-
163-
raise "Cannot find asset: #{source}" if assets.empty?
116+
def javascript_include_tag name
117+
sprockets = @server.sprockets
118+
prefix = @server.prefix
119+
asset = sprockets[name]
120+
raise "Cannot find asset: #{name}" if asset.nil?
121+
scripts = []
164122

165-
scripts = assets.map do |a|
166-
%Q{<script src="/assets/#{ a.logical_path }?body=1"></script>}
123+
if @server.debug
124+
asset.to_a.map do |dependency|
125+
scripts << %{<script src="#{prefix}/#{dependency.logical_path}?body=1"></script>}
167126
end
168-
169-
scripts.join "\n"
170127
else
171-
"<script src=\"/assets/#{source}.js\"></script>"
128+
scripts << %{<script src="#{prefix}/#{name}.js"></script>}
172129
end
130+
131+
scripts << %{<script>#{Opal::Processor.load_asset_code(sprockets, name)}</script>}
132+
133+
scripts.join "\n"
173134
end
174135

175-
SOURCE = <<-HTML
176-
<!DOCTYPE html>
177-
<html>
178-
<head>
179-
<title>Opal Server</title>
180-
</head>
181-
<body>
182-
<%= javascript_include_tag @server.main %>
183-
</body>
184-
</html>
185-
HTML
136+
def source
137+
<<-HTML
138+
<!DOCTYPE html>
139+
<html>
140+
<head>
141+
<title>Opal Server</title>
142+
</head>
143+
<body>
144+
#{javascript_include_tag @server.main}
145+
</body>
146+
</html>
147+
HTML
148+
end
186149
end
187150
end
188151
end

Diff for: ‎lib/opal/sprockets/source_map_header_patch.rb

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'sprockets/server'
2+
3+
module Opal
4+
module Sprockets
5+
module SourceMapHeaderPatch
6+
# Adds the source map header to all sprocket responses for assets
7+
# with a .rb or .opal extension in the extension chain.
8+
def headers_with_opal_source_maps(env, asset, length)
9+
headers_without_opal_source_maps(env, asset, length).tap do |current_headers|
10+
if asset.pathname.to_s =~ /\.(rb|opal)\b/
11+
base_path = asset.logical_path.gsub('.js', '')
12+
current_headers['X-SourceMap'] = "#{::Opal::Sprockets::SourceMapHeaderPatch.prefix}/#{base_path}.map"
13+
end
14+
end
15+
end
16+
17+
def self.included(base)
18+
# Poor man's alias_method_chain :)
19+
base.send(:alias_method, :headers_without_opal_source_maps, :headers)
20+
base.send(:alias_method, :headers, :headers_with_opal_source_maps)
21+
end
22+
23+
def self.inject!(prefix)
24+
self.prefix = prefix
25+
unless ::Sprockets::Server.ancestors.include?(self)
26+
::Sprockets::Server.send :include, self
27+
end
28+
end
29+
30+
def self.prefix
31+
@prefix
32+
end
33+
34+
def self.prefix= val
35+
@prefix = val
36+
end
37+
end
38+
end
39+
end
40+
41+

Diff for: ‎lib/opal/sprockets/source_map_server.rb

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
module Opal
2+
class SourceMapServer
3+
# Carelessly taken from Sprockets::Caching (Sprockets v2)
4+
class Cache
5+
def initialize
6+
@cache = {}
7+
end
8+
9+
attr_reader :cache
10+
11+
def cache_get(key)
12+
# `Cache#get(key)` for Memcache
13+
if cache.respond_to?(:get)
14+
cache.get(key)
15+
16+
# `Cache#[key]` so `Hash` can be used
17+
elsif cache.respond_to?(:[])
18+
cache[key]
19+
20+
# `Cache#read(key)` for `ActiveSupport::Cache` support
21+
elsif cache.respond_to?(:read)
22+
cache.read(key)
23+
24+
else
25+
nil
26+
end
27+
end
28+
29+
def cache_set(key, value)
30+
# `Cache#set(key, value)` for Memcache
31+
if cache.respond_to?(:set)
32+
cache.set(key, value)
33+
34+
# `Cache#[key]=value` so `Hash` can be used
35+
elsif cache.respond_to?(:[]=)
36+
cache[key] = value
37+
38+
# `Cache#write(key, value)` for `ActiveSupport::Cache` support
39+
elsif cache.respond_to?(:write)
40+
cache.write(key, value)
41+
end
42+
43+
value
44+
end
45+
end
46+
47+
def self.get_map_cache(sprockets, logical_path)
48+
logical_path = logical_path.gsub(/\.js$/, '')
49+
cache_key = cache_key_for_path(logical_path+'.map')
50+
cache(sprockets).cache_get(cache_key)
51+
end
52+
53+
def self.set_map_cache(sprockets, logical_path, map_contents)
54+
logical_path = logical_path.gsub(/\.js$/, '')
55+
cache_key = cache_key_for_path(logical_path+'.map')
56+
cache(sprockets).cache_set(cache_key, map_contents)
57+
end
58+
59+
def self.cache(sprockets)
60+
@cache ||= sprockets.cache ? sprockets : Cache.new
61+
end
62+
63+
def self.cache_key_for_path(logical_path)
64+
base_name = logical_path.gsub(/\.js$/, '')
65+
File.join('opal', 'source_maps', base_name)
66+
end
67+
68+
69+
def initialize sprockets, prefix = '/'
70+
@sprockets = sprockets
71+
@prefix = prefix
72+
end
73+
74+
attr_reader :sprockets, :prefix
75+
76+
def inspect
77+
"#<#{self.class}:#{object_id}>"
78+
end
79+
80+
def call(env)
81+
prefix_regex = %r{^(?:#{prefix}/|/)}
82+
path_info = env['PATH_INFO'].to_s.sub(prefix_regex, '')
83+
84+
case path_info
85+
when %r{^(.*)\.map$}
86+
path = $1
87+
asset = sprockets[path]
88+
return not_found(path) if asset.nil?
89+
90+
# "logical_name" of a BundledAsset keeps the .js extension
91+
source = SourceMapServer.get_map_cache(sprockets, asset.logical_path)
92+
return not_found(asset) if source.nil?
93+
94+
map = JSON.parse(source)
95+
map['sources'] = map['sources'].map {|s| "#{prefix}/#{s}"}
96+
source = map.to_json
97+
98+
return [200, {"Content-Type" => "text/json"}, [source.to_s]]
99+
when %r{^(.*)\.rb$}
100+
begin
101+
asset = sprockets.resolve(path_info.sub(/\.rb$/,''))
102+
rescue Sprockets::FileNotFound
103+
return not_found(path_info)
104+
end
105+
return [200, {"Content-Type" => "text/ruby"}, [Pathname(asset).read]]
106+
else
107+
not_found(path_info)
108+
end
109+
end
110+
111+
def not_found(*messages)
112+
not_found = [404, {}, [{not_found: messages}.inspect]]
113+
end
114+
end
115+
end

Diff for: ‎spec/lib/sprockets/processor_spec.rb

+41-14
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33

44
describe Opal::Processor do
55
let(:pathname) { Pathname("/Code/app/mylib/opal/foo.#{ext}") }
6-
let(:_context) { double('_context', :logical_path => "foo.#{ext}", :pathname => pathname) }
7-
let(:env) { double('env') }
8-
9-
before do
10-
env.stub(:resolve) { pathname.expand_path.to_s }
11-
env.stub(:[])
12-
_context.stub(:environment) { env }
13-
end
6+
let(:environment) { double('environment',
7+
cache: nil,
8+
:[] => nil,
9+
resolve: pathname.expand_path.to_s,
10+
) }
11+
let(:sprockets_context) { double('context',
12+
logical_path: "foo.#{ext}",
13+
environment: environment,
14+
pathname: pathname,
15+
is_a?: true,
16+
) }
1417

1518
%w[rb js.rb opal js.opal].each do |ext|
1619
let(:ext) { ext }
@@ -22,14 +25,38 @@
2225

2326
it "compiles and evaluates the template on #render" do
2427
template = described_class.new { |t| "puts 'Hello, World!'\n" }
25-
expect(template.render(_context)).to include('"Hello, World!"')
26-
end
27-
28-
it "can be rendered more than once" do
29-
template = described_class.new(_context) { |t| "puts 'Hello, World!'\n" }
30-
3.times { expect(template.render(_context)).to include('"Hello, World!"') }
28+
expect(template.render(sprockets_context)).to include('"Hello, World!"')
3129
end
3230
end
3331
end
3432

33+
describe '.stubbed_files' do
34+
around do |e|
35+
described_class.stubbed_files.clear
36+
e.run
37+
described_class.stubbed_files.clear
38+
end
39+
40+
let(:stubbed_file) { 'foo' }
41+
let(:template) { described_class.new { |t| "require #{stubbed_file.inspect}" } }
42+
43+
it 'usually require files' do
44+
sprockets_context.should_receive(:require_asset).with(stubbed_file)
45+
template.render(sprockets_context)
46+
end
47+
48+
it 'skips require of stubbed file' do
49+
described_class.stub_file stubbed_file
50+
sprockets_context.should_not_receive(:require_asset).with(stubbed_file)
51+
template.render(sprockets_context)
52+
end
53+
54+
it 'marks a stubbed file as loaded' do
55+
described_class.stub_file stubbed_file
56+
asset = double(dependencies: [], pathname: Pathname('bar'), logical_path: 'bar')
57+
environment.stub(:[]).with('bar.js') { asset }
58+
code = described_class.load_asset_code(environment, 'bar')
59+
code.should match stubbed_file
60+
end
61+
end
3562
end

Diff for: ‎spec/lib/sprockets/server_spec.rb

+20-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
describe Opal::Server do
66
include Rack::Test::Methods
77

8+
let(:maps_prefix) { described_class::SOURCE_MAPS_PREFIX_PATH }
9+
810
def app
911
described_class.new { |s|
1012
s.main = 'opal'
@@ -24,46 +26,53 @@ def app
2426
get '/assets/source_map.js'
2527
expect(last_response).to be_ok
2628

27-
get '/assets/source_map.map'
29+
get maps_prefix+'/source_map.map'
2830
expect(last_response).to be_ok
2931
end
3032

3133
it 'serves map on a subfolder file' do
3234
js_path = '/assets/source_map/subfolder/other_file.js'
33-
map_path = '/assets/source_map/subfolder/other_file.map'
35+
map_path = maps_prefix+'/source_map/subfolder/other_file.map'
3436

3537
get js_path
3638

3739
expect(last_response).to be_ok
38-
received_map_path = extract_linked_map(last_response.body)
40+
received_map_path = extract_map_path(last_response)
3941
expect(expand_path(received_map_path, js_path+'/..')).to eq(map_path)
4042

41-
get '/assets/source_map/subfolder/other_file.map'
43+
get maps_prefix+'/source_map/subfolder/other_file.map'
4244
expect(last_response).to be_ok
4345
end
4446

4547
it 'serves map on a subfolder file' do
4648
js_path = '/assets/source_map/subfolder/other_file.js'
47-
map_path = '/assets/source_map/subfolder/other_file.map'
49+
map_path = maps_prefix+'/source_map/subfolder/other_file.map'
4850

4951
get js_path
5052

5153
expect(last_response).to be_ok
52-
received_map_path = extract_linked_map(last_response.body)
54+
received_map_path = extract_map_path(last_response)
5355
expect(expand_path(received_map_path, js_path+'/..')).to eq(map_path)
5456

5557

56-
get '/assets/source_map/subfolder/other_file.map'
58+
get maps_prefix+'/source_map/subfolder/other_file.map'
5759
expect(last_response).to be_ok
5860
map = ::SourceMap::Map.from_json(last_response.body)
59-
expect(map.sources).to include('/assets/source_map/subfolder/other_file.rb')
61+
expect(map.sources).to include(maps_prefix+'/source_map/subfolder/other_file.rb')
6062
end
6163
end
6264

63-
def extract_linked_map(body)
65+
def extract_map_path(response)
6466
source_map_comment_regexp = %r{//# sourceMappingURL=(.*)$}
65-
expect(body).to match(source_map_comment_regexp)
66-
body.scan(source_map_comment_regexp).first.first
67+
68+
case
69+
when response.body =~ source_map_comment_regexp
70+
body.scan(source_map_comment_regexp).first.first
71+
when response.headers['X-SourceMap']
72+
response.headers['X-SourceMap']
73+
else
74+
raise "cannot find source map in response: #{response.inspect}"
75+
end
6776
end
6877

6978
def expand_path(file_name, dir_string)

0 commit comments

Comments
 (0)
Please sign in to comment.