wasmify-rails 0.1.4 → 0.2.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.
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