sass-embedded 0.1.3 → 0.3.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/lib/sass/error.rb CHANGED
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- class BaseError < StandardError; end
4
+ class SassError < StandardError; end
5
5
 
6
- class ProtocolError < BaseError; end
6
+ class ProtocolError < SassError; end
7
7
 
8
- class NotRenderedError < BaseError; end
9
-
10
- class InvalidStyleError < BaseError; end
11
-
12
- class UnsupportedValue < BaseError; end
13
-
14
- class CompilationError < BaseError
8
+ # The error returned by {Sass.render}
9
+ class RenderError < SassError
15
10
  attr_accessor :formatted, :file, :line, :column, :status
16
11
 
17
12
  def initialize(message, formatted, file, line, column, status)
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'observer'
5
+ require_relative '../../ext/embedded_sass_pb'
6
+
7
+ module Sass
8
+ # The interface for communicating with dart-sass-embedded.
9
+ # It handles message serialization and deserialization as well as
10
+ # tracking concurrent request and response
11
+ class Transport
12
+ include Observable
13
+
14
+ DART_SASS_EMBEDDED = File.absolute_path(
15
+ "../../ext/sass_embedded/dart-sass-embedded#{Platform::OS == 'windows' ? '.bat' : ''}", __dir__
16
+ )
17
+
18
+ PROTOCOL_ERROR_ID = 4_294_967_295
19
+
20
+ def initialize
21
+ @stdin_semaphore = Mutex.new
22
+ @observerable_semaphore = Mutex.new
23
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
24
+ pipe @stderr, $stderr
25
+ receive
26
+ end
27
+
28
+ def send(req, res_id)
29
+ mutex = Mutex.new
30
+ resource = ConditionVariable.new
31
+
32
+ req_kind = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
33
+
34
+ message = EmbeddedProtocol::InboundMessage.new(req_kind => req)
35
+
36
+ error = nil
37
+ res = nil
38
+
39
+ @observerable_semaphore.synchronize do
40
+ MessageObserver.new self, res_id do |e, r|
41
+ mutex.synchronize do
42
+ error = e
43
+ res = r
44
+
45
+ resource.signal
46
+ end
47
+ end
48
+ end
49
+
50
+ mutex.synchronize do
51
+ write message.to_proto
52
+
53
+ resource.wait(mutex)
54
+ end
55
+
56
+ raise error if error
57
+
58
+ res
59
+ end
60
+
61
+ def close
62
+ delete_observers
63
+ @stdin.close unless @stdin.closed?
64
+ @stdout.close unless @stdout.closed?
65
+ @stderr.close unless @stderr.closed?
66
+ nil
67
+ end
68
+
69
+ private
70
+
71
+ def receive
72
+ Thread.new do
73
+ loop do
74
+ bits = length = 0
75
+ loop do
76
+ byte = @stdout.readbyte
77
+ length += (byte & 0x7f) << bits
78
+ bits += 7
79
+ break if byte <= 0x7f
80
+ end
81
+ changed
82
+ payload = @stdout.read length
83
+ @observerable_semaphore.synchronize do
84
+ notify_observers nil, EmbeddedProtocol::OutboundMessage.decode(payload)
85
+ end
86
+ rescue Interrupt
87
+ break
88
+ rescue IOError => e
89
+ notify_observers e, nil
90
+ close
91
+ break
92
+ end
93
+ end
94
+ end
95
+
96
+ def pipe(readable, writeable)
97
+ Thread.new do
98
+ loop do
99
+ writeable.write readable.read
100
+ rescue Interrupt
101
+ break
102
+ rescue IOError => e
103
+ @observerable_semaphore.synchronize do
104
+ notify_observers e, nil
105
+ end
106
+ close
107
+ break
108
+ end
109
+ end
110
+ end
111
+
112
+ def write(payload)
113
+ @stdin_semaphore.synchronize do
114
+ length = payload.length
115
+ while length.positive?
116
+ @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
117
+ length >>= 7
118
+ end
119
+ @stdin.write payload
120
+ end
121
+ end
122
+
123
+ # The observer used to listen on messages from stdout, check if id
124
+ # matches the given request id, and yield back to the given block.
125
+ class MessageObserver
126
+ def initialize(obs, id, &block)
127
+ @obs = obs
128
+ @id = id
129
+ @block = block
130
+ @obs.add_observer self
131
+ end
132
+
133
+ def update(error, message)
134
+ if error
135
+ @obs.delete_observer self
136
+ @block.call error, nil
137
+ elsif message.error&.id == Transport::PROTOCOL_ERROR_ID
138
+ @obs.delete_observer self
139
+ @block.call ProtocolError.new(message.error.message), nil
140
+ else
141
+ res = message[message.message.to_s]
142
+ if (res['compilation_id'] || res['id']) == @id
143
+ @obs.delete_observer self
144
+ @block.call error, res
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
data/lib/sass/util.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
+ # Utilities functions
4
5
  module Util
5
6
  module_function
6
7
 
data/lib/sass/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- VERSION = '0.1.3'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -15,11 +15,11 @@ Gem::Specification.new do |spec|
15
15
  spec.license = 'MIT'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0")
18
- spec.extensions = ['ext/sass_embedded/extconf.rb']
18
+ spec.extensions = ['ext/extconf.rb']
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
 
22
- spec.required_ruby_version = '>= 2.0.0'
22
+ spec.required_ruby_version = '>= 2.6'
23
23
 
24
24
  spec.require_paths = ['lib']
25
25
 
@@ -33,4 +33,6 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency 'rake'
34
34
  spec.add_development_dependency 'rake-compiler'
35
35
  spec.add_development_dependency 'rubocop'
36
+ spec.add_development_dependency 'rubocop-minitest'
37
+ spec.add_development_dependency 'rubocop-rake'
36
38
  end
@@ -3,19 +3,19 @@
3
3
  require_relative 'test_helper'
4
4
 
5
5
  module Sass
6
- class CompilerTest < MiniTest::Test
6
+ class EmbeddedTest < MiniTest::Test
7
7
  include TempFileTest
8
8
 
9
9
  def setup
10
- @compiler = Embedded::Compiler.new
10
+ @embedded = Embedded.new
11
11
  end
12
12
 
13
13
  def teardown
14
- @compiler.close
14
+ @embedded.close
15
15
  end
16
16
 
17
17
  def render(data)
18
- @compiler.render({ data: data })[:css]
18
+ @embedded.render(data: data)[:css]
19
19
  end
20
20
 
21
21
  def test_line_comments
@@ -31,10 +31,8 @@ module Sass
31
31
  baz: bang;
32
32
  }
33
33
  CSS
34
- output = @compiler.render({
35
- data: template,
36
- source_comments: true
37
- })
34
+ output = @embedded.render(data: template,
35
+ source_comments: true)
38
36
  assert_equal expected_output, output[:css]
39
37
  end
40
38
 
@@ -97,10 +95,8 @@ module Sass
97
95
  baz: 0.33333333;
98
96
  }
99
97
  CSS
100
- output = @compiler.render({
101
- data: template,
102
- precision: 8
103
- })
98
+ output = @embedded.render(data: template,
99
+ precision: 8)
104
100
  assert_equal expected_output, output
105
101
  end
106
102
 
@@ -134,18 +130,14 @@ module Sass
134
130
  padding: 20px;
135
131
  }
136
132
  SCSS
137
- output = @compiler.render({
138
- data: File.read('style.scss'),
139
- source_map: 'style.scss.map'
140
- })
133
+ output = @embedded.render(data: File.read('style.scss'),
134
+ source_map: 'style.scss.map')
141
135
 
142
136
  assert output[:map].start_with? '{"version":3,'
143
137
  end
144
138
 
145
139
  def test_no_source_map
146
- output = @compiler.render({
147
- data: '$size: 30px;'
148
- })
140
+ output = @embedded.render(data: '$size: 30px;')
149
141
  assert_equal '', output[:map]
150
142
  end
151
143
 
@@ -157,12 +149,10 @@ module Sass
157
149
  temp_file('included_2/import.scss', "@use 'import_parent' as *; $size: $s;")
158
150
  temp_file('styles.scss', "@use 'import.scss' as *; .hi { width: $size; }")
159
151
 
160
- assert_equal ".hi {\n width: 30px;\n}", @compiler.render({
161
- data: File.read('styles.scss'),
162
- include_paths: %w[
163
- included_1 included_2
164
- ]
165
- })[:css]
152
+ assert_equal ".hi {\n width: 30px;\n}", @embedded.render(data: File.read('styles.scss'),
153
+ include_paths: %w[
154
+ included_1 included_2
155
+ ])[:css]
166
156
  end
167
157
 
168
158
  def test_global_include_paths
@@ -186,7 +176,7 @@ module Sass
186
176
 
187
177
  ENV['SASS_PATH'] = expected_include_paths.join(File::PATH_SEPARATOR)
188
178
 
189
- assert_equal expected_include_paths, Sass.include_paths
179
+ assert_equal expected_include_paths, ::Sass.include_paths
190
180
 
191
181
  ::Sass.include_paths.clear
192
182
  end
@@ -198,7 +188,7 @@ module Sass
198
188
  temp_file('included_6/import.scss', "@use 'import_parent' as *; $size: $s;")
199
189
  temp_file('styles.scss', "@use 'import.scss' as *; .hi { width: $size; }")
200
190
 
201
- assert_raises(CompilationError) do
191
+ assert_raises(RenderError) do
202
192
  render(File.read('styles.scss'))
203
193
  end
204
194
  end
@@ -216,9 +206,9 @@ module Sass
216
206
  }
217
207
  CSS
218
208
 
219
- assert_equal css, @compiler.render({ data: sass, indented_syntax: true })[:css]
220
- assert_raises(CompilationError) do
221
- @compiler.render({ data: sass, indented_syntax: false })
209
+ assert_equal css, @embedded.render(data: sass, indented_syntax: true)[:css]
210
+ assert_raises(RenderError) do
211
+ @embedded.render(data: sass, indented_syntax: false)
222
212
  end
223
213
  end
224
214
 
@@ -230,12 +220,10 @@ module Sass
230
220
  baz: bang; }
231
221
  SCSS
232
222
 
233
- output = @compiler.render({
234
- data: template,
235
- source_map: '.',
236
- source_map_embed: true,
237
- source_map_contents: true
238
- })[:css]
223
+ output = @embedded.render(data: template,
224
+ source_map: '.',
225
+ source_map_embed: true,
226
+ source_map_contents: true)[:css]
239
227
 
240
228
  assert_match(/sourceMappingURL/, output)
241
229
  assert_match(/.foo/, output)
@@ -263,9 +251,7 @@ module Sass
263
251
  threads = []
264
252
  10.times do |i|
265
253
  threads << Thread.new(i) do |id|
266
- output = @compiler.render({
267
- data: "div { width: #{id} }"
268
- })[:css]
254
+ output = @embedded.render(data: "div { width: #{id} }")[:css]
269
255
  assert_match(/#{id}/, output)
270
256
  end
271
257
  end
@@ -7,15 +7,15 @@ module Sass
7
7
  include TempFileTest
8
8
 
9
9
  def setup
10
- @compiler = Embedded::Compiler.new
10
+ @embedded = Embedded.new
11
11
  end
12
12
 
13
13
  def teardown
14
- @compiler.close
14
+ @embedded.close
15
15
  end
16
16
 
17
17
  def render(data, importer)
18
- @compiler.render({ data: data, importer: importer })[:css]
18
+ @embedded.render(data: data, importer: importer)[:css]
19
19
  end
20
20
 
21
21
  def test_custom_importer_works
@@ -86,17 +86,16 @@ module Sass
86
86
  end
87
87
 
88
88
  def test_custom_importer_that_does_not_resolve
89
- assert_raises(CompilationError) do
89
+ assert_raises(RenderError) do
90
90
  render("@import 'test.scss';", [
91
91
  lambda { |_url, _prev|
92
- return nil
93
92
  }
94
93
  ])
95
94
  end
96
95
  end
97
96
 
98
97
  def test_custom_importer_that_returns_error
99
- assert_raises(CompilationError) do
98
+ assert_raises(RenderError) do
100
99
  render("@import 'test.scss';", [
101
100
  lambda { |_url, _prev|
102
101
  IOError.new 'test error'
@@ -106,7 +105,7 @@ module Sass
106
105
  end
107
106
 
108
107
  def test_custom_importer_that_raises_error
109
- assert_raises(CompilationError) do
108
+ assert_raises(RenderError) do
110
109
  render("@import 'test.scss';", [
111
110
  lambda { |_url, _prev|
112
111
  raise IOError, 'test error'
@@ -116,15 +115,13 @@ module Sass
116
115
  end
117
116
 
118
117
  def test_parent_path_is_accessible
119
- output = @compiler.render({
120
- data: "@import 'parent.scss';",
121
- file: 'import-parent-filename.scss',
122
- importer: [
123
- lambda { |_url, prev|
124
- { contents: ".#{prev} { color: red; }" }
125
- }
126
- ]
127
- })[:css]
118
+ output = @embedded.render(data: "@import 'parent.scss';",
119
+ file: 'import-parent-filename.scss',
120
+ importer: [
121
+ lambda { |_url, prev|
122
+ { contents: ".#{prev} { color: red; }" }
123
+ }
124
+ ])[:css]
128
125
 
129
126
  assert_equal <<~CSS.chomp, output
130
127
  .import-parent-filename.scss {
@@ -133,24 +130,20 @@ module Sass
133
130
  CSS
134
131
  end
135
132
 
136
- def test_call_compiler_importer
137
- output = @compiler.render({
138
- data: "@import 'parent.scss';",
139
- importer: [
140
- lambda { |_url, _prev|
141
- {
142
- contents: @compiler.render({
143
- data: "@import 'parent-parent.scss'",
144
- importer: [
145
- lambda { |_url, _prev|
146
- { contents: 'h1 { color: black; }' }
147
- }
148
- ]
149
- })[:css]
150
- }
133
+ def test_call_embedded_importer
134
+ output = @embedded.render(data: "@import 'parent.scss';",
135
+ importer: [
136
+ lambda { |_url, _prev|
137
+ {
138
+ contents: @embedded.render(data: "@import 'parent-parent.scss'",
139
+ importer: [
140
+ lambda { |_url, _prev|
141
+ { contents: 'h1 { color: black; }' }
142
+ }
143
+ ])[:css]
151
144
  }
152
- ]
153
- })[:css]
145
+ }
146
+ ])[:css]
154
147
 
155
148
  assert_equal <<~CSS.chomp, output
156
149
  h1 {