@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
@@ -9,6 +9,28 @@ const GEMFILE_HASH_URL = `/ruby.wasm.hash`;
9
9
  const WC_WASM_LOG_PATH = `/ruby.wasm.log.txt`;
10
10
  const WC_WASM_PATH = `/ruby.wasm`;
11
11
 
12
+ async function clearWasmCache(): Promise<void> {
13
+ const opfsRoot = await navigator.storage.getDirectory();
14
+ let count = 0;
15
+
16
+ for await (const [name] of opfsRoot.entries()) {
17
+ if (name.includes('ruby') && name.endsWith('.wasm')) {
18
+ await opfsRoot.removeEntry(name);
19
+ console.log(`Deleted: ${name}`);
20
+ count++;
21
+ }
22
+ }
23
+
24
+ if (count === 0) {
25
+ console.log('No cached WASM files found.');
26
+ } else {
27
+ console.log(`Cleared ${count} cached WASM file(s). Hard-refresh the page (Cmd+Shift+R) to download a fresh binary.`);
28
+ }
29
+ }
30
+
31
+ // Expose to browser console: type clearWasmCache() to purge cached WASM binaries
32
+ (window as any).clearWasmCache = clearWasmCache;
33
+
12
34
  export function FileManager() {
13
35
  const lessonLoaded = useStore(tutorialStore.lessonFullyLoaded);
14
36
  const files = useStore(tutorialStore.files);
@@ -70,6 +92,7 @@ export function FileManager() {
70
92
  const opfsRoot = await navigator.storage.getDirectory();
71
93
  const fileHandle = await opfsRoot.getFileHandle(cacheFileName, { create: true });
72
94
  const writable = await fileHandle.createWritable();
95
+ // @ts-ignore
73
96
  await writable.write(wasmData);
74
97
  await writable.close();
75
98
  console.log(`Ruby WASM file ${cacheFileName} cached`);
@@ -134,6 +157,16 @@ export function FileManager() {
134
157
  }
135
158
  });
136
159
 
160
+ // Also ensure template workspace bin files are executable
161
+ try {
162
+ const templateBins = await wc.fs.readdir('workspace/bin');
163
+ templateBins.forEach((bin) => {
164
+ chmodx(wc, `/home/tutorial/workspace/bin/${bin}`);
165
+ });
166
+ } catch {
167
+ // workspace/bin may not exist for all templates
168
+ }
169
+
137
170
  if (!wasmCached.current) {
138
171
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: init');
139
172
 
@@ -16,7 +16,7 @@
16
16
  }
17
17
 
18
18
  :global(.rails-path-link code) {
19
- color: #0969da;
19
+ color: var(--tk-rails-path-color, #3B82F6);
20
20
  background-color: #f6f8fa;
21
21
  padding: 0.2em 0.4em;
22
22
  border-radius: 0.25rem;
@@ -27,20 +27,20 @@
27
27
 
28
28
  /* Dark mode support */
29
29
  :global(.dark .rails-path-link code) {
30
- color: #58a6ff;
30
+ color: var(--tk-rails-path-color-dark, #60A5FA);
31
31
  background-color: #161b22;
32
32
  }
33
33
 
34
34
  /* Hover state */
35
35
  :global(.rails-path-link:hover code) {
36
- color: #0860ca;
36
+ color: var(--tk-rails-path-hover-color, #2563EB);
37
37
  background-color: #e7f1ff;
38
38
  text-decoration: underline;
39
39
  text-underline-offset: 0.2em;
40
40
  }
41
41
 
42
42
  :global(.dark .rails-path-link:hover code) {
43
- color: #79c0ff;
43
+ color: var(--tk-rails-path-hover-color-dark, #93C5FD);
44
44
  background-color: #1f2937;
45
45
  }
46
46
 
@@ -55,11 +55,11 @@
55
55
  }
56
56
 
57
57
  :global(.rails-path-link:focus-visible code) {
58
- outline: 2px solid #0969da;
58
+ outline: 2px solid var(--tk-rails-path-color, #3B82F6);
59
59
  outline-offset: 2px;
60
60
  }
61
61
 
62
62
  :global(.dark .rails-path-link:focus-visible code) {
63
- outline-color: #58a6ff;
63
+ outline-color: var(--tk-rails-path-color-dark, #60A5FA);
64
64
  }
65
65
  </style>
@@ -60,7 +60,7 @@ export function HelpDropdown() {
60
60
  </div>
61
61
  <button
62
62
  onClick={handleReload}
63
- className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition-colors"
63
+ className="w-full bg-accent-600 hover:bg-accent-700 text-white font-medium py-2 px-4 rounded transition-colors"
64
64
  >
65
65
  Reload
66
66
  </button>
@@ -47,7 +47,7 @@ export default function RailsPathLinkHandler() {
47
47
  const railsPath = link.getAttribute('data-rails-path');
48
48
 
49
49
  if (railsPath) {
50
- tutorialStore.setSelectedFile(`/workspace/store/${railsPath}`);
50
+ tutorialStore.setSelectedFile(`/workspace/${railsPath}`);
51
51
  }
52
52
 
53
53
  return;
@@ -68,7 +68,7 @@ export default function RailsPathLinkHandler() {
68
68
  try {
69
69
  await ensureRailsServerStarted(railsPreview);
70
70
  } catch (error) {
71
- console.error('failed to start Rails server', e);
71
+ console.error('failed to start Rails server', error);
72
72
  return;
73
73
  }
74
74
 
@@ -15,6 +15,7 @@ export const ShellConfigurator: React.FC = () => {
15
15
  const storeRef = useStore(tutorialStore.ref);
16
16
  const terminalConfig = useStore(tutorialStore.terminalConfig);
17
17
  const lessonLoaded = useStore(tutorialStore.lessonFullyLoaded);
18
+ const isPreparing = useStore(tutorialStore.isPreparing);
18
19
  const [state, set] = useState(0);
19
20
 
20
21
  useEffect(() => {
@@ -40,6 +41,10 @@ export const ShellConfigurator: React.FC = () => {
40
41
  return;
41
42
  }
42
43
 
44
+ if (isPreparing) {
45
+ return;
46
+ }
47
+
43
48
  if (!terminal) {
44
49
  return;
45
50
  }
@@ -89,7 +94,7 @@ export const ShellConfigurator: React.FC = () => {
89
94
  }, 100);
90
95
 
91
96
  return () => clearInterval(interval);
92
- }, [boot, terminalConfig, storeRef, lessonLoaded]);
97
+ }, [boot, terminalConfig, storeRef, isPreparing, lessonLoaded]);
93
98
 
94
99
  return null;
95
100
  };
@@ -4,7 +4,7 @@ title: Creating your first Rails app
4
4
  editor: false
5
5
  custom:
6
6
  shell:
7
- workdir: "/workspace/store"
7
+ workdir: "/workspace"
8
8
  ---
9
9
 
10
10
  Creating Your First Rails App
@@ -16,10 +16,10 @@ all of the commands.
16
16
  `rails new` generates the foundation of a fresh Rails application for you, so
17
17
  let's start there.
18
18
 
19
- To create our `store` application, run the following command in your terminal:
19
+ To create our application, run the following command in your terminal:
20
20
 
21
21
  ```bash
22
- $ rails new store
22
+ $ rails new demo_app
23
23
  ```
24
24
 
25
25
  :::info
@@ -30,5 +30,5 @@ these options, run `rails new --help`.
30
30
  After your new application is created, switch to its directory:
31
31
 
32
32
  ```bash
33
- $ cd store
33
+ $ cd demo_app
34
34
  ```
@@ -3,13 +3,13 @@ type: lesson
3
3
  title: Rails Console
4
4
  custom:
5
5
  shell:
6
- workdir: "/workspace/store"
6
+ workdir: "/workspace"
7
7
  ---
8
8
 
9
9
  Rails Console
10
10
  -------------------
11
11
 
12
- Now that we have created our products table, we can interact with it in Rails.
12
+ Now that we have our demo app set up, we can interact with it using Rails.
13
13
  Let's try it out.
14
14
 
15
15
  For this, we're going to use a Rails feature called the *console*. The console
@@ -23,14 +23,14 @@ You should see a prompt like the following:
23
23
 
24
24
  ```irb
25
25
  Loading development environment (Rails 8.0.2)
26
- store(dev)>
26
+ demo_app(dev)>
27
27
  ```
28
28
 
29
29
  Now we can type code that will be executed when we hit `Enter`. Try
30
30
  printing out the Rails version:
31
31
 
32
32
  ```irb
33
- store(dev)> Rails.version
33
+ demo_app(dev)> Rails.version
34
34
  <!-- hit Enter -->
35
35
  ```
36
36
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: lesson
3
3
  title: CRUD Operations
4
- focus: /workspace/store/app/controllers/products_controller.rb
4
+ focus: /workspace/app/controllers/products_controller.rb
5
5
  previews: [3000]
6
6
  mainCommand: ['node scripts/rails.js server', 'Starting Rails server']
7
7
  prepareCommands:
@@ -9,7 +9,7 @@ prepareCommands:
9
9
  - ['node scripts/rails.js db:prepare', 'Prepare development database']
10
10
  custom:
11
11
  shell:
12
- workdir: '/workspace/store'
12
+ workdir: '/workspace'
13
13
  ---
14
14
 
15
15
  # CRUD Operations in Rails
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../../../templates/rails-app"
3
+ }
@@ -0,0 +1,65 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ class HttpDemoController < ApplicationController
5
+ # ── CORS proxy configuration ─────────────────────────────────
6
+ # Deploy your own Cloudflare Worker (see cors-proxy/ in the
7
+ # project source), then paste your worker URL below.
8
+ #
9
+ # The proxy is needed because most APIs don't set CORS headers,
10
+ # and this Rails app runs inside the browser.
11
+ PROXY_URL = "https://cors-proxy.YOUR-SUBDOMAIN.workers.dev/proxy?"
12
+ PROXY_HOSTS = ["httpbin.org"]
13
+ # ─────────────────────────────────────────────────────────────
14
+
15
+ before_action :configure_proxy
16
+
17
+ def index
18
+ end
19
+
20
+ def fetch_get
21
+ uri = URI("https://httpbin.org/get?tutorial=rails-wasm&lesson=outbound-http")
22
+ response = Net::HTTP.get_response(uri)
23
+
24
+ render json: {
25
+ status: response.code.to_i,
26
+ headers: response.each_header.to_h,
27
+ body: safe_parse_json(response.body)
28
+ }
29
+ rescue => e
30
+ render json: { error: "#{e.class}: #{e.message}" }, status: 500
31
+ end
32
+
33
+ def fetch_post
34
+ uri = URI("https://httpbin.org/post")
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+ http.use_ssl = true
37
+
38
+ request = Net::HTTP::Post.new(uri.path)
39
+ request["Content-Type"] = "application/json"
40
+ request.body = { message: "Hello from Rails WASM!", timestamp: Time.now.iso8601 }.to_json
41
+
42
+ response = http.request(request)
43
+
44
+ render json: {
45
+ status: response.code.to_i,
46
+ headers: response.each_header.to_h,
47
+ body: safe_parse_json(response.body)
48
+ }
49
+ rescue => e
50
+ render json: { error: "#{e.class}: #{e.message}" }, status: 500
51
+ end
52
+
53
+ private
54
+
55
+ def configure_proxy
56
+ WasmHTTP::Connection.proxy_url = PROXY_URL
57
+ WasmHTTP::Connection.proxy_hosts = PROXY_HOSTS
58
+ end
59
+
60
+ def safe_parse_json(str)
61
+ JSON.parse(str)
62
+ rescue JSON::ParserError
63
+ str
64
+ end
65
+ end
@@ -0,0 +1,172 @@
1
+ <% content_for(:title) { "Outbound HTTP Demo" } %>
2
+
3
+ <div class="http-demo">
4
+ <h1>Outbound HTTP from Rails WASM</h1>
5
+ <p class="subtitle">
6
+ These buttons make real HTTP requests from Ruby to
7
+ <a href="https://httpbin.org" target="_blank">httpbin.org</a>
8
+ — routed through a CORS proxy.
9
+ </p>
10
+
11
+ <div class="actions">
12
+ <button id="btn-get" class="btn btn--primary" onclick="makeRequest('get')">
13
+ Fetch from httpbin.org
14
+ </button>
15
+ <button id="btn-post" class="btn btn--secondary" onclick="makeRequest('post')">
16
+ Post to httpbin.org
17
+ </button>
18
+ </div>
19
+
20
+ <div id="status" class="status" style="display: none;"></div>
21
+
22
+ <pre id="result" class="result"><span class="placeholder">Click a button above to make a request...</span></pre>
23
+ </div>
24
+
25
+ <script>
26
+ async function makeRequest(type) {
27
+ const resultEl = document.getElementById("result");
28
+ const statusEl = document.getElementById("status");
29
+ const btnGet = document.getElementById("btn-get");
30
+ const btnPost = document.getElementById("btn-post");
31
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
32
+
33
+ // Disable buttons
34
+ btnGet.disabled = true;
35
+ btnPost.disabled = true;
36
+
37
+ // Show loading
38
+ statusEl.style.display = "block";
39
+ statusEl.className = "status status--loading";
40
+ statusEl.textContent = "Sending request...";
41
+ resultEl.textContent = "";
42
+
43
+ try {
44
+ const url = type === "get" ? "/http_demo/fetch_get" : "/http_demo/fetch_post";
45
+ const options = {
46
+ method: "POST",
47
+ headers: {
48
+ "X-CSRF-Token": csrfToken || "",
49
+ "Content-Type": "application/x-www-form-urlencoded"
50
+ }
51
+ };
52
+
53
+ const response = await fetch(url, options);
54
+ const data = await response.json();
55
+
56
+ if (data.error) {
57
+ statusEl.className = "status status--error";
58
+ statusEl.textContent = "Error";
59
+ resultEl.textContent = data.error;
60
+ } else {
61
+ statusEl.className = "status status--success";
62
+ statusEl.textContent = `${type.toUpperCase()} request succeeded (HTTP ${data.status})`;
63
+ resultEl.textContent = JSON.stringify(data.body, null, 2);
64
+ }
65
+ } catch (err) {
66
+ statusEl.className = "status status--error";
67
+ statusEl.textContent = "Request failed";
68
+ resultEl.textContent = err.message;
69
+ } finally {
70
+ btnGet.disabled = false;
71
+ btnPost.disabled = false;
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <style>
77
+ .http-demo {
78
+ max-width: 48rem;
79
+ margin: 0 auto;
80
+ padding: var(--space-lg) var(--space-md);
81
+ }
82
+
83
+ .http-demo h1 {
84
+ margin-bottom: var(--space-xs);
85
+ }
86
+
87
+ .subtitle {
88
+ color: var(--color-text-muted);
89
+ margin-bottom: var(--space-lg);
90
+ }
91
+
92
+ .subtitle a {
93
+ color: var(--color-primary);
94
+ }
95
+
96
+ .actions {
97
+ display: flex;
98
+ gap: var(--space-sm);
99
+ margin-bottom: var(--space-md);
100
+ }
101
+
102
+ .btn {
103
+ padding: var(--space-sm) var(--space-md);
104
+ border: none;
105
+ border-radius: var(--radius-md);
106
+ font-size: var(--font-size-base);
107
+ cursor: pointer;
108
+ transition: opacity 0.15s;
109
+ }
110
+
111
+ .btn:disabled {
112
+ opacity: 0.6;
113
+ cursor: not-allowed;
114
+ }
115
+
116
+ .btn--primary {
117
+ background: var(--color-primary);
118
+ color: var(--color-text-inverse);
119
+ }
120
+
121
+ .btn--secondary {
122
+ background: var(--color-bg-white);
123
+ color: var(--color-text);
124
+ border: 1px solid var(--color-border);
125
+ }
126
+
127
+ .status {
128
+ padding: var(--space-sm) var(--space-md);
129
+ border-radius: var(--radius-md);
130
+ margin-bottom: var(--space-md);
131
+ font-size: var(--font-size-sm);
132
+ font-weight: 500;
133
+ }
134
+
135
+ .status--loading {
136
+ background: #fff3cd;
137
+ color: #856404;
138
+ border: 1px solid #ffc107;
139
+ }
140
+
141
+ .status--success {
142
+ background: #d4edda;
143
+ color: #155724;
144
+ border: 1px solid #28a745;
145
+ }
146
+
147
+ .status--error {
148
+ background: #f8d7da;
149
+ color: #721c24;
150
+ border: 1px solid #dc3545;
151
+ }
152
+
153
+ .result {
154
+ background: var(--color-bg-white);
155
+ border: 1px solid var(--color-border);
156
+ border-radius: var(--radius-md);
157
+ padding: var(--space-md);
158
+ overflow-x: auto;
159
+ font-size: var(--font-size-sm);
160
+ line-height: 1.6;
161
+ white-space: pre-wrap;
162
+ word-wrap: break-word;
163
+ min-height: 8rem;
164
+ max-height: 24rem;
165
+ overflow-y: auto;
166
+ }
167
+
168
+ .placeholder {
169
+ color: var(--color-text-muted);
170
+ font-style: italic;
171
+ }
172
+ </style>
@@ -0,0 +1,8 @@
1
+ Rails.application.routes.draw do
2
+ root "http_demo#index"
3
+
4
+ post "http_demo/fetch_get"
5
+ post "http_demo/fetch_post"
6
+
7
+ get "up" => "rails/health#show", as: :rails_health_check
8
+ end
@@ -0,0 +1,97 @@
1
+ ---
2
+ type: lesson
3
+ title: "Making HTTP Requests"
4
+ focus: /workspace/app/controllers/http_demo_controller.rb
5
+ previews: [3000]
6
+ mainCommand: ['node scripts/rails.js server', 'Starting Rails server']
7
+ prepareCommands:
8
+ - ['npm install', 'Preparing Ruby runtime']
9
+ - ['node scripts/rails.js db:prepare', 'Prepare database']
10
+ custom:
11
+ shell:
12
+ workdir: '/workspace'
13
+ ---
14
+
15
+ # Making Outbound HTTP Requests from Rails
16
+
17
+ Ruby on Rails running in WebAssembly can make **outbound HTTP requests** to external APIs — just like a normal Rails app. This lesson shows how it works.
18
+
19
+ ## How it works
20
+
21
+ ```
22
+ Ruby Net::HTTP -> JS fetch bridge -> CORS proxy -> External API
23
+ ```
24
+
25
+ Since the Rails app runs inside your browser, outbound requests go through the browser's `fetch()` API. Most server APIs don't include CORS headers, so a **CORS proxy** is needed to relay requests.
26
+
27
+ The HTTP bridge automatically intercepts `Net::HTTP` calls (and anything built on top of it, like Faraday) and routes them through the proxy.
28
+
29
+ ## Deploy your own CORS proxy
30
+
31
+ This tutorial includes a ready-to-deploy Cloudflare Worker that acts as a CORS proxy. You'll find it in the `cors-proxy/` directory of this project's source code.
32
+
33
+ ### Steps
34
+
35
+ 1. **Install Wrangler** (Cloudflare's CLI):
36
+
37
+ ```bash
38
+ npm install -g wrangler
39
+ wrangler login
40
+ ```
41
+
42
+ 2. **Navigate to the proxy directory** in the tutorialkit.rb source:
43
+
44
+ ```bash
45
+ cd packages/template/cors-proxy
46
+ ```
47
+
48
+ 3. **Edit `wrangler.toml`** to set your allowed hosts:
49
+
50
+ ```toml
51
+ [vars]
52
+ ALLOWED_HOSTS = "httpbin.org,api.example.com"
53
+ ```
54
+
55
+ This is a comma-separated list of hostnames the proxy will forward requests to. Keep it restrictive — only add hosts your tutorial lessons actually need.
56
+
57
+ 4. **Deploy**:
58
+
59
+ ```bash
60
+ npm install
61
+ wrangler deploy
62
+ ```
63
+
64
+ Wrangler will print your worker URL, e.g. `https://cors-proxy.<your-subdomain>.workers.dev`.
65
+
66
+ ## Try it out
67
+
68
+ 1. Open `http_demo_controller.rb` in the editor and replace `YOUR-SUBDOMAIN` in `PROXY_URL` with your actual Cloudflare Workers subdomain
69
+ 2. Click **Fetch from httpbin.org** to make an outbound GET request
70
+ 3. Click **Post to httpbin.org** to make an outbound POST request
71
+
72
+ The proxy configuration lives at the top of the controller as constants. Since the controller is reloaded on each request, you can edit `PROXY_URL` and your changes take effect immediately — no server restart needed.
73
+
74
+ ## The code
75
+
76
+ **Controller** (`http_demo_controller.rb`): Configures the CORS proxy via a `before_action`, then makes outbound requests using `Net::HTTP` — the same standard library you'd use in any Rails app.
77
+
78
+ ```ruby
79
+ PROXY_URL = "https://cors-proxy.<your-subdomain>.workers.dev/proxy?"
80
+ PROXY_HOSTS = ["httpbin.org"]
81
+
82
+ before_action :configure_proxy
83
+
84
+ def fetch_get
85
+ uri = URI("https://httpbin.org/get")
86
+ response = Net::HTTP.get_response(uri)
87
+ render json: { status: response.code.to_i, body: JSON.parse(response.body) }
88
+ end
89
+ ```
90
+
91
+ The `configure_proxy` callback sets `WasmHTTP::Connection.proxy_url` and `proxy_hosts` before each request, so the HTTP bridge knows which hosts to route through the proxy.
92
+
93
+ **View** (`http_demo/index.html.erb`): Buttons trigger requests and the JSON response is displayed in a `<pre>` block.
94
+
95
+ :::note
96
+ Responses are synchronous — the browser waits for the full response before rendering. This is because the Ruby WASM runtime processes requests through a single-threaded queue.
97
+ :::
@@ -0,0 +1,4 @@
1
+ ---
2
+ type: part
3
+ title: Outbound HTTP
4
+ ---
@@ -1,8 +1,13 @@
1
1
  ---
2
2
  type: tutorial
3
3
  openInStackBlitz: false
4
+ meta:
5
+ image: /cover.png
6
+ title: My Rails Tutorial
7
+ description: An interactive Ruby on Rails tutorial powered by WebAssembly
4
8
  prepareCommands:
5
9
  - ['npm install', 'Preparing Ruby runtime']
10
+ terminalBlockingPrepareCommandsCount: 1
6
11
  previews: false
7
12
  filesystem:
8
13
  watch: ['/*.json', '/workspace/**/*']
@@ -0,0 +1,14 @@
1
+ import { defineMiddleware } from 'astro:middleware';
2
+
3
+ export const onRequest = defineMiddleware(async (context, next) => {
4
+ const response = await next();
5
+
6
+ // Required for WebContainer (SharedArrayBuffer)
7
+ response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
8
+ response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
9
+
10
+ // Allow embedding in cross-origin pages
11
+ response.headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
12
+
13
+ return response;
14
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Boot progress reporting for Rails WASM initialization.
3
+ * Allows consumers to track the boot sequence stages.
4
+ */
5
+
6
+ export class BootProgress {
7
+ constructor() {
8
+ this.listeners = new Set();
9
+ this.currentStep = "Initializing...";
10
+ this.currentValue = 0;
11
+ }
12
+
13
+ addListener(listener) {
14
+ this.listeners.add(listener);
15
+ listener({ step: this.currentStep, value: this.currentValue });
16
+ }
17
+
18
+ removeListener(listener) {
19
+ this.listeners.delete(listener);
20
+ }
21
+
22
+ updateStep(step) {
23
+ this.currentStep = step;
24
+ this.currentValue = 0;
25
+ this.notifyListeners();
26
+ }
27
+
28
+ updateProgress(value) {
29
+ this.currentValue = Math.min(100, Math.max(0, value));
30
+ this.notifyListeners();
31
+ }
32
+
33
+ notifyListeners() {
34
+ const state = { step: this.currentStep, value: this.currentValue };
35
+ for (const listener of this.listeners) {
36
+ try {
37
+ listener(state);
38
+ } catch (e) {
39
+ console.error("Boot progress listener error:", e);
40
+ }
41
+ }
42
+ }
43
+
44
+ log(message) {
45
+ console.log(`[boot] ${message}`);
46
+ }
47
+ }
48
+
49
+ export const bootProgress = new BootProgress();