react-email-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/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +68 -0
- data/LICENSE.md +21 -0
- data/README.md +492 -0
- data/SECURITY.md +5 -0
- data/lib/generators/react_email_rails/USAGE +19 -0
- data/lib/generators/react_email_rails/email_generator.rb +224 -0
- data/lib/generators/react_email_rails/install_generator.rb +211 -0
- data/lib/generators/react_email_rails/templates/USAGE +33 -0
- data/lib/generators/react_email_rails/templates/email/application_mailer.rb.tt +5 -0
- data/lib/generators/react_email_rails/templates/email/component.tsx +14 -0
- data/lib/generators/react_email_rails/templates/email/mailer.rb.tt +17 -0
- data/lib/generators/react_email_rails/templates/email/mailer_preview.rb.tt +14 -0
- data/lib/generators/react_email_rails/templates/email/mailer_test.rb.tt +28 -0
- data/lib/generators/react_email_rails/templates/initializer.rb +3 -0
- data/lib/generators/react_email_rails/templates/vite.config.ts +6 -0
- data/lib/react-email-rails.rb +1 -0
- data/lib/react_email_rails/action_mailer.rb +31 -0
- data/lib/react_email_rails/configuration.rb +145 -0
- data/lib/react_email_rails/props_resolver.rb +48 -0
- data/lib/react_email_rails/railtie.rb +16 -0
- data/lib/react_email_rails/render_error.rb +1 -0
- data/lib/react_email_rails/render_modes/persistent/command_runner.rb +44 -0
- data/lib/react_email_rails/render_modes/persistent/server.rb +204 -0
- data/lib/react_email_rails/render_modes/persistent.rb +28 -0
- data/lib/react_email_rails/render_modes/subprocess/command_runner.rb +56 -0
- data/lib/react_email_rails/render_modes/subprocess.rb +99 -0
- data/lib/react_email_rails/render_modes.rb +1 -0
- data/lib/react_email_rails/render_protocol.rb +21 -0
- data/lib/react_email_rails/rendered_email.rb +3 -0
- data/lib/react_email_rails/version.rb +3 -0
- data/lib/react_email_rails.rb +58 -0
- metadata +194 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
require("rails/generators/named_base")
|
|
2
|
+
require("json")
|
|
3
|
+
require("open3")
|
|
4
|
+
require("timeout")
|
|
5
|
+
|
|
6
|
+
module ReactEmailRails; end
|
|
7
|
+
module ReactEmailRails::Generators; end
|
|
8
|
+
|
|
9
|
+
class ReactEmailRails::Generators::EmailGenerator < Rails::Generators::NamedBase
|
|
10
|
+
source_root(File.expand_path("templates/email", __dir__))
|
|
11
|
+
|
|
12
|
+
argument(:actions, type: :array, default: [], banner: "method method")
|
|
13
|
+
|
|
14
|
+
class_option(
|
|
15
|
+
:skip_preview,
|
|
16
|
+
type: :boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
desc: "Skip creating the mailer preview",
|
|
19
|
+
)
|
|
20
|
+
class_option(
|
|
21
|
+
:skip_test,
|
|
22
|
+
type: :boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
desc: "Skip creating the mailer test",
|
|
25
|
+
)
|
|
26
|
+
class_option(
|
|
27
|
+
:emails_path,
|
|
28
|
+
type: :string,
|
|
29
|
+
desc: "Directory containing React Email components",
|
|
30
|
+
)
|
|
31
|
+
class_option(
|
|
32
|
+
:extension,
|
|
33
|
+
type: :string,
|
|
34
|
+
desc: "React Email component extension, such as tsx or jsx",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
check_class_collision(suffix: "Mailer")
|
|
38
|
+
|
|
39
|
+
def create_mailer_file
|
|
40
|
+
template("mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb"))
|
|
41
|
+
|
|
42
|
+
in_root do
|
|
43
|
+
if behavior == :invoke && !File.exist?(application_mailer_file_name)
|
|
44
|
+
template("application_mailer.rb", application_mailer_file_name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def copy_email_components
|
|
50
|
+
empty_directory(component_base_path)
|
|
51
|
+
|
|
52
|
+
actions.each do |action|
|
|
53
|
+
@action = action
|
|
54
|
+
@component_name = action.camelize
|
|
55
|
+
@path = File.join(component_base_path, "#{action}#{component_extension}")
|
|
56
|
+
template("component.tsx", @path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_test_file
|
|
61
|
+
return if options[:skip_test]
|
|
62
|
+
|
|
63
|
+
template("mailer_test.rb", File.join("test/mailers", class_path, "#{file_name}_mailer_test.rb"))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_preview_file
|
|
67
|
+
return if options[:skip_preview]
|
|
68
|
+
|
|
69
|
+
template("mailer_preview.rb", File.join("test/mailers/previews", class_path, "#{file_name}_mailer_preview.rb"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def file_name
|
|
75
|
+
@_file_name ||= super.sub(/_mailer\z/i, "")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def mailer_file_path
|
|
79
|
+
"#{file_path}_mailer"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def component_base_path
|
|
83
|
+
File.join(emails_path, "#{file_path}_mailer")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def emails_path
|
|
87
|
+
@emails_path ||= normalize_emails_path(
|
|
88
|
+
options[:emails_path].presence ||
|
|
89
|
+
vite_plugin_metadata.dig("emails", "path") ||
|
|
90
|
+
emails_path_from_vite_config ||
|
|
91
|
+
"app/javascript/emails",
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def component_extension
|
|
96
|
+
@component_extension ||= normalize_extension(
|
|
97
|
+
options[:extension].presence ||
|
|
98
|
+
vite_plugin_metadata.dig("emails", "extensions")&.first ||
|
|
99
|
+
extension_from_vite_config ||
|
|
100
|
+
extension_from_existing_email_components ||
|
|
101
|
+
extension_from_existing_app_components ||
|
|
102
|
+
extension_from_typescript_signal ||
|
|
103
|
+
".tsx",
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def application_mailer_file_name
|
|
108
|
+
@_application_mailer_file_name ||= if mountable_engine?
|
|
109
|
+
"app/mailers/#{namespaced_path}/application_mailer.rb"
|
|
110
|
+
else
|
|
111
|
+
"app/mailers/application_mailer.rb"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def vite_plugin_metadata
|
|
116
|
+
@vite_plugin_metadata ||= begin
|
|
117
|
+
command = vite_config_command
|
|
118
|
+
if command
|
|
119
|
+
stdout, _stderr, status = Timeout.timeout(10) do
|
|
120
|
+
Open3.capture3(command, chdir: destination_root)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
status.success? ? JSON.parse(stdout) : {}
|
|
124
|
+
else
|
|
125
|
+
{}
|
|
126
|
+
end
|
|
127
|
+
rescue JSON::ParserError, Timeout::Error
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def vite_config_command
|
|
133
|
+
[
|
|
134
|
+
"node_modules/.bin/react-email-rails-config",
|
|
135
|
+
"node_modules/.bin/react-email-rails-config.cmd",
|
|
136
|
+
].find { |path| File.exist?(File.join(destination_root, path)) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def emails_path_from_vite_config
|
|
140
|
+
source = vite_config_source
|
|
141
|
+
return unless source
|
|
142
|
+
|
|
143
|
+
source[
|
|
144
|
+
/reactEmailRails\s*\(\s*\{.*?emails:\s*["']([^"']+)["']/m,
|
|
145
|
+
1,
|
|
146
|
+
] || source[
|
|
147
|
+
/reactEmailRails\s*\(\s*\{.*?emails:\s*\{.*?path:\s*["']([^"']+)["']/m,
|
|
148
|
+
1,
|
|
149
|
+
]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def extension_from_vite_config
|
|
153
|
+
source = vite_config_source
|
|
154
|
+
return unless source
|
|
155
|
+
|
|
156
|
+
source[
|
|
157
|
+
/reactEmailRails\s*\(\s*\{.*?emails:\s*\{.*?extension:\s*["']([^"']+)["']/m,
|
|
158
|
+
1,
|
|
159
|
+
] || source[
|
|
160
|
+
/reactEmailRails\s*\(\s*\{.*?emails:\s*\{.*?extension:\s*\[\s*["']([^"']+)["']/m,
|
|
161
|
+
1,
|
|
162
|
+
]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def vite_config_source
|
|
166
|
+
@vite_config_source ||= begin
|
|
167
|
+
path = [
|
|
168
|
+
"vite.config.ts",
|
|
169
|
+
"vite.config.mts",
|
|
170
|
+
"vite.config.js",
|
|
171
|
+
"vite.config.mjs",
|
|
172
|
+
"vite.config.cts",
|
|
173
|
+
"vite.config.cjs",
|
|
174
|
+
].find { |candidate| File.exist?(File.join(destination_root, candidate)) }
|
|
175
|
+
|
|
176
|
+
File.read(File.join(destination_root, path)) if path
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def extension_from_existing_email_components
|
|
181
|
+
dominant_extension(Dir[File.join(destination_root, emails_path, "**/*.{tsx,jsx}")])
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def extension_from_existing_app_components
|
|
185
|
+
dominant_extension(Dir[File.join(destination_root, "app/javascript/**/*.{tsx,jsx}")])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def dominant_extension(paths)
|
|
189
|
+
paths
|
|
190
|
+
.map { |path| File.extname(path) }
|
|
191
|
+
.select { |extension| [".tsx", ".jsx"].include?(extension) }
|
|
192
|
+
.tally
|
|
193
|
+
.max_by { |_extension, count| count }
|
|
194
|
+
&.first
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def extension_from_typescript_signal
|
|
198
|
+
return ".tsx" if File.exist?(File.join(destination_root, "tsconfig.json"))
|
|
199
|
+
|
|
200
|
+
package_path = File.join(destination_root, "package.json")
|
|
201
|
+
return unless File.exist?(package_path)
|
|
202
|
+
|
|
203
|
+
package = JSON.parse(File.read(package_path))
|
|
204
|
+
dependencies = package.fetch("dependencies", {}).merge(package.fetch("devDependencies", {}))
|
|
205
|
+
".tsx" if dependencies.key?("typescript")
|
|
206
|
+
rescue JSON::ParserError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def normalize_emails_path(path)
|
|
211
|
+
path.to_s.delete_prefix("/").delete_suffix("/")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def normalize_extension(extension)
|
|
215
|
+
extension = extension.to_s
|
|
216
|
+
extension = ".#{extension}" unless extension.start_with?(".")
|
|
217
|
+
|
|
218
|
+
unless extension.match?(/\A\.[a-zA-Z0-9_.-]+\z/)
|
|
219
|
+
raise(Thor::Error, "Invalid React Email component extension: #{extension.inspect}")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
extension
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
require("json")
|
|
2
|
+
|
|
3
|
+
module ReactEmailRails; end
|
|
4
|
+
module ReactEmailRails::Generators; end
|
|
5
|
+
|
|
6
|
+
class ReactEmailRails::Generators::InstallGenerator < Rails::Generators::Base
|
|
7
|
+
JAVASCRIPT_PACKAGES = [
|
|
8
|
+
"react-email-rails",
|
|
9
|
+
"@react-email/render",
|
|
10
|
+
"@react-email/components",
|
|
11
|
+
"react",
|
|
12
|
+
"react-dom",
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
PACKAGE_MANAGER_LOCKFILES = {
|
|
16
|
+
"pnpm-lock.yaml" => "pnpm",
|
|
17
|
+
"yarn.lock" => "yarn",
|
|
18
|
+
"bun.lock" => "bun",
|
|
19
|
+
"bun.lockb" => "bun",
|
|
20
|
+
"package-lock.json" => "npm",
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
SUPPORTED_PACKAGE_MANAGERS = ["bun", "npm", "pnpm", "yarn"].freeze
|
|
24
|
+
VITE_CONFIG_FILES = [
|
|
25
|
+
"vite.config.ts",
|
|
26
|
+
"vite.config.mts",
|
|
27
|
+
"vite.config.js",
|
|
28
|
+
"vite.config.mjs",
|
|
29
|
+
"vite.config.cts",
|
|
30
|
+
"vite.config.cjs",
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
VITE_IMPORT = 'import { reactEmailRails } from "react-email-rails"'
|
|
34
|
+
|
|
35
|
+
source_root(File.expand_path("templates", __dir__))
|
|
36
|
+
|
|
37
|
+
class_option(
|
|
38
|
+
:package_manager,
|
|
39
|
+
type: :string,
|
|
40
|
+
desc: "JavaScript package manager to use: npm, pnpm, yarn, or bun",
|
|
41
|
+
)
|
|
42
|
+
class_option(
|
|
43
|
+
:skip_package_install,
|
|
44
|
+
type: :boolean,
|
|
45
|
+
default: false,
|
|
46
|
+
desc: "Skip installing JavaScript dependencies",
|
|
47
|
+
)
|
|
48
|
+
class_option(
|
|
49
|
+
:skip_vite,
|
|
50
|
+
type: :boolean,
|
|
51
|
+
default: false,
|
|
52
|
+
desc: "Skip updating vite.config.*",
|
|
53
|
+
)
|
|
54
|
+
def copy_initializer
|
|
55
|
+
template("initializer.rb", "config/initializers/react_email_rails.rb")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def install_javascript_dependencies
|
|
59
|
+
return if options[:skip_package_install]
|
|
60
|
+
|
|
61
|
+
package = package_json
|
|
62
|
+
unless package
|
|
63
|
+
say_status(:skip, "JavaScript dependencies; package.json was not found", :yellow)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
missing = missing_javascript_packages(package)
|
|
68
|
+
if missing.empty?
|
|
69
|
+
say_status(:identical, "JavaScript dependencies", :green)
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
manager = package_manager(package)
|
|
74
|
+
unless manager
|
|
75
|
+
say_status(
|
|
76
|
+
:skip,
|
|
77
|
+
"JavaScript dependencies; could not detect npm, pnpm, yarn, or bun",
|
|
78
|
+
:yellow,
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
run(javascript_install_command(manager, missing))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def configure_vite
|
|
87
|
+
return if options[:skip_vite]
|
|
88
|
+
|
|
89
|
+
if (config = vite_config_path)
|
|
90
|
+
update_vite_config(config)
|
|
91
|
+
else
|
|
92
|
+
template("vite.config.ts", "vite.config.ts")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def create_email_directory
|
|
97
|
+
empty_directory("app/javascript/emails")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def destination_path(path)
|
|
103
|
+
File.join(destination_root, path)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def package_json_path
|
|
107
|
+
destination_path("package.json")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def package_json
|
|
111
|
+
return unless File.exist?(package_json_path)
|
|
112
|
+
|
|
113
|
+
JSON.parse(File.read(package_json_path))
|
|
114
|
+
rescue JSON::ParserError => e
|
|
115
|
+
say_status(:skip, "JavaScript dependencies; package.json is invalid: #{e.message}", :yellow)
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def missing_javascript_packages(package)
|
|
120
|
+
dependencies = package.fetch("dependencies", {}).merge(package.fetch("devDependencies", {}))
|
|
121
|
+
|
|
122
|
+
JAVASCRIPT_PACKAGES.reject { |name| dependencies.key?(name) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def package_manager(package)
|
|
126
|
+
manager = options[:package_manager].presence || package_manager_from_package_json(package) || package_manager_from_lockfile
|
|
127
|
+
return unless manager
|
|
128
|
+
|
|
129
|
+
manager = manager.to_s
|
|
130
|
+
raise(Thor::Error, "Unsupported package manager: #{manager}") if SUPPORTED_PACKAGE_MANAGERS.exclude?(manager)
|
|
131
|
+
|
|
132
|
+
manager
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def package_manager_from_package_json(package)
|
|
136
|
+
package.fetch("packageManager", "").to_s.split("@").first.presence
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def package_manager_from_lockfile
|
|
140
|
+
lockfile = PACKAGE_MANAGER_LOCKFILES.keys.find { |path| File.exist?(destination_path(path)) }
|
|
141
|
+
PACKAGE_MANAGER_LOCKFILES[lockfile]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def javascript_install_command(manager, packages)
|
|
145
|
+
case manager
|
|
146
|
+
when "npm"
|
|
147
|
+
"npm install #{packages.join(" ")}"
|
|
148
|
+
when "pnpm"
|
|
149
|
+
"pnpm add #{packages.join(" ")}"
|
|
150
|
+
when "yarn"
|
|
151
|
+
"yarn add #{packages.join(" ")}"
|
|
152
|
+
when "bun"
|
|
153
|
+
"bun add #{packages.join(" ")}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def vite_config_path
|
|
158
|
+
VITE_CONFIG_FILES.find { |path| File.exist?(destination_path(path)) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def update_vite_config(path)
|
|
162
|
+
full_path = destination_path(path)
|
|
163
|
+
source = File.read(full_path)
|
|
164
|
+
|
|
165
|
+
if configured_for_react_email?(source)
|
|
166
|
+
say_status(:identical, path, :green)
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
updated = insert_react_email_plugin(source)
|
|
171
|
+
unless updated
|
|
172
|
+
say_status(:skip, "#{path}; add reactEmailRails() to the Vite plugins array", :yellow)
|
|
173
|
+
return
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
File.write(full_path, ensure_react_email_import(updated))
|
|
177
|
+
say_status(:insert, path, :green)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def configured_for_react_email?(source)
|
|
181
|
+
source.match?(/reactEmailRails\s*\(/)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def insert_react_email_plugin(source)
|
|
185
|
+
return source if configured_for_react_email?(source)
|
|
186
|
+
|
|
187
|
+
if source.match?(/plugins:\s*\[\s*\]/)
|
|
188
|
+
source.sub(/plugins:\s*\[\s*\]/, "plugins: [reactEmailRails()]")
|
|
189
|
+
elsif source.match?(/plugins:\s*\[/)
|
|
190
|
+
source.sub(/plugins:\s*\[/) { |match| "#{match}reactEmailRails(), " }
|
|
191
|
+
elsif source.match?(/defineConfig\(\s*\{/)
|
|
192
|
+
source.sub(/defineConfig\(\s*\{/) { |match| "#{match}\n plugins: [reactEmailRails()]," }
|
|
193
|
+
elsif source.match?(/export\s+default\s+\{/)
|
|
194
|
+
source.sub(/export\s+default\s+\{/) { |match| "#{match}\n plugins: [reactEmailRails()]," }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def ensure_react_email_import(source)
|
|
199
|
+
return source if source.match?(/from\s+["']react-email-rails["']/)
|
|
200
|
+
|
|
201
|
+
lines = source.lines
|
|
202
|
+
last_import_index = (lines.length - 1).downto(0).find { |index| lines[index].match?(/\Aimport\b/) }
|
|
203
|
+
|
|
204
|
+
if last_import_index
|
|
205
|
+
lines.insert(last_import_index + 1, "#{VITE_IMPORT}\n")
|
|
206
|
+
lines.join
|
|
207
|
+
else
|
|
208
|
+
"#{VITE_IMPORT}\n\n#{source}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate an Action Mailer class backed by React Email components.
|
|
3
|
+
|
|
4
|
+
Example:
|
|
5
|
+
bin/rails generate react_email_rails:email Account created invited
|
|
6
|
+
|
|
7
|
+
This creates:
|
|
8
|
+
app/mailers/account_mailer.rb
|
|
9
|
+
app/javascript/emails/account_mailer/created.tsx
|
|
10
|
+
app/javascript/emails/account_mailer/invited.tsx
|
|
11
|
+
test/mailers/account_mailer_test.rb
|
|
12
|
+
test/mailers/previews/account_mailer_preview.rb
|
|
13
|
+
|
|
14
|
+
The generator follows Rails' mailer generator shape:
|
|
15
|
+
|
|
16
|
+
NAME [method method]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--emails-path=app/emails
|
|
20
|
+
Override the React Email component directory. By default this is
|
|
21
|
+
detected from reactEmailRails({ emails: ... }) in vite.config.* and
|
|
22
|
+
falls back to app/javascript/emails.
|
|
23
|
+
|
|
24
|
+
--extension=tsx|jsx|email.tsx
|
|
25
|
+
Override the generated component extension. By default this is detected
|
|
26
|
+
from the Vite plugin config, existing components, TypeScript signals,
|
|
27
|
+
and finally falls back to tsx.
|
|
28
|
+
|
|
29
|
+
--skip-test
|
|
30
|
+
Do not create a mailer test.
|
|
31
|
+
|
|
32
|
+
--skip-preview
|
|
33
|
+
Do not create a mailer preview.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Body, Container, Heading, Html, Text } from "@react-email/components"
|
|
2
|
+
|
|
3
|
+
export default function <%= @component_name %>() {
|
|
4
|
+
return (
|
|
5
|
+
<Html>
|
|
6
|
+
<Body>
|
|
7
|
+
<Container>
|
|
8
|
+
<Heading><%= class_name %>Mailer#<%= @action %></Heading>
|
|
9
|
+
<Text>Hi, find me in <%= @path %></Text>
|
|
10
|
+
</Container>
|
|
11
|
+
</Body>
|
|
12
|
+
</Html>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% module_namespacing do -%>
|
|
2
|
+
class <%= class_name %>Mailer < ApplicationMailer
|
|
3
|
+
<% actions.each_with_index do |action, index| -%>
|
|
4
|
+
<% if index != 0 -%>
|
|
5
|
+
|
|
6
|
+
<% end -%>
|
|
7
|
+
# Subject can be set in your I18n file at config/locales/en.yml
|
|
8
|
+
# with the following lookup:
|
|
9
|
+
#
|
|
10
|
+
# en.<%= mailer_file_path.tr("/", ".") %>.<%= action %>.subject
|
|
11
|
+
#
|
|
12
|
+
def <%= action %>
|
|
13
|
+
mail to: "to@example.org", react: true
|
|
14
|
+
end
|
|
15
|
+
<% end -%>
|
|
16
|
+
end
|
|
17
|
+
<% end -%>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% module_namespacing do -%>
|
|
2
|
+
# Preview all emails at http://localhost:3000/rails/mailers/<%= mailer_file_path %>
|
|
3
|
+
class <%= class_name %>MailerPreview < ActionMailer::Preview
|
|
4
|
+
<% actions.each_with_index do |action, index| -%>
|
|
5
|
+
<% if index != 0 -%>
|
|
6
|
+
|
|
7
|
+
<% end -%>
|
|
8
|
+
# Preview this email at http://localhost:3000/rails/mailers/<%= mailer_file_path %>/<%= action %>
|
|
9
|
+
def <%= action %>
|
|
10
|
+
<%= class_name %>Mailer.<%= action %>
|
|
11
|
+
end
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
|
14
|
+
<% end -%>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>MailerTest < ActionMailer::TestCase
|
|
5
|
+
<% actions.each_with_index do |action, index| -%>
|
|
6
|
+
<% if index != 0 -%>
|
|
7
|
+
|
|
8
|
+
<% end -%>
|
|
9
|
+
test "<%= action %>" do
|
|
10
|
+
rendered = ReactEmailRails::RenderedEmail.new(html: "<p>Hi</p>", text: "Hi")
|
|
11
|
+
|
|
12
|
+
ReactEmailRails.stub(:render, rendered) do
|
|
13
|
+
mail = <%= class_name %>Mailer.<%= action %>
|
|
14
|
+
|
|
15
|
+
assert_equal <%= action.to_s.humanize.inspect %>, mail.subject
|
|
16
|
+
assert_equal ["to@example.org"], mail.to
|
|
17
|
+
assert_equal ["from@example.com"], mail.from
|
|
18
|
+
assert_match "Hi", mail.body.encoded
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
<% end -%>
|
|
22
|
+
<% if actions.blank? -%>
|
|
23
|
+
# test "the truth" do
|
|
24
|
+
# assert true
|
|
25
|
+
# end
|
|
26
|
+
<% end -%>
|
|
27
|
+
end
|
|
28
|
+
<% end -%>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative("react_email_rails")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ReactEmailRails::ActionMailer
|
|
2
|
+
extend(ActiveSupport::Concern)
|
|
3
|
+
|
|
4
|
+
prepended do
|
|
5
|
+
class_attribute(:react_email_use_instance_props, default: false)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def use_react_instance_props
|
|
10
|
+
self.react_email_use_instance_props = true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mail(headers = {}, &block)
|
|
15
|
+
return super unless headers.is_a?(Hash) && headers.key?(:react)
|
|
16
|
+
|
|
17
|
+
headers = headers.dup
|
|
18
|
+
react = headers.delete(:react)
|
|
19
|
+
props = headers.delete(:props) if headers.key?(:props)
|
|
20
|
+
|
|
21
|
+
component, resolved_props = ReactEmailRails::PropsResolver.new(self).resolve(react, props)
|
|
22
|
+
render_options = ReactEmailRails.configuration.resolve_render_options(self)
|
|
23
|
+
rendered = ReactEmailRails.render(component:, props: resolved_props, render_options:)
|
|
24
|
+
|
|
25
|
+
super(headers) do |format|
|
|
26
|
+
format.html { rendered.html }
|
|
27
|
+
format.text { rendered.text } if rendered.text.present?
|
|
28
|
+
yield(format) if block
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|