archipelago-rails 0.1.0
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 +7 -0
- data/Appraisals +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +17 -0
- data/app/controllers/archipelago/application_controller.rb +10 -0
- data/app/controllers/archipelago/islands_controller.rb +158 -0
- data/config/database.yml +5 -0
- data/config/routes.rb +6 -0
- data/lib/archipelago/action.rb +121 -0
- data/lib/archipelago/broadcasts.rb +17 -0
- data/lib/archipelago/channel.rb +36 -0
- data/lib/archipelago/configuration.rb +23 -0
- data/lib/archipelago/context.rb +14 -0
- data/lib/archipelago/engine.rb +21 -0
- data/lib/archipelago/params_dsl.rb +143 -0
- data/lib/archipelago/registry.rb +25 -0
- data/lib/archipelago/resolver.rb +54 -0
- data/lib/archipelago/response.rb +35 -0
- data/lib/archipelago/security/origin_validator.rb +32 -0
- data/lib/archipelago/security/redirect_validator.rb +30 -0
- data/lib/archipelago/test_helpers.rb +31 -0
- data/lib/archipelago/view_helper.rb +31 -0
- data/lib/archipelago-rails.rb +3 -0
- data/lib/archipelago.rb +71 -0
- data/lib/generators/archipelago/install/install_generator.rb +43 -0
- data/lib/generators/archipelago/install/react/react_generator.rb +380 -0
- data/lib/generators/archipelago/install/react/templates/entry.js.tt +13 -0
- data/lib/generators/archipelago/install/react/templates/generate_registry.mjs.tt +96 -0
- data/lib/generators/archipelago/install/react_generator.rb +3 -0
- data/lib/generators/archipelago/install_generator.rb +3 -0
- data/lib/generators/archipelago/island/island_generator.rb +44 -0
- data/lib/generators/archipelago/island/templates/action.rb.tt +11 -0
- data/lib/generators/archipelago/island/templates/component.tsx.tt +14 -0
- data/lib/generators/archipelago/island_generator.rb +3 -0
- data/test/archipelago/action_test.rb +136 -0
- data/test/archipelago/broadcasts_test.rb +29 -0
- data/test/archipelago/channel_test.rb +15 -0
- data/test/archipelago/notifications_test.rb +60 -0
- data/test/archipelago/origin_validator_test.rb +36 -0
- data/test/archipelago/params_dsl_test.rb +51 -0
- data/test/archipelago/redirect_validator_test.rb +28 -0
- data/test/archipelago/resolver_test.rb +80 -0
- data/test/archipelago/response_test.rb +30 -0
- data/test/archipelago/view_helper_test.rb +32 -0
- data/test/controllers/islands_controller_test.rb +115 -0
- data/test/generators/install_generator_test.rb +26 -0
- data/test/generators/island_generator_test.rb +20 -0
- data/test/generators/react_install_generator_test.rb +67 -0
- data/test/test_helper.rb +35 -0
- metadata +180 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Archipelago
|
|
8
|
+
module Generators
|
|
9
|
+
module Install
|
|
10
|
+
class ReactGenerator < Rails::Generators::Base
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
class_option :bundler,
|
|
14
|
+
type: :string,
|
|
15
|
+
default: "auto",
|
|
16
|
+
enum: %w[auto esbuild vite],
|
|
17
|
+
desc: "JavaScript bundler to target"
|
|
18
|
+
|
|
19
|
+
class_option :typescript,
|
|
20
|
+
type: :string,
|
|
21
|
+
default: "auto",
|
|
22
|
+
enum: %w[auto true false],
|
|
23
|
+
desc: "Generate TSX or JSX bootstrap file"
|
|
24
|
+
|
|
25
|
+
class_option :install,
|
|
26
|
+
type: :boolean,
|
|
27
|
+
default: false,
|
|
28
|
+
desc: "Install npm packages after generating files"
|
|
29
|
+
|
|
30
|
+
class_option :package_manager,
|
|
31
|
+
type: :string,
|
|
32
|
+
default: "auto",
|
|
33
|
+
enum: %w[auto yarn npm pnpm bun],
|
|
34
|
+
desc: "Package manager used when --install is enabled"
|
|
35
|
+
|
|
36
|
+
class_option :local_monorepo_path,
|
|
37
|
+
type: :string,
|
|
38
|
+
default: nil,
|
|
39
|
+
desc: "Path to local Archipelago monorepo for file: package installs"
|
|
40
|
+
|
|
41
|
+
class_option :interactive,
|
|
42
|
+
type: :boolean,
|
|
43
|
+
default: true,
|
|
44
|
+
desc: "Prompt for setup choices with auto-detected defaults"
|
|
45
|
+
|
|
46
|
+
class_option :auto_registry,
|
|
47
|
+
type: :boolean,
|
|
48
|
+
default: true,
|
|
49
|
+
desc: "For esbuild, auto-generate component registry from app/javascript/islands"
|
|
50
|
+
|
|
51
|
+
desc "Sets up React + Archipelago frontend bootstrapping for a Rails app."
|
|
52
|
+
|
|
53
|
+
def detect_stack
|
|
54
|
+
@detected_bundler = detect_bundler
|
|
55
|
+
@detected_typescript = detect_typescript?
|
|
56
|
+
@detected_package_manager = detect_package_manager
|
|
57
|
+
@detected_local_monorepo_path = detect_local_monorepo_path
|
|
58
|
+
|
|
59
|
+
@bundler = options[:bundler] == "auto" ? @detected_bundler : options[:bundler].to_sym
|
|
60
|
+
@use_typescript = case options[:typescript]
|
|
61
|
+
when "true"
|
|
62
|
+
true
|
|
63
|
+
when "false"
|
|
64
|
+
false
|
|
65
|
+
else
|
|
66
|
+
@detected_typescript
|
|
67
|
+
end
|
|
68
|
+
@should_install = options[:install]
|
|
69
|
+
@package_manager = options[:package_manager] == "auto" ? @detected_package_manager : options[:package_manager]
|
|
70
|
+
@local_monorepo_path = options[:local_monorepo_path] || @detected_local_monorepo_path
|
|
71
|
+
@auto_registry = options[:auto_registry]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def interactive_preferences
|
|
75
|
+
unless interactive_mode?
|
|
76
|
+
if options[:interactive]
|
|
77
|
+
say_status :info, "Interactive prompts skipped (non-TTY). Using detected/default options.", :yellow
|
|
78
|
+
end
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
say_status :info, "Interactive Archipelago React setup", :blue
|
|
83
|
+
|
|
84
|
+
bundler_default = @bundler == :unknown ? "esbuild" : @bundler.to_s
|
|
85
|
+
@bundler = ask_choice("Bundler", %w[esbuild vite], default: bundler_default).to_sym
|
|
86
|
+
@use_typescript = ask_yes_no("Use TypeScript for island entry files?", default: @use_typescript)
|
|
87
|
+
if @bundler == :esbuild
|
|
88
|
+
@auto_registry = ask_yes_no(
|
|
89
|
+
"Enable esbuild auto-registry for islands (no manual component map)?",
|
|
90
|
+
default: @auto_registry
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
@auto_registry = false
|
|
94
|
+
end
|
|
95
|
+
@should_install = ask_yes_no("Install frontend npm packages now?", default: @should_install)
|
|
96
|
+
|
|
97
|
+
if @should_install
|
|
98
|
+
@package_manager = ask_choice(
|
|
99
|
+
"Package manager",
|
|
100
|
+
%w[yarn npm pnpm bun],
|
|
101
|
+
default: @package_manager
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@local_monorepo_path = prompt_for_local_monorepo_path(@local_monorepo_path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ensure_supported_stack
|
|
109
|
+
if @bundler == :vite
|
|
110
|
+
say_status :info, "Vite detected. Generating React entry; wire it into your Vite entrypoints.", :blue
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return if @bundler == :esbuild
|
|
115
|
+
|
|
116
|
+
raise Thor::Error, "Could not detect JS bundler. Pass --bundler=esbuild or --bundler=vite."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def create_archipelago_entry
|
|
120
|
+
extension = @use_typescript ? "tsx" : "jsx"
|
|
121
|
+
@entry_relative_path = "app/javascript/archipelago/entry.#{extension}"
|
|
122
|
+
template "entry.js.tt", @entry_relative_path
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def setup_esbuild_auto_registry
|
|
126
|
+
return unless @bundler == :esbuild
|
|
127
|
+
return unless @auto_registry
|
|
128
|
+
|
|
129
|
+
template "generate_registry.mjs.tt", "app/javascript/archipelago/generate_registry.mjs"
|
|
130
|
+
create_file registry_relative_path, initial_registry_source
|
|
131
|
+
wire_esbuild_package_scripts
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def wire_esbuild_entry
|
|
135
|
+
return unless @bundler == :esbuild
|
|
136
|
+
|
|
137
|
+
app_entry = preferred_application_entry
|
|
138
|
+
if app_entry.nil?
|
|
139
|
+
say_status :info, "No app/javascript/application.(js|ts) file found. Import manually:", :yellow
|
|
140
|
+
say " import \"./archipelago/entry\""
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
import_line = 'import "./archipelago/entry"'
|
|
145
|
+
append_to_file app_entry, "\n#{import_line}\n" unless File.read(path_for(app_entry)).include?(import_line)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def install_packages
|
|
149
|
+
return unless @should_install
|
|
150
|
+
return unless path_exists?("package.json")
|
|
151
|
+
|
|
152
|
+
if @local_monorepo_path
|
|
153
|
+
run "#{resolved_package_manager} add react react-dom #{archipelago_client_package}"
|
|
154
|
+
run "#{resolved_package_manager} add #{archipelago_react_package}"
|
|
155
|
+
else
|
|
156
|
+
run "#{resolved_package_manager} add #{packages_for_install.join(' ')}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def print_next_steps
|
|
161
|
+
say ""
|
|
162
|
+
say "Archipelago React frontend scaffolding created:"
|
|
163
|
+
say " #{@entry_relative_path}"
|
|
164
|
+
say ""
|
|
165
|
+
unless @should_install
|
|
166
|
+
say "Install packages:"
|
|
167
|
+
say " #{resolved_package_manager} add #{packages_for_install.join(' ')}"
|
|
168
|
+
end
|
|
169
|
+
say ""
|
|
170
|
+
if @bundler == :esbuild && @auto_registry
|
|
171
|
+
say "Islands in app/javascript/islands/**/* are auto-registered before esbuild runs."
|
|
172
|
+
say "Manual refresh command: #{script_run_command('archipelago:registry')}"
|
|
173
|
+
else
|
|
174
|
+
say "Register each island component in app/javascript/archipelago/entry.* under `registry`."
|
|
175
|
+
end
|
|
176
|
+
say "If streaming is needed, assign an ActionCable consumer to `window.Archipelago.cable`."
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def detect_bundler
|
|
182
|
+
return :vite if gemfile_mentions?("vite_rails")
|
|
183
|
+
return :vite if path_exists?("vite.config.ts") || path_exists?("vite.config.js")
|
|
184
|
+
return :esbuild if gemfile_mentions?("jsbundling-rails")
|
|
185
|
+
return :esbuild if path_exists?("app/javascript/application.js") || path_exists?("app/javascript/application.ts")
|
|
186
|
+
|
|
187
|
+
:unknown
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def detect_typescript?
|
|
191
|
+
return true if path_exists?("tsconfig.json")
|
|
192
|
+
return true if path_exists?("app/javascript/application.ts")
|
|
193
|
+
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def detect_package_manager
|
|
198
|
+
return "yarn" if path_exists?("yarn.lock")
|
|
199
|
+
return "pnpm" if path_exists?("pnpm-lock.yaml")
|
|
200
|
+
return "bun" if path_exists?("bun.lockb")
|
|
201
|
+
return "npm" if path_exists?("package-lock.json")
|
|
202
|
+
|
|
203
|
+
"yarn"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def detect_local_monorepo_path
|
|
207
|
+
candidates = []
|
|
208
|
+
|
|
209
|
+
env_path = ENV["ARCHIPELAGO_MONOREPO_PATH"]
|
|
210
|
+
candidates << Pathname.new(env_path) if env_path && !env_path.strip.empty?
|
|
211
|
+
candidates << Pathname.new(destination_root).join("..", "cdx")
|
|
212
|
+
candidates << Pathname.new(destination_root).join("..", "archipelago", "cdx")
|
|
213
|
+
candidates << Pathname.new(__dir__).join("../../../../../../")
|
|
214
|
+
|
|
215
|
+
candidates.map(&:expand_path).uniq.find do |candidate|
|
|
216
|
+
local_packages_available?(candidate)
|
|
217
|
+
end&.to_s
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def preferred_application_entry
|
|
221
|
+
return "app/javascript/application.ts" if path_exists?("app/javascript/application.ts")
|
|
222
|
+
return "app/javascript/application.js" if path_exists?("app/javascript/application.js")
|
|
223
|
+
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def gemfile_mentions?(name)
|
|
228
|
+
return false unless path_exists?("Gemfile")
|
|
229
|
+
|
|
230
|
+
File.read(path_for("Gemfile")).include?(name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def resolved_package_manager
|
|
234
|
+
@package_manager
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def packages_for_install
|
|
238
|
+
[
|
|
239
|
+
"react",
|
|
240
|
+
"react-dom",
|
|
241
|
+
archipelago_client_package,
|
|
242
|
+
archipelago_react_package
|
|
243
|
+
]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def registry_relative_path
|
|
247
|
+
extension = @use_typescript ? "ts" : "js"
|
|
248
|
+
"app/javascript/archipelago/registry.generated.#{extension}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def initial_registry_source
|
|
252
|
+
if @use_typescript
|
|
253
|
+
<<~TS
|
|
254
|
+
import type { IslandRegistry } from "@archipelago-js/react"
|
|
255
|
+
|
|
256
|
+
// Auto-generated by Archipelago. Run `#{script_run_command('archipelago:registry')}` to refresh.
|
|
257
|
+
const registry: IslandRegistry = {}
|
|
258
|
+
|
|
259
|
+
export default registry
|
|
260
|
+
TS
|
|
261
|
+
else
|
|
262
|
+
<<~JS
|
|
263
|
+
// Auto-generated by Archipelago. Run `#{script_run_command('archipelago:registry')}` to refresh.
|
|
264
|
+
const registry = {}
|
|
265
|
+
|
|
266
|
+
export default registry
|
|
267
|
+
JS
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def wire_esbuild_package_scripts
|
|
272
|
+
return unless path_exists?("package.json")
|
|
273
|
+
|
|
274
|
+
package_json_path = path_for("package.json")
|
|
275
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
276
|
+
scripts = package_json["scripts"] ||= {}
|
|
277
|
+
registry_command = "node app/javascript/archipelago/generate_registry.mjs"
|
|
278
|
+
|
|
279
|
+
scripts["archipelago:registry"] ||= registry_command
|
|
280
|
+
|
|
281
|
+
scripts.each do |name, command|
|
|
282
|
+
next unless command.is_a?(String)
|
|
283
|
+
next unless command.include?("esbuild")
|
|
284
|
+
next if command.include?("archipelago:registry") || command.include?("generate_registry.mjs")
|
|
285
|
+
|
|
286
|
+
scripts[name] = "#{registry_command} && #{command}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def path_exists?(relative_path)
|
|
293
|
+
File.exist?(path_for(relative_path))
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def path_for(relative_path)
|
|
297
|
+
File.expand_path(relative_path, destination_root)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def script_run_command(script_name)
|
|
301
|
+
case resolved_package_manager
|
|
302
|
+
when "npm"
|
|
303
|
+
"npm run #{script_name}"
|
|
304
|
+
when "pnpm"
|
|
305
|
+
"pnpm #{script_name}"
|
|
306
|
+
when "bun"
|
|
307
|
+
"bun run #{script_name}"
|
|
308
|
+
else
|
|
309
|
+
"yarn #{script_name}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def interactive_mode?
|
|
314
|
+
options[:interactive] && $stdin.tty? && $stdout.tty?
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def ask_choice(label, choices, default:)
|
|
318
|
+
answer = ask("#{label} [#{choices.join('/')}] (default: #{default})")
|
|
319
|
+
normalized = answer.to_s.strip
|
|
320
|
+
normalized = default if normalized.empty?
|
|
321
|
+
return normalized if choices.include?(normalized)
|
|
322
|
+
|
|
323
|
+
say_status :warning, "Invalid choice '#{normalized}', using '#{default}'.", :yellow
|
|
324
|
+
default
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def ask_yes_no(question, default:)
|
|
328
|
+
answer = ask("#{question} [#{default ? 'Y/n' : 'y/N'}]")
|
|
329
|
+
normalized = answer.to_s.strip.downcase
|
|
330
|
+
return default if normalized.empty?
|
|
331
|
+
return true if %w[y yes].include?(normalized)
|
|
332
|
+
return false if %w[n no].include?(normalized)
|
|
333
|
+
|
|
334
|
+
say_status :warning, "Invalid answer '#{answer}', using default.", :yellow
|
|
335
|
+
default
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def prompt_for_local_monorepo_path(current_path)
|
|
339
|
+
if current_path
|
|
340
|
+
use_detected = ask_yes_no(
|
|
341
|
+
"Use local Archipelago packages from #{current_path}?",
|
|
342
|
+
default: true
|
|
343
|
+
)
|
|
344
|
+
return current_path if use_detected
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
entered = ask("Local Archipelago monorepo path for file: installs (leave blank to use npm registry)")
|
|
348
|
+
normalized = entered.to_s.strip
|
|
349
|
+
return nil if normalized.empty?
|
|
350
|
+
|
|
351
|
+
root = Pathname.new(normalized).expand_path
|
|
352
|
+
unless local_packages_available?(root)
|
|
353
|
+
say_status :warning, "No packages/client and packages/react under #{root}; using npm registry.", :yellow
|
|
354
|
+
return nil
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
root.to_s
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def local_packages_available?(root)
|
|
361
|
+
root.join("packages/client").directory? && root.join("packages/react").directory?
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def archipelago_client_package
|
|
365
|
+
return "@archipelago-js/client" unless @local_monorepo_path
|
|
366
|
+
|
|
367
|
+
root = Pathname.new(@local_monorepo_path).expand_path
|
|
368
|
+
"@archipelago-js/client@file:#{root.join('packages/client')}"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def archipelago_react_package
|
|
372
|
+
return "@archipelago-js/react" unless @local_monorepo_path
|
|
373
|
+
|
|
374
|
+
root = Pathname.new(@local_monorepo_path).expand_path
|
|
375
|
+
"@archipelago-js/react@file:#{root.join('packages/react')}"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { bootArchipelagoIslands } from "@archipelago-js/react"
|
|
2
|
+
|
|
3
|
+
<% if @bundler == :esbuild && @auto_registry %>
|
|
4
|
+
import registry from "./registry.generated"
|
|
5
|
+
<% else %>
|
|
6
|
+
// Register your islands here.
|
|
7
|
+
const registry = {
|
|
8
|
+
// Example:
|
|
9
|
+
// TeamMembers: TeamMembersIsland
|
|
10
|
+
}
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
void bootArchipelagoIslands(registry)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const __dirname = path.dirname(__filename)
|
|
7
|
+
const archipelagoDir = path.resolve(__dirname)
|
|
8
|
+
const islandsRoot = path.resolve(archipelagoDir, "../islands")
|
|
9
|
+
const outputFile = path.resolve(
|
|
10
|
+
archipelagoDir,
|
|
11
|
+
"registry.generated.<%= @use_typescript ? "ts" : "js" %>"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
function walk(dir) {
|
|
15
|
+
if (!fs.existsSync(dir)) {
|
|
16
|
+
return []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
20
|
+
const files = []
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = path.join(dir, entry.name)
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
files.push(...walk(fullPath))
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!entry.isFile()) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!/\.(jsx?|tsx?)$/.test(entry.name)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (entry.name.endsWith(".d.ts")) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
files.push(fullPath)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return files
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toPascalCase(value) {
|
|
48
|
+
return value
|
|
49
|
+
.split(/[_-]/)
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
52
|
+
.join("")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function componentNameFromRelative(relativeWithoutExt) {
|
|
56
|
+
const parts = relativeWithoutExt.split(path.sep).filter(Boolean)
|
|
57
|
+
return parts.map(toPascalCase).join("__")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const islandFiles = walk(islandsRoot).sort()
|
|
61
|
+
|
|
62
|
+
const imports = []
|
|
63
|
+
const registryRows = []
|
|
64
|
+
|
|
65
|
+
islandFiles.forEach((absolutePath, index) => {
|
|
66
|
+
const relativeFromIslands = path.relative(islandsRoot, absolutePath)
|
|
67
|
+
const relativeWithoutExt = relativeFromIslands.replace(/\.[^.]+$/, "")
|
|
68
|
+
const importPath = `../islands/${relativeWithoutExt.split(path.sep).join("/")}`
|
|
69
|
+
const componentName = componentNameFromRelative(relativeWithoutExt)
|
|
70
|
+
const importName = `Island${index + 1}`
|
|
71
|
+
|
|
72
|
+
imports.push(`import ${importName} from "${importPath}"`)
|
|
73
|
+
registryRows.push(` "${componentName}": ${importName}`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const source = [
|
|
77
|
+
"// This file is auto-generated by app/javascript/archipelago/generate_registry.mjs.",
|
|
78
|
+
"// Do not edit manually.",
|
|
79
|
+
"",
|
|
80
|
+
...imports,
|
|
81
|
+
imports.length > 0 ? "" : "",
|
|
82
|
+
<% if @use_typescript %>
|
|
83
|
+
"import type { IslandRegistry } from \"@archipelago-js/react\"",
|
|
84
|
+
"",
|
|
85
|
+
"const registry: IslandRegistry = {",
|
|
86
|
+
<% else %>
|
|
87
|
+
"const registry = {",
|
|
88
|
+
<% end %>
|
|
89
|
+
...registryRows,
|
|
90
|
+
"}",
|
|
91
|
+
"",
|
|
92
|
+
"export default registry",
|
|
93
|
+
""
|
|
94
|
+
].join("\n")
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(outputFile, source, "utf8")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/named_base"
|
|
5
|
+
|
|
6
|
+
module Archipelago
|
|
7
|
+
module Generators
|
|
8
|
+
class IslandGenerator < Rails::Generators::NamedBase
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
argument :operations, type: :array, default: [], banner: "operation operation"
|
|
12
|
+
|
|
13
|
+
def ensure_operations
|
|
14
|
+
raise ArgumentError, "At least one operation is required" if operations.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_action_files
|
|
18
|
+
operations.each do |operation|
|
|
19
|
+
@operation = operation
|
|
20
|
+
template "action.rb.tt", File.join("app/islands", file_path, "#{operation}.rb")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_component_file
|
|
25
|
+
@component_name = class_name
|
|
26
|
+
template "component.tsx.tt", File.join("app/javascript/islands", "#{class_name}.tsx")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def component_module_parts
|
|
32
|
+
class_name.split("::")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def component_module
|
|
36
|
+
component_module_parts.join("::")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def operation_class
|
|
40
|
+
@operation.camelize
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { useIslandForm, useIslandProps } from "@archipelago-js/react"
|
|
3
|
+
|
|
4
|
+
export default function <%= @component_name %>() {
|
|
5
|
+
const { props } = useIslandProps()
|
|
6
|
+
const form = useIslandForm({ initialData: {} })
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<pre>{JSON.stringify(props, null, 2)}</pre>
|
|
11
|
+
<button onClick={() => form.post("<%= operations.first %>")}>Run <%= operations.first %></button>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|