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,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Archipelago
|
|
7
|
+
module ParamsDSL
|
|
8
|
+
MISSING = Object.new.freeze
|
|
9
|
+
|
|
10
|
+
ParamDefinition = Struct.new(
|
|
11
|
+
:name,
|
|
12
|
+
:type,
|
|
13
|
+
:required,
|
|
14
|
+
:default,
|
|
15
|
+
:strip,
|
|
16
|
+
:downcase,
|
|
17
|
+
:upcase,
|
|
18
|
+
keyword_init: true
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
module ClassMethods
|
|
22
|
+
def param_definitions
|
|
23
|
+
@param_definitions ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def param(name, type, required: false, default: MISSING, strip: false, downcase: false, upcase: false)
|
|
27
|
+
symbol_name = name.to_sym
|
|
28
|
+
|
|
29
|
+
param_definitions[symbol_name] = ParamDefinition.new(
|
|
30
|
+
name: symbol_name,
|
|
31
|
+
type: type,
|
|
32
|
+
required: required,
|
|
33
|
+
default: default,
|
|
34
|
+
strip: strip,
|
|
35
|
+
downcase: downcase,
|
|
36
|
+
upcase: upcase
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
define_method(symbol_name) do
|
|
40
|
+
@coerced_params[symbol_name]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.included(base)
|
|
46
|
+
base.extend(ClassMethods)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def coerce_declared_params(raw_params)
|
|
50
|
+
coerced = {}
|
|
51
|
+
errors = Hash.new { |hash, key| hash[key] = [] }
|
|
52
|
+
|
|
53
|
+
self.class.param_definitions.each_value do |definition|
|
|
54
|
+
raw_value = fetch_param(raw_params, definition.name)
|
|
55
|
+
|
|
56
|
+
if blank_value?(raw_value)
|
|
57
|
+
if definition.default != MISSING
|
|
58
|
+
coerced[definition.name] = definition.default.respond_to?(:call) ? definition.default.call : definition.default
|
|
59
|
+
elsif definition.required
|
|
60
|
+
errors[definition.name.to_s] << "is required"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
coerced_value = coerce_value(raw_value, definition)
|
|
68
|
+
coerced[definition.name] = coerced_value
|
|
69
|
+
rescue ArgumentError, TypeError, JSON::ParserError
|
|
70
|
+
errors[definition.name.to_s] << "is invalid"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
[coerced, errors]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def fetch_param(raw_params, key)
|
|
80
|
+
raw_params[key] || raw_params[key.to_s]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def blank_value?(value)
|
|
84
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def coerce_value(raw_value, definition)
|
|
88
|
+
value = cast(raw_value, definition.type)
|
|
89
|
+
|
|
90
|
+
return value unless value.is_a?(String)
|
|
91
|
+
|
|
92
|
+
value = value.strip if definition.strip
|
|
93
|
+
value = value.downcase if definition.downcase
|
|
94
|
+
value = value.upcase if definition.upcase
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def cast(value, type)
|
|
99
|
+
case type
|
|
100
|
+
when :string
|
|
101
|
+
String(value)
|
|
102
|
+
when :integer
|
|
103
|
+
Integer(value)
|
|
104
|
+
when :boolean
|
|
105
|
+
cast_boolean(value)
|
|
106
|
+
when :float
|
|
107
|
+
Float(value)
|
|
108
|
+
when :date
|
|
109
|
+
Date.parse(String(value))
|
|
110
|
+
when :datetime
|
|
111
|
+
Time.parse(String(value))
|
|
112
|
+
when :array
|
|
113
|
+
cast_array(value)
|
|
114
|
+
when :json
|
|
115
|
+
cast_json(value)
|
|
116
|
+
else
|
|
117
|
+
raise ArgumentError, "Unsupported param type: #{type}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cast_boolean(value)
|
|
122
|
+
return true if [true, 1, "1", "true", "on", "yes"].include?(value)
|
|
123
|
+
return false if [false, 0, "0", "false", "off", "no"].include?(value)
|
|
124
|
+
|
|
125
|
+
raise TypeError, "Invalid boolean value"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cast_array(value)
|
|
129
|
+
return value if value.is_a?(Array)
|
|
130
|
+
|
|
131
|
+
parsed = JSON.parse(String(value))
|
|
132
|
+
raise TypeError, "Array expected" unless parsed.is_a?(Array)
|
|
133
|
+
|
|
134
|
+
parsed
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def cast_json(value)
|
|
138
|
+
return value if value.is_a?(Hash) || value.is_a?(Array)
|
|
139
|
+
|
|
140
|
+
JSON.parse(String(value))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize
|
|
6
|
+
@map = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def map(key, handler)
|
|
10
|
+
@map[key] = handler
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(key)
|
|
14
|
+
@map[key]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
@map.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear!
|
|
22
|
+
@map.clear
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class Resolver
|
|
5
|
+
COMPONENT_PATTERN = /\A[A-Z][A-Za-z0-9_]*\z/
|
|
6
|
+
OPERATION_PATTERN = /\A[a-z][a-z0-9_]*\z/
|
|
7
|
+
|
|
8
|
+
def initialize(configuration: Archipelago.configuration, registry: Archipelago.registry)
|
|
9
|
+
@configuration = configuration
|
|
10
|
+
@registry = registry
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(component:, operation:)
|
|
14
|
+
Archipelago.instrument(
|
|
15
|
+
"archipelago.action.resolve",
|
|
16
|
+
component: component,
|
|
17
|
+
operation: operation
|
|
18
|
+
) do
|
|
19
|
+
validate!(component: component, operation: operation)
|
|
20
|
+
|
|
21
|
+
override = @registry.resolve("#{component}##{operation}")
|
|
22
|
+
return validate_handler!(override) if override
|
|
23
|
+
|
|
24
|
+
constant_name = convention_constant_name(component: component, operation: operation)
|
|
25
|
+
klass = constant_name.safe_constantize
|
|
26
|
+
raise Archipelago::ResolutionError, "Unknown island action" unless klass
|
|
27
|
+
|
|
28
|
+
validate_handler!(klass)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def validate!(component:, operation:)
|
|
35
|
+
raise Archipelago::ResolutionError, "Invalid component name" unless component.match?(COMPONENT_PATTERN)
|
|
36
|
+
raise Archipelago::ResolutionError, "Invalid operation name" unless operation.match?(OPERATION_PATTERN)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_handler!(handler)
|
|
40
|
+
unless handler.is_a?(Class) && handler < Archipelago::Action
|
|
41
|
+
raise Archipelago::ResolutionError, "Resolved handler must inherit from Archipelago::Action"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
handler
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def convention_constant_name(component:, operation:)
|
|
48
|
+
component_parts = component.split("__").map { |part| part.underscore.camelize }
|
|
49
|
+
operation_class = operation.camelize
|
|
50
|
+
|
|
51
|
+
[@configuration.root_namespace, *component_parts, operation_class].join("::")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module Response
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def ok(props:, version:)
|
|
8
|
+
{
|
|
9
|
+
status: "ok",
|
|
10
|
+
props: props,
|
|
11
|
+
version: version
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def redirect(location:)
|
|
16
|
+
{
|
|
17
|
+
status: "redirect",
|
|
18
|
+
location: location
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error(errors:)
|
|
23
|
+
{
|
|
24
|
+
status: "error",
|
|
25
|
+
errors: errors
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def forbidden
|
|
30
|
+
{
|
|
31
|
+
status: "forbidden"
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module Security
|
|
5
|
+
class OriginValidator
|
|
6
|
+
def initialize(request, configuration: Archipelago.configuration)
|
|
7
|
+
@request = request
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate!
|
|
12
|
+
return true unless @configuration.strict_origin_check
|
|
13
|
+
|
|
14
|
+
origin = @request.headers["Origin"]
|
|
15
|
+
return true if origin.nil? || origin.empty?
|
|
16
|
+
|
|
17
|
+
uri = URI.parse(origin)
|
|
18
|
+
expected_scheme = @request.protocol.delete_suffix("://")
|
|
19
|
+
|
|
20
|
+
valid = uri.scheme == expected_scheme &&
|
|
21
|
+
uri.host == @request.host &&
|
|
22
|
+
uri.port == @request.port
|
|
23
|
+
|
|
24
|
+
raise Archipelago::InvalidOrigin, "Origin mismatch" unless valid
|
|
25
|
+
|
|
26
|
+
true
|
|
27
|
+
rescue URI::InvalidURIError
|
|
28
|
+
raise Archipelago::InvalidOrigin, "Invalid origin URI"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module Security
|
|
5
|
+
class RedirectValidator
|
|
6
|
+
def initialize(configuration: Archipelago.configuration)
|
|
7
|
+
@configuration = configuration
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate!(location)
|
|
11
|
+
return location if relative_path?(location)
|
|
12
|
+
|
|
13
|
+
uri = URI.parse(location)
|
|
14
|
+
unless uri.is_a?(URI::HTTP) && @configuration.allowed_redirect_hosts.include?(uri.host)
|
|
15
|
+
raise Archipelago::InvalidRedirect, "Unsafe redirect host"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
location
|
|
19
|
+
rescue URI::InvalidURIError
|
|
20
|
+
raise Archipelago::InvalidRedirect, "Invalid redirect URI"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def relative_path?(location)
|
|
26
|
+
location.start_with?("/") && !location.start_with?("//")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module TestHelpers
|
|
5
|
+
def island_post(component, operation, params = {})
|
|
6
|
+
post "/islands/#{component}/#{operation}", params: params, as: :json
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parsed_island_response
|
|
10
|
+
JSON.parse(response.body)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def assert_island_props(key, value)
|
|
14
|
+
body = parsed_island_response
|
|
15
|
+
assert_equal "ok", body["status"]
|
|
16
|
+
assert_equal value, body.fetch("props").fetch(key.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def assert_island_redirect(path)
|
|
20
|
+
body = parsed_island_response
|
|
21
|
+
assert_equal "redirect", body["status"]
|
|
22
|
+
assert_equal path, body["location"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def assert_island_errors(field)
|
|
26
|
+
body = parsed_island_response
|
|
27
|
+
assert_equal "error", body["status"]
|
|
28
|
+
assert body.fetch("errors").key?(field.to_s)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module ViewHelper
|
|
5
|
+
def archipelago_island(component, props:, params: {}, instance: nil, stream: nil, **html_options)
|
|
6
|
+
stream_name = resolve_stream_name(component: component, instance: instance, stream: stream)
|
|
7
|
+
|
|
8
|
+
data_attributes = {
|
|
9
|
+
island: true,
|
|
10
|
+
component: component,
|
|
11
|
+
props: props.to_json,
|
|
12
|
+
params: params.to_json,
|
|
13
|
+
instance: instance,
|
|
14
|
+
stream: stream_name
|
|
15
|
+
}.compact
|
|
16
|
+
|
|
17
|
+
content_tag(:div, "", html_options.merge(data: data_attributes))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def resolve_stream_name(component:, instance:, stream:)
|
|
23
|
+
return nil if stream.nil?
|
|
24
|
+
return stream if stream.is_a?(String)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "instance is required when stream: true" if instance.blank?
|
|
27
|
+
|
|
28
|
+
"#{component}:#{instance}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/archipelago.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
require "active_support/core_ext/object/blank"
|
|
6
|
+
require "active_support/core_ext/string/inflections"
|
|
7
|
+
require "active_support/notifications"
|
|
8
|
+
require "json"
|
|
9
|
+
require "uri"
|
|
10
|
+
|
|
11
|
+
module Archipelago
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class Forbidden < Error; end
|
|
14
|
+
class ResolutionError < Error; end
|
|
15
|
+
class InvalidOrigin < Error; end
|
|
16
|
+
class InvalidRedirect < Error; end
|
|
17
|
+
class MissingAuthorization < Error; end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def configure
|
|
21
|
+
yield(configuration)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset_configuration!
|
|
29
|
+
@configuration = Configuration.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registry
|
|
33
|
+
@registry ||= Registry.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def map(mapping)
|
|
37
|
+
mapping.each { |key, value| registry.map(key, value) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def next_version
|
|
41
|
+
configuration.version_source.call
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def broadcast(stream_name, props:, version: next_version)
|
|
45
|
+
Broadcasts.broadcast(stream_name, props: props, version: version)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def instrument(event, payload = {}, &block)
|
|
49
|
+
ActiveSupport::Notifications.instrument(event, payload, &block)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
require "archipelago/configuration"
|
|
55
|
+
require "archipelago/registry"
|
|
56
|
+
require "archipelago/response"
|
|
57
|
+
require "archipelago/params_dsl"
|
|
58
|
+
require "archipelago/context"
|
|
59
|
+
require "archipelago/security/origin_validator"
|
|
60
|
+
require "archipelago/security/redirect_validator"
|
|
61
|
+
require "archipelago/action"
|
|
62
|
+
require "archipelago/resolver"
|
|
63
|
+
require "archipelago/broadcasts"
|
|
64
|
+
require "archipelago/channel"
|
|
65
|
+
require "archipelago/view_helper"
|
|
66
|
+
require "archipelago/test_helpers"
|
|
67
|
+
begin
|
|
68
|
+
require "archipelago/engine"
|
|
69
|
+
rescue LoadError, NameError
|
|
70
|
+
# Engine support requires railties and loads when running inside Rails.
|
|
71
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Archipelago
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
desc "Installs Archipelago into a Rails app"
|
|
9
|
+
|
|
10
|
+
def create_islands_directory
|
|
11
|
+
empty_directory "app/islands"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_initializer
|
|
15
|
+
create_file "config/initializers/archipelago.rb", <<~RUBY
|
|
16
|
+
Archipelago.configure do |config|
|
|
17
|
+
config.root_namespace = "Islands"
|
|
18
|
+
config.current_user_method = :current_user
|
|
19
|
+
config.authorize_by_default = true
|
|
20
|
+
config.strict_origin_check = false
|
|
21
|
+
config.allowed_redirect_hosts = []
|
|
22
|
+
end
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def mount_engine
|
|
27
|
+
route_line = "mount Archipelago::Engine => \"/islands\""
|
|
28
|
+
routes_path = "config/routes.rb"
|
|
29
|
+
|
|
30
|
+
if File.exist?(routes_path) && !File.read(routes_path).include?(route_line)
|
|
31
|
+
route(route_line)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def print_next_steps
|
|
36
|
+
say "Install JS packages: yarn add @archipelago-js/client @archipelago-js/react"
|
|
37
|
+
say "Optional React bootstrap wizard: rails g archipelago:install:react"
|
|
38
|
+
say "Non-interactive mode: rails g archipelago:install:react --interactive=false"
|
|
39
|
+
say "esbuild users get auto-registry wiring by default in install:react"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|