esbuild 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 773fae0a508c3fa01fdf258941d7929f7a00db2d75a31cd717277a1c5268421f
4
+ data.tar.gz: 14e77b505c53e4f9e2af88e7ce868d154baaddb0387b0875d10e520daacd7c83
5
+ SHA512:
6
+ metadata.gz: d1e6acf3d2b2cadecc2bbc321d88aa539e70234bf97dbda4619dca661c7832338a53c9b1d9ce971264e5c1d655f7e9bfcee3864e68ce6446f59164c8b3f2ba09
7
+ data.tar.gz: 07e6efd4130d489aa6f8fda5eea9fc2017bd37793ee38d4246143bd4b12bde5fac063f9d9919ea0109a0676065b25fe7b751d886728dfa914f14676d8b46b744
@@ -0,0 +1,24 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ strategy:
8
+ matrix:
9
+ os:
10
+ - ubuntu-latest
11
+ - macos-latest
12
+ runs-on: ${{ matrix.os }}
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: 3.0.0
19
+ - name: Run the default task
20
+ run: |
21
+ gem install bundler -v 2.2.15
22
+ bundle install
23
+ bundle exec rake # Install binary
24
+ bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .DS_Store
10
+ .idea
11
+ /tmp/
12
+ /node_modules
13
+ /bin/esbuild
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in esbuild.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ esbuild (0.1.0)
5
+ concurrent-ruby (~> 1.1.8)
6
+ rake (~> 13.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ concurrent-ruby (1.1.8)
13
+ minitest (5.14.4)
14
+ parallel (1.20.1)
15
+ parser (3.0.0.0)
16
+ ast (~> 2.4.1)
17
+ rainbow (3.0.0)
18
+ rake (13.0.3)
19
+ regexp_parser (2.1.1)
20
+ rexml (3.2.4)
21
+ rubocop (1.11.0)
22
+ parallel (~> 1.10)
23
+ parser (>= 3.0.0.0)
24
+ rainbow (>= 2.2.2, < 4.0)
25
+ regexp_parser (>= 1.8, < 3.0)
26
+ rexml
27
+ rubocop-ast (>= 1.2.0, < 2.0)
28
+ ruby-progressbar (~> 1.7)
29
+ unicode-display_width (>= 1.4.0, < 3.0)
30
+ rubocop-ast (1.4.1)
31
+ parser (>= 2.7.1.5)
32
+ rubocop-performance (1.10.1)
33
+ rubocop (>= 0.90.0, < 2.0)
34
+ rubocop-ast (>= 0.4.0)
35
+ ruby-progressbar (1.11.0)
36
+ standard (1.0.4)
37
+ rubocop (= 1.11.0)
38
+ rubocop-performance (= 1.10.1)
39
+ unicode-display_width (2.0.0)
40
+
41
+ PLATFORMS
42
+ arm64-darwin-20
43
+
44
+ DEPENDENCIES
45
+ esbuild!
46
+ minitest (~> 5.14.4)
47
+ standard (~> 1.0.4)
48
+
49
+ BUNDLED WITH
50
+ 2.2.15
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Bouke van der Bijl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Esbuild
2
+
3
+ Use [esbuild](https://github.com/evanw/esbuild) from Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'esbuild'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install esbuild
20
+
21
+ ## Contributing
22
+
23
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bouk/esbuild-ruby.
24
+
25
+ ## License
26
+
27
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "bundler/gem_tasks"
5
+ require "rake/testtask"
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ t.verbose = false
10
+ end
11
+
12
+ task :download_binary do
13
+ require_relative "lib/esbuild/binary_installer"
14
+ esbuild_bin = File.join(__dir__, "bin", "esbuild")
15
+ installer = Esbuild::BinaryInstaller.new(RUBY_PLATFORM, esbuild_bin)
16
+ installer.install
17
+ end
18
+
19
+ task default: :download_binary
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "esbuild"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/esbuild.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/esbuild/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "esbuild"
7
+ spec.version = Esbuild::VERSION
8
+ spec.authors = ["Bouke van der Bijl"]
9
+ spec.email = ["i@bou.ke"]
10
+
11
+ spec.summary = "Use esbuild from Ruby"
12
+ spec.homepage = "https://github.com/bouk/esbuild-ruby"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+ spec.extensions = ["Rakefile"]
27
+ spec.add_dependency "concurrent-ruby", "~> 1.1.8"
28
+ spec.add_development_dependency "minitest", "~> 5.14.4"
29
+ spec.add_development_dependency "standard", "~> 1.0.4"
30
+ spec.add_dependency "rake", "~> 13.0"
31
+ end
data/lib/esbuild.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "esbuild/version"
4
+ require_relative "esbuild/packet"
5
+ require_relative "esbuild/stdio_protocol"
6
+ require_relative "esbuild/service"
7
+
8
+ module Esbuild
9
+ class << self
10
+ def build(options)
11
+ service.build_or_serve(options)
12
+ end
13
+
14
+ def serve(serve_options, build_options)
15
+ service.build_or_serve(build_options, serve_options)
16
+ end
17
+
18
+ def transform(input, options = {})
19
+ service.transform(input, options)
20
+ end
21
+
22
+ private
23
+
24
+ def service
25
+ @service ||= Service.new
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "version"
2
+ require "fileutils"
3
+ require "open3"
4
+ require "net/https"
5
+
6
+ module Esbuild
7
+ class BinaryInstaller
8
+ attr_reader :platform, :path
9
+ def initialize(platform, path)
10
+ package = package_from_platform(platform)
11
+ raise ArgumentError, "Unknown platform #{platform}" unless package
12
+ @package = package
13
+ @path = path
14
+ end
15
+
16
+ def install
17
+ tempfile = "#{@path}__"
18
+ if ENV["ESBUILD_BINARY_PATH"]
19
+ FileUtils.cp(ENV["ESBUILD_BINARY_PATH"], tempfile)
20
+ else
21
+ # TODO: use cache
22
+ download(tempfile)
23
+ end
24
+
25
+ validate_binary_version!(tempfile)
26
+ FileUtils.mv(tempfile, @path)
27
+ end
28
+
29
+ private
30
+
31
+ def download(target)
32
+ url = "https://unpkg.com/#{@package}@#{ESBUILD_VERSION}/bin/esbuild"
33
+ warn "Downloading esbuild binary from #{url}"
34
+
35
+ uri = URI(url)
36
+ http = Net::HTTP.new(uri.host, uri.port)
37
+ http.use_ssl = true
38
+ http.start do
39
+ request = Net::HTTP::Get.new uri
40
+ http.request(request) do |response|
41
+ File.open(target, "wb", 0o755) do |f|
42
+ response.read_body(f)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def validate_binary_version!(path)
49
+ version, _ = Open3.capture2(path, "--version")
50
+ version = version.strip
51
+ raise "Expected #{ESBUILD_VERSION} but got #{version}" unless ESBUILD_VERSION == version
52
+ end
53
+
54
+ def package_from_platform(platform)
55
+ case platform
56
+ when /^x86_64-darwin/
57
+ "esbuild-darwin-64"
58
+ when /^arm64-darwin/
59
+ "esbuild-darwin-arm64"
60
+ when "x86_64-linux"
61
+ "esbuild-linux-64"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ require "forwardable"
2
+ require "json"
3
+
4
+ module Esbuild
5
+ class BuildResult
6
+ extend Forwardable
7
+
8
+ class OutputFile
9
+ attr_reader :path
10
+ attr_reader :contents
11
+
12
+ def initialize(path, contents)
13
+ @path = path
14
+ @contents = contents
15
+ end
16
+
17
+ def text
18
+ @text ||= contents.dup.force_encoding(Encoding::UTF_8)
19
+ end
20
+ end
21
+
22
+ class Metafile
23
+ class Input < Struct.new(:bytes, :imports)
24
+ def initialize(hash)
25
+ super(hash["bytes"], hash["imports"])
26
+ end
27
+ end
28
+
29
+ class Output < Struct.new(:imports, :exports, :entry_point, :inputs)
30
+ class Input < Struct.new(:bytes_in_output)
31
+ def initialize(hash)
32
+ super(hash["bytesInOutput"])
33
+ end
34
+ end
35
+
36
+ def initialize(hash)
37
+ inputs = hash["inputs"].transform_values! { |v| Input.new(v) }
38
+ super(hash["imports"], hash["exports"], hash["entryPoint"], inputs)
39
+ end
40
+ end
41
+
42
+ attr_reader :inputs
43
+ attr_reader :outputs
44
+
45
+ def initialize(json)
46
+ hash = JSON.parse(json)
47
+ @inputs = hash["inputs"].transform_values! { |v| Input.new(v) }
48
+ @outputs = hash["outputs"].transform_values! { |v| Output.new(v) }
49
+ end
50
+ end
51
+
52
+ attr_reader :warnings
53
+ attr_reader :output_files
54
+ attr_reader :metafile
55
+ def_delegators :@state, :stop, :rebuild, :dispose
56
+
57
+ def initialize(response, state)
58
+ @state = state
59
+ @warnings = response["warnings"] # TODO: symbolize keys
60
+
61
+ if response["outputFiles"]
62
+ @output_files = response["outputFiles"].map { |f| OutputFile.new(f["path"], f["contents"]) }
63
+ end
64
+
65
+ if response["metafile"]
66
+ @metafile = Metafile.new(response["metafile"])
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,66 @@
1
+ module Esbuild
2
+ class BuildState
3
+ def initialize(service, on_rebuild)
4
+ @service = service
5
+ @rebuild_id = nil
6
+ @watch_id = nil
7
+ @on_rebuild = on_rebuild
8
+ end
9
+
10
+ def stop
11
+ return unless @watch_id
12
+ @service.stop_watch(@watch_id)
13
+ @watch_id = nil
14
+ end
15
+
16
+ def dispose
17
+ return unless @rebuild_id
18
+ res = @service.send_request("command" => "rebuild-dispose", "rebuildID" => @rebuild_id)
19
+ @rebuild_id = nil
20
+ res
21
+ end
22
+
23
+ def rebuild
24
+ raise "Cannot rebuild" if @rebuild_id.nil?
25
+ rebuild_response = @service.send_request("command" => "rebuild", "rebuildID" => @rebuild_id)
26
+ response_to_result(rebuild_response)
27
+ end
28
+
29
+ def handle_watch(error, response)
30
+ return @on_rebuild.call(error, nil) if error
31
+ unless response["errors"].empty?
32
+ error = BuildFailureError.new(response["errors"], response["warnings"])
33
+ @on_rebuild.call(error, nil)
34
+ return
35
+ end
36
+
37
+ result = BuildResult.new(response, self)
38
+ @on_rebuild.call(nil, result)
39
+ end
40
+
41
+ def response_to_result(res)
42
+ unless res["errors"].empty?
43
+ raise BuildFailureError, res["errors"], res["warnings"]
44
+ end
45
+ if res["writeToStdout"]
46
+ $stdout.puts res["writeToStdout"].rstrip
47
+ end
48
+
49
+ result = BuildResult.new(res, self)
50
+
51
+ # Handle incremental rebuilds
52
+ if res["rebuildID"] && !@rebuild_id
53
+ @rebuild_id = res["rebuildID"]
54
+ end
55
+
56
+ # Handle watch mode
57
+ if res["watchID"] && !@watch_id
58
+ @watch_id = res["watchID"]
59
+ if @on_rebuild
60
+ @service.start_watch(@watch_id, ->(error, watch_response) { handle_watch(error, watch_response) })
61
+ end
62
+ end
63
+ result
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,218 @@
1
+ module Esbuild
2
+ module Flags
3
+ extend self
4
+
5
+ class OneOf
6
+ def initialize(*classes)
7
+ @classes = classes
8
+ end
9
+
10
+ def ===(other)
11
+ @classes.any? { |klass| klass === other }
12
+ end
13
+
14
+ def to_s
15
+ @classes.join(" or ")
16
+ end
17
+
18
+ def self.[](*classes)
19
+ new(*classes)
20
+ end
21
+ end
22
+
23
+ BOOL = OneOf[true, false]
24
+ STRING_OR_ARRAY = OneOf[String, Array]
25
+ STRING_OR_OBJECT = OneOf[String, Hash]
26
+ BOOL_OR_OBJECT = OneOf[BOOL, Hash]
27
+ STRING_OR_BOOL = OneOf[String, BOOL]
28
+ STRING_OR_SYMBOL = OneOf[String, Symbol]
29
+ ARRAY_OR_OBJECT = OneOf[Array, Hash]
30
+
31
+ def flags_for_transform_options(options)
32
+ flags = []
33
+ options = options.dup
34
+ push_log_flags(flags, options, :silent)
35
+ push_common_flags(flags, options)
36
+ get_flag(options, :source_map, STRING_OR_BOOL) { |v| flags << "--source-map=#{v == true ? "external" : v}" if v }
37
+ get_flag(options, :tsconfig_raw, STRING_OR_OBJECT) { |v| flags << "--tsconfig-raw=#{v.is_a?(String) ? v : JSON.dump(v)}" }
38
+ get_flag(options, :source_file, String) { |v| flags << "--sourcefile=#{v}" }
39
+ get_flag(options, :loader, STRING_OR_SYMBOL) { |v| flags << "--loader=#{v}" }
40
+ get_flag(options, :banner, String) { |v| flags << "--banner=#{v}" }
41
+ get_flag(options, :footer, String) { |v| flags << "--footer=#{v}" }
42
+ raise ArgumentError, "Invalid option in transform() call: #{options.keys.first}" unless options.empty?
43
+ flags
44
+ end
45
+
46
+ def flags_for_build_options(options)
47
+ flags = []
48
+ options = options.dup
49
+ push_log_flags(flags, options, :info)
50
+ push_common_flags(flags, options)
51
+ get_flag(options, :source_map, STRING_OR_BOOL) { |v| flags << "--source-map#{v == true ? "" : "=#{v}"}" if v }
52
+ get_flag(options, :bundle, BOOL) { |v| flags << "--bundle" if v }
53
+ watch_mode = nil
54
+ get_flag(options, :watch, BOOL_OR_OBJECT) do |v|
55
+ break unless v
56
+ flags << "--watch"
57
+ watch_mode = {}
58
+ unless v == true
59
+ watch_options = v.dup
60
+ get_flag(watch_options, :on_rebuild, Proc) { |v| watch_mode[:on_rebuild] = v }
61
+ raise ArgumentError, "Invalid option in watch options: #{watch_options.keys.first}" unless watch_options.empty?
62
+ end
63
+ end
64
+ get_flag(options, :splitting, BOOL) { |v| flags << "--splitting" if v }
65
+ get_flag(options, :preserve_symlinks, BOOL) { |v| flags << "--preserve-symlinks" if v }
66
+ get_flag(options, :metafile, BOOL) { |v| flags << "--metafile" if v }
67
+ get_flag(options, :outfile, String) { |v| flags << "--outfile=#{v}" }
68
+ get_flag(options, :outdir, String) { |v| flags << "--outdir=#{v}" }
69
+ get_flag(options, :outbase, String) { |v| flags << "--outbase=#{v}" }
70
+ get_flag(options, :platform, String) { |v| flags << "--platform=#{v}" }
71
+ get_flag(options, :tsconfig, String) { |v| flags << "--tsconfig=#{v}" }
72
+ get_flag(options, :resolve_extensions, Array) do |v|
73
+ exts = v.map do |ext|
74
+ ext = ext.to_s
75
+ raise ArgumentError, "Invalid resolve extension: #{ext}" if ext.include?(",")
76
+ ext
77
+ end
78
+ flags << "--resolve-extensions=#{exts.join(",")}"
79
+ end
80
+ get_flag(options, :public_path, String) { |v| flags << "--public-path=#{v}" }
81
+ get_flag(options, :entry_names, String) { |v| flags << "--entry-names=#{v}" }
82
+ get_flag(options, :chunk_names, String) { |v| flags << "--chunk-names=#{v}" }
83
+ get_flag(options, :asset_names, String) { |v| flags << "--asset-names=#{v}" }
84
+ get_flag(options, :main_fields, Array) do |v|
85
+ values = v.map do |value|
86
+ value = value.to_s
87
+ raise ArgumentError, "Invalid main field: #{value}" if value.include?(",")
88
+ value
89
+ end
90
+ flags << "--main-fields=#{values.join(",")}"
91
+ end
92
+ get_flag(options, :conditions, Array) do |v|
93
+ values = v.map do |value|
94
+ value = value.to_s
95
+ raise ArgumentError, "Invalid condition: #{value}" if value.include?(",")
96
+ value
97
+ end
98
+ flags << "--conditions=#{values.join(",")}"
99
+ end
100
+ get_flag(options, :external, Array) { |v| v.each { |name| flags << "--external:#{name}" } }
101
+ get_flag(options, :banner, Hash) do |v|
102
+ v.each do |type, value|
103
+ raise ArgumentError, "Invalid banner file type: #{type}" if type.include?("=")
104
+ flags << "--banner:#{type}=#{value}"
105
+ end
106
+ end
107
+ get_flag(options, :footer, Hash) do |v|
108
+ v.each do |type, value|
109
+ raise ArgumentError, "Invalid footer file type: #{type}" if type.include?("=")
110
+ flags << "--footer:#{type}=#{value}"
111
+ end
112
+ end
113
+ get_flag(options, :inject, Array) { |v| v.each { |name| flags << "--inject:#{name}" } }
114
+ get_flag(options, :loader, Hash) do |v|
115
+ v.each do |ext, loader|
116
+ raise ArgumentError, "Invalid loader extension: #{ext}" if ext.include?("=")
117
+ flags << "--loader:#{ext}=#{loader}"
118
+ end
119
+ end
120
+ get_flag(options, :out_extension, Hash) do |v|
121
+ v.each do |ext, extension|
122
+ raise ArgumentError, "Invalid out extension: #{ext}" if ext.include?("=")
123
+ flags << "--out-extension:#{ext}=#{extension}"
124
+ end
125
+ end
126
+ entries = []
127
+ get_flag(options, :entry_points, ARRAY_OR_OBJECT) do |v|
128
+ if v.is_a?(Array)
129
+ v.each { |entry_point| entries << ["", entry_point] }
130
+ else
131
+ v.each { |key, entry_point| entries << [key.to_s, entry_point.to_s] }
132
+ end
133
+ end
134
+ stdin_resolve_dir = nil
135
+ stdin_contents = nil
136
+ get_flag(options, :stdin, Hash) do |v|
137
+ stdin_options = v.dup
138
+ stdin_contents = ""
139
+ get_flag(stdin_options, :contents, String) { |v| stdin_contents = v }
140
+ get_flag(stdin_options, :resolve_dir, String) { |v| stdin_resolve_dir = v }
141
+ get_flag(stdin_options, :sourcefile, String) { |v| flags << "--sourcefile=#{v}" }
142
+ get_flag(stdin_options, :loader, String) { |v| flags << "--loader=#{v}" }
143
+ raise ArgumentError, "Invalid option in stdin options: #{stdin_options.keys.first}" unless stdin_options.empty?
144
+ end
145
+ node_paths = []
146
+ get_flag(options, :node_paths, Array) { |v| v.each { |path| node_paths << path.to_s } }
147
+ write = true
148
+ get_flag(options, :write, BOOL) { |v| write = v }
149
+ abs_working_dir = nil
150
+ get_flag(options, :abs_working_dir, String) { |v| abs_working_dir = v }
151
+ incremental = false
152
+ get_flag(options, :incremental, BOOL) { |v| incremental = v }
153
+ raise ArgumentError, "Invalid option in build() call: #{options.keys.first}" unless options.empty?
154
+ {
155
+ entries: entries,
156
+ flags: flags,
157
+ write: write,
158
+ stdin_contents: stdin_contents,
159
+ stdin_resolve_dir: stdin_resolve_dir,
160
+ abs_working_dir: abs_working_dir,
161
+ incremental: incremental,
162
+ node_paths: node_paths,
163
+ watch: watch_mode
164
+ }
165
+ end
166
+
167
+ def push_log_flags(flags, options, log_level_default)
168
+ get_flag(options, :color, BOOL) { |v| flags << "--color=#{v}" if v }
169
+ log_level = log_level_default
170
+ get_flag(options, :log_level, STRING_OR_SYMBOL) { |v| log_level = v }
171
+ flags << "--log-level=#{log_level}"
172
+ log_limit = 0
173
+ get_flag(options, :log_limit, Numeric) { |v| log_limit = v }
174
+ flags << "--log-limit=#{log_limit}"
175
+ end
176
+
177
+ def push_common_flags(flags, options)
178
+ get_flag(options, :source_root, String) { |v| flags << "--source-root=#{v}" }
179
+ get_flag(options, :sources_content, BOOL) { |v| flags << "--sources-content=#{v}" }
180
+ get_flag(options, :target, STRING_OR_ARRAY) do |v|
181
+ v = [v] unless Array === v
182
+ targets = v.map do |t|
183
+ t = t.to_s
184
+ raise ArgumentError, "Invalid target: #{t}" if t.include?(",")
185
+ t
186
+ end
187
+ flags << "--target=#{targets.join(",")}"
188
+ end
189
+ get_flag(options, :format, String) { |v| flags << "--format=#{v}" }
190
+ get_flag(options, :global_name, String) { |v| flags << "--global-name=#{v}" }
191
+ get_flag(options, :minify, BOOL) { |v| flags << "--minify" if v }
192
+ get_flag(options, :minify_syntax, BOOL) { |v| flags << "--minify-syntax" if v }
193
+ get_flag(options, :minify_whitespace, BOOL) { |v| flags << "--minify-whitespace" if v }
194
+ get_flag(options, :minify_identifiers, BOOL) { |v| flags << "--minify-identifiers" if v }
195
+ get_flag(options, :charset, String) { |v| flags << "--charset=#{v}" }
196
+ get_flag(options, :tree_shaking, STRING_OR_BOOL) { |v| flags << "--tree-shaking=#{v}" if v != true }
197
+ get_flag(options, :jsx_factory, String) { |v| flags << "--jsx-factory=#{v}" }
198
+ get_flag(options, :jsx_fragment, String) { |v| flags << "--jsx-fragment=#{v}" }
199
+ get_flag(options, :define, Hash) do |v|
200
+ v.each do |key, value|
201
+ raise "Invalid define: #{key}" if key.include? "="
202
+ flags << "--define:#{key}=#{value}"
203
+ end
204
+ end
205
+ get_flag(options, :pure, Array) do |v|
206
+ v.each { |fn| flags << "--pure:#{fn}" }
207
+ end
208
+ get_flag(options, :keep_names, BOOL) { |v| flags << "--keep-names" if v }
209
+ end
210
+
211
+ def get_flag(options, sym, check)
212
+ return unless options.has_key?(sym)
213
+ value = options.delete(sym)
214
+ raise "#{sym} must be #{check}" unless check === value
215
+ yield value
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,3 @@
1
+ module Esbuild
2
+ Packet = Struct.new(:id, :is_request, :value)
3
+ end
@@ -0,0 +1,22 @@
1
+ module Esbuild
2
+ class ServeResult
3
+ def initialize(response, wait, stop)
4
+ @port = response["port"]
5
+ @host = response["host"]
6
+ @wait = wait
7
+ @stop = stop
8
+ @is_stopped = false
9
+ end
10
+
11
+ def wait
12
+ @wait.wait!
13
+ @wait.value
14
+ end
15
+
16
+ def stop
17
+ return if @is_stopped
18
+ @is_stopped = true
19
+ @stop.call
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,279 @@
1
+ require "concurrent"
2
+ require "esbuild/stdio_protocol"
3
+ require "esbuild/flags"
4
+ require "esbuild/build_result"
5
+ require "esbuild/serve_result"
6
+ require "esbuild/build_state"
7
+ require "esbuild/transform_result"
8
+
9
+ module Esbuild
10
+ class Service
11
+ # TODO: plugins
12
+
13
+ def initialize
14
+ @request_id = 0
15
+ @serve_id = 0
16
+ @build_key = 0
17
+ @first_packet = true
18
+ @response_callbacks = Concurrent::Map.new
19
+ @plugin_callbacks = Concurrent::Map.new
20
+ @watch_callbacks = Concurrent::Map.new
21
+ @serve_callbacks = Concurrent::Map.new
22
+ @buffer = String.new(encoding: Encoding::BINARY)
23
+
24
+ child_read, child_stdout = IO.pipe
25
+ child_stdin, @child_write = IO.pipe
26
+ bin = binary_path
27
+ pid = spawn(bin, "--service=#{ESBUILD_VERSION}", "--ping", out: child_stdout, err: :err, in: child_stdin)
28
+ child_stdin.close
29
+ child_stdout.close
30
+
31
+ Thread.new { worker_thread(pid, child_read) }
32
+ end
33
+
34
+ def build_or_serve(options, serve_options = nil)
35
+ key = @build_key
36
+ @build_key += 1
37
+ opts = Flags.flags_for_build_options(options)
38
+ on_rebuild = opts[:watch]&.fetch(:on_rebuild, nil)
39
+
40
+ request = {
41
+ "command" => "build",
42
+ "key" => key,
43
+ "entries" => opts[:entries],
44
+ "flags" => opts[:flags],
45
+ "write" => opts[:write],
46
+ "stdinContents" => opts[:stdin_contents],
47
+ "stdinResolveDir" => opts[:stdin_resolve_dir],
48
+ "absWorkingDir" => opts[:abs_working_dir] || Dir.pwd,
49
+ "incremental" => opts[:incremental],
50
+ "nodePaths" => opts[:node_paths],
51
+ "hasOnRebuild" => !!on_rebuild
52
+ }
53
+ serve = serve_options && build_serve_data(serve_options, request)
54
+
55
+ response = send_request(request)
56
+ if serve
57
+ ServeResult.new(response, serve[:wait], serve[:stop])
58
+ else
59
+ build_state = BuildState.new(self, on_rebuild)
60
+ build_state.response_to_result(response)
61
+ end
62
+ end
63
+
64
+ def start_watch(watch_id, proc)
65
+ @watch_callbacks[watch_id] = proc
66
+ end
67
+
68
+ def stop_watch(watch_id)
69
+ @watch_callbacks.delete(watch_id)
70
+ send_request(
71
+ "command" => "watch-stop",
72
+ "watchID" => watch_id
73
+ )
74
+ end
75
+
76
+ def transform(input, options)
77
+ flags = Flags.flags_for_transform_options(options)
78
+ res = send_request(
79
+ "command" => "transform",
80
+ "flags" => flags,
81
+ "inputFS" => false,
82
+ "input" => input
83
+ )
84
+
85
+ unless res["errors"].empty?
86
+ raise BuildFailureError, res["errors"], res["warnings"]
87
+ end
88
+
89
+ TransformResult.new(res)
90
+ end
91
+
92
+ def send_request(request)
93
+ @request_id += 1
94
+ id = @request_id
95
+ encoder = StdioProtocol::PacketEncoder.new
96
+ encoded = encoder.encode_packet(Packet.new(id, true, request))
97
+ @child_write.write encoded
98
+ ivar = Concurrent::IVar.new
99
+ @response_callbacks[id] = ivar
100
+ ivar.wait!
101
+ ivar.value
102
+ end
103
+
104
+ private
105
+
106
+ def read_from_stdout(chunk)
107
+ @buffer << chunk
108
+ offset = 0
109
+ while offset + 4 < @buffer.bytesize
110
+ size = @buffer.getbyte(offset) | (@buffer.getbyte(offset + 1) << 8) | (@buffer.getbyte(offset + 2) << 16) | (@buffer.getbyte(offset + 3) << 24)
111
+ if offset + 4 + size > @buffer.bytesize
112
+ break
113
+ end
114
+ offset += 4
115
+ handle_incoming_packet(@buffer, offset, size)
116
+ offset += size
117
+ end
118
+ @buffer.slice!(0, offset)
119
+ end
120
+
121
+ def send_response(id, response)
122
+ encoder = StdioProtocol::PacketEncoder.new
123
+ encoded = encoder.encode_packet(Packet.new(id, false, response))
124
+ @child_write.write encoded
125
+ end
126
+
127
+ def build_serve_data(options, request)
128
+ serve_id = @serve_id
129
+ @serve_id += 1
130
+ options = options.dup
131
+ on_request = nil
132
+ Flags.get_flag(options, :port, Numeric) { |v| request[:port] = v }
133
+ Flags.get_flag(options, :host, String) { |v| request[:host] = v }
134
+ Flags.get_flag(options, :serve_dir, String) { |v| request[:serveDir] = v }
135
+ Flags.get_flag(options, :on_request, Proc) { |v| on_request = v }
136
+ raise ArgumentError, "Invalid option in serve() call: #{options.keys.first}" unless options.empty?
137
+ request[:serve] = {serveID: serve_id}
138
+ wait = Concurrent::IVar.new
139
+ @serve_callbacks[serve_id] = {
140
+ on_request: on_request,
141
+ on_wait: ->(error) do
142
+ @serve_callbacks.delete(serve_id)
143
+ if error
144
+ wait.fail StandardError.new(error)
145
+ else
146
+ wait.set
147
+ end
148
+ end
149
+ }
150
+
151
+ {
152
+ wait: wait,
153
+ stop: -> do
154
+ send_request("command" => "serve-stop", "serveID" => serve_id)
155
+ end
156
+ }
157
+ end
158
+
159
+ def handle_request(id, request)
160
+ case request["command"]
161
+ when "ping"
162
+ send_response(id, {})
163
+ when "resolve", "load"
164
+ callback = @plugin_callbacks[request["key"]]
165
+ response = {}
166
+ if callback
167
+ response = callback.call(request)
168
+ end
169
+ send_response(id, response)
170
+ when "serve-request"
171
+ callback = @serve_callbacks[request["serveID"]]
172
+ if callback && callback[:on_request]
173
+ on_request = callback[:on_request]
174
+ if on_request.arity == 1
175
+ on_request.call(request["args"])
176
+ else
177
+ on_request.call
178
+ end
179
+ end
180
+ send_response(id, {})
181
+ when "serve-wait"
182
+ callback = @serve_callbacks[request["serveID"]]
183
+ if callback && callback[:on_wait]
184
+ callback[:on_wait].call(request["error"])
185
+ end
186
+ send_response(id, {})
187
+ when "watch-rebuild"
188
+ callback = @watch_callbacks[request["watchID"]]
189
+ callback&.call(nil, request["args"])
190
+ send_response(id, {})
191
+ else
192
+ raise "Unknown command #{request["command"]}"
193
+ end
194
+ rescue => e
195
+ send_response(id, {errors: [{text: e.message}]})
196
+ end
197
+
198
+ def handle_incoming_packet(bytes, offset, size)
199
+ if @first_packet
200
+ @first_packet = false
201
+ version = bytes.slice(offset, size)
202
+ raise "Version mismatch #{ESBUILD_VERSION} != #{version}" if ESBUILD_VERSION != version
203
+
204
+ return
205
+ end
206
+
207
+ decoder = StdioProtocol::PacketDecoder.new(bytes, offset, size)
208
+ packet = decoder.decode_packet
209
+
210
+ if packet.is_request
211
+ handle_request packet.id, packet.value
212
+ else
213
+ callback = @response_callbacks.delete(packet.id)
214
+ if packet.value["error"]
215
+ callback.fail(StandardError.new(packet.value["error"]))
216
+ else
217
+ callback.set(packet.value)
218
+ end
219
+ end
220
+ end
221
+
222
+ def worker_thread(pid, child_read)
223
+ buffer = String.new(encoding: Encoding::BINARY)
224
+ loop do
225
+ done = Process.waitpid(pid, Process::WNOHANG)
226
+ if done
227
+ error = StandardError.new("Closed")
228
+ close_callbacks(error)
229
+ break
230
+ end
231
+
232
+ begin
233
+ chunk = child_read.read_nonblock(16384, buffer)
234
+ read_from_stdout chunk
235
+ rescue IO::WaitReadable
236
+ IO.select([child_read])
237
+ retry
238
+ end
239
+ end
240
+ ensure
241
+ @child_write.close
242
+ child_read.close
243
+ end
244
+
245
+ def close_callbacks(error)
246
+ @response_callbacks.each_value do |callback|
247
+ callback.fail(error)
248
+ end
249
+ @response_callbacks.clear
250
+ end
251
+
252
+ def binary_path
253
+ ENV["ESBUILD_BINARY_PATH"] || File.expand_path("../../bin/esbuild", __dir__)
254
+ end
255
+ end
256
+
257
+ class BuildFailureError < StandardError
258
+ attr_reader :errors
259
+ attr_reader :warnings
260
+
261
+ def initialize(errors, warnings)
262
+ @errors = errors
263
+ @warnings = warnings
264
+ summary = ""
265
+ unless errors.empty?
266
+ limit = 5
267
+ details = errors.slice(0, limit + 1).each_with_index.map do |error, index|
268
+ break "\n..." if index == limit
269
+ location = error["location"]
270
+ break "\nerror: #{error["text"]}" unless location
271
+ "\n#{location["file"]}:#{location["line"]}:#{location["column"]}: error: #{error["text"]}"
272
+ end.join
273
+ summary = "with #{errors.size} error#{errors.size > 1 ? "s" : ""}:#{details}"
274
+ end
275
+
276
+ super "Build failed#{summary}"
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,150 @@
1
+ require "esbuild/packet"
2
+
3
+ module Esbuild
4
+ module StdioProtocol
5
+ extend self
6
+
7
+ class PacketDecoder
8
+ attr_reader :offset
9
+
10
+ def initialize(buf, offset, size)
11
+ @buf = buf
12
+ @offset = offset
13
+ @end = offset + size
14
+ end
15
+
16
+ def decode_packet
17
+ id = read32
18
+ is_request = (id & 1) == 0
19
+ id >>= 1
20
+
21
+ value = visit
22
+ Packet.new(id, is_request, value)
23
+ end
24
+
25
+ def visit
26
+ kind = read8
27
+ case kind
28
+ when 0
29
+ # Null
30
+ nil
31
+ when 1
32
+ # Bool
33
+ read8 == 1
34
+ when 2
35
+ # Integer
36
+ read32
37
+ when 3
38
+ # String
39
+ read_string.force_encoding(Encoding::UTF_8)
40
+ when 4
41
+ # Bytes
42
+ read_string
43
+ when 5
44
+ # Array
45
+ size = read32
46
+ size.times.map { visit }
47
+ when 6
48
+ # Object
49
+ result = {}
50
+ size = read32
51
+ size.times do
52
+ key = read_string.force_encoding(Encoding::UTF_8)
53
+ result[key] = visit
54
+ end
55
+ result
56
+ else
57
+ raise ArgumentError, "Invalid packet #{kind}"
58
+ end
59
+ end
60
+
61
+ def read_string
62
+ size = read32
63
+ result = @buf.byteslice(@offset, size)
64
+ @offset += size
65
+ result
66
+ end
67
+
68
+ def read8
69
+ raise ArgumentError, "Reading past buffer" if @offset >= @end
70
+ byte = @buf.getbyte(@offset)
71
+ @offset += 1
72
+ byte
73
+ end
74
+
75
+ def read32
76
+ read8 | (read8 << 8) | (read8 << 16) | (read8 << 24)
77
+ end
78
+ end
79
+
80
+ class PacketEncoder
81
+ def initialize
82
+ @format = ""
83
+ @elements = []
84
+ @size = 0
85
+ end
86
+
87
+ def encode_packet(packet)
88
+ write32 0
89
+ write32((packet.id << 1) | (packet.is_request ? 0 : 1))
90
+ visit(packet.value)
91
+ @elements[0] = @size - 4
92
+ @elements.pack(@format)
93
+ end
94
+
95
+ def visit(value)
96
+ case value
97
+ when nil
98
+ write8 0
99
+ when true, false
100
+ write8 1
101
+ write8(value ? 1 : 0)
102
+ when Integer
103
+ write8 2
104
+ write32 value
105
+ when String
106
+ if value.encoding == Encoding::BINARY
107
+ write8 4
108
+ else
109
+ write8 3
110
+ value = value.encode(Encoding::UTF_8) unless value.encoding == Encoding::UTF_8
111
+ end
112
+ write_string value
113
+ when Array
114
+ write8 5
115
+ write32 value.size
116
+ value.each { |item| visit(item) }
117
+ when Hash
118
+ write8 6
119
+ write32 value.size
120
+ value.each do |key, val|
121
+ write_string key
122
+ visit val
123
+ end
124
+ else
125
+ raise ArgumentError, "Don't know how to encode #{value.inspect}"
126
+ end
127
+ end
128
+
129
+ def write_string(string)
130
+ string = string.to_s
131
+ write32 string.bytesize
132
+ @elements << string
133
+ @format << "a*"
134
+ @size += string.bytesize
135
+ end
136
+
137
+ def write32(value)
138
+ @elements << value
139
+ @format << "L<"
140
+ @size += 4
141
+ end
142
+
143
+ def write8(value)
144
+ @elements << value
145
+ @format << "C"
146
+ @size += 1
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,25 @@
1
+ module Esbuild
2
+ class TransformResult
3
+ attr_reader :code, :map, :warnings
4
+
5
+ def initialize(result)
6
+ @code = read_file(result["codeFS"], result["code"])
7
+ @map = read_file(result["mapFS"], result["map"])
8
+ @warnings = result["warnings"]
9
+ end
10
+
11
+ private
12
+
13
+ # If the files are too big, esbuild will create a tempfile to pass back the result
14
+ # We need to delete it
15
+ def read_file(fs, name)
16
+ if fs
17
+ contents = File.read(name)
18
+ File.delete(name)
19
+ contents
20
+ else
21
+ name
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esbuild
4
+ VERSION = "0.1.0"
5
+ ESBUILD_VERSION = "0.11.9"
6
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: esbuild
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bouke van der Bijl
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.14.4
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 5.14.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: standard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.4
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.4
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ description:
70
+ email:
71
+ - i@bou.ke
72
+ executables: []
73
+ extensions:
74
+ - Rakefile
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".github/workflows/main.yml"
78
+ - ".gitignore"
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/console
85
+ - bin/setup
86
+ - esbuild.gemspec
87
+ - lib/esbuild.rb
88
+ - lib/esbuild/binary_installer.rb
89
+ - lib/esbuild/build_result.rb
90
+ - lib/esbuild/build_state.rb
91
+ - lib/esbuild/flags.rb
92
+ - lib/esbuild/packet.rb
93
+ - lib/esbuild/serve_result.rb
94
+ - lib/esbuild/service.rb
95
+ - lib/esbuild/stdio_protocol.rb
96
+ - lib/esbuild/transform_result.rb
97
+ - lib/esbuild/version.rb
98
+ homepage: https://github.com/bouk/esbuild-ruby
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ allowed_push_host: https://rubygems.org
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 2.4.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.2.3
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Use esbuild from Ruby
122
+ test_files: []