ruact 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -1
- data/lib/generators/ruact/install/install_generator.rb +264 -5
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +3 -0
- data/lib/generators/ruact/install/templates/globals.css.tt +20 -0
- data/lib/generators/ruact/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruact/install/templates/package.json.tt +7 -1
- data/lib/generators/ruact/install/templates/tsconfig.json.tt +18 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +10 -0
- data/lib/ruact/configuration.rb +73 -0
- data/lib/ruact/controller/document_rendering.rb +210 -0
- data/lib/ruact/controller.rb +5 -46
- data/lib/ruact/doctor.rb +37 -5
- data/lib/ruact/layout_source.rb +59 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +10 -1
- data/lib/ruact.rb +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +3 -0
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +16 -0
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +24 -0
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +15 -0
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +17 -0
- data/spec/ruact/controller_request_spec.rb +203 -0
- data/spec/ruact/doctor_spec.rb +81 -6
- data/spec/ruact/install_generator_spec.rb +442 -70
- data/spec/ruact/layout_source_spec.rb +108 -0
- data/spec/ruact/scaffold_generator_spec.rb +14 -0
- metadata +18 -4
|
@@ -115,6 +115,14 @@ module ControllerRequestSpecSupport
|
|
|
115
115
|
get "/errors-demo/new", to: "controller_request_spec_support/errors_demo#new"
|
|
116
116
|
post "/errors-demo/create", to: "controller_request_spec_support/errors_demo#create"
|
|
117
117
|
post "/errors-demo/create_valid", to: "controller_request_spec_support/errors_demo#create_valid"
|
|
118
|
+
# The layout owns the document — a page rendered through a migrated
|
|
119
|
+
# host layout, and one through a layout that never calls
|
|
120
|
+
# `ruact_js_assets`.
|
|
121
|
+
get "/layout-demo/show", to: "controller_request_spec_support/layout_demo#show"
|
|
122
|
+
get "/unwired-layout-demo/show", to: "controller_request_spec_support/unwired_layout_demo#show"
|
|
123
|
+
get "/exploding-layout-demo/show", to: "controller_request_spec_support/exploding_layout_demo#show"
|
|
124
|
+
get "/rootless-layout-demo/show", to: "controller_request_spec_support/rootless_layout_demo#show"
|
|
125
|
+
get "/ghost-layout-demo/show", to: "controller_request_spec_support/ghost_layout_demo#show"
|
|
118
126
|
end
|
|
119
127
|
end
|
|
120
128
|
end
|
|
@@ -186,6 +194,73 @@ module ControllerRequestSpecSupport
|
|
|
186
194
|
|
|
187
195
|
def show; end
|
|
188
196
|
end
|
|
197
|
+
|
|
198
|
+
# The layout owns the document. Both controllers declare a NAMED layout
|
|
199
|
+
# instead of relying on `layouts/application`, so adding them cannot change
|
|
200
|
+
# what every other controller in this file renders (the rest have no
|
|
201
|
+
# resolvable layout and must keep getting ruact's built-in shell).
|
|
202
|
+
class LayoutDemoController < ActionController::Base
|
|
203
|
+
include Ruact::Controller
|
|
204
|
+
|
|
205
|
+
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
206
|
+
layout "ruact_host"
|
|
207
|
+
|
|
208
|
+
def show
|
|
209
|
+
ruact_render
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# An unmigrated layout that RAISES if rendered (it reads an ivar a ruact
|
|
214
|
+
# action never sets). With the default config ruact must never execute it.
|
|
215
|
+
class ExplodingLayoutDemoController < ActionController::Base
|
|
216
|
+
include Ruact::Controller
|
|
217
|
+
|
|
218
|
+
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
219
|
+
layout "exploding_host"
|
|
220
|
+
|
|
221
|
+
def show
|
|
222
|
+
ruact_render
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Declares a layout that does not exist. `_default_layout` hands back the
|
|
227
|
+
# String path WITHOUT checking, so "not nil" read as resolvable and the render
|
|
228
|
+
# raised MissingTemplate instead of degrading.
|
|
229
|
+
class GhostLayoutDemoController < ActionController::Base
|
|
230
|
+
include Ruact::Controller
|
|
231
|
+
|
|
232
|
+
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
233
|
+
layout "no_such_host"
|
|
234
|
+
|
|
235
|
+
def show
|
|
236
|
+
ruact_render
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# A layout that calls `ruact_js_assets` but has no root div to mount into.
|
|
241
|
+
class RootlessLayoutDemoController < ActionController::Base
|
|
242
|
+
include Ruact::Controller
|
|
243
|
+
|
|
244
|
+
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
245
|
+
layout "rootless_host"
|
|
246
|
+
|
|
247
|
+
def show
|
|
248
|
+
ruact_render
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Same page, but through a layout that never calls `ruact_js_assets` — the
|
|
253
|
+
# state every app installed before the layout owned the document is in.
|
|
254
|
+
class UnwiredLayoutDemoController < ActionController::Base
|
|
255
|
+
include Ruact::Controller
|
|
256
|
+
|
|
257
|
+
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
258
|
+
layout "bare_host"
|
|
259
|
+
|
|
260
|
+
def show
|
|
261
|
+
ruact_render
|
|
262
|
+
end
|
|
263
|
+
end
|
|
189
264
|
end
|
|
190
265
|
|
|
191
266
|
# Story 10.0 — write the implicit-render template at file-load time (before the
|
|
@@ -251,6 +326,134 @@ module Ruact # rubocop:disable Style/OneClassPerFile
|
|
|
251
326
|
end
|
|
252
327
|
end
|
|
253
328
|
|
|
329
|
+
# The bug this closes: a ruact page could not carry ANY of the host app's
|
|
330
|
+
# CSS. `ruact_render` rendered the view with `layout: false` and then wrapped
|
|
331
|
+
# the payload in a hardcoded shell whose `<head>` has no stylesheet slot, so
|
|
332
|
+
# `stylesheet_link_tag` in the app's layout never reached the browser. The
|
|
333
|
+
# generated `--shadcn` scaffold was therefore unstyled by construction, and
|
|
334
|
+
# Epic 12 (`ruact_meta` → tags in `<head>`) had no surface to write into.
|
|
335
|
+
#
|
|
336
|
+
# The opt-in is EXPLICIT (`Ruact.config.layout`). ruact used to infer
|
|
337
|
+
# readiness from the layout itself; three review rounds each found another
|
|
338
|
+
# template shape that fooled the inference, so the question is no longer
|
|
339
|
+
# asked. These examples pin both halves: nothing happens without the config,
|
|
340
|
+
# and everything happens with it.
|
|
341
|
+
describe "the layout owns the document" do
|
|
342
|
+
def configure_layout(value)
|
|
343
|
+
Ruact.configure do |c|
|
|
344
|
+
c.manifest_path = ControllerRequestSpecSupport.manifest_path
|
|
345
|
+
c.layout = value
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# The whole backward-compatibility story, and it is now structural rather
|
|
350
|
+
# than argued: with the default config ruact never looks at, resolves, or
|
|
351
|
+
# renders a layout at all.
|
|
352
|
+
context "with the default configuration (layout = false)" do
|
|
353
|
+
it "renders the built-in shell, exactly as before the layout path existed" do
|
|
354
|
+
get "/layout-demo/show"
|
|
355
|
+
|
|
356
|
+
expect(last_response.status).to eq(200)
|
|
357
|
+
expect(last_response.body).to include("Rails RSC")
|
|
358
|
+
expect(last_response.body).not_to include("host-app.css")
|
|
359
|
+
expect(last_response.body).to include("DemoButton")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# This layout raises if it is rendered (it reads an ivar a ruact action
|
|
363
|
+
# never sets) — the shape of a real pre-migration app. Nothing may
|
|
364
|
+
# execute it.
|
|
365
|
+
it "never executes an unmigrated layout, so a working page cannot become a 500" do
|
|
366
|
+
get "/exploding-layout-demo/show"
|
|
367
|
+
|
|
368
|
+
expect(last_response.status).to(eq(200),
|
|
369
|
+
"expected the layout NOT to be rendered; got " \
|
|
370
|
+
"#{last_response.status} body=#{last_response.body[0, 300]}")
|
|
371
|
+
expect(last_response.body).to include("Rails RSC")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
context "when the app opts in (layout = true)" do
|
|
376
|
+
before { configure_layout(true) }
|
|
377
|
+
|
|
378
|
+
it "puts the host app's stylesheet on a ruact page (the CSS could not arrive before)" do
|
|
379
|
+
get "/layout-demo/show"
|
|
380
|
+
|
|
381
|
+
expect(last_response.status).to(eq(200),
|
|
382
|
+
"expected 200, got #{last_response.status} " \
|
|
383
|
+
"body=#{last_response.body[0, 400]}")
|
|
384
|
+
expect(last_response.body).to include('<link rel="stylesheet" href="/host-app.css" />')
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "keeps the host layout's own <title> instead of ruact's placeholder" do
|
|
388
|
+
get "/layout-demo/show"
|
|
389
|
+
|
|
390
|
+
expect(last_response.body).to include("<title>Host App Title</title>")
|
|
391
|
+
expect(last_response.body).not_to include("Rails RSC")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it "still ships the Flight payload and the component, through the layout's ruact_js_assets" do
|
|
395
|
+
get "/layout-demo/show"
|
|
396
|
+
|
|
397
|
+
expect(last_response.body).to include("__FLIGHT_DATA")
|
|
398
|
+
expect(last_response.body).to include("DemoButton")
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "does not apply the layout to a Flight request (the wire shape is unchanged)" do
|
|
402
|
+
get "/layout-demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
403
|
+
|
|
404
|
+
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
405
|
+
expect(last_response.body).not_to include("host-app.css")
|
|
406
|
+
expect(last_response.body).to include("DemoButton")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Opting in and pointing ruact at a layout that cannot mount the app is
|
|
410
|
+
# a configuration error, and silence would cost a blank page.
|
|
411
|
+
it "fails loudly when the opted-in layout never calls ruact_js_assets" do
|
|
412
|
+
expect { get "/unwired-layout-demo/show" }
|
|
413
|
+
.to raise_error(Ruact::Error, /ruact_js_assets/)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it "fails loudly when the opted-in layout has no root div to mount into" do
|
|
417
|
+
expect { get "/rootless-layout-demo/show" }
|
|
418
|
+
.to raise_error(Ruact::Error, /root/)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# A controller that declares a layout which does not exist: Rails'
|
|
422
|
+
# `_default_layout` returns the String path without checking it, so
|
|
423
|
+
# "resolvable" has to mean "the template is actually there".
|
|
424
|
+
it "degrades to the shell when a declared layout does not exist" do
|
|
425
|
+
get "/ghost-layout-demo/show"
|
|
426
|
+
|
|
427
|
+
expect(last_response.status).to(eq(200),
|
|
428
|
+
"expected a degrade, got #{last_response.status} " \
|
|
429
|
+
"body=#{last_response.body[0, 300]}")
|
|
430
|
+
expect(last_response.body).to include("Rails RSC")
|
|
431
|
+
expect(last_response.body).to include("DemoButton")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# `layout false` on one controller inside an app that opted in globally
|
|
435
|
+
# is a normal Rails pattern, not a mistake — it must degrade, not raise.
|
|
436
|
+
it "degrades to the shell for a controller with no resolvable layout" do
|
|
437
|
+
get "/demo/show"
|
|
438
|
+
|
|
439
|
+
expect(last_response.status).to eq(200)
|
|
440
|
+
expect(last_response.body).to include("Rails RSC")
|
|
441
|
+
expect(last_response.body).to include("DemoButton")
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
context "when a named layout is configured" do
|
|
446
|
+
before { configure_layout("ruact_host") }
|
|
447
|
+
|
|
448
|
+
it "renders every ruact page through that layout" do
|
|
449
|
+
get "/demo/show"
|
|
450
|
+
|
|
451
|
+
expect(last_response.body).to include("host-app.css")
|
|
452
|
+
expect(last_response.body).to include("DemoButton")
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
254
457
|
describe "regression guard: render context reaches ViewHelper" do
|
|
255
458
|
it "ViewHelper#__ruact_component__ does NOT raise the outside-flow error" do
|
|
256
459
|
# If the handoff regresses, the request returns 500 with this exact
|
data/spec/ruact/doctor_spec.rb
CHANGED
|
@@ -23,10 +23,20 @@ RSpec.describe Ruact::Doctor do
|
|
|
23
23
|
File.write(dir.join("application_controller.rb"), content)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# `with_assets` is the second half of a migrated layout: the React root alone
|
|
27
|
+
# leaves ruact on its built-in (CSS-less) shell, so the two markers are
|
|
28
|
+
# independent inputs, not one sentinel.
|
|
29
|
+
def make_layout(with_sentinel: true, with_assets: true)
|
|
27
30
|
dir = tmpdir.join("app", "views", "layouts")
|
|
28
31
|
FileUtils.mkdir_p(dir)
|
|
29
|
-
content =
|
|
32
|
+
content =
|
|
33
|
+
if !with_sentinel
|
|
34
|
+
"<body></body>\n"
|
|
35
|
+
elsif with_assets
|
|
36
|
+
"<%# ruact: root %>\n<div id=\"root\"></div>\n<%= ruact_js_assets %>\n"
|
|
37
|
+
else
|
|
38
|
+
"<%# ruact: root %>\n<div id=\"root\"></div>\n"
|
|
39
|
+
end
|
|
30
40
|
File.write(dir.join("application.html.erb"), content)
|
|
31
41
|
end
|
|
32
42
|
|
|
@@ -131,8 +141,11 @@ RSpec.describe Ruact::Doctor do
|
|
|
131
141
|
describe "#check_layout (AC#1, #5)" do
|
|
132
142
|
subject(:doctor) { described_class.new }
|
|
133
143
|
|
|
134
|
-
context "when layout
|
|
135
|
-
before
|
|
144
|
+
context "when the layout is wired and the app opted in" do
|
|
145
|
+
before do
|
|
146
|
+
make_layout(with_sentinel: true, with_assets: true)
|
|
147
|
+
Ruact.configure { |c| c.layout = true }
|
|
148
|
+
end
|
|
136
149
|
|
|
137
150
|
it "returns :pass" do
|
|
138
151
|
status, = doctor.send(:check_layout)
|
|
@@ -140,7 +153,60 @@ RSpec.describe Ruact::Doctor do
|
|
|
140
153
|
end
|
|
141
154
|
end
|
|
142
155
|
|
|
143
|
-
|
|
156
|
+
# Both halves are silent when wrong, and they are DIFFERENT fixes — "add one
|
|
157
|
+
# line to the layout" versus "flip one setting" — so they report separately.
|
|
158
|
+
context "when the layout is wired but Ruact.config.layout is false" do
|
|
159
|
+
before do
|
|
160
|
+
make_layout(with_sentinel: true, with_assets: true)
|
|
161
|
+
Ruact.configure { |c| c.layout = false }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "returns :warn naming the setting, not the layout" do
|
|
165
|
+
status, message, remediation = doctor.send(:check_layout)
|
|
166
|
+
|
|
167
|
+
expect(status).to eq(:warn)
|
|
168
|
+
expect(message).to include("config.layout is false")
|
|
169
|
+
expect(remediation).to include("config.layout = true")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "does not fail the doctor run" do
|
|
173
|
+
expect(described_class::SUCCESS_STATUSES).to include(:warn)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context "when the layout has the React root but never calls ruact_js_assets" do
|
|
178
|
+
before do
|
|
179
|
+
make_layout(with_sentinel: true, with_assets: false)
|
|
180
|
+
Ruact.configure { |c| c.layout = true }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "fails and names the helper to add" do
|
|
184
|
+
status, _message, remediation = doctor.send(:check_layout)
|
|
185
|
+
|
|
186
|
+
expect(status).to eq(:fail)
|
|
187
|
+
expect(remediation).to include("ruact_js_assets")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The drift this shares with the runtime and the generator: a MENTION of the
|
|
192
|
+
# helper in a comment is not a call, and reporting :pass on one sent a
|
|
193
|
+
# developer looking anywhere but at the real problem.
|
|
194
|
+
context "when the layout only mentions the helper in a comment" do
|
|
195
|
+
before do
|
|
196
|
+
dir = tmpdir.join("app", "views", "layouts")
|
|
197
|
+
FileUtils.mkdir_p(dir)
|
|
198
|
+
File.write(dir.join("application.html.erb"),
|
|
199
|
+
"<%# ruact: root %>\n<div id=\"root\"></div>\n<%# TODO: add ruact_js_assets %>\n")
|
|
200
|
+
Ruact.configure { |c| c.layout = true }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "does not report it as wired" do
|
|
204
|
+
status, = doctor.send(:check_layout)
|
|
205
|
+
expect(status).to eq(:fail)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
context "when the layout has neither the root nor the helper" do
|
|
144
210
|
before { make_layout(with_sentinel: false) }
|
|
145
211
|
|
|
146
212
|
it "returns :fail" do
|
|
@@ -148,8 +214,17 @@ RSpec.describe Ruact::Doctor do
|
|
|
148
214
|
expect(status).to eq(:fail)
|
|
149
215
|
end
|
|
150
216
|
|
|
151
|
-
it "
|
|
217
|
+
it "names both halves, since either could be the missing one" do
|
|
152
218
|
_, msg = doctor.send(:check_layout)
|
|
219
|
+
expect(msg).to include("React root").and include("ruact_js_assets")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
context "when there is no application layout file at all" do
|
|
224
|
+
it "reports the file, not its contents" do
|
|
225
|
+
status, msg = doctor.send(:check_layout)
|
|
226
|
+
|
|
227
|
+
expect(status).to eq(:fail)
|
|
153
228
|
expect(msg).to eq("React shell missing from application.html.erb")
|
|
154
229
|
end
|
|
155
230
|
end
|