@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.
- package/dist/index.js +47 -38
- package/package.json +1 -1
- package/template/.claude/skills/rails-file-management/SKILL.md +211 -0
- package/template/.claude/skills/rails-lesson-recipes/SKILL.md +415 -0
- package/template/.claude/skills/rails-wasm-author-constraints/SKILL.md +181 -0
- package/template/.claude/skills/tutorial-content-structure/SKILL.md +377 -0
- package/template/.claude/skills/tutorial-lesson-config/SKILL.md +389 -0
- package/template/.claude/skills/tutorial-quickstart/SKILL.md +440 -0
- package/template/.github/workflows/deploy.yml +85 -0
- package/template/.gitignore +10 -0
- package/template/CLAUDE.md +47 -0
- package/template/astro.config.ts +12 -0
- package/template/bin/build-pack +84 -0
- package/template/bin/build-ruby-base +180 -0
- package/template/bin/link-local +13 -0
- package/template/bin/unlink-local +7 -0
- package/template/cors-proxy/README.md +63 -0
- package/template/cors-proxy/package-lock.json +1504 -0
- package/template/cors-proxy/package.json +12 -0
- package/template/cors-proxy/src/index.ts +87 -0
- package/template/cors-proxy/wrangler.toml +7 -0
- package/template/netlify.toml +9 -0
- package/template/package.json +12 -4
- package/template/ruby-wasm/Gemfile +6 -0
- package/template/ruby-wasm/Gemfile.base +19 -0
- package/template/ruby-wasm/Gemfile.base.lock +50 -0
- package/template/ruby-wasm/Gemfile.lock +8 -0
- package/template/ruby-wasm/bin/pack-gems +368 -0
- package/template/ruby-wasm/package.json +6 -0
- package/template/src/components/FileManager.tsx +33 -0
- package/template/src/components/HeadTags.astro +6 -6
- package/template/src/components/HelpDropdown.tsx +1 -1
- package/template/src/components/RailsPathLinkHandler.tsx +2 -2
- package/template/src/components/ShellConfigurator.tsx +6 -1
- package/template/src/content/tutorial/1-getting-started/1-creating-your-first-rails-app/content.md +4 -4
- package/template/src/content/tutorial/1-getting-started/2-rails-console/content.md +4 -4
- package/template/src/content/tutorial/2-controllers/2-crud-operations/content.md +2 -2
- package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/.tk-config.json +3 -0
- package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/app/controllers/http_demo_controller.rb +65 -0
- package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/app/views/http_demo/index.html.erb +172 -0
- package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/_files/workspace/config/routes.rb +8 -0
- package/template/src/content/tutorial/9-outbound-http/1-making-http-requests/content.md +97 -0
- package/template/src/content/tutorial/9-outbound-http/meta.md +4 -0
- package/template/src/content/tutorial/meta.md +5 -0
- package/template/src/middleware.ts +14 -0
- package/template/src/templates/default/lib/boot-progress.js +49 -0
- package/template/src/templates/default/lib/http-bridge.js +55 -0
- package/template/src/templates/default/lib/patches/http_bridge.rb +167 -0
- package/template/src/templates/default/lib/rails.js +52 -5
- package/template/src/templates/default/lib/server.js +33 -1
- package/template/src/templates/default/package.json +4 -1
- package/template/src/templates/default/scripts/rails.js +1 -1
- package/template/src/templates/default/scripts/smoke-test.js +349 -0
- package/template/src/templates/default/scripts/wasi-loader.mjs +10 -0
- package/template/src/templates/default/workspace/_debug_app/.github/dependabot.yml +12 -0
- package/template/src/templates/default/workspace/_debug_app/.github/workflows/ci.yml +51 -0
- package/template/src/templates/default/workspace/_debug_app/.ruby-version +1 -0
- package/template/src/templates/default/workspace/_debug_app/Gemfile +37 -0
- package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/application.html.erb +1 -2
- package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/pwa/manifest.json.erb +2 -2
- package/template/src/templates/default/workspace/_debug_app/bin/dev +2 -0
- package/template/src/templates/default/workspace/_debug_app/bin/rake +4 -0
- package/template/src/templates/default/workspace/_debug_app/bin/setup +34 -0
- package/template/src/templates/default/workspace/_debug_app/config/application.rb +30 -0
- package/template/src/templates/default/workspace/_debug_app/config/cable.yml +10 -0
- package/template/src/templates/default/workspace/_debug_app/config/credentials.yml.enc +1 -0
- package/template/src/templates/default/workspace/_debug_app/config/master.key +1 -0
- package/template/src/templates/default/workspace/_debug_app/config/routes.rb +14 -0
- package/template/src/templates/default/workspace/_debug_app/test/models/.keep +0 -0
- package/template/src/templates/default/workspace/_debug_app/test/test_helper.rb +15 -0
- package/template/src/templates/default/workspace/_debug_app/tmp/.keep +0 -0
- package/template/src/templates/default/workspace/_debug_app/tmp/pids/.keep +0 -0
- package/template/src/templates/default/workspace/_debug_app/tmp/storage/.keep +0 -0
- package/template/src/templates/default/workspace/_debug_app/vendor/.keep +0 -0
- package/template/src/templates/rails-app/workspace/README.md +24 -0
- package/template/src/templates/rails-app/workspace/Rakefile +6 -0
- package/template/src/templates/rails-app/workspace/app/assets/images/.keep +0 -0
- package/template/src/templates/rails-app/workspace/app/assets/stylesheets/application.css +534 -0
- package/template/src/templates/rails-app/workspace/app/controllers/application_controller.rb +2 -0
- package/template/src/templates/rails-app/workspace/app/helpers/application_helper.rb +2 -0
- package/template/src/templates/rails-app/workspace/app/jobs/application_job.rb +7 -0
- package/template/src/templates/rails-app/workspace/app/mailers/application_mailer.rb +4 -0
- package/template/src/templates/rails-app/workspace/app/models/application_record.rb +3 -0
- package/template/src/templates/rails-app/workspace/app/models/concerns/.keep +0 -0
- package/template/src/templates/rails-app/workspace/app/views/layouts/application.html.erb +38 -0
- package/template/src/templates/rails-app/workspace/app/views/layouts/mailer.html.erb +13 -0
- package/template/src/templates/rails-app/workspace/app/views/layouts/mailer.text.erb +1 -0
- package/template/src/templates/rails-app/workspace/bin/rails +4 -0
- package/template/src/templates/rails-app/workspace/{store/config → config}/application.rb +1 -1
- package/template/src/templates/rails-app/workspace/config/boot.rb +3 -0
- package/template/src/templates/rails-app/workspace/config/database.yml +32 -0
- package/template/src/templates/rails-app/workspace/config/environment.rb +5 -0
- package/template/src/templates/rails-app/workspace/config/environments/development.rb +69 -0
- package/template/src/templates/rails-app/workspace/config/environments/production.rb +89 -0
- package/template/src/templates/rails-app/workspace/config/environments/test.rb +54 -0
- package/template/src/templates/rails-app/workspace/config/initializers/assets.rb +7 -0
- package/template/src/templates/rails-app/workspace/config/initializers/content_security_policy.rb +25 -0
- package/template/src/templates/rails-app/workspace/config/initializers/filter_parameter_logging.rb +8 -0
- package/template/src/templates/rails-app/workspace/config/initializers/inflections.rb +16 -0
- package/template/src/templates/rails-app/workspace/config/locales/en.yml +31 -0
- package/template/src/templates/rails-app/workspace/config/puma.rb +41 -0
- package/template/src/templates/rails-app/workspace/{store/config → config}/routes.rb +1 -1
- package/template/src/templates/rails-app/workspace/config/storage.yml +34 -0
- package/template/src/templates/rails-app/workspace/config.ru +6 -0
- package/template/src/templates/rails-app/workspace/db/seeds.rb +9 -0
- package/template/src/templates/rails-app/workspace/log/.keep +0 -0
- package/template/src/templates/rails-app/workspace/public/400.html +114 -0
- package/template/src/templates/rails-app/workspace/public/404.html +114 -0
- package/template/src/templates/rails-app/workspace/public/406-unsupported-browser.html +114 -0
- package/template/src/templates/rails-app/workspace/public/422.html +114 -0
- package/template/src/templates/rails-app/workspace/public/500.html +114 -0
- package/template/src/templates/rails-app/workspace/public/icon.png +0 -0
- package/template/src/templates/rails-app/workspace/public/icon.svg +3 -0
- package/template/src/templates/rails-app/workspace/public/robots.txt +1 -0
- package/template/src/templates/rails-app/workspace/script/.keep +0 -0
- package/template/src/templates/rails-app/workspace/storage/.keep +0 -0
- package/template/src/templates/rails-app/workspace/test/controllers/.keep +0 -0
- package/template/src/templates/rails-app/workspace/test/helpers/.keep +0 -0
- package/template/src/templates/rails-app/workspace/test/integration/.keep +0 -0
- package/template/src/templates/rails-app/workspace/tmp/.keep +0 -0
- package/template/src/templates/rails-app/workspace/tmp/cache/.keep +0 -0
- package/template/src/templates/rails-app/workspace/tmp/pids/.keep +0 -0
- package/template/src/templates/rails-app/workspace/tmp/sockets/.keep +0 -0
- package/template/src/templates/rails-app/workspace/tmp/storage/.keep +0 -0
- package/template/src/templates/rails-app/workspace/vendor/javascripts/.keep +0 -0
- package/template/tsconfig.json +3 -1
- package/template/uno.config.ts +17 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/README.md +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/Rakefile +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/assets/images/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/assets/stylesheets/application.css +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/controllers/application_controller.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store/app/models → default/workspace/_debug_app/app/controllers}/concerns/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/helpers/application_helper.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/jobs/application_job.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/mailers/application_mailer.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/models/application_record.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store/log → default/workspace/_debug_app/app/models/concerns}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/mailer.html.erb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/layouts/mailer.text.erb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/app/views/pwa/service-worker.js +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/bin/rails +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/boot.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/database.yml +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environment.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/development.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/production.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/environments/test.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/assets.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/content_security_policy.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/filter_parameter_logging.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/initializers/inflections.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/locales/en.yml +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/puma.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config/storage.yml +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/config.ru +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/db/seeds.rb +0 -0
- /package/template/src/templates/{rails-app/workspace/store/script → default/workspace/_debug_app/lib/tasks}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/storage → default/workspace/_debug_app/log}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/400.html +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/404.html +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/406-unsupported-browser.html +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/422.html +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/500.html +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/icon.png +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/icon.svg +0 -0
- /package/template/src/templates/{rails-app/workspace/store → default/workspace/_debug_app}/public/robots.txt +0 -0
- /package/template/src/templates/{rails-app/workspace/store/test/controllers → default/workspace/_debug_app/script}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/test/helpers → default/workspace/_debug_app/storage}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/test/integration → default/workspace/_debug_app/test/controllers}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/tmp → default/workspace/_debug_app/test/fixtures/files}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/tmp/pids → default/workspace/_debug_app/test/helpers}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/tmp/storage → default/workspace/_debug_app/test/integration}/.keep +0 -0
- /package/template/src/templates/{rails-app/workspace/store/vendor/javascripts → default/workspace/_debug_app/test/mailers}/.keep +0 -0
- /package/template/src/templates/rails-app/workspace/{store/.ruby-version → .ruby-version} +0 -0
- /package/template/src/templates/rails-app/workspace/{store/Gemfile → Gemfile} +0 -0
- /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/application.js +0 -0
- /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/controllers/application.js +0 -0
- /package/template/src/templates/rails-app/workspace/{store/app → app}/javascript/controllers/index.js +0 -0
- /package/template/src/templates/rails-app/workspace/{store/bin → bin}/importmap +0 -0
- /package/template/src/templates/rails-app/workspace/{store/config → config}/cable.yml +0 -0
- /package/template/src/templates/rails-app/workspace/{store/config → config}/credentials.yml.enc +0 -0
- /package/template/src/templates/rails-app/workspace/{store/config → config}/importmap.rb +0 -0
- /package/template/src/templates/rails-app/workspace/{store/config → config}/master.key +0 -0
- /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
|
|
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
|
|
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
|
|
22
|
+
const totalTimer = timer();
|
|
23
|
+
const { args, skipRails, wasmPath } = vmopts;
|
|
13
24
|
const env = vmopts.env || {};
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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 () => {
|