@tutorialkit-rb/cli 0.1.5 → 0.1.7

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 +1 -1
  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,12 @@
1
+ {
2
+ "name": "cors-proxy",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "dev": "wrangler dev",
7
+ "deploy": "wrangler deploy"
8
+ },
9
+ "devDependencies": {
10
+ "wrangler": "^4.0.0"
11
+ }
12
+ }
@@ -0,0 +1,87 @@
1
+ interface Env {
2
+ ALLOWED_HOSTS: string;
3
+ }
4
+
5
+ export default {
6
+ async fetch(request: Request, env: Env): Promise<Response> {
7
+ // Handle CORS preflight
8
+ if (request.method === 'OPTIONS') {
9
+ return new Response(null, {
10
+ headers: corsHeaders(request),
11
+ });
12
+ }
13
+
14
+ // Extract target URL from query string: /proxy?https://api.example.com/data
15
+ const url = new URL(request.url);
16
+ const targetUrl = url.search.slice(1); // Remove leading '?'
17
+
18
+ if (!targetUrl) {
19
+ return new Response(JSON.stringify({ error: 'Missing target URL. Usage: /proxy?https://api.example.com/path' }), {
20
+ status: 400,
21
+ headers: { 'content-type': 'application/json', ...corsHeaders(request) },
22
+ });
23
+ }
24
+
25
+ // Validate target URL
26
+ let target: URL;
27
+ try {
28
+ target = new URL(targetUrl);
29
+ } catch {
30
+ return new Response(JSON.stringify({ error: 'Invalid target URL' }), {
31
+ status: 400,
32
+ headers: { 'content-type': 'application/json', ...corsHeaders(request) },
33
+ });
34
+ }
35
+
36
+ // Check allowed hosts
37
+ if (env.ALLOWED_HOSTS) {
38
+ const allowed = env.ALLOWED_HOSTS.split(',').map(h => h.trim()).filter(Boolean);
39
+ if (allowed.length > 0 && !allowed.some(h => target.hostname === h || target.hostname.endsWith(`.${h}`))) {
40
+ return new Response(JSON.stringify({ error: `Host ${target.hostname} is not allowed` }), {
41
+ status: 403,
42
+ headers: { 'content-type': 'application/json', ...corsHeaders(request) },
43
+ });
44
+ }
45
+ }
46
+
47
+ // Forward the request
48
+ const headers = new Headers(request.headers);
49
+ headers.delete('host');
50
+ headers.delete('origin');
51
+ headers.delete('referer');
52
+
53
+ try {
54
+ const response = await fetch(targetUrl, {
55
+ method: request.method,
56
+ headers,
57
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined,
58
+ });
59
+
60
+ // Return response with CORS headers
61
+ const responseHeaders = new Headers(response.headers);
62
+ for (const [k, v] of Object.entries(corsHeaders(request))) {
63
+ responseHeaders.set(k, v);
64
+ }
65
+
66
+ return new Response(response.body, {
67
+ status: response.status,
68
+ statusText: response.statusText,
69
+ headers: responseHeaders,
70
+ });
71
+ } catch (err) {
72
+ return new Response(JSON.stringify({ error: `Proxy error: ${(err as Error).message}` }), {
73
+ status: 502,
74
+ headers: { 'content-type': 'application/json', ...corsHeaders(request) },
75
+ });
76
+ }
77
+ },
78
+ };
79
+
80
+ function corsHeaders(request: Request): Record<string, string> {
81
+ return {
82
+ 'access-control-allow-origin': request.headers.get('origin') || '*',
83
+ 'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
84
+ 'access-control-allow-headers': request.headers.get('access-control-request-headers') || '*',
85
+ 'access-control-max-age': '86400',
86
+ };
87
+ }
@@ -0,0 +1,7 @@
1
+ name = "cors-proxy"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2024-12-01"
4
+
5
+ [vars]
6
+ # Comma-separated list of allowed target hosts (empty = allow all)
7
+ ALLOWED_HOSTS = "httpbin.org"
@@ -0,0 +1,9 @@
1
+ [build]
2
+ publish = "./dist"
3
+ command = "# no build needed — built in CI"
4
+
5
+ [[headers]]
6
+ for = "/*"
7
+ [headers.values]
8
+ Cross-Origin-Embedder-Policy = "require-corp"
9
+ Cross-Origin-Opener-Policy = "same-origin"
@@ -7,7 +7,14 @@
7
7
  "astro": "astro",
8
8
  "build": "astro check && astro build",
9
9
  "build:wasm": "./bin/build-wasm",
10
+ "build:wasm:clean": "cd ruby-wasm && rm -rf build rubies tmp dist vendor .bundle && cd .. && npm run build:wasm",
11
+ "build:wasm:base": "./bin/build-ruby-base",
12
+ "build:wasm:base:clean": "cd ruby-wasm && rm -rf build rubies tmp dist/ruby-base.wasm dist/usr-dir.txt vendor .bundle Gemfile.base.lock && cd .. && npm run build:wasm:base",
13
+ "pack:wasm": "./bin/build-pack",
14
+ "pack:wasm:clean": "cd ruby-wasm && rm -rf tmp/gems_bundle dist/ruby.wasm vendor .bundle && rm -f ../public/ruby.wasm ../public/ruby.wasm.hash && cd .. && npm run pack:wasm",
10
15
  "dev": "astro dev",
16
+ "link:local": "./bin/link-local",
17
+ "unlink:local": "./bin/unlink-local",
11
18
  "preview": "astro preview",
12
19
  "start": "astro dev"
13
20
  },
@@ -15,7 +22,8 @@
15
22
  "@codemirror/lang-yaml": "^6.1.2",
16
23
  "@codemirror/legacy-modes": "^6.5.1",
17
24
  "@nanostores/react": "0.7.2",
18
- "@tutorialkit-rb/react": "0.1.5",
25
+ "@tutorialkit-rb/react": "0.1.7",
26
+ "@tutorialkit-rb/runtime": "0.1.7",
19
27
  "nanostores": "^0.10.3",
20
28
  "react": "^18.3.1",
21
29
  "react-dom": "^18.3.1"
@@ -23,9 +31,9 @@
23
31
  "devDependencies": {
24
32
  "@astrojs/check": "^0.7.0",
25
33
  "@astrojs/react": "^3.6.0",
26
- "@tutorialkit-rb/astro": "0.1.5",
27
- "@tutorialkit-rb/theme": "0.1.5",
28
- "@tutorialkit-rb/types": "0.1.5",
34
+ "@tutorialkit-rb/astro": "0.1.7",
35
+ "@tutorialkit-rb/theme": "0.1.7",
36
+ "@tutorialkit-rb/types": "0.1.7",
29
37
  "@types/mdast": "^4.0.4",
30
38
  "@types/node": "^20.14.6",
31
39
  "@types/react": "^18.3.3",
@@ -20,3 +20,9 @@ gem "solid_cable"
20
20
  gem "image_processing", "~> 1.2"
21
21
 
22
22
  gem "tzinfo-data"
23
+
24
+ # Pin json < 2.8 — newer versions redefine rb_hash_bulk_insert as static,
25
+ # which conflicts with Ruby 3.3's headers during WASM cross-compilation.
26
+ gem "json", "< 2.8"
27
+
28
+ # gem "ruby_llm" — removed; outbound HTTP demo uses Net::HTTP only
@@ -0,0 +1,19 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Pin to match monolithic pipeline's Gemfile.lock. Unpinned, Bundler resolves
4
+ # to the latest (2.8.1), which may differ from the monolithic lockfile (2.7.1).
5
+ gem "js", "2.7.1"
6
+
7
+ # Gems with C extensions that must be statically linked into the base binary.
8
+ # WASI has no dlopen() — C extensions cannot be loaded at runtime from the
9
+ # filesystem. They must be compiled and linked during `rbwasm build`.
10
+ # Pure Ruby gems are handled by the dynamic packing layer (bin/pack-gems).
11
+ gem "bcrypt", "~> 3.1.7"
12
+ gem "websocket-driver", "~> 0.7"
13
+
14
+ # Pin to < 2.8: ruby_wasm 2.8+ adds ac_cv_func_chmod=no which causes Ruby
15
+ # to generate its own chmod stub (returns ENOSYS) that shadows wasi-libc's
16
+ # stub (returns ENOTSUP). FileUtils rescues ENOTSUP but not ENOSYS, so
17
+ # `rails new` crashes. Without the flag, wasi-libc's ENOTSUP stub is used
18
+ # and FileUtils handles it — matching the monolithic approach.
19
+ gem "ruby_wasm", "2.7.1", group: :development
@@ -0,0 +1,50 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ base64 (0.3.0)
5
+ bcrypt (3.1.22)
6
+ js (2.7.1)
7
+ ruby_wasm (2.7.1)
8
+ ruby_wasm (2.7.1-aarch64-linux)
9
+ ruby_wasm (2.7.1-aarch64-linux-musl)
10
+ ruby_wasm (2.7.1-arm64-darwin)
11
+ ruby_wasm (2.7.1-x86_64-darwin)
12
+ ruby_wasm (2.7.1-x86_64-linux)
13
+ ruby_wasm (2.7.1-x86_64-linux-musl)
14
+ websocket-driver (0.8.0)
15
+ base64
16
+ websocket-extensions (>= 0.1.0)
17
+ websocket-extensions (0.1.5)
18
+
19
+ PLATFORMS
20
+ aarch64-linux
21
+ aarch64-linux-musl
22
+ arm64-darwin
23
+ ruby
24
+ wasm32-wasi
25
+ x86_64-darwin
26
+ x86_64-linux
27
+ x86_64-linux-musl
28
+
29
+ DEPENDENCIES
30
+ bcrypt (~> 3.1.7)
31
+ js (= 2.7.1)
32
+ ruby_wasm (= 2.7.1)
33
+ websocket-driver (~> 0.7)
34
+
35
+ CHECKSUMS
36
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
37
+ bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
38
+ js (2.7.1) sha256=a609ea04ae96f67f2005f339f2400f3a6e52002cadaac59e7718bc63d13d45f0
39
+ ruby_wasm (2.7.1) sha256=b3c9c6cfab73c3c47924cde88c552487fb2ff15d69f9dff3a812372ea4af1c33
40
+ ruby_wasm (2.7.1-aarch64-linux) sha256=e4c7984effc226308d245237205a607f88956b4e6cda08c911e411a16ea45b27
41
+ ruby_wasm (2.7.1-aarch64-linux-musl) sha256=46123cfcc66db9f18c39c640edc26578ce441311dd44051d2f76d4eab289d561
42
+ ruby_wasm (2.7.1-arm64-darwin) sha256=f46a720d6375ad246c8a19da47dc15aad3ae51e01f893a84b02890d75aac9ce0
43
+ ruby_wasm (2.7.1-x86_64-darwin) sha256=d41b5e18aa6f5da8aee736e5373305695b6e15db0874d4e3443837c635d7d0e8
44
+ ruby_wasm (2.7.1-x86_64-linux) sha256=22390b51ae5eb437eb50aff773c89b75cca53188c9bb641265eb128406598d22
45
+ ruby_wasm (2.7.1-x86_64-linux-musl) sha256=245e75ee1ce23340c2f7a75916a8ddf03ff37fbed1360f23bb19270d38b412fe
46
+ websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
47
+ websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
48
+
49
+ BUNDLED WITH
50
+ 4.0.7
@@ -85,6 +85,7 @@ GEM
85
85
  erubi (1.13.1)
86
86
  et-orbi (1.2.11)
87
87
  tzinfo
88
+ ffi (1.17.1)
88
89
  ffi (1.17.1-aarch64-linux-gnu)
89
90
  ffi (1.17.1-aarch64-linux-musl)
90
91
  ffi (1.17.1-arm-linux-gnu)
@@ -116,6 +117,7 @@ GEM
116
117
  actionview (>= 5.0.0)
117
118
  activesupport (>= 5.0.0)
118
119
  js (2.7.1)
120
+ json (2.7.6)
119
121
  logger (1.7.0)
120
122
  loofah (2.24.0)
121
123
  crass (~> 1.0.2)
@@ -128,6 +130,7 @@ GEM
128
130
  marcel (1.0.4)
129
131
  mini_magick (4.13.2)
130
132
  mini_mime (1.1.5)
133
+ mini_portile2 (2.8.9)
131
134
  minitest (5.25.5)
132
135
  net-imap (0.5.8)
133
136
  date
@@ -139,6 +142,9 @@ GEM
139
142
  net-smtp (0.5.1)
140
143
  net-protocol
141
144
  nio4r (2.7.4)
145
+ nokogiri (1.18.8)
146
+ mini_portile2 (~> 2.8.2)
147
+ racc (~> 1.4)
142
148
  nokogiri (1.18.8-aarch64-linux-gnu)
143
149
  racc (~> 1.4)
144
150
  nokogiri (1.18.8-aarch64-linux-musl)
@@ -268,6 +274,7 @@ PLATFORMS
268
274
  arm-linux-gnu
269
275
  arm-linux-musl
270
276
  arm64-darwin
277
+ wasm32-wasi
271
278
  x86_64-darwin
272
279
  x86_64-linux
273
280
  x86_64-linux-gnu
@@ -278,6 +285,7 @@ DEPENDENCIES
278
285
  image_processing (~> 1.2)
279
286
  importmap-rails
280
287
  jbuilder
288
+ json (< 2.8)
281
289
  propshaft
282
290
  rails (~> 8.0.0)
283
291
  solid_cable
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Cross-compile gems for wasm32-wasi and pack them into a Ruby WASM base
5
+ # binary via wasi-vfs (single-pack strategy).
6
+ #
7
+ # This replaces the monolithic wasmify-rails build with a faster approach:
8
+ # a raw pre-VFS base binary (Ruby + statically-linked C extensions) gets
9
+ # all gems + stdlib + config packed in a single wasi-vfs pass (~30 seconds).
10
+ #
11
+ # The base binary has NO embedded VFS data. This script does the only
12
+ # wasi-vfs pack, mapping three directories: /bundle (gems), /usr (stdlib),
13
+ # /rails-vm (boot config). Uses RubyWasmExt::WasiVfs (bundled in ruby_wasm
14
+ # gem) to ensure version parity with the monolithic pipeline.
15
+ #
16
+ # Usage: bundle exec ruby bin/pack-gems
17
+ # (called from bin/build-pack orchestrator)
18
+
19
+ require "bundler/setup"
20
+ require "fileutils"
21
+ require "find"
22
+
23
+ # Load RubyWasmExt::WasiVfs — the bundled wasi-vfs implementation from the
24
+ # ruby_wasm gem. This ensures version parity with the monolithic pipeline
25
+ # (wasi-vfs 0.5.3-p1 + Wizer 6.0.0) instead of using the Homebrew CLI
26
+ # (wasi-vfs 0.5.5 + Wizer 7.0.0) which produces different binary layouts.
27
+ require "ruby_wasm"
28
+ begin
29
+ RUBY_VERSION =~ /(\d+\.\d+)/
30
+ require "ruby_wasm/#{$1}/ruby_wasm.so"
31
+ rescue LoadError
32
+ require "ruby_wasm/ruby_wasm.so"
33
+ end
34
+
35
+ # --- Configuration -----------------------------------------------------------
36
+
37
+ SCRIPT_DIR = File.expand_path("..", __dir__)
38
+ TMP_DIR = File.join(SCRIPT_DIR, "tmp", "gems_bundle")
39
+ BUNDLE_DIR = File.join(TMP_DIR, "bundle")
40
+ RAILS_VM_DIR = File.join(TMP_DIR, "rails-vm")
41
+ OUTPUT_DIR = File.join(SCRIPT_DIR, "dist")
42
+ OUTPUT_FILE = File.join(OUTPUT_DIR, "ruby.wasm")
43
+
44
+ # Gems whose cross-compiled native extensions don't work in WASM.
45
+ # We strip their .so files so Ruby falls back to the stdlib versions
46
+ # compiled into the base WASM binary. We keep gem.build_complete markers
47
+ # so Bundler considers them installed.
48
+ STRIP_EXTENSION_GEMS = %w[nio4r io-console psych bigdecimal date].freeze
49
+
50
+ # Build-time gems that should not be packed into the WASM VFS.
51
+ # ruby_wasm: provides rbwasm CLI + 23MB native extension for host machine
52
+ # ffi: dependency of ruby_wasm, not WASM-compatible
53
+ # NOTE: wasmify-rails MUST stay — boot.rb requires it at runtime for WASM shims
54
+ EXCLUDE_FROM_PACK = %w[ruby_wasm ffi].freeze
55
+
56
+ # Paths to strip from the bundle to reduce size
57
+ STRIP_PATTERNS = %w[
58
+ **/cache/*.gem
59
+ **/cache/bundler
60
+ **/*.o
61
+ **/*.a
62
+ ].freeze
63
+
64
+ # --- Helpers -----------------------------------------------------------------
65
+
66
+ def human_size(bytes)
67
+ if bytes < 1024
68
+ "#{bytes} bytes"
69
+ elsif bytes < 1024 * 1024
70
+ "#{(bytes.to_f / 1024).round(1)} KB"
71
+ else
72
+ "#{(bytes.to_f / 1024 / 1024).round(1)} MB"
73
+ end
74
+ end
75
+
76
+ def find_rbconfig
77
+ # 1. Shipped rbconfig in this repo (checked-in)
78
+ shipped = File.join(SCRIPT_DIR, "rbconfig", "wasm32-wasi", "rbconfig.rb")
79
+ return shipped if File.exist?(shipped)
80
+
81
+ # 2. From @tutorialkit-rb/ruby-3.3 npm package
82
+ npm_pkg = File.join(SCRIPT_DIR, "node_modules", "@tutorialkit-rb", "ruby-3.3",
83
+ "rbconfig", "wasm32-wasi", "rbconfig.rb")
84
+ return npm_pkg if File.exist?(npm_pkg)
85
+
86
+ # 3. Fallback: generated by a previous base binary build
87
+ Dir.glob(File.join(SCRIPT_DIR, "rubies", "*/usr/local/lib/ruby/*/wasm32-wasi/rbconfig.rb")).first
88
+ end
89
+
90
+ def find_base_wasm
91
+ # 1. Locally-built custom binary (from bin/build-ruby-base)
92
+ local = File.join(SCRIPT_DIR, "dist", "ruby-base.wasm")
93
+ return local if File.exist?(local)
94
+
95
+ # 2. From @tutorialkit-rb/ruby-3.3 npm package
96
+ npm_pkg = File.join(SCRIPT_DIR, "node_modules", "@tutorialkit-rb", "ruby-3.3",
97
+ "dist", "ruby-base.wasm")
98
+ return npm_pkg if File.exist?(npm_pkg)
99
+
100
+ $stderr.puts "Error: No base WASM binary found."
101
+ $stderr.puts "Either run 'bin/build-ruby-base' to build a custom binary,"
102
+ $stderr.puts "or install @tutorialkit-rb/ruby-3.3 npm package."
103
+ exit 1
104
+ end
105
+
106
+ def find_usr_dir
107
+ # 1. Path recorded by build-ruby-base (local build, single-pack strategy)
108
+ txt = File.join(SCRIPT_DIR, "dist", "usr-dir.txt")
109
+ if File.exist?(txt)
110
+ path = File.read(txt).strip
111
+ return path if Dir.exist?(path)
112
+ end
113
+
114
+ # 2. From @tutorialkit-rb/ruby-3.3 npm package
115
+ npm_pkg = File.join(SCRIPT_DIR, "node_modules", "@tutorialkit-rb", "ruby-3.3",
116
+ "rubies", "usr")
117
+ return npm_pkg if Dir.exist?(npm_pkg)
118
+
119
+ # 3. Fallback: glob for the rubies/ directory left by rbwasm
120
+ candidates = Dir.glob(File.join(SCRIPT_DIR, "rubies", "ruby-3.3-wasm32-unknown-wasip1-full-*", "usr"))
121
+ return candidates.first if candidates.any?
122
+
123
+ nil
124
+ end
125
+
126
+ # --- Main --------------------------------------------------------------------
127
+
128
+ puts "=== Cross-compiling gems + wasi-vfs pack ==="
129
+ puts
130
+
131
+ # 1. Find rbconfig
132
+ rbconfig_path = find_rbconfig
133
+ unless rbconfig_path
134
+ $stderr.puts "Error: Could not find rbconfig.rb for cross-compilation"
135
+ $stderr.puts "Expected at: rbconfig/wasm32-wasi/rbconfig.rb"
136
+ $stderr.puts "Or install @tutorialkit-rb/ruby-3.3 npm package."
137
+ exit 1
138
+ end
139
+ puts "rbconfig: #{rbconfig_path}"
140
+
141
+ # 2. Find base WASM binary
142
+ base_wasm = find_base_wasm
143
+ puts "base WASM: #{base_wasm} (#{human_size(File.size(base_wasm))})"
144
+
145
+ # 2b. Find usr/ directory for single-pack /usr mapping
146
+ usr_dir = find_usr_dir
147
+ if usr_dir
148
+ puts "usr dir: #{usr_dir} (will map to /usr in wasi-vfs pack)"
149
+ else
150
+ $stderr.puts "Warning: Could not find usr/ directory for /usr mapping."
151
+ $stderr.puts " The packed binary may be missing Ruby stdlib at /usr."
152
+ $stderr.puts " Run 'bin/build-ruby-base' or install @tutorialkit-rb/ruby-3.3."
153
+ end
154
+ puts
155
+
156
+ # 3. Add wasm32-wasi platform to lockfile if missing
157
+ Bundler.with_unbundled_env do
158
+ lockfile = File.join(SCRIPT_DIR, "Gemfile.lock")
159
+ if File.exist?(lockfile) && !File.read(lockfile).include?("wasm32-wasi")
160
+ puts "Adding wasm32-wasi platform to Gemfile.lock..."
161
+ system("bundle", "lock", "--add-platform", "wasm32-wasi", chdir: SCRIPT_DIR) or abort "Failed to add platform"
162
+ puts
163
+ end
164
+ end
165
+
166
+ # 4. Cross-compile gems into Bundler layout
167
+ puts "Cross-compiling gems..."
168
+ FileUtils.rm_rf(TMP_DIR)
169
+ FileUtils.mkdir_p(TMP_DIR)
170
+
171
+ # Use a temp config dir so .bundle/config (which sets BUNDLE_PATH=vendor/bundle)
172
+ # doesn't override our BUNDLE_PATH for cross-compilation.
173
+ xcompile_config = File.join(TMP_DIR, ".bundle")
174
+ FileUtils.mkdir_p(xcompile_config)
175
+
176
+ env = {
177
+ "BUNDLE_APP_CONFIG" => xcompile_config,
178
+ "BUNDLE_PATH" => BUNDLE_DIR,
179
+ "BUNDLE_FROZEN" => "true",
180
+ "BUNDLE_WITHOUT" => "development:test",
181
+ }
182
+
183
+ cmd = ["bundle", "install", "--target-rbconfig", rbconfig_path]
184
+
185
+ Bundler.with_unbundled_env do
186
+ env.each { |k, v| ENV[k] = v }
187
+ success = system(*cmd, chdir: SCRIPT_DIR)
188
+ unless success
189
+ $stderr.puts "Bundle install (cross-compile) failed"
190
+ exit 1
191
+ end
192
+ end
193
+
194
+ # 5. Strip broken native extensions
195
+ puts
196
+ puts "Stripping incompatible native extensions..."
197
+
198
+ ext_dir = Dir.glob(File.join(BUNDLE_DIR, "ruby/*/extensions")).first
199
+ if ext_dir
200
+ STRIP_EXTENSION_GEMS.each do |gem_name|
201
+ Dir.glob(File.join(ext_dir, "**/#{gem_name}-*")).each do |ext_gem_dir|
202
+ Dir.glob(File.join(ext_gem_dir, "**/*.so")).each { |so| FileUtils.rm(so) }
203
+ puts " Stripped .so: #{gem_name}"
204
+ end
205
+ end
206
+ end
207
+
208
+ # 6. Remove build-time gems that shouldn't be packed into the WASM VFS
209
+ puts
210
+ puts "Removing build-time gems..."
211
+
212
+ gems_src_dir = Dir.glob(File.join(BUNDLE_DIR, "ruby/*/gems")).first
213
+ if gems_src_dir
214
+ EXCLUDE_FROM_PACK.each do |gem_name|
215
+ Dir.glob(File.join(gems_src_dir, "#{gem_name}-*")).each do |gem_dir|
216
+ size = `du -sh "#{gem_dir}" 2>/dev/null`.split("\t").first rescue "?"
217
+ FileUtils.rm_rf(gem_dir)
218
+ puts " Removed #{File.basename(gem_dir)} (#{size})"
219
+ end
220
+ end
221
+ end
222
+
223
+ # 7. Clean up the bundle to reduce size
224
+ puts
225
+ puts "Cleaning bundle..."
226
+
227
+ STRIP_PATTERNS.each do |pattern|
228
+ Dir.glob(File.join(TMP_DIR, pattern)).each do |path|
229
+ FileUtils.rm_rf(path)
230
+ end
231
+ end
232
+
233
+ # Strip build artifacts from gems
234
+ if gems_src_dir
235
+ Dir.glob(File.join(gems_src_dir, "*")).each do |gem_dir|
236
+ %w[test spec ext ports gumbo-parser patches sig rakelib samples benchmarks].each do |subdir|
237
+ path = File.join(gem_dir, subdir)
238
+ FileUtils.rm_rf(path) if Dir.exist?(path)
239
+ end
240
+ end
241
+ end
242
+
243
+ # 8. Reorganize from Bundler layout to rbwasm layout
244
+ #
245
+ # Bundler layout: bundle/ruby/3.3.0/gems/<name>/lib/...
246
+ # rbwasm layout: bundle/gems/<name>/lib/...
247
+ #
248
+ # We move gems out of the ruby/<version> nesting and drop specifications/
249
+ # extensions directories (not needed at runtime with our generated setup.rb).
250
+ puts
251
+ puts "Reorganizing to rbwasm layout..."
252
+
253
+ bundler_gems_dir = gems_src_dir # bundle/ruby/3.3.0/gems
254
+ rbwasm_gems_dir = File.join(BUNDLE_DIR, "gems")
255
+
256
+ if bundler_gems_dir && Dir.exist?(bundler_gems_dir)
257
+ FileUtils.mkdir_p(rbwasm_gems_dir)
258
+ Dir.glob(File.join(bundler_gems_dir, "*")).each do |gem_dir|
259
+ dest = File.join(rbwasm_gems_dir, File.basename(gem_dir))
260
+ FileUtils.mv(gem_dir, dest)
261
+ end
262
+
263
+ # Remove the now-empty ruby/<version> tree
264
+ ruby_dir = File.join(BUNDLE_DIR, "ruby")
265
+ FileUtils.rm_rf(ruby_dir)
266
+
267
+ puts " Moved #{Dir.glob(File.join(rbwasm_gems_dir, '*')).length} gems to bundle/gems/"
268
+ else
269
+ $stderr.puts "Warning: Could not find Bundler gems directory for reorganization"
270
+ end
271
+
272
+ # 9. Generate /bundle/setup.rb
273
+ # Each gem gets its lib/ added to $LOAD_PATH. One exception:
274
+ # concurrent-ruby uses lib/concurrent-ruby/ as its require path
275
+ # (the only gem in the Rails dependency tree with this pattern).
276
+ puts
277
+ puts "Generating setup.rb..."
278
+
279
+ # Gems whose require_path is not the standard "lib"
280
+ CUSTOM_REQUIRE_PATHS = {
281
+ "concurrent-ruby" => "lib/concurrent-ruby",
282
+ }.freeze
283
+
284
+ setup_lines = ["# Auto-generated by pack-gems"]
285
+ Dir.glob(File.join(rbwasm_gems_dir, "*")).sort.each do |gem_dir|
286
+ gem_name = File.basename(gem_dir)
287
+ # Extract gem base name (without version) for lookup
288
+ base_name = gem_name.sub(/-\d+(\.\d+)*\z/, "")
289
+ rpath = CUSTOM_REQUIRE_PATHS.fetch(base_name, "lib")
290
+ full_path = File.join(gem_dir, rpath)
291
+ if Dir.exist?(full_path)
292
+ setup_lines << "$:.unshift File.expand_path(\"/bundle/gems/#{gem_name}/#{rpath}\")"
293
+ end
294
+ end
295
+
296
+ setup_rb = File.join(BUNDLE_DIR, "setup.rb")
297
+ File.write(setup_rb, setup_lines.join("\n") + "\n")
298
+ puts " Generated setup.rb with #{setup_lines.length - 1} gem load paths"
299
+
300
+ # 10. Prepare /rails-vm/ files
301
+ puts
302
+ puts "Preparing /rails-vm/ files..."
303
+ FileUtils.mkdir_p(RAILS_VM_DIR)
304
+
305
+ %w[boot.rb Gemfile Gemfile.lock .railsrc].each do |file|
306
+ src = File.join(SCRIPT_DIR, file)
307
+ if File.exist?(src)
308
+ FileUtils.cp(src, RAILS_VM_DIR)
309
+ puts " Copied #{file} → rails-vm/"
310
+ end
311
+ end
312
+
313
+ # 11. Stage usr/ directory (strip non-runtime files to reduce binary size)
314
+ #
315
+ # The rubies/ directory has the full install tree including bin/ruby (32MB),
316
+ # include/ (headers), and libruby-static.a — none of which are needed at
317
+ # runtime. We create a staging copy with only the runtime lib/ files,
318
+ # matching what rbwasm's remove_non_runtime_files() does.
319
+ usr_staging_dir = nil
320
+ if usr_dir
321
+ puts
322
+ puts "Staging usr/ directory (stripping non-runtime files)..."
323
+ usr_staging_dir = File.join(TMP_DIR, "usr_staging", "usr")
324
+ FileUtils.rm_rf(File.join(TMP_DIR, "usr_staging"))
325
+ FileUtils.mkdir_p(usr_staging_dir)
326
+
327
+ # Copy only local/lib/ and local/share/ (runtime files)
328
+ %w[local/lib local/share].each do |subdir|
329
+ src = File.join(usr_dir, subdir)
330
+ if Dir.exist?(src)
331
+ dest = File.join(usr_staging_dir, subdir)
332
+ FileUtils.mkdir_p(File.dirname(dest))
333
+ FileUtils.cp_r(src, dest)
334
+ end
335
+ end
336
+
337
+ # Remove libruby-static.a (not needed at runtime)
338
+ Dir.glob(File.join(usr_staging_dir, "**/libruby-static.a")).each do |f|
339
+ FileUtils.rm(f)
340
+ end
341
+
342
+ staged_size = `du -sh "#{usr_staging_dir}" 2>/dev/null`.split("\t").first rescue "?"
343
+ puts " Staged usr/ size: #{staged_size} (stripped bin/, include/, libruby-static.a)"
344
+ end
345
+
346
+ # 12. Pack via RubyWasmExt::WasiVfs (bundled in ruby_wasm gem)
347
+ #
348
+ # Uses the same wasi-vfs implementation as the monolithic pipeline
349
+ # (RubyWasmExt::WasiVfs from ruby_wasm 2.7.1, which bundles wasi-vfs 0.5.3
350
+ # and Wizer 6.0.0) instead of shelling out to the Homebrew wasi-vfs CLI
351
+ # (v0.5.5 + Wizer 7.0.0). This ensures identical binary layout.
352
+ puts
353
+ puts "Packing with RubyWasmExt::WasiVfs (bundled wasi-vfs)..."
354
+
355
+ FileUtils.mkdir_p(OUTPUT_DIR)
356
+
357
+ wasi_vfs = RubyWasmExt::WasiVfs.new
358
+ wasi_vfs.map_dir("/bundle", BUNDLE_DIR)
359
+ wasi_vfs.map_dir("/rails-vm", RAILS_VM_DIR)
360
+ wasi_vfs.map_dir("/usr", usr_staging_dir) if usr_staging_dir
361
+
362
+ wasm_bytes = wasi_vfs.pack(File.binread(base_wasm))
363
+ File.binwrite(OUTPUT_FILE, wasm_bytes)
364
+
365
+ puts
366
+ puts "=== Pack Complete ==="
367
+ puts " Output: #{OUTPUT_FILE}"
368
+ puts " Size: #{human_size(File.size(OUTPUT_FILE))}"
@@ -0,0 +1,6 @@
1
+ {
2
+ "private": true,
3
+ "optionalDependencies": {
4
+ "@tutorialkit-rb/ruby-3.3": "^3.3.0"
5
+ }
6
+ }