wasmify-rails 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d34168c0b9ab6af658deb9e91559fc45ff5d17bb7d015ef8859c216ab9736b41
4
- data.tar.gz: 7049aa93891ea2fde7f911ed87d5f0356f01f99769b77a430aae69dd9db2b8c4
3
+ metadata.gz: 55efaa8fa8cd15d3eb906efe9342baa453af8e53d363f12bc72fb4c3f2afc1a8
4
+ data.tar.gz: a20c26e0a6525f9f0bb183a4d59620895864c6f450739677d39e39a95c471a49
5
5
  SHA512:
6
- metadata.gz: 43003eafb292715b34a9360426a2f8cd4bc572f8ce9f0494a19999abe48487fa766656bfa6da57bfa3a9e1ab74bb2b1e41db9191208015a85699364e1a9eda29
7
- data.tar.gz: 8879ab43592abc34a0675d7e80763e9030b86cd2145535ec26fe0446465379d0c10f3a7c685d47b4a8165abe6e94714987ad8774ff3e16ce781abaea21699a35
6
+ metadata.gz: 78329c56067b9bf41c72bd46fb02686ceea78b12f9785f6f2623bf12558476d36d45db1e6ac0402be16f070fc83e2ce4490ef816d55626c1ea847762059d5fff
7
+ data.tar.gz: 3e783be0989c85ba28841078b7f2ea7626f7b682d9c807fa411307a08988d73046c8f56aa8eecc1ad4a5c0c41d757c1003e0499378465616a46d04242002c22c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.2.0
6
+
7
+ - Add pglite support.
8
+
9
+ ## 0.1.5
10
+
11
+ - Added async mode to rack.js.
12
+
13
+ - Added ability to pass env vars to Ruby VM.
14
+
15
+ - Rails 8 support for `sqlite3_wasm` adapter.
16
+
5
17
  ## 0.1.4
6
18
 
7
19
  - Improve `wasmify:install`.
data/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  This gem provides tools and extensions to compile Rails applications to WebAssembly.
6
6
 
7
+ > [!NOTE]
8
+ > Read more in our handbook: [Rails on Wasm](https://writebook-on-wasm.fly.dev/5/ruby-on-rails-on-webassembly)
9
+
7
10
  ## Installation
8
11
 
9
12
  Adding to your Gemfile:
@@ -169,7 +172,8 @@ This gem provides a variety of _adapters_ and plugins to make your Rails applica
169
172
 
170
173
  - Active Record
171
174
 
172
- - `sqlite3_wasm` adapter: work with `sqlite3` Wasm just like with a regular SQLite database.
175
+ - `sqlite3_wasm` adapter: works with `sqlite3` Wasm just like with a regular SQLite database.
176
+ - `pglite` adapter: uses [pglite](https://pglite.dev) as a database.
173
177
  - `nulldb` adapter for testing purposes.
174
178
 
175
179
  - Active Storage
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Update the $LOAD_PATH to include the pg stub
4
+ $LOAD_PATH.unshift(File.expand_path(File.join(__dir__, "pglite_shims")))
5
+
6
+ module PGlite
7
+ class << self
8
+ attr_accessor :logger
9
+
10
+ def log(message)
11
+ logger&.debug "[pglite] #{message}"
12
+ end
13
+ end
14
+
15
+ class Result
16
+ def initialize(res)
17
+ @res = res
18
+ end
19
+
20
+ def map_types!(map)
21
+ self
22
+ end
23
+
24
+ def values
25
+ results = []
26
+ columns = self.fields
27
+ @res[:rows].to_a.each do |raw_row|
28
+ row = []
29
+ columns.each do |col|
30
+ value = raw_row[col]
31
+ row << translate_value(value)
32
+ end
33
+ results << row
34
+ end
35
+ results
36
+ end
37
+
38
+ def fields
39
+ @res[:fields].to_a.map { |col| col[:name].to_s }
40
+ end
41
+
42
+ def ftype(index)
43
+ @res[:fields][index][:dataTypeID]
44
+ end
45
+
46
+ def fmod(index)
47
+ 0
48
+ end
49
+
50
+ def cmd_tuples
51
+ @res[:affectedRows].to_i
52
+ end
53
+
54
+ def clear
55
+ end
56
+
57
+ include Enumerable
58
+
59
+ def each
60
+ columns = self.fields
61
+ @res[:rows].to_a.each do |raw_row|
62
+ row = {}
63
+ columns.each do |col|
64
+ value = raw_row[col]
65
+ row[col.to_s] = translate_value(value)
66
+ end
67
+ yield row
68
+ end
69
+ end
70
+
71
+ private
72
+ def translate_value(value)
73
+ case value.typeof
74
+ when "number"
75
+ value.to_i
76
+ when "boolean"
77
+ value == JS::True
78
+ when "undefined"
79
+ nil
80
+ else
81
+ if value == JS::Null
82
+ nil
83
+ else
84
+ value.to_s
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ require "active_record/connection_adapters/postgresql_adapter"
92
+
93
+ module ActiveRecord
94
+ module ConnectionHandling # :nodoc:
95
+ def pglite_adapter_class
96
+ ConnectionAdapters::PGliteAdapter
97
+ end
98
+
99
+ def pglite_connection(config)
100
+ pglite_adapter_class.new(config)
101
+ end
102
+ end
103
+
104
+ module ConnectionAdapters
105
+ class PGliteAdapter < PostgreSQLAdapter
106
+ class ExternalInterface
107
+ private attr_reader :js_interface
108
+
109
+ def initialize(config)
110
+ @js_interface = config.fetch(:js_interface, "pglite4rails").to_sym
111
+ @last_result = nil
112
+ @prepared_statements_map = {}
113
+ end
114
+
115
+ def set_client_encoding(encoding)
116
+ end
117
+
118
+ def transaction_status
119
+ PG::PQTRANS_IDLE
120
+ end
121
+
122
+ def escape(str)
123
+ str
124
+ end
125
+
126
+ def raw_query(sql, params)
127
+ PGlite.log "[pglite] query: #{sql} with params: #{params}"
128
+ params = params.map { |param| param.to_js }
129
+ raw_res = JS.global[js_interface].query(sql, params.to_js).await
130
+ result = PGlite::Result.new(raw_res)
131
+ PGlite.log "[pglite] result: #{result.values}"
132
+ @last_result = result
133
+ result
134
+ rescue => e
135
+ raise PG::Error, e.message
136
+ end
137
+
138
+ def exec(sql)
139
+ raw_query(sql, [])
140
+ end
141
+
142
+ alias query exec
143
+ alias async_exec exec
144
+ alias async_query exec
145
+
146
+ def exec_params(sql, params)
147
+ if params.empty?
148
+ return exec(sql)
149
+ end
150
+ raw_query(sql, params)
151
+ end
152
+
153
+ def exec_prepared(name, params)
154
+ sql = @prepared_statements_map[name]
155
+ exec_params(sql, params)
156
+ end
157
+
158
+ def prepare(name, sql, param_types = nil)
159
+ @prepared_statements_map[name] = sql
160
+ end
161
+
162
+ def get_last_result
163
+ @last_result
164
+ end
165
+
166
+ def reset
167
+ @prepared_statements_map = {}
168
+ end
169
+ end
170
+
171
+ class << self
172
+ def database_exists?(config)
173
+ true
174
+ end
175
+
176
+ def new_client(...) = ExternalInterface.new(...)
177
+ end
178
+
179
+ def initialize(...)
180
+ AbstractAdapter.instance_method(:initialize).bind_call(self, ...)
181
+ @connection_parameters = @config.compact
182
+
183
+ @max_identifier_length = nil
184
+ @type_map = ActiveRecord::Type::HashLookupTypeMap.new
185
+ @raw_connection = nil
186
+ @notice_receiver_sql_warnings = []
187
+
188
+ @use_insert_returning = true
189
+ end
190
+
191
+ def get_database_version = 150000 # 15devel
192
+
193
+ def configure_connection
194
+ end
195
+
196
+ module Type
197
+ class BigintArray < ActiveRecord::Type::Value
198
+ def deserialize(value)
199
+ return nil if value.nil? || value == ""
200
+ value[1..-2].split(",").map(&:to_i)
201
+ end
202
+
203
+ def serialize(value)
204
+ return nil if value.nil? || value == ""
205
+ "{" + value.map(&:to_s).join(", ") + "}"
206
+ end
207
+
208
+ def cast(value)
209
+ value
210
+ end
211
+ end
212
+ end
213
+
214
+ def get_oid_type(oid, fmod, column_name, sql_type = "")
215
+ # https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
216
+ oid = oid.to_i
217
+ ty = case oid
218
+ when 16
219
+ ActiveRecord::Type::Boolean.new
220
+ when 20
221
+ ActiveRecord::Type::Integer.new(limit: 8)
222
+ when 21
223
+ ActiveRecord::Type::Integer.new
224
+ when 23
225
+ ActiveRecord::Type::Integer.new
226
+ when 25
227
+ ActiveRecord::Type::String.new
228
+ when 114
229
+ ActiveRecord::Type::String.new
230
+ when 700, 701
231
+ ActiveRecord::Type::Float.new
232
+ when 1015
233
+ ActiveRecord::Type::String.new
234
+ when 1016 # bigint[]
235
+ Type::BigintArray.new
236
+ when 1043
237
+ ActiveRecord::Type::String.new
238
+ when 1082
239
+ ActiveRecord::Type::Date.new
240
+ when 1083
241
+ ActiveRecord::Type::Time.new
242
+ when 1114
243
+ ActiveRecord::Type::DateTime.new
244
+ when 1184
245
+ ActiveRecord::Type::DateTime.new
246
+ when 1700
247
+ ActiveRecord::Type::Decimal.new
248
+ when 3802
249
+ ActiveRecord::Type::Json.new
250
+ else
251
+ ActiveRecord::Type.default_value
252
+ end
253
+ @type_map.register_type(oid, ty)
254
+ ty
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # pg gem stub
4
+ module PG
5
+ PQTRANS_IDLE = 0 # (connection idle)
6
+ PQTRANS_ACTIVE = 1 # (command in progress)
7
+ PQTRANS_INTRANS = 2 # (idle, within transaction block)
8
+ PQTRANS_INERROR = 3 # (idle, within failed transaction)
9
+ PQTRANS_UNKNOWN = 4 # (cannot determine status)
10
+
11
+ class Error < StandardError; end
12
+ class ConnectionBad < Error; end
13
+
14
+ class Connection
15
+ class << self
16
+ def quote_ident(str)
17
+ str = str.to_s
18
+ return '""' if str.empty?
19
+ if str =~ /[^a-zA-Z_0-9]/ || str =~ /^[0-9]/
20
+ '"' + str.gsub('"', '""') + '"'
21
+ else
22
+ str
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Just a stub for now
29
+ class SimpleDecoder
30
+ end
31
+ end
@@ -1,12 +1,12 @@
1
- # Stub: SQLite3::ForkSafety.suppress_warnings!
1
+ # Rails 8 compatibility
2
2
  module SQLite3
3
- class BusyException < StandardError
4
- end
5
-
6
3
  module ForkSafety
7
4
  def self.suppress_warnings!
8
5
  end
9
6
  end
7
+
8
+ class BusyException < StandardError
9
+ end
10
10
  end
11
11
 
12
12
  require "active_record/connection_adapters/sqlite3_adapter"
@@ -44,6 +44,12 @@ module ActiveRecord
44
44
  JS.global[js_interface].exec(...)
45
45
  end
46
46
 
47
+ def execute_batch2(...)
48
+ # TODO: it's used by fixtures and truncate_all, so
49
+ # we don't need it right now
50
+ raise NotImplementedError, "sqlite3_wasm#execute_batch2 is not implemented yet"
51
+ end
52
+
47
53
  def busy_timeout(...) = nil
48
54
 
49
55
  def execute(sql)
@@ -195,6 +201,29 @@ module ActiveRecord
195
201
  def database_exists? = true
196
202
 
197
203
  def database_version = SQLite3Adapter::Version.new("3.45.1")
204
+
205
+ # Rails 8 interface
206
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
207
+ if batch
208
+ raw_connection.execute_batch2(sql)
209
+ elsif prepare
210
+ raise NotImplementedError, "sqlite3_wasm prepared statements are not implemented yet"
211
+ else
212
+ stmt = raw_connection.prepare(sql)
213
+ result =
214
+ if stmt.column_count.zero?
215
+ ActiveRecord::Result.empty
216
+ else
217
+ ActiveRecord::Result.new(stmt.columns, stmt.to_a)
218
+ end
219
+ end
220
+
221
+ @last_affected_rows = raw_connection.changes
222
+ verified!
223
+
224
+ notification_payload[:row_count] = result&.length || 0
225
+ result
226
+ end
198
227
  end
199
228
  end
200
229
  end
@@ -11,13 +11,14 @@ module Wasmify
11
11
  class Builder
12
12
  ORIGINAL_EXCLUDED_GEMS = RubyWasm::Packager::EXCLUDED_GEMS.dup.freeze
13
13
 
14
- attr_reader :output_dir
14
+ attr_reader :output_dir, :target
15
15
 
16
- def initialize(output_dir: Wasmify::Rails.config.tmp_dir)
16
+ def initialize(output_dir: Wasmify::Rails.config.tmp_dir, target: Wasmify::Rails.config.wasm_target)
17
17
  @output_dir = output_dir
18
+ @target = target
18
19
  end
19
20
 
20
- def run(name:, exclude_gems: [])
21
+ def run(name:, exclude_gems: [], opts: "")
21
22
  # Reset excluded gems
22
23
  RubyWasm::Packager::EXCLUDED_GEMS.replace(ORIGINAL_EXCLUDED_GEMS)
23
24
 
@@ -31,11 +32,22 @@ module Wasmify
31
32
  RubyWasm::Packager::EXCLUDED_GEMS << gem_name
32
33
  end
33
34
 
35
+ opts = opts&.split(" ") || []
36
+
34
37
  args = %W(
35
38
  build
36
39
  --ruby-version #{Wasmify::Rails.config.short_ruby_version}
40
+ --target #{target}
37
41
  -o #{File.join(output_dir, name)}
38
- )
42
+ ) + opts
43
+
44
+ patches_dir = ::Rails.root.join("ruby_wasm_patches").to_s
45
+
46
+ if File.directory?(patches_dir)
47
+ Dir.children(patches_dir).each do |patch|
48
+ args << "--patch=#{File.join(patches_dir, patch)}"
49
+ end
50
+ end
39
51
 
40
52
  FileUtils.mkdir_p(output_dir)
41
53
  RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr).run(args)
@@ -9,7 +9,7 @@ module Wasmify
9
9
 
10
10
  class Configuration
11
11
  attr_reader :pack_directories, :exclude_gems, :ruby_version,
12
- :tmp_dir, :output_dir,
12
+ :tmp_dir, :output_dir, :wasm_target,
13
13
  :skip_assets_precompile
14
14
 
15
15
  def initialize
@@ -24,6 +24,7 @@ module Wasmify
24
24
  @tmp_dir = config["tmp_dir"] || ::Rails.root.join("tmp", "wasmify")
25
25
  @output_dir = config["output_dir"] || ::Rails.root.join("dist")
26
26
  @skip_assets_precompile = config["skip_assets_precompile"] || false
27
+ @wasm_target = config["wasm_target"] || "wasm32-unknown-wasip1"
27
28
  end
28
29
 
29
30
  def short_ruby_version
@@ -12,28 +12,38 @@ namespace :wasmify do
12
12
  end
13
13
 
14
14
  desc "Build ruby.wasm with all dependencies"
15
- task :build do
15
+ task :build, [:args] do |_, args|
16
+ opts = args.args
16
17
  unless ENV["BUNDLE_ONLY"] == "wasm"
17
- next spawn("RAILS_ENV=wasm BUNDLE_ONLY=wasm bundle exec rails wasmify:build").then { Process.wait(_1) }
18
+ next spawn(%Q(RAILS_ENV=wasm BUNDLE_ONLY=wasm bundle exec rails 'wasmify:build[#{opts}]')).then do
19
+ Process.wait2(_1)
20
+ end.then do |(_, status)|
21
+ status.success? or exit(status.exitstatus)
22
+ end
18
23
  end
19
24
 
20
25
  require "wasmify/rails/builder"
21
26
 
22
27
  builder = Wasmify::Rails::Builder.new
23
- builder.run(name: "ruby.wasm")
28
+ builder.run(name: "ruby.wasm", opts:)
24
29
  end
25
30
 
26
31
  namespace :build do
27
32
  desc "Build ruby.wasm with all dependencies but JS shim (to use with wasmtime)"
28
- task :core do
33
+ task :core, [:args] do |_, args|
34
+ opts = args.args
29
35
  unless ENV["BUNDLE_ONLY"] == "wasm"
30
- next spawn("RAILS_ENV=wasm BUNDLE_ONLY=wasm bundle exec rails wasmify:build:core").then { Process.wait(_1) }
36
+ next spawn(%Q(RAILS_ENV=wasm BUNDLE_ONLY=wasm bundle exec rails 'wasmify:build:core[#{opts}]')).then do
37
+ Process.wait2(_1)
38
+ end.then do |(_, status)|
39
+ status.success? or exit(status.exitstatus)
40
+ end
31
41
  end
32
42
 
33
43
  require "wasmify/rails/builder"
34
44
 
35
45
  builder = Wasmify::Rails::Builder.new
36
- builder.run(name: "ruby-core.wasm", exclude_gems: ["js"])
46
+ builder.run(name: "ruby-core.wasm", exclude_gems: ["js"], opts:)
37
47
  end
38
48
 
39
49
  namespace :core do
@@ -55,11 +65,18 @@ namespace :wasmify do
55
65
  end
56
66
 
57
67
  desc "Pack the application into to a single module"
58
- task pack: :build do
68
+ task :pack, [:args] do |_, args|
69
+ opts = args.args
70
+ Rails::Command.invoke "wasmify:build[#{opts}]"
71
+
59
72
  # First, precompile assets
60
73
  unless Wasmify::Rails.config.skip_assets_precompile
61
74
  Bundler.with_unbundled_env do
62
- spawn("SECRET_KEY_BASE_DUMMY=1 RAILS_ENV=production bundle exec rails assets:precompile").then { Process.wait(_1) }
75
+ spawn("SECRET_KEY_BASE_DUMMY=1 RAILS_ENV=production bundle exec rails assets:precompile").then do
76
+ Process.wait2(_1)
77
+ end.then do |(_, status)|
78
+ status.success? or exit(status.exitstatus)
79
+ end
63
80
  end
64
81
  end
65
82
 
@@ -68,15 +85,27 @@ namespace :wasmify do
68
85
  packer = Wasmify::Rails::Packer.new
69
86
 
70
87
  packer.run(name: "app.wasm", ruby_wasm_path: File.join(Wasmify::Rails.config.tmp_dir, "ruby.wasm"))
88
+ ensure
89
+ # Clean up precompiled assets
90
+ unless Wasmify::Rails.config.skip_assets_precompile
91
+ FileUtils.rm_rf(Rails.root.join("public/assets")) if File.directory?(Rails.root.join("public/assets"))
92
+ end
71
93
  end
72
94
 
73
95
  namespace :pack do
74
96
  desc "Pack the application into to a single module without JS shim"
75
- task :core do
97
+ task :core, [:args] do |_, args|
98
+ opts = args.args
99
+ Rails::Command.invoke "wasmify:build:core[#{opts}]"
100
+
76
101
  # First, precompile assets
77
102
  unless Wasmify::Rails.config.skip_assets_precompile
78
103
  Bundler.with_unbundled_env do
79
- spawn("SECRET_KEY_BASE_DUMMY=1 RAILS_ENV=production bundle exec rails assets:precompile").then { Process.wait(_1) }
104
+ spawn("SECRET_KEY_BASE_DUMMY=1 RAILS_ENV=production bundle exec rails assets:precompile").then do
105
+ Process.wait2(_1)
106
+ end.then do |(_, status)|
107
+ status.success? or exit(status.exitstatus)
108
+ end
80
109
  end
81
110
  end
82
111
 
@@ -91,6 +120,11 @@ namespace :wasmify do
91
120
  ruby_wasm_path: File.join(Wasmify::Rails.config.tmp_dir, "ruby-core.wasm"),
92
121
  storage_dir: "storage"
93
122
  )
123
+ ensure
124
+ # Clean up precompiled assets
125
+ unless Wasmify::Rails.config.skip_assets_precompile
126
+ FileUtils.rm_rf(Rails.root.join("public/assets")) if File.directory?(Rails.root.join("public/assets"))
127
+ end
94
128
  end
95
129
 
96
130
  namespace :core do
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Wasmify
4
4
  module Rails # :nodoc:
5
- VERSION = "0.1.4"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/wasmify-rails.rb CHANGED
@@ -25,3 +25,6 @@ ActiveRecord::ConnectionAdapters.register("nulldb", "ActiveRecord::ConnectionAda
25
25
 
26
26
  # SQLite3 Wasm adapter
27
27
  ActiveRecord::ConnectionAdapters.register("sqlite3_wasm", "ActiveRecord::ConnectionAdapters::SQLite3WasmAdapter", "active_record/connection_adapters/sqlite3_wasm_adapter")
28
+
29
+ # PGlite adapter
30
+ ActiveRecord::ConnectionAdapters.register("pglite", "ActiveRecord::ConnectionAdapters::PGliteAdapter", "active_record/connection_adapters/pglite_adapter")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wasmify-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-27 00:00:00.000000000 Z
11
+ date: 2024-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -134,6 +134,8 @@ files:
134
134
  - lib/active_record/connection_adapters/nulldb_adapter/quoting.rb
135
135
  - lib/active_record/connection_adapters/nulldb_adapter/statement.rb
136
136
  - lib/active_record/connection_adapters/nulldb_adapter/table_definition.rb
137
+ - lib/active_record/connection_adapters/pglite_adapter.rb
138
+ - lib/active_record/connection_adapters/pglite_shims/pg.rb
137
139
  - lib/active_record/connection_adapters/sqlite3_wasm_adapter.rb
138
140
  - lib/generators/wasmify/install/USAGE
139
141
  - lib/generators/wasmify/install/install_generator.rb
@@ -194,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
196
  - !ruby/object:Gem::Version
195
197
  version: '0'
196
198
  requirements: []
197
- rubygems_version: 3.5.16
199
+ rubygems_version: 3.5.22
198
200
  signing_key:
199
201
  specification_version: 4
200
202
  summary: Tools and extensions to package Rails apps as Wasm modules