@tutorialkit-rb/cli 0.1.5 → 0.1.8

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.
Files changed (185) hide show
  1. package/dist/index.js +47 -38
  2. package/package.json +1 -1
  3. package/template/.claude/skills/rails-file-management/SKILL.md +211 -0
  4. package/template/.claude/skills/rails-lesson-recipes/SKILL.md +415 -0
  5. package/template/.claude/skills/rails-wasm-author-constraints/SKILL.md +181 -0
  6. package/template/.claude/skills/tutorial-content-structure/SKILL.md +377 -0
  7. package/template/.claude/skills/tutorial-lesson-config/SKILL.md +389 -0
  8. package/template/.claude/skills/tutorial-quickstart/SKILL.md +440 -0
  9. package/template/.github/workflows/deploy.yml +85 -0
  10. package/template/.gitignore +10 -0
  11. package/template/CLAUDE.md +47 -0
  12. package/template/astro.config.ts +12 -0
  13. package/template/bin/build-pack +84 -0
  14. package/template/bin/build-ruby-base +180 -0
  15. package/template/bin/link-local +13 -0
  16. package/template/bin/unlink-local +7 -0
  17. package/template/cors-proxy/README.md +63 -0
  18. package/template/cors-proxy/package-lock.json +1504 -0
  19. package/template/cors-proxy/package.json +12 -0
  20. package/template/cors-proxy/src/index.ts +87 -0
  21. package/template/cors-proxy/wrangler.toml +7 -0
  22. package/template/netlify.toml +9 -0
  23. package/template/package.json +12 -4
  24. package/template/ruby-wasm/Gemfile +6 -0
  25. package/template/ruby-wasm/Gemfile.base +19 -0
  26. package/template/ruby-wasm/Gemfile.base.lock +50 -0
  27. package/template/ruby-wasm/Gemfile.lock +8 -0
  28. package/template/ruby-wasm/bin/pack-gems +368 -0
  29. package/template/ruby-wasm/package.json +6 -0
  30. package/template/src/components/FileManager.tsx +33 -0
  31. package/template/src/components/HeadTags.astro +6 -6
  32. package/template/src/components/HelpDropdown.tsx +1 -1
  33. package/template/src/components/RailsPathLinkHandler.tsx +2 -2
  34. package/template/src/components/ShellConfigurator.tsx +6 -1
  35. package/template/src/content/tutorial/1-getting-started/1-creating-your-first-rails-app/content.md +4 -4
  36. package/template/src/content/tutorial/1-getting-started/2-rails-console/content.md +4 -4
  37. package/template/src/content/tutorial/2-controllers/2-crud-operations/content.md +2 -2
  38. package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/.tk-config.json +3 -0
  39. package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/app/controllers/http_demo_controller.rb +65 -0
  40. package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/app/views/http_demo/index.html.erb +172 -0
  41. package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/config/routes.rb +8 -0
  42. package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/content.md +97 -0
  43. package/template/src/content/tutorial/9-outbound-http/meta.md +4 -0
  44. package/template/src/content/tutorial/meta.md +5 -0
  45. package/template/src/middleware.ts +14 -0
  46. package/template/src/templates/default/lib/boot-progress.js +49 -0
  47. package/template/src/templates/default/lib/http-bridge.js +55 -0
  48. package/template/src/templates/default/lib/patches/http_bridge.rb +167 -0
  49. package/template/src/templates/default/lib/rails.js +52 -5
  50. package/template/src/templates/default/lib/server.js +33 -1
  51. package/template/src/templates/default/package.json +4 -1
  52. package/template/src/templates/default/scripts/rails.js +1 -1
  53. package/template/src/templates/default/scripts/smoke-test.js +349 -0
  54. package/template/src/templates/default/scripts/wasi-loader.mjs +10 -0
  55. package/template/src/templates/default/workspace/_debug_app/.github/dependabot.yml +12 -0
  56. package/template/src/templates/default/workspace/_debug_app/.github/workflows/ci.yml +51 -0
  57. package/template/src/templates/default/workspace/_debug_app/.ruby-version +1 -0
  58. package/template/src/templates/default/workspace/_debug_app/Gemfile +37 -0
  59. package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/application.html.erb +1 -2
  60. package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/pwa/manifest.json.erb +2 -2
  61. package/template/src/templates/default/workspace/_debug_app/bin/dev +2 -0
  62. package/template/src/templates/default/workspace/_debug_app/bin/rake +4 -0
  63. package/template/src/templates/default/workspace/_debug_app/bin/setup +34 -0
  64. package/template/src/templates/default/workspace/_debug_app/config/application.rb +30 -0
  65. package/template/src/templates/default/workspace/_debug_app/config/cable.yml +10 -0
  66. package/template/src/templates/default/workspace/_debug_app/config/credentials.yml.enc +1 -0
  67. package/template/src/templates/default/workspace/_debug_app/config/master.key +1 -0
  68. package/template/src/templates/default/workspace/_debug_app/config/routes.rb +14 -0
  69. package/template/src/templates/default/workspace/_debug_app/test/models/.keep +0 -0
  70. package/template/src/templates/default/workspace/_debug_app/test/test_helper.rb +15 -0
  71. package/template/src/templates/default/workspace/_debug_app/tmp/.keep +0 -0
  72. package/template/src/templates/default/workspace/_debug_app/tmp/pids/.keep +0 -0
  73. package/template/src/templates/default/workspace/_debug_app/tmp/storage/.keep +0 -0
  74. package/template/src/templates/default/workspace/_debug_app/vendor/.keep +0 -0
  75. package/template/src/templates/rails-app/workspace/README.md +24 -0
  76. package/template/src/templates/rails-app/workspace/Rakefile +6 -0
  77. package/template/src/templates/rails-app/workspace/app/assets/images/.keep +0 -0
  78. package/template/src/templates/rails-app/workspace/app/assets/stylesheets/application.css +534 -0
  79. package/template/src/templates/rails-app/workspace/app/controllers/application_controller.rb +2 -0
  80. package/template/src/templates/rails-app/workspace/app/helpers/application_helper.rb +2 -0
  81. package/template/src/templates/rails-app/workspace/app/jobs/application_job.rb +7 -0
  82. package/template/src/templates/rails-app/workspace/app/mailers/application_mailer.rb +4 -0
  83. package/template/src/templates/rails-app/workspace/app/models/application_record.rb +3 -0
  84. package/template/src/templates/rails-app/workspace/app/models/concerns/.keep +0 -0
  85. package/template/src/templates/rails-app/workspace/app/views/layouts/application.html.erb +38 -0
  86. package/template/src/templates/rails-app/workspace/app/views/layouts/mailer.html.erb +13 -0
  87. package/template/src/templates/rails-app/workspace/app/views/layouts/mailer.text.erb +1 -0
  88. package/template/src/templates/rails-app/workspace/bin/rails +4 -0
  89. package/template/src/templates/rails-app/workspace/{store/config → config}/application.rb +1 -1
  90. package/template/src/templates/rails-app/workspace/config/boot.rb +3 -0
  91. package/template/src/templates/rails-app/workspace/config/database.yml +32 -0
  92. package/template/src/templates/rails-app/workspace/config/environment.rb +5 -0
  93. package/template/src/templates/rails-app/workspace/config/environments/development.rb +69 -0
  94. package/template/src/templates/rails-app/workspace/config/environments/production.rb +89 -0
  95. package/template/src/templates/rails-app/workspace/config/environments/test.rb +54 -0
  96. package/template/src/templates/rails-app/workspace/config/initializers/assets.rb +7 -0
  97. package/template/src/templates/rails-app/workspace/config/initializers/content_security_policy.rb +25 -0
  98. package/template/src/templates/rails-app/workspace/config/initializers/filter_parameter_logging.rb +8 -0
  99. package/template/src/templates/rails-app/workspace/config/initializers/inflections.rb +16 -0
  100. package/template/src/templates/rails-app/workspace/config/locales/en.yml +31 -0
  101. package/template/src/templates/rails-app/workspace/config/puma.rb +41 -0
  102. package/template/src/templates/rails-app/workspace/{store/config → config}/routes.rb +1 -1
  103. package/template/src/templates/rails-app/workspace/config/storage.yml +34 -0
  104. package/template/src/templates/rails-app/workspace/config.ru +6 -0
  105. package/template/src/templates/rails-app/workspace/db/seeds.rb +9 -0
  106. package/template/src/templates/rails-app/workspace/log/.keep +0 -0
  107. package/template/src/templates/rails-app/workspace/public/400.html +114 -0
  108. package/template/src/templates/rails-app/workspace/public/404.html +114 -0
  109. package/template/src/templates/rails-app/workspace/public/406-unsupported-browser.html +114 -0
  110. package/template/src/templates/rails-app/workspace/public/422.html +114 -0
  111. package/template/src/templates/rails-app/workspace/public/500.html +114 -0
  112. package/template/src/templates/rails-app/workspace/public/icon.png +0 -0
  113. package/template/src/templates/rails-app/workspace/public/icon.svg +3 -0
  114. package/template/src/templates/rails-app/workspace/public/robots.txt +1 -0
  115. package/template/src/templates/rails-app/workspace/script/.keep +0 -0
  116. package/template/src/templates/rails-app/workspace/storage/.keep +0 -0
  117. package/template/src/templates/rails-app/workspace/test/controllers/.keep +0 -0
  118. package/template/src/templates/rails-app/workspace/test/helpers/.keep +0 -0
  119. package/template/src/templates/rails-app/workspace/test/integration/.keep +0 -0
  120. package/template/src/templates/rails-app/workspace/tmp/.keep +0 -0
  121. package/template/src/templates/rails-app/workspace/tmp/cache/.keep +0 -0
  122. package/template/src/templates/rails-app/workspace/tmp/pids/.keep +0 -0
  123. package/template/src/templates/rails-app/workspace/tmp/sockets/.keep +0 -0
  124. package/template/src/templates/rails-app/workspace/tmp/storage/.keep +0 -0
  125. package/template/src/templates/rails-app/workspace/vendor/javascripts/.keep +0 -0
  126. package/template/tsconfig.json +3 -1
  127. package/template/uno.config.ts +17 -0
  128. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/README.md +0 -0
  129. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/Rakefile +0 -0
  130. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/assets/images/.keep +0 -0
  131. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/assets/stylesheets/application.css +0 -0
  132. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/controllers/application_controller.rb +0 -0
  133. /package/template/src/templates/{rails-app/workspace/store/app/models → default/workspace/_debug_app/app/controllers}/concerns/.keep +0 -0
  134. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/helpers/application_helper.rb +0 -0
  135. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/jobs/application_job.rb +0 -0
  136. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/mailers/application_mailer.rb +0 -0
  137. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/models/application_record.rb +0 -0
  138. /package/template/src/templates/{rails-app/workspace/store/log → default/workspace/_debug_app/app/models/concerns}/.keep +0 -0
  139. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/mailer.html.erb +0 -0
  140. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/mailer.text.erb +0 -0
  141. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/pwa/service-worker.js +0 -0
  142. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/bin/rails +0 -0
  143. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/boot.rb +0 -0
  144. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/database.yml +0 -0
  145. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environment.rb +0 -0
  146. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/development.rb +0 -0
  147. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/production.rb +0 -0
  148. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/test.rb +0 -0
  149. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/assets.rb +0 -0
  150. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/content_security_policy.rb +0 -0
  151. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/filter_parameter_logging.rb +0 -0
  152. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/inflections.rb +0 -0
  153. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/locales/en.yml +0 -0
  154. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/puma.rb +0 -0
  155. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/storage.yml +0 -0
  156. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config.ru +0 -0
  157. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/db/seeds.rb +0 -0
  158. /package/template/src/templates/{rails-app/workspace/store/script → default/workspace/_debug_app/lib/tasks}/.keep +0 -0
  159. /package/template/src/templates/{rails-app/workspace/store/storage → default/workspace/_debug_app/log}/.keep +0 -0
  160. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/400.html +0 -0
  161. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/404.html +0 -0
  162. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/406-unsupported-browser.html +0 -0
  163. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/422.html +0 -0
  164. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/500.html +0 -0
  165. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/icon.png +0 -0
  166. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/icon.svg +0 -0
  167. /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/robots.txt +0 -0
  168. /package/template/src/templates/{rails-app/workspace/store/test/controllers → default/workspace/_debug_app/script}/.keep +0 -0
  169. /package/template/src/templates/{rails-app/workspace/store/test/helpers → default/workspace/_debug_app/storage}/.keep +0 -0
  170. /package/template/src/templates/{rails-app/workspace/store/test/integration → default/workspace/_debug_app/test/controllers}/.keep +0 -0
  171. /package/template/src/templates/{rails-app/workspace/store/tmp → default/workspace/_debug_app/test/fixtures/files}/.keep +0 -0
  172. /package/template/src/templates/{rails-app/workspace/store/tmp/pids → default/workspace/_debug_app/test/helpers}/.keep +0 -0
  173. /package/template/src/templates/{rails-app/workspace/store/tmp/storage → default/workspace/_debug_app/test/integration}/.keep +0 -0
  174. /package/template/src/templates/{rails-app/workspace/store/vendor/javascripts → default/workspace/_debug_app/test/mailers}/.keep +0 -0
  175. /package/template/src/templates/rails-app/workspace/{store/.ruby-version → .ruby-version} +0 -0
  176. /package/template/src/templates/rails-app/workspace/{store/Gemfile → Gemfile} +0 -0
  177. /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/application.js +0 -0
  178. /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/controllers/application.js +0 -0
  179. /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/controllers/index.js +0 -0
  180. /package/template/src/templates/rails-app/workspace/{store/bin → bin}/importmap +0 -0
  181. /package/template/src/templates/rails-app/workspace/{store/config → config}/cable.yml +0 -0
  182. /package/template/src/templates/rails-app/workspace/{store/config → config}/credentials.yml.enc +0 -0
  183. /package/template/src/templates/rails-app/workspace/{store/config → config}/importmap.rb +0 -0
  184. /package/template/src/templates/rails-app/workspace/{store/config → config}/master.key +0 -0
  185. /package/template/src/templates/rails-app/workspace/{store/test → test}/test_helper.rb +0 -0
@@ -0,0 +1,55 @@
1
+ // HTTP bridge for Ruby WASM — registers global.wasmHttpBridge
2
+ // Follows the same pattern as PGLite (global.pglite = new PGLite4Rails(...))
3
+
4
+ export function initHttpBridge() {
5
+ global.wasmHttpBridge = {
6
+ async fetch(url, method, headersJson, body) {
7
+ try {
8
+ const headers = JSON.parse(headersJson);
9
+ const options = { method, headers };
10
+ if (body && method !== 'GET' && method !== 'HEAD') {
11
+ options.body = body;
12
+ }
13
+
14
+ console.log(`[http-bridge] ${method} ${url}`);
15
+ options.signal = AbortSignal.timeout(30000);
16
+ const response = await fetch(url, options);
17
+ console.log(`[http-bridge] ${response.status} ${url}`);
18
+
19
+ const contentType = response.headers.get('content-type') || '';
20
+ const isBinary = /octet-stream|image\/|audio\/|video\/|application\/pdf|application\/zip/.test(contentType);
21
+
22
+ let responseBody;
23
+ if (isBinary) {
24
+ const buffer = await response.arrayBuffer();
25
+ const bytes = new Uint8Array(buffer);
26
+ let binary = '';
27
+ for (let i = 0; i < bytes.byteLength; i++) {
28
+ binary += String.fromCharCode(bytes[i]);
29
+ }
30
+ responseBody = btoa(binary);
31
+ } else {
32
+ responseBody = await response.text();
33
+ }
34
+
35
+ const responseHeaders = {};
36
+ response.headers.forEach((v, k) => { responseHeaders[k] = v; });
37
+
38
+ return JSON.stringify({
39
+ ok: true,
40
+ status: response.status,
41
+ headers: responseHeaders,
42
+ body: responseBody,
43
+ binary: isBinary
44
+ });
45
+ } catch (error) {
46
+ const detail = error.cause ? `${error.message} (cause: ${error.cause.message || error.cause})` : error.message;
47
+ console.error(`[http-bridge] ERROR ${method} ${url}: ${detail}`);
48
+ return JSON.stringify({
49
+ ok: false,
50
+ error: detail
51
+ });
52
+ }
53
+ }
54
+ };
55
+ }
@@ -0,0 +1,167 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "base64"
4
+ require "uri"
5
+
6
+ # WASM has no socket extension, so SocketError is never defined.
7
+ # faraday-net_http references it at class load time.
8
+ SocketError = Class.new(StandardError) unless defined?(SocketError)
9
+
10
+ # OpenSSL is not available in WASM. Stub the module so code that
11
+ # references OpenSSL::SSL (e.g. Faraday, Net::HTTP) doesn't crash.
12
+ # Actual HTTPS is handled by the browser's native fetch via the JS bridge.
13
+ unless defined?(OpenSSL::SSL)
14
+ module OpenSSL
15
+ class OpenSSLError < StandardError; end
16
+ module SSL
17
+ class SSLError < OpenSSLError; end
18
+ VERIFY_NONE = 0
19
+ VERIFY_PEER = 1
20
+ VERIFY_FAIL_IF_NO_PEER_CERT = 2
21
+ VERIFY_CLIENT_ONCE = 4
22
+ OP_ALL = 0x80000BFF
23
+ OP_NO_SSLv2 = 0x01000000
24
+ OP_NO_SSLv3 = 0x02000000
25
+ OP_NO_TLSv1 = 0x04000000
26
+
27
+ class SSLContext
28
+ attr_accessor :verify_mode, :cert_store, :min_version, :max_version, :options
29
+ def set_params(**params); self; end
30
+ end
31
+
32
+ class SSLSocket
33
+ def initialize(io, ctx = nil); end
34
+ def connect; self; end
35
+ def hostname=(h); end
36
+ def sync_close=(v); end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module WasmHTTP
43
+ MESSAGES = {
44
+ 200 => "OK", 201 => "Created", 202 => "Accepted", 204 => "No Content",
45
+ 301 => "Moved Permanently", 302 => "Found", 303 => "See Other", 304 => "Not Modified",
46
+ 307 => "Temporary Redirect", 308 => "Permanent Redirect",
47
+ 400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden",
48
+ 404 => "Not Found", 405 => "Method Not Allowed", 408 => "Request Timeout",
49
+ 409 => "Conflict", 422 => "Unprocessable Entity", 429 => "Too Many Requests",
50
+ 500 => "Internal Server Error", 502 => "Bad Gateway", 503 => "Service Unavailable",
51
+ 504 => "Gateway Timeout"
52
+ }.freeze
53
+
54
+ class Connection
55
+ class << self
56
+ attr_accessor :proxy_url, :proxy_hosts
57
+ end
58
+
59
+ def request(uri, method: "GET", headers: {}, body: nil)
60
+ target = resolve_proxy(uri.to_s)
61
+
62
+ result_js = JS.global[:wasmHttpBridge].fetch(
63
+ target,
64
+ method.to_s,
65
+ headers.to_json,
66
+ body.to_s
67
+ ).await
68
+
69
+ result = JSON.parse(result_js.to_s)
70
+
71
+ unless result["ok"]
72
+ raise SocketError, "HTTP request failed: #{result["error"]}"
73
+ end
74
+
75
+ build_response(result)
76
+ end
77
+
78
+ private
79
+
80
+ def resolve_proxy(url)
81
+ return url unless self.class.proxy_url
82
+
83
+ begin
84
+ host = URI(url).host
85
+ rescue URI::InvalidURIError
86
+ return url
87
+ end
88
+ return url unless host
89
+
90
+ if self.class.proxy_hosts&.any? { |h| host == h || host.end_with?(".#{h}") }
91
+ "#{self.class.proxy_url}#{url}"
92
+ else
93
+ url
94
+ end
95
+ end
96
+
97
+ def build_response(result)
98
+ status = result["status"]
99
+ klass = Net::HTTPResponse::CODE_TO_OBJ[status.to_s] || Net::HTTPUnknownResponse
100
+ response = klass.new("1.1", status, MESSAGES[status] || "Unknown")
101
+ response.instance_variable_set(:@read, true)
102
+
103
+ result["headers"]&.each { |k, v| response[k] = v }
104
+
105
+ body = result["binary"] ? Base64.decode64(result["body"]) : result["body"]
106
+ response.instance_variable_set(:@body, body)
107
+
108
+ response
109
+ end
110
+ end
111
+ end
112
+
113
+ Net::HTTP.prepend(Module.new do
114
+ def request(req, body = nil, &block)
115
+ req.body = body if body && req.body.nil?
116
+
117
+ scheme = use_ssl? ? "https" : "http"
118
+ default_port = use_ssl? ? 443 : 80
119
+ port_str = port == default_port ? "" : ":#{port}"
120
+ uri = "#{scheme}://#{address}#{port_str}#{req.path}"
121
+
122
+ headers = {}
123
+ req.each_header { |k, v| headers[k] = v }
124
+
125
+ response = WasmHTTP::Connection.new.request(
126
+ uri, method: req.method, headers: headers, body: req.body
127
+ )
128
+
129
+ # Faraday's NetHttp adapter passes a block to request() where it
130
+ # calls save_http_response to set env.status. Without yielding,
131
+ # the status is never set and RubyLLM's error middleware fails.
132
+ yield response if block
133
+
134
+ response
135
+ end
136
+
137
+ def connect; end
138
+
139
+ def do_start
140
+ @started = true
141
+ self
142
+ end
143
+
144
+ def do_finish
145
+ @started = false
146
+ end
147
+ end)
148
+
149
+ if defined?(Faraday)
150
+ class Faraday::Adapter::WasmHTTP < Faraday::Adapter
151
+ def call(env)
152
+ super
153
+ response = ::WasmHTTP::Connection.new.request(
154
+ env.url.to_s,
155
+ method: env.method.to_s.upcase,
156
+ headers: env.request_headers.to_h,
157
+ body: env.body
158
+ )
159
+ save_response(env, response.code.to_i, response.body) do |resp_headers|
160
+ response.each_header { |k, v| resp_headers[k] = v }
161
+ end
162
+ end
163
+ end
164
+
165
+ Faraday::Adapter.register_middleware(wasm_http: Faraday::Adapter::WasmHTTP)
166
+ Faraday.default_adapter = :wasm_http
167
+ end
@@ -1,18 +1,35 @@
1
+ import { bootProgress } from './boot-progress.js';
1
2
  import { RubyVM } from "@ruby/wasm-wasi";
2
3
  import { WASI } from "wasi";
3
4
  import fs from "fs/promises";
4
5
  import { PGLite4Rails } from "./database.js";
6
+ import { initHttpBridge } from './http-bridge.js';
7
+
8
+ function timer() {
9
+ const start = performance.now();
10
+ return () => {
11
+ const ms = performance.now() - start;
12
+ return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
13
+ };
14
+ }
5
15
 
6
- const rubyWasm = new URL("../node_modules/@ruby/wasm-wasi/dist/ruby.wasm", import.meta.url).pathname;
16
+ const defaultWasmPath = new URL("../node_modules/@ruby/wasm-wasi/dist/ruby.wasm", import.meta.url).pathname;
7
17
 
8
- const railsRootDir = new URL("../workspace/store", import.meta.url).pathname;
18
+ const railsRootDir = new URL("../workspace", import.meta.url).pathname;
9
19
  const pgDataDir = new URL("../pgdata", import.meta.url).pathname;
10
20
 
11
21
  export default async function initVM(vmopts = {}) {
12
- const { args, skipRails } = vmopts;
22
+ const totalTimer = timer();
23
+ const { args, skipRails, wasmPath } = vmopts;
13
24
  const env = vmopts.env || {};
14
- const binary = await fs.readFile(rubyWasm);
25
+
26
+ // --- WASM load + compile ---
27
+ const wasmTimer = timer();
28
+ bootProgress.updateStep('Loading Ruby WASM...');
29
+ const binary = await fs.readFile(wasmPath || defaultWasmPath);
15
30
  const module = await WebAssembly.compile(binary);
31
+ bootProgress.updateProgress(100);
32
+ bootProgress.log(`WASM load + compile (${wasmTimer()})`);
16
33
 
17
34
  const RAILS_ENV = env.RAILS_ENV || process.env.RAILS_ENV;
18
35
  if (RAILS_ENV) env.RAILS_ENV = RAILS_ENV;
@@ -24,6 +41,9 @@ export default async function initVM(vmopts = {}) {
24
41
 
25
42
  const cliArgs = args?.length ? ['ruby.wasm'].concat(args) : undefined;
26
43
 
44
+ // --- VM instantiation ---
45
+ const vmTimer = timer();
46
+ bootProgress.updateStep('Initializing Ruby VM...');
27
47
  const wasi = new WASI(
28
48
  {
29
49
  env: {"RUBYOPT": "-EUTF-8 -W0", ...env},
@@ -41,29 +61,56 @@ export default async function initVM(vmopts = {}) {
41
61
  wasip1: wasi,
42
62
  args: cliArgs
43
63
  });
64
+ bootProgress.log(`VM instantiation (${vmTimer()})`);
44
65
 
45
66
  if (!skipRails) {
67
+ const railsTimer = timer();
68
+ bootProgress.updateStep('Bootstrapping Rails...');
69
+
46
70
  const pglite = new PGLite4Rails(pgDataDir);
47
71
  global.pglite = pglite;
48
72
 
73
+ initHttpBridge();
74
+
75
+ const httpBridgePatch = await fs.readFile(new URL("./patches/http_bridge.rb", import.meta.url).pathname, 'utf8');
49
76
  const authenticationPatch = await fs.readFile(new URL("./patches/authentication.rb", import.meta.url).pathname, 'utf8');
50
77
  const appGeneratorPatch = await fs.readFile(new URL("./patches/app_generator.rb", import.meta.url).pathname, 'utf8');
51
78
 
52
79
  vm.eval(`
80
+ def _boot_time(label)
81
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ yield
83
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t
84
+ $stderr.puts "[boot] #{label} (#{"%.1f" % elapsed}s)"
85
+ end
86
+
53
87
  Dir.chdir("${workdir}") unless "${workdir}".empty?
54
88
 
55
89
  ENV["RACK_HANDLER"] = "wasi"
90
+ ENV["BUNDLE_GEMFILE"] = "/rails-vm/Gemfile"
91
+ # Prevent Minitest from enabling parallel mode
92
+ ENV["MT_CPU"] = "1"
56
93
 
57
- require "/rails-vm/boot"
94
+ _boot_time("require /rails-vm/boot") { require "/rails-vm/boot" }
58
95
 
59
96
  require "js"
60
97
 
98
+ ${httpBridgePatch}
99
+
61
100
  Wasmify::ExternalCommands.register(:server, :console)
62
101
 
63
102
  ${authenticationPatch}
64
103
  ${appGeneratorPatch}
65
104
  `)
105
+
106
+ bootProgress.updateProgress(100);
107
+ bootProgress.log(`Rails bootstrap (${railsTimer()})`);
66
108
  }
67
109
 
110
+ bootProgress.log(`Total (${totalTimer()})`);
111
+ bootProgress.updateStep('Ready');
112
+
68
113
  return vm;
69
114
  }
115
+
116
+ export { bootProgress };
@@ -92,6 +92,9 @@ class ResponseOutparam {
92
92
  if (location.startsWith("http://localhost:3000/")) {
93
93
  res.set("location", location.replace("http://localhost:3000", ""));
94
94
  }
95
+ if (location.startsWith("https://localhost:3000/")) {
96
+ res.set("location", location.replace("https://localhost:3000", ""));
97
+ }
95
98
  }
96
99
 
97
100
  let body = response.call("body").toJS();
@@ -293,14 +296,43 @@ export const createRackServer = async (vm, opts = {}) => {
293
296
 
294
297
  const app = express();
295
298
 
299
+ // --- Observability: log every request before anything else ---
300
+ app.use((req, res, next) => {
301
+ const start = Date.now();
302
+ console.log(`[express] --> ${req.method} ${req.url}`);
303
+ res.on('finish', () => {
304
+ console.log(`[express] <-- ${req.method} ${req.url} ${res.statusCode} (${Date.now() - start}ms)`);
305
+ });
306
+ next();
307
+ });
308
+
309
+ // --- Health endpoint: bypasses Rails entirely ---
310
+ app.get('/__healthz', (req, res) => {
311
+ res.json({ status: 'ok', uptime: process.uptime() });
312
+ });
313
+
296
314
  const upload = multer({ storage: multer.memoryStorage() });
297
315
  app.use(upload.any());
298
316
  app.use(createFrameLocationTrackingMiddleware());
299
317
 
300
318
  const queue = new RequestQueue((req, res) => requestHandler(vm, req, res));
301
319
 
320
+ // --- Request timeout: never hang forever ---
321
+ const REQUEST_TIMEOUT_MS = 60000;
322
+
302
323
  app.all('*path', async (req, res) => {
303
- await queue.respond(req, res)
324
+ const timer = setTimeout(() => {
325
+ if (!res.headersSent) {
326
+ console.error(`[express] TIMEOUT ${req.method} ${req.url} after ${REQUEST_TIMEOUT_MS}ms`);
327
+ res.status(504).json({ error: 'Request timed out', url: req.url, timeout_ms: REQUEST_TIMEOUT_MS });
328
+ }
329
+ }, REQUEST_TIMEOUT_MS);
330
+
331
+ try {
332
+ await queue.respond(req, res);
333
+ } finally {
334
+ clearTimeout(timer);
335
+ }
304
336
  });
305
337
 
306
338
  return app;
@@ -7,7 +7,10 @@
7
7
  "dev": "vite",
8
8
  "test": "node --disable-warning=ExperimentalWarning ./scripts/test.js",
9
9
  "test:watch": "node --disable-warning=ExperimentalWarning --watch --watch-path=./workspace ./scripts/test.js",
10
- "postinstall": "chmod +x bin/* && node --disable-warning=ExperimentalWarning ./scripts/wait-for-wasm.js"
10
+ "postinstall": "chmod +x bin/* && node --disable-warning=ExperimentalWarning ./scripts/wait-for-wasm.js",
11
+ "smoke": "node --disable-warning=ExperimentalWarning --no-turbo-fast-api-calls --loader ./scripts/wasi-loader.mjs ./scripts/smoke-test.js",
12
+ "smoke:fetch": "node --disable-warning=ExperimentalWarning --no-turbo-fast-api-calls --loader ./scripts/wasi-loader.mjs ./scripts/smoke-test.js --fetch",
13
+ "smoke:http": "node --disable-warning=ExperimentalWarning --no-turbo-fast-api-calls --loader ./scripts/wasi-loader.mjs ./scripts/smoke-test.js --http"
11
14
  },
12
15
  "dependencies": {
13
16
  "@electric-sql/pglite": "^0.3.1",
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import { join } from 'path';
3
3
  import { spawn } from 'child_process';
4
4
 
5
- const railsRootDir = new URL("../workspace/store", import.meta.url).pathname;
5
+ const railsRootDir = new URL("../workspace", import.meta.url).pathname;
6
6
  const railsPath = join(railsRootDir, 'bin/rails');
7
7
 
8
8
  const waitBinRails = async () => {