ruact 0.0.7 → 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 +59 -1
- data/docs/internal/decisions/server-functions-api.md +55 -0
- data/lib/generators/ruact/install/install_generator.rb +378 -6
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +159 -0
- 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 +14 -46
- data/lib/ruact/doctor.rb +102 -12
- data/lib/ruact/erb_preprocessor.rb +113 -0
- data/lib/ruact/errors.rb +16 -0
- data/lib/ruact/layout_source.rb +59 -0
- data/lib/ruact/manifest_resolver.rb +2 -2
- data/lib/ruact/serializable.rb +98 -6
- data/lib/ruact/server.rb +163 -0
- data/lib/ruact/server_functions/introspection.rb +81 -0
- data/lib/ruact/server_functions.rb +26 -4
- data/lib/ruact/testing/component_query.rb +113 -0
- data/lib/ruact/testing/flight_extractor.rb +221 -0
- data/lib/ruact/testing/flight_structure_diff.rb +267 -0
- data/lib/ruact/testing/flight_wire_parser.rb +138 -0
- data/lib/ruact/testing.rb +90 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +10 -1
- data/lib/ruact.rb +1 -0
- data/lib/tasks/ruact.rake +55 -2
- 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 +222 -6
- data/spec/ruact/erb_preprocessor_spec.rb +145 -0
- data/spec/ruact/install_generator_spec.rb +727 -70
- data/spec/ruact/layout_source_spec.rb +108 -0
- data/spec/ruact/manifest_resolver_spec.rb +15 -0
- data/spec/ruact/scaffold_generator_spec.rb +14 -0
- data/spec/ruact/serializable_spec.rb +126 -0
- data/spec/ruact/server_bucket_request_spec.rb +291 -0
- data/spec/ruact/server_functions/introspection_spec.rb +135 -0
- data/spec/ruact/tasks_json_introspection_spec.rb +141 -0
- data/spec/ruact/testing/have_ruact_component_spec.rb +170 -0
- data/spec/ruact/testing/no_production_load_spec.rb +41 -0
- data/spec/support/flight_wire_parser.rb +12 -126
- data/spec/support/matchers/flight_fixture_matcher.rb +7 -258
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +25 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +104 -7
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +173 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
- metadata +30 -4
|
@@ -88,22 +88,6 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
88
88
|
:injected
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# Reproduces inject_layout_shell logic from the generator
|
|
92
|
-
def inject_layout_shell(dest_root)
|
|
93
|
-
layout_file = File.join(dest_root, "app/views/layouts/application.html.erb")
|
|
94
|
-
return :missing unless File.exist?(layout_file)
|
|
95
|
-
|
|
96
|
-
content = File.read(layout_file)
|
|
97
|
-
return :already_present if content.include?("ruact: root")
|
|
98
|
-
|
|
99
|
-
modified = content.sub(
|
|
100
|
-
" </body>",
|
|
101
|
-
" <%# ruact: root %>\n <div id=\"root\"></div>\n </body>"
|
|
102
|
-
)
|
|
103
|
-
File.write(layout_file, modified)
|
|
104
|
-
:injected
|
|
105
|
-
end
|
|
106
|
-
|
|
107
91
|
describe "ApplicationController injection (AC#1, AC#3)" do
|
|
108
92
|
let(:controller_content) do
|
|
109
93
|
"class ApplicationController < ActionController::Base\nend\n"
|
|
@@ -137,60 +121,6 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
137
121
|
end
|
|
138
122
|
end
|
|
139
123
|
|
|
140
|
-
describe "Layout injection (AC#1, AC#3)" do
|
|
141
|
-
let(:layout_content) do
|
|
142
|
-
<<~HTML
|
|
143
|
-
<!DOCTYPE html>
|
|
144
|
-
<html>
|
|
145
|
-
<body>
|
|
146
|
-
<%= yield %>
|
|
147
|
-
</body>
|
|
148
|
-
</html>
|
|
149
|
-
HTML
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
it "injects the RSC root div before </body>" do
|
|
153
|
-
write_file("app/views/layouts/application.html.erb", layout_content)
|
|
154
|
-
result = inject_layout_shell(tmpdir)
|
|
155
|
-
|
|
156
|
-
expect(result).to eq(:injected)
|
|
157
|
-
content = read_file("app/views/layouts/application.html.erb")
|
|
158
|
-
expect(content).to include('<div id="root"></div>')
|
|
159
|
-
expect(content).to include("ruact: root")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
it "returns :already_present on second run (idempotent, AC#3)" do
|
|
163
|
-
content_with_marker = layout_content.sub(
|
|
164
|
-
" </body>",
|
|
165
|
-
" <%# ruact: root %>\n <div id=\"root\"></div>\n </body>"
|
|
166
|
-
)
|
|
167
|
-
write_file("app/views/layouts/application.html.erb", content_with_marker)
|
|
168
|
-
|
|
169
|
-
result = inject_layout_shell(tmpdir)
|
|
170
|
-
expect(result).to eq(:already_present)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
it "does not duplicate the root div when run twice (AC#3)" do
|
|
174
|
-
write_file("app/views/layouts/application.html.erb", layout_content)
|
|
175
|
-
inject_layout_shell(tmpdir)
|
|
176
|
-
inject_layout_shell(tmpdir)
|
|
177
|
-
|
|
178
|
-
content = read_file("app/views/layouts/application.html.erb")
|
|
179
|
-
occurrences = content.scan('<div id="root">').size
|
|
180
|
-
expect(occurrences).to eq(1)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
it "places the root div before the closing </body> tag" do
|
|
184
|
-
write_file("app/views/layouts/application.html.erb", layout_content)
|
|
185
|
-
inject_layout_shell(tmpdir)
|
|
186
|
-
|
|
187
|
-
content = read_file("app/views/layouts/application.html.erb")
|
|
188
|
-
root_pos = content.index('<div id="root">')
|
|
189
|
-
body_pos = content.index("</body>")
|
|
190
|
-
expect(root_pos).to be < body_pos
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
124
|
describe "vite.config.js template (AC#2)" do
|
|
195
125
|
let(:template_path) do
|
|
196
126
|
File.expand_path(
|
|
@@ -359,6 +289,7 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
359
289
|
gen.create_server_functions_directory
|
|
360
290
|
gen.append_gitignore_entries
|
|
361
291
|
gen.create_vite_config
|
|
292
|
+
gen.create_agents_md
|
|
362
293
|
end.not_to raise_error
|
|
363
294
|
end
|
|
364
295
|
|
|
@@ -371,6 +302,7 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
371
302
|
expect(File.read(File.join(app_root, ".gitignore")))
|
|
372
303
|
.to include("app/javascript/.ruact/server-functions.ts")
|
|
373
304
|
expect(File).to exist(File.join(app_root, "vite.config.js"))
|
|
305
|
+
expect(File).to exist(File.join(app_root, "AGENTS.md"))
|
|
374
306
|
end
|
|
375
307
|
end
|
|
376
308
|
end
|
|
@@ -889,4 +821,729 @@ RSpec.describe Ruact do # rubocop:disable RSpec/SpecFilePathFormat
|
|
|
889
821
|
end
|
|
890
822
|
end
|
|
891
823
|
end
|
|
824
|
+
|
|
825
|
+
# Story 15.1 (FR105) — `ruact:install` emits an AGENTS.md teaching coding
|
|
826
|
+
# agents the ruact conventions, traps, and verification commands. The ruact
|
|
827
|
+
# content is delimited by explicit markers so re-runs are idempotent, a
|
|
828
|
+
# user-authored AGENTS.md is appended to (never clobbered), and --force
|
|
829
|
+
# refreshes ONLY the marked section. Content assertions grep stable tokens
|
|
830
|
+
# (commands, file paths, API names) — never full-file snapshots, since later
|
|
831
|
+
# stories (15.2/15.3/15.4) deliberately evolve the prose.
|
|
832
|
+
describe "install generator — AGENTS.md emission (Story 15.1 — FR105)", :story_15_1 do
|
|
833
|
+
require "stringio"
|
|
834
|
+
require "generators/ruact/install/install_generator"
|
|
835
|
+
|
|
836
|
+
let(:app_root) { Dir.mktmpdir("ruact_install_1501") }
|
|
837
|
+
let(:agents_md_path) { File.join(app_root, "AGENTS.md") }
|
|
838
|
+
let(:begin_marker) { "<!-- ruact:begin -->" }
|
|
839
|
+
let(:end_marker) { "<!-- ruact:end -->" }
|
|
840
|
+
let(:template_path) do
|
|
841
|
+
File.expand_path("../../lib/generators/ruact/install/templates/AGENTS.md.tt", __dir__)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
after { FileUtils.rm_rf(app_root) }
|
|
845
|
+
|
|
846
|
+
def build_generator(root, opts = {})
|
|
847
|
+
Ruact::Generators::InstallGenerator.new([], opts, destination_root: root)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def silently
|
|
851
|
+
original = $stdout
|
|
852
|
+
$stdout = StringIO.new
|
|
853
|
+
yield
|
|
854
|
+
ensure
|
|
855
|
+
$stdout = original
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Captures and returns the generator's say/say_status stdout for text asserts.
|
|
859
|
+
def capture_stdout
|
|
860
|
+
original = $stdout
|
|
861
|
+
$stdout = StringIO.new
|
|
862
|
+
yield
|
|
863
|
+
$stdout.string
|
|
864
|
+
ensure
|
|
865
|
+
$stdout = original
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
describe "fresh emission (AC#1)" do
|
|
869
|
+
it "creates AGENTS.md delimited by the ruact markers", :aggregate_failures do
|
|
870
|
+
silently { build_generator(app_root).create_agents_md }
|
|
871
|
+
|
|
872
|
+
expect(File).to exist(agents_md_path)
|
|
873
|
+
content = File.read(agents_md_path)
|
|
874
|
+
expect(content).to start_with(begin_marker)
|
|
875
|
+
expect(content.rstrip).to end_with(end_marker)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
it "covers the model, codegen, queries and verification areas", :aggregate_failures do
|
|
879
|
+
silently { build_generator(app_root).create_agents_md }
|
|
880
|
+
content = File.read(agents_md_path)
|
|
881
|
+
|
|
882
|
+
# The verb rule (mutations = non-GET routed actions on Ruact::Server)
|
|
883
|
+
expect(content).to include("Ruact::Server")
|
|
884
|
+
expect(content).to include("non-GET")
|
|
885
|
+
# Route-driven codegen: ground-truth file + regenerate command
|
|
886
|
+
expect(content).to include("app/javascript/.ruact/server-functions.ts")
|
|
887
|
+
expect(content).to include("bin/rails ruact:server_functions:generate")
|
|
888
|
+
# Queries (reads)
|
|
889
|
+
expect(content).to include("Ruact::Query")
|
|
890
|
+
expect(content).to include("app/queries/")
|
|
891
|
+
expect(content).to include("ruact_queries")
|
|
892
|
+
expect(content).to include("useQuery")
|
|
893
|
+
# Verification commands (the only two that exist today)
|
|
894
|
+
expect(content).to include("bin/rails ruact:doctor")
|
|
895
|
+
# Docs pointers
|
|
896
|
+
expect(content).to include("https://ruact.dev")
|
|
897
|
+
expect(content).to include("llms.txt")
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
it "covers the five traps and the safety areas", :aggregate_failures do
|
|
901
|
+
silently { build_generator(app_root).create_agents_md }
|
|
902
|
+
content = File.read(agents_md_path)
|
|
903
|
+
|
|
904
|
+
# Trap 1 — children unsupported / self-closing only
|
|
905
|
+
expect(content).to include("self-closing")
|
|
906
|
+
expect(content).to include("children")
|
|
907
|
+
# Trap 2 — Ruby (not JS) inside {} props
|
|
908
|
+
expect(content).to include("Ruby, not JavaScript")
|
|
909
|
+
# Trap 3 — Accept-header dual shape (exact application/json)
|
|
910
|
+
expect(content).to include("Accept")
|
|
911
|
+
expect(content).to include("application/json")
|
|
912
|
+
# Trap 4 — ruact_errors fall-through
|
|
913
|
+
expect(content).to include("ruact_errors")
|
|
914
|
+
expect(content).to include("fall-through")
|
|
915
|
+
# Trap 5 — name derivation from routes
|
|
916
|
+
expect(content).to include("createPost")
|
|
917
|
+
expect(content).to include("ruact_function_name")
|
|
918
|
+
# Serialization allowlist
|
|
919
|
+
expect(content).to include("ruact_props")
|
|
920
|
+
expect(content).to include("strict_serialization")
|
|
921
|
+
# SGID helpers (purpose + expiry required grain)
|
|
922
|
+
expect(content).to include("Ruact.signed_global_id")
|
|
923
|
+
expect(content).to include("Ruact.locate_signed")
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
it "makes no claim before its artifact (AC#4 — content honesty)", :aggregate_failures do
|
|
927
|
+
silently { build_generator(app_root).create_agents_md }
|
|
928
|
+
content = File.read(agents_md_path)
|
|
929
|
+
|
|
930
|
+
# (The loud children `PreprocessorError` is Story 15.2's shipped
|
|
931
|
+
# artifact, and the `--json` introspection commands are Story 15.3's —
|
|
932
|
+
# both may now be claimed; see the `:story_15_2` / `:story_15_3` guards
|
|
933
|
+
# below that PIN each claim's presence. Story 15.4's public render
|
|
934
|
+
# helpers are now shipped too — see the `:story_15_4` guard below.)
|
|
935
|
+
#
|
|
936
|
+
# tsc does not run in a fresh install (no typescript devDep / tsconfig) —
|
|
937
|
+
# it may only appear conditionally phrased (D1).
|
|
938
|
+
expect(content).to match(/if your app has typescript tooling/i) if content.include?("tsc")
|
|
939
|
+
# Volatile strings must not be quoted: no gem version pins.
|
|
940
|
+
expect(content).not_to match(/\b0\.0\.\d+\b/)
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# Story 15.2 (D2) — now that the loud children error is a shipped artifact,
|
|
944
|
+
# trap #1 truthfully claims it fails LOUDLY (was "fails silently" in 15.1).
|
|
945
|
+
it "trap #1 claims the loud PreprocessorError (Story 15.2 D2)", :aggregate_failures, :story_15_2 do
|
|
946
|
+
silently { build_generator(app_root).create_agents_md }
|
|
947
|
+
content = File.read(agents_md_path)
|
|
948
|
+
|
|
949
|
+
expect(content).to include("PreprocessorError")
|
|
950
|
+
expect(content).to match(/fail LOUDLY/i)
|
|
951
|
+
expect(content).not_to match(/fail silently/i)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Story 15.3 (D2) — now that the `--json` introspection surface is a shipped
|
|
955
|
+
# artifact, the "Verify your work" section references it (relaxes the 15.1
|
|
956
|
+
# honesty guard above). This PINS the claim AND its EXPERIMENTAL framing so
|
|
957
|
+
# the template never presents the JSON shape as a stable contract.
|
|
958
|
+
it "references --json introspection as EXPERIMENTAL (15.3 D2)", :aggregate_failures, :story_15_3 do
|
|
959
|
+
silently { build_generator(app_root).create_agents_md }
|
|
960
|
+
content = File.read(agents_md_path)
|
|
961
|
+
|
|
962
|
+
expect(content).to include("ruact:routes -- --json")
|
|
963
|
+
expect(content).to include("ruact:doctor")
|
|
964
|
+
expect(content).to include("Append `-- --json`")
|
|
965
|
+
expect(content).to match(/EXPERIMENTAL/)
|
|
966
|
+
expect(content).to include("schema_version")
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# Story 15.4 (D6) — now that the public render-assertion helpers are a
|
|
970
|
+
# shipped artifact (FR108), the "Verify your work" section references them.
|
|
971
|
+
# This PINS the claim (name + explicit require + the JSON-vs-Flight rule)
|
|
972
|
+
# so it stays consistent with `website/public/llms.txt`.
|
|
973
|
+
it "references the ruact/testing render helpers (15.4 D6)", :aggregate_failures, :story_15_4 do
|
|
974
|
+
silently { build_generator(app_root).create_agents_md }
|
|
975
|
+
content = File.read(agents_md_path)
|
|
976
|
+
|
|
977
|
+
expect(content).to include(%(require "ruact/testing"))
|
|
978
|
+
expect(content).to include("have_ruact_component")
|
|
979
|
+
expect(content).to include("JSON.parse(response.body)")
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
describe "idempotency (AC#2 — double-run zero diff)" do
|
|
984
|
+
it "leaves the file byte-identical on a second run and reports a skip", :aggregate_failures do
|
|
985
|
+
silently { build_generator(app_root).create_agents_md }
|
|
986
|
+
first = File.read(agents_md_path)
|
|
987
|
+
|
|
988
|
+
output = capture_stdout { build_generator(app_root).create_agents_md }
|
|
989
|
+
|
|
990
|
+
expect(File.read(agents_md_path)).to eq(first)
|
|
991
|
+
expect(output).to include("skip")
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
describe "append mode (AC#2 — user-authored AGENTS.md)" do
|
|
996
|
+
let(:user_content) { "# My app\n\nUse pnpm here. Never touch config/secrets.yml.\n" }
|
|
997
|
+
|
|
998
|
+
it "appends the marked section preserving every pre-existing user byte", :aggregate_failures do
|
|
999
|
+
File.write(agents_md_path, user_content)
|
|
1000
|
+
|
|
1001
|
+
silently { build_generator(app_root).create_agents_md }
|
|
1002
|
+
content = File.read(agents_md_path)
|
|
1003
|
+
|
|
1004
|
+
expect(content).to start_with(user_content)
|
|
1005
|
+
expect(content.scan(begin_marker).count).to eq(1)
|
|
1006
|
+
expect(content.scan(end_marker).count).to eq(1)
|
|
1007
|
+
expect(content).to include("\n\n#{begin_marker}")
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
it "blank-line separates the section when the user file lacks a trailing newline" do
|
|
1011
|
+
File.write(agents_md_path, "# My app — no trailing newline")
|
|
1012
|
+
|
|
1013
|
+
silently { build_generator(app_root).create_agents_md }
|
|
1014
|
+
|
|
1015
|
+
expect(File.read(agents_md_path))
|
|
1016
|
+
.to include("# My app — no trailing newline\n\n#{begin_marker}")
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
it "appending then re-running is idempotent (skip, zero diff)" do
|
|
1020
|
+
File.write(agents_md_path, user_content)
|
|
1021
|
+
silently { build_generator(app_root).create_agents_md }
|
|
1022
|
+
appended = File.read(agents_md_path)
|
|
1023
|
+
|
|
1024
|
+
silently { build_generator(app_root).create_agents_md }
|
|
1025
|
+
|
|
1026
|
+
expect(File.read(agents_md_path)).to eq(appended)
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
describe "--force refresh (AC#2 — marked-block-only)" do
|
|
1031
|
+
it "replaces ONLY the between-marker content, never user bytes outside", :aggregate_failures do
|
|
1032
|
+
silently { build_generator(app_root).create_agents_md }
|
|
1033
|
+
section = File.read(agents_md_path)
|
|
1034
|
+
stale = section.sub("## Five traps", "## STALE CONTENT FROM AN OLDER GEM")
|
|
1035
|
+
File.write(agents_md_path, "# mine, above\n\n#{stale}\n# mine, below\n")
|
|
1036
|
+
|
|
1037
|
+
silently { build_generator(app_root, { force: true }).create_agents_md }
|
|
1038
|
+
content = File.read(agents_md_path)
|
|
1039
|
+
|
|
1040
|
+
expect(content).to start_with("# mine, above\n")
|
|
1041
|
+
expect(content).to end_with("# mine, below\n")
|
|
1042
|
+
expect(content).to include("## Five traps")
|
|
1043
|
+
expect(content).not_to include("STALE CONTENT FROM AN OLDER GEM")
|
|
1044
|
+
expect(content.scan(begin_marker).count).to eq(1)
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
it "creates the file under --force when none exists" do
|
|
1048
|
+
silently { build_generator(app_root, { force: true }).create_agents_md }
|
|
1049
|
+
expect(File.read(agents_md_path)).to include(begin_marker)
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# Codex R1 — an incomplete marker state is ambiguous in BOTH directions
|
|
1054
|
+
# (appending would duplicate content next to a stray marker; replacing
|
|
1055
|
+
# would guess the range), so the only byte-safe move is warn + no-op.
|
|
1056
|
+
describe "broken marker pair (byte-safety)" do
|
|
1057
|
+
[
|
|
1058
|
+
["a begin marker without its end marker",
|
|
1059
|
+
"# mine\n\n<!-- ruact:begin -->\ntruncated ruact section, no end marker\n"],
|
|
1060
|
+
["an end marker without its begin marker",
|
|
1061
|
+
"# mine\n\nstray tail of a ruact section\n<!-- ruact:end -->\n"],
|
|
1062
|
+
["an end marker preceding the begin marker",
|
|
1063
|
+
"# mine\n\n<!-- ruact:end -->\nreversed\n<!-- ruact:begin -->\n"],
|
|
1064
|
+
# Codex R2 — a VALID pair plus stray extra markers is still ambiguous:
|
|
1065
|
+
# skip/refresh would leave (or eat past) the unmatched marker.
|
|
1066
|
+
["a stray end marker before a valid pair",
|
|
1067
|
+
"<!-- ruact:end -->\nstray\n\n<!-- ruact:begin -->\nsection\n<!-- ruact:end -->\n"],
|
|
1068
|
+
["a stray begin marker after a valid pair",
|
|
1069
|
+
"<!-- ruact:begin -->\nsection\n<!-- ruact:end -->\n\nstray\n<!-- ruact:begin -->\n"],
|
|
1070
|
+
["two complete marker pairs",
|
|
1071
|
+
"<!-- ruact:begin -->\none\n<!-- ruact:end -->\n\n<!-- ruact:begin -->\ntwo\n<!-- ruact:end -->\n"]
|
|
1072
|
+
].each do |(label, broken)|
|
|
1073
|
+
it "warns and leaves the file untouched with #{label} (no --force)", :aggregate_failures do
|
|
1074
|
+
File.write(agents_md_path, broken)
|
|
1075
|
+
|
|
1076
|
+
output = capture_stdout { build_generator(app_root).create_agents_md }
|
|
1077
|
+
|
|
1078
|
+
expect(File.read(agents_md_path)).to eq(broken)
|
|
1079
|
+
expect(output).to include("warn")
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
it "warns and leaves the file untouched with #{label} (--force)", :aggregate_failures do
|
|
1083
|
+
File.write(agents_md_path, broken)
|
|
1084
|
+
|
|
1085
|
+
output = capture_stdout { build_generator(app_root, { force: true }).create_agents_md }
|
|
1086
|
+
|
|
1087
|
+
expect(File.read(agents_md_path)).to eq(broken)
|
|
1088
|
+
expect(output).to include("warn")
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
describe "action ordering + template budget" do
|
|
1094
|
+
it "defines create_agents_md BEFORE install_javascript_dependencies" do
|
|
1095
|
+
klass = Ruact::Generators::InstallGenerator
|
|
1096
|
+
line = ->(name) { klass.instance_method(name).source_location.last }
|
|
1097
|
+
expect(line.call(:create_agents_md)).to be < line.call(:install_javascript_dependencies)
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# AC#3 tripwire — the ~150-line budget is the epic's scope probe; the
|
|
1101
|
+
# tripwire's tolerance is 160 (the tripwire's, not the product contract's).
|
|
1102
|
+
it "keeps the template body at or under 160 lines" do
|
|
1103
|
+
expect(File.read(template_path).lines.count).to be <= 160
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
# The `--shadcn` prerequisites. The gap these close is concrete: shadcn's CLI
|
|
1109
|
+
# aborts on a ruact app with "No Tailwind CSS configuration found" and "Could
|
|
1110
|
+
# not find valid path aliases", because a ruact app ships neither Tailwind nor
|
|
1111
|
+
# a tsconfig.json. Without them `ruact:scaffold --shadcn` emitted components
|
|
1112
|
+
# whose classes resolved to nothing — styled markup with no styles.
|
|
1113
|
+
describe "install generator — --shadcn prerequisites" do
|
|
1114
|
+
require "stringio"
|
|
1115
|
+
require "generators/ruact/install/install_generator"
|
|
1116
|
+
|
|
1117
|
+
let(:app_root) { Dir.mktmpdir("ruact_install_shadcn") }
|
|
1118
|
+
|
|
1119
|
+
after { FileUtils.rm_rf(app_root) }
|
|
1120
|
+
|
|
1121
|
+
def build_generator(root, opts = {})
|
|
1122
|
+
Ruact::Generators::InstallGenerator.new([], opts, destination_root: root)
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def silently
|
|
1126
|
+
original = $stdout
|
|
1127
|
+
$stdout = StringIO.new
|
|
1128
|
+
yield
|
|
1129
|
+
ensure
|
|
1130
|
+
$stdout = original
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
def read(relative)
|
|
1134
|
+
File.read(File.join(app_root, relative))
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
describe "with --shadcn", :aggregate_failures do
|
|
1138
|
+
let(:generator) { build_generator(app_root, shadcn: true) }
|
|
1139
|
+
|
|
1140
|
+
it "writes the Tailwind entry shadcn points components.json at" do
|
|
1141
|
+
silently { generator.create_shadcn_prerequisites }
|
|
1142
|
+
|
|
1143
|
+
expect(File).to exist(File.join(app_root, "app/javascript/styles/globals.css"))
|
|
1144
|
+
expect(read("app/javascript/styles/globals.css")).to include('@import "tailwindcss"')
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
it "writes the tsconfig import alias shadcn probes for" do
|
|
1148
|
+
silently { generator.create_shadcn_prerequisites }
|
|
1149
|
+
|
|
1150
|
+
tsconfig = JSON.parse(read("tsconfig.json"))
|
|
1151
|
+
expect(tsconfig.dig("compilerOptions", "paths", "@/*")).to eq(["app/javascript/*"])
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
it "creates the builds directory Propshaft serves the compiled stylesheet from" do
|
|
1155
|
+
silently { generator.create_shadcn_prerequisites }
|
|
1156
|
+
|
|
1157
|
+
expect(File).to exist(File.join(app_root, "app/assets/builds/.keep"))
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
it "declares Tailwind in package.json with a build:css script" do
|
|
1161
|
+
silently { generator.create_package_json }
|
|
1162
|
+
|
|
1163
|
+
pkg = JSON.parse(read("package.json"))
|
|
1164
|
+
expect(pkg.dig("devDependencies", "tailwindcss")).to be_a(String)
|
|
1165
|
+
expect(pkg.dig("devDependencies", "@tailwindcss/cli")).to be_a(String)
|
|
1166
|
+
expect(pkg.dig("devDependencies", "tw-animate-css")).to be_a(String)
|
|
1167
|
+
expect(pkg.dig("scripts", "build:css")).to include("app/assets/builds/tailwind.css")
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
it "adds a css watch process so bin/dev rebuilds the stylesheet" do
|
|
1171
|
+
silently { generator.create_launch_files }
|
|
1172
|
+
|
|
1173
|
+
expect(read("Procfile.dev")).to match(/^css: .*tailwind\.css --watch$/)
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
it "gitignores the compiled stylesheet (a build artifact of globals.css)" do
|
|
1177
|
+
File.write(File.join(app_root, ".gitignore"), "/log/*\n")
|
|
1178
|
+
silently { generator.append_gitignore_entries }
|
|
1179
|
+
|
|
1180
|
+
expect(read(".gitignore")).to include("app/assets/builds/tailwind.css")
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
# The one thing a reader cannot guess: current shadcn defaults to Base UI,
|
|
1184
|
+
# but the components the scaffold generates import Radix primitives.
|
|
1185
|
+
it "prints the two shadcn commands, pinning --base radix" do
|
|
1186
|
+
expect { generator.send(:show_shadcn_next_steps) }
|
|
1187
|
+
.to output(/npx shadcn@latest init --base radix/).to_stdout
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
it "prints the complete add-list, so the scaffold pre-flight cannot fail on a gap" do
|
|
1191
|
+
expect { generator.send(:show_shadcn_next_steps) }
|
|
1192
|
+
.to output(/add button input textarea switch select label badge table alert-dialog dropdown-menu/)
|
|
1193
|
+
.to_stdout
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
# Codex review finding: the layout-migration branch was pinned only by the
|
|
1198
|
+
# REPRODUCTION helper above, which cannot see the real generator's anchor
|
|
1199
|
+
# regex. Invoking the actual generator caught that a layout written with
|
|
1200
|
+
# single quotes matched nothing while the generator still printed a success
|
|
1201
|
+
# message. These specs drive `InstallGenerator#inject_layout_shell` itself.
|
|
1202
|
+
# Thor registers every PUBLIC instance method of a generator as a command and
|
|
1203
|
+
# runs it in declaration order. A helper left public therefore executes on
|
|
1204
|
+
# its own, out of context — this has bitten three times in this branch: a
|
|
1205
|
+
# message printed twice, a `--shadcn`-only notice firing on the default
|
|
1206
|
+
# path, and a helper taking a required argument being invoked with none.
|
|
1207
|
+
# Pin the command list so the next one is caught here rather than in an app.
|
|
1208
|
+
describe "Thor command surface" do
|
|
1209
|
+
it "registers only the install steps, never the private helpers" do
|
|
1210
|
+
expect(Ruact::Generators::InstallGenerator.commands.keys).to contain_exactly(
|
|
1211
|
+
"create_initializer",
|
|
1212
|
+
"inject_controller_concern",
|
|
1213
|
+
"inject_layout_shell",
|
|
1214
|
+
"create_shadcn_prerequisites",
|
|
1215
|
+
"create_components_directory",
|
|
1216
|
+
"create_server_functions_directory",
|
|
1217
|
+
"append_gitignore_entries",
|
|
1218
|
+
"prime_server_functions_codegen",
|
|
1219
|
+
"create_vite_config",
|
|
1220
|
+
"create_package_json",
|
|
1221
|
+
"create_launch_files",
|
|
1222
|
+
"advise_plumbing_migration",
|
|
1223
|
+
"create_agents_md",
|
|
1224
|
+
"install_javascript_dependencies",
|
|
1225
|
+
"show_post_install_message"
|
|
1226
|
+
)
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
it "has no command requiring arguments (Thor invokes them with none)" do
|
|
1230
|
+
requiring_args = Ruact::Generators::InstallGenerator.commands.keys.reject do |name|
|
|
1231
|
+
Ruact::Generators::InstallGenerator.instance_method(name).arity.zero?
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
expect(requiring_args).to be_empty
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
# Migrating an existing app is the path that matters most here, and Thor's
|
|
1239
|
+
# `template` handles it badly: it raises an overwrite CONFLICT, offering a
|
|
1240
|
+
# choice between losing every setting the app already had and ending up
|
|
1241
|
+
# half-migrated (layout edited, `config.layout` still off) in silence.
|
|
1242
|
+
describe "create_initializer on an app that already has one", :aggregate_failures do
|
|
1243
|
+
def write_initializer(body)
|
|
1244
|
+
path = File.join(app_root, "config/initializers/ruact.rb")
|
|
1245
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
1246
|
+
File.write(path, body)
|
|
1247
|
+
path
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
it "adds config.layout without touching the app's existing settings" do
|
|
1251
|
+
path = write_initializer(<<~RUBY)
|
|
1252
|
+
Ruact.configure do |config|
|
|
1253
|
+
config.strict_serialization = true
|
|
1254
|
+
config.max_upload_bytes = 25 * 1024 * 1024
|
|
1255
|
+
end
|
|
1256
|
+
RUBY
|
|
1257
|
+
|
|
1258
|
+
silently { build_generator(app_root).create_initializer }
|
|
1259
|
+
|
|
1260
|
+
result = File.read(path)
|
|
1261
|
+
expect(result).to include("config.layout = true")
|
|
1262
|
+
expect(result).to include("config.strict_serialization = true")
|
|
1263
|
+
expect(result).to include("config.max_upload_bytes = 25 * 1024 * 1024")
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
# `config.layout = false` is a deliberate choice (keep the built-in
|
|
1267
|
+
# shell). Re-running install must not quietly reverse it.
|
|
1268
|
+
it "leaves an explicit opt-out alone" do
|
|
1269
|
+
path = write_initializer("Ruact.configure do |config|\n config.layout = false\nend\n")
|
|
1270
|
+
|
|
1271
|
+
silently { build_generator(app_root).create_initializer }
|
|
1272
|
+
|
|
1273
|
+
expect(File.read(path)).to include("config.layout = false")
|
|
1274
|
+
expect(File.read(path)).not_to include("config.layout = true")
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
it "is idempotent" do
|
|
1278
|
+
path = write_initializer("Ruact.configure do |config|\nend\n")
|
|
1279
|
+
|
|
1280
|
+
silently { build_generator(app_root).create_initializer }
|
|
1281
|
+
silently { build_generator(app_root).create_initializer }
|
|
1282
|
+
|
|
1283
|
+
expect(File.read(path).scan("config.layout").size).to eq(1)
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
# Never guess at an initializer we do not recognise — say what to add,
|
|
1287
|
+
# because the silent outcome is an app whose CSS never reaches the page.
|
|
1288
|
+
it "tells the user what to add when it cannot find the configure block" do
|
|
1289
|
+
write_initializer("# hand-rolled\nRuact.config\n")
|
|
1290
|
+
|
|
1291
|
+
output = capture_generator_output { build_generator(app_root).create_initializer }
|
|
1292
|
+
|
|
1293
|
+
expect(output).to include("could not find the Ruact.configure block")
|
|
1294
|
+
expect(output).to include("config.layout = true")
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
def capture_generator_output
|
|
1298
|
+
original = $stdout
|
|
1299
|
+
$stdout = StringIO.new
|
|
1300
|
+
yield
|
|
1301
|
+
$stdout.string
|
|
1302
|
+
ensure
|
|
1303
|
+
$stdout = original
|
|
1304
|
+
end
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
describe "inject_layout_shell — the REAL generator", :aggregate_failures do
|
|
1308
|
+
def write_layout(body)
|
|
1309
|
+
path = File.join(app_root, "app/views/layouts/application.html.erb")
|
|
1310
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
1311
|
+
File.write(path, body)
|
|
1312
|
+
path
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
def layout_after_run(body)
|
|
1316
|
+
path = write_layout(body)
|
|
1317
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1318
|
+
File.read(path)
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
# The fresh-install path. Previously covered only by a REPRODUCTION of the
|
|
1322
|
+
# generator's logic, which twice drifted from the generator itself and hid
|
|
1323
|
+
# real defects; the reproduction is gone and these drive the real thing.
|
|
1324
|
+
it "adds both the root div and the asset call to a layout that has neither" do
|
|
1325
|
+
path = write_layout(<<~HTML)
|
|
1326
|
+
<html>
|
|
1327
|
+
<body>
|
|
1328
|
+
<%= yield %>
|
|
1329
|
+
</body>
|
|
1330
|
+
</html>
|
|
1331
|
+
HTML
|
|
1332
|
+
|
|
1333
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1334
|
+
|
|
1335
|
+
result = File.read(path)
|
|
1336
|
+
expect(result).to include(%(<div id="root"></div>))
|
|
1337
|
+
expect(result).to include("<%= ruact_js_assets %>")
|
|
1338
|
+
expect(result).to include("ruact: root")
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
it "places them inside the body, before the closing tag" do
|
|
1342
|
+
path = write_layout("<html>\n <body>\n <%= yield %>\n </body>\n</html>\n")
|
|
1343
|
+
|
|
1344
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1345
|
+
|
|
1346
|
+
result = File.read(path)
|
|
1347
|
+
expect(result.index(%(<div id="root">))).to be < result.index("</body>")
|
|
1348
|
+
expect(result.index(%(<div id="root">))).to be < result.index("ruact_js_assets")
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
it "is a no-op on a layout that is already migrated" do
|
|
1352
|
+
path = write_layout(<<~HTML)
|
|
1353
|
+
<html>
|
|
1354
|
+
<body>
|
|
1355
|
+
<%# ruact: root %>
|
|
1356
|
+
<div id="root"></div>
|
|
1357
|
+
<%= ruact_js_assets %>
|
|
1358
|
+
</body>
|
|
1359
|
+
</html>
|
|
1360
|
+
HTML
|
|
1361
|
+
before = File.read(path)
|
|
1362
|
+
|
|
1363
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1364
|
+
|
|
1365
|
+
expect(File.read(path)).to eq(before)
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
it "migrates a layout whose root div uses single quotes" do
|
|
1369
|
+
result = layout_after_run(<<~HTML)
|
|
1370
|
+
<html>
|
|
1371
|
+
<body>
|
|
1372
|
+
<%# ruact: root %>
|
|
1373
|
+
<div id='root'></div>
|
|
1374
|
+
</body>
|
|
1375
|
+
</html>
|
|
1376
|
+
HTML
|
|
1377
|
+
|
|
1378
|
+
expect(result).to include("<%= ruact_js_assets %>")
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
it "migrates a layout whose root div carries extra attributes" do
|
|
1382
|
+
result = layout_after_run(<<~HTML)
|
|
1383
|
+
<html>
|
|
1384
|
+
<body>
|
|
1385
|
+
<%# ruact: root %>
|
|
1386
|
+
<div class="app" id="root" data-turbo="false"></div>
|
|
1387
|
+
</body>
|
|
1388
|
+
</html>
|
|
1389
|
+
HTML
|
|
1390
|
+
|
|
1391
|
+
expect(result).to include("<%= ruact_js_assets %>")
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
it "migrates a layout with the marker and the div on one line" do
|
|
1395
|
+
result = layout_after_run(
|
|
1396
|
+
%(<html><body><%# ruact: root %><div id="root"></div></body></html>\n)
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
expect(result).to include("<%= ruact_js_assets %>")
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
it "is idempotent — a second run adds nothing" do
|
|
1403
|
+
path = write_layout(<<~HTML)
|
|
1404
|
+
<html>
|
|
1405
|
+
<body>
|
|
1406
|
+
<%# ruact: root %>
|
|
1407
|
+
<div id="root"></div>
|
|
1408
|
+
</body>
|
|
1409
|
+
</html>
|
|
1410
|
+
HTML
|
|
1411
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1412
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1413
|
+
|
|
1414
|
+
expect(File.read(path).scan("ruact_js_assets").size).to eq(1)
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
# Round-2 finding: the anchor matched any attribute ENDING in `id`, so a
|
|
1418
|
+
# layout with `data-id="root"` and no real mount point got the helper
|
|
1419
|
+
# injected and a success message. It now reads as "no root present" and
|
|
1420
|
+
# the generator writes a proper root + helper instead of warning — the
|
|
1421
|
+
# look-alike div is left alone, being none of its business.
|
|
1422
|
+
it "does not mistake a look-alike attribute for the React root" do
|
|
1423
|
+
path = write_layout(<<~HTML)
|
|
1424
|
+
<html>
|
|
1425
|
+
<body>
|
|
1426
|
+
<%# ruact: root %>
|
|
1427
|
+
<div data-id="root"></div>
|
|
1428
|
+
</body>
|
|
1429
|
+
</html>
|
|
1430
|
+
HTML
|
|
1431
|
+
|
|
1432
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1433
|
+
|
|
1434
|
+
result = File.read(path)
|
|
1435
|
+
expect(result).to match(%r{<div id="root"></div>})
|
|
1436
|
+
expect(result).to include("<%= ruact_js_assets %>")
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
# The mirror of the legacy migration: the call is there, the mount target
|
|
1440
|
+
# is not. Injecting the whole block would duplicate the helper.
|
|
1441
|
+
it "adds only the missing root when the layout already calls the helper" do
|
|
1442
|
+
path = write_layout(<<~HTML)
|
|
1443
|
+
<html>
|
|
1444
|
+
<body>
|
|
1445
|
+
<%= yield %>
|
|
1446
|
+
<%= ruact_js_assets %>
|
|
1447
|
+
</body>
|
|
1448
|
+
</html>
|
|
1449
|
+
HTML
|
|
1450
|
+
|
|
1451
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1452
|
+
|
|
1453
|
+
result = File.read(path)
|
|
1454
|
+
expect(result).to match(%r{<div id="root"></div>})
|
|
1455
|
+
expect(result.scan("ruact_js_assets").size).to eq(1)
|
|
1456
|
+
expect(result.index(%(<div id="root">))).to be < result.index("ruact_js_assets")
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1459
|
+
# A root that is NOT an empty paired div (a spinner placeholder, say) has
|
|
1460
|
+
# nothing safe to inject after, so the generator says so instead of
|
|
1461
|
+
# guessing where the call belongs.
|
|
1462
|
+
it "warns when the root div is not the empty form it knows how to anchor on" do
|
|
1463
|
+
write_layout(<<~HTML)
|
|
1464
|
+
<html>
|
|
1465
|
+
<body>
|
|
1466
|
+
<%# ruact: root %>
|
|
1467
|
+
<div id="root">loading…</div>
|
|
1468
|
+
</body>
|
|
1469
|
+
</html>
|
|
1470
|
+
HTML
|
|
1471
|
+
|
|
1472
|
+
output = capture_generator_output { build_generator(app_root).inject_layout_shell }
|
|
1473
|
+
|
|
1474
|
+
expect(output).to include("could not locate the React root div")
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
# Round-2 finding: the "already migrated, skip" guard was a substring
|
|
1478
|
+
# match, so a TODO note about the helper made the generator skip an app
|
|
1479
|
+
# that was never migrated.
|
|
1480
|
+
it "does not treat a TODO comment naming the helper as already migrated" do
|
|
1481
|
+
path = write_layout(<<~HTML)
|
|
1482
|
+
<html>
|
|
1483
|
+
<body>
|
|
1484
|
+
<%# TODO: add ruact_js_assets %>
|
|
1485
|
+
<%# ruact: root %>
|
|
1486
|
+
<div id="root"></div>
|
|
1487
|
+
</body>
|
|
1488
|
+
</html>
|
|
1489
|
+
HTML
|
|
1490
|
+
|
|
1491
|
+
silently { build_generator(app_root).inject_layout_shell }
|
|
1492
|
+
|
|
1493
|
+
expect(File.read(path)).to include("<%= ruact_js_assets %>")
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
# Silence here is the dangerous outcome: the app keeps rendering through
|
|
1497
|
+
# ruact's CSS-less shell and nothing ever says why.
|
|
1498
|
+
it "says so LOUDLY when it cannot find the root div, instead of claiming success" do
|
|
1499
|
+
write_layout(<<~HTML)
|
|
1500
|
+
<html>
|
|
1501
|
+
<body>
|
|
1502
|
+
<%# ruact: root %>
|
|
1503
|
+
<span id="root"></span>
|
|
1504
|
+
</body>
|
|
1505
|
+
</html>
|
|
1506
|
+
HTML
|
|
1507
|
+
|
|
1508
|
+
output = capture_generator_output { build_generator(app_root).inject_layout_shell }
|
|
1509
|
+
|
|
1510
|
+
expect(output).to include("could not locate the React root div")
|
|
1511
|
+
expect(output).not_to include("added ruact_js_assets")
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def capture_generator_output
|
|
1515
|
+
original = $stdout
|
|
1516
|
+
$stdout = StringIO.new
|
|
1517
|
+
yield
|
|
1518
|
+
$stdout.string
|
|
1519
|
+
ensure
|
|
1520
|
+
$stdout = original
|
|
1521
|
+
end
|
|
1522
|
+
end
|
|
1523
|
+
|
|
1524
|
+
describe "without --shadcn (the default path is untouched)", :aggregate_failures do
|
|
1525
|
+
let(:generator) { build_generator(app_root) }
|
|
1526
|
+
|
|
1527
|
+
it "writes no Tailwind entry and no tsconfig" do
|
|
1528
|
+
silently { generator.create_shadcn_prerequisites }
|
|
1529
|
+
|
|
1530
|
+
expect(File).not_to exist(File.join(app_root, "app/javascript/styles/globals.css"))
|
|
1531
|
+
expect(File).not_to exist(File.join(app_root, "tsconfig.json"))
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
it "leaves package.json free of Tailwind" do
|
|
1535
|
+
silently { generator.create_package_json }
|
|
1536
|
+
|
|
1537
|
+
pkg = JSON.parse(read("package.json"))
|
|
1538
|
+
expect(pkg["devDependencies"].keys).not_to include("tailwindcss", "@tailwindcss/cli")
|
|
1539
|
+
expect(pkg["scripts"]).not_to have_key("build:css")
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1542
|
+
it "leaves Procfile.dev at the two processes it always had" do
|
|
1543
|
+
silently { generator.create_launch_files }
|
|
1544
|
+
|
|
1545
|
+
expect(read("Procfile.dev")).not_to include("css:")
|
|
1546
|
+
end
|
|
1547
|
+
end
|
|
1548
|
+
end
|
|
892
1549
|
end
|