react-email-rails 0.1.0 → 0.1.1

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.
package/bin/build.mjs ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import { createBuilder } from "vite"
3
+ import { fail, isolatedViteConfig, loadReactEmailRailsConfig } from "./shared.mjs"
4
+ import { RENDER_PROTOCOL_VERSION, VERSION } from "../dist/version.js"
5
+
6
+ if (process.argv.includes("--health")) {
7
+ process.stdout.write(
8
+ JSON.stringify({ ok: true, protocolVersion: RENDER_PROTOCOL_VERSION, packageVersion: VERSION }),
9
+ )
10
+ process.exit(0)
11
+ }
12
+
13
+ const args = process.argv.slice(2)
14
+ const readOption = (long, short) => {
15
+ const prefixed = args.find((arg) => arg.startsWith(`${long}=`))
16
+ if (prefixed) return prefixed.slice(long.length + 1)
17
+
18
+ const longIndex = args.indexOf(long)
19
+ if (longIndex !== -1) return args[longIndex + 1]
20
+
21
+ if (!short) return undefined
22
+ const shortIndex = args.indexOf(short)
23
+ return shortIndex === -1 ? undefined : args[shortIndex + 1]
24
+ }
25
+
26
+ const root = args.find((arg, index) => {
27
+ if (arg.startsWith("-")) return false
28
+ const previous = args[index - 1]
29
+ return (
30
+ previous !== "--mode" &&
31
+ previous !== "-m" &&
32
+ previous !== "--config" &&
33
+ previous !== "-c" &&
34
+ previous !== "--configLoader" &&
35
+ previous !== "--logLevel" &&
36
+ previous !== "-l"
37
+ )
38
+ })
39
+ const mode = readOption("--mode", "-m") ?? "production"
40
+ const configFile = readOption("--config", "-c")
41
+ const configLoader = readOption("--configLoader")
42
+ const logLevel = readOption("--logLevel", "-l")
43
+
44
+ process.env.NODE_ENV ??= "production"
45
+
46
+ const { userConfig, plugin, vite } = await loadReactEmailRailsConfig({
47
+ command: "build",
48
+ mode,
49
+ root,
50
+ configFile,
51
+ logLevel,
52
+ configLoader,
53
+ })
54
+
55
+ // Build production emails with the same isolation principle as the dev renderer:
56
+ // keep component resolution and transforms, but leave unrelated app plugins out
57
+ // of the email environment so client/global plugin hooks cannot break SSR output.
58
+ const builder = await createBuilder(
59
+ isolatedViteConfig(userConfig, vite, {
60
+ root: root ?? userConfig.root,
61
+ configFile: false,
62
+ mode,
63
+ builder: {},
64
+ plugins: [plugin],
65
+ appType: "custom",
66
+ clearScreen: false,
67
+ logLevel,
68
+ }),
69
+ null,
70
+ )
71
+
72
+ const environment = builder.environments.email
73
+ if (!environment) fail("react-email-rails: email build environment not found")
74
+
75
+ await builder.build(environment)
package/bin/config.mjs CHANGED
@@ -1,19 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { loadConfigFromFile } from "vite"
2
+ import { loadReactEmailRailsConfig } from "./shared.mjs"
3
3
 
4
- const loaded = await loadConfigFromFile({ command: "serve", mode: process.env.NODE_ENV ?? "development" })
5
- const plugins = (loaded?.config?.plugins ?? []).flat(Infinity).filter(Boolean)
6
- const plugin = plugins.find((plugin) => plugin.name === "react-email-rails")
7
- const metadata = plugin?.[Symbol.for("react-email-rails.config")]
8
-
9
- if (!plugin) {
10
- process.stderr.write("react-email-rails: reactEmailRails() plugin not found in the Vite config\n")
11
- process.exit(1)
12
- }
13
-
14
- if (!metadata) {
15
- process.stderr.write("react-email-rails: reactEmailRails() plugin metadata not found\n")
16
- process.exit(1)
17
- }
4
+ const { metadata } = await loadReactEmailRailsConfig({
5
+ command: "serve",
6
+ mode: process.env.NODE_ENV ?? "development",
7
+ })
18
8
 
19
9
  process.stdout.write(JSON.stringify(metadata))
package/bin/dev.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createServer, isRunnableDevEnvironment, loadConfigFromFile } from "vite"
2
+ import { createServer, isRunnableDevEnvironment } from "vite"
3
+ import { fail, isolatedViteConfig, loadReactEmailRailsConfig } from "./shared.mjs"
3
4
  import { RENDER_PROTOCOL_VERSION, VERSION } from "../dist/version.js"
4
5
 
5
6
  if (process.argv.includes("--health")) {
@@ -19,40 +20,31 @@ const logger = {
19
20
  }
20
21
 
21
22
  // Load only this plugin and aliases; host dev-server plugins have global side effects.
22
- const loaded = await loadConfigFromFile({ command: "serve", mode: "development" })
23
- const userConfig = loaded?.config ?? {}
24
- const emailPlugin = (userConfig.plugins ?? [])
25
- .flat(Infinity)
26
- .find((plugin) => plugin?.name === "react-email-rails")
27
-
28
- if (!emailPlugin) {
29
- process.stderr.write("react-email-rails: reactEmailRails() plugin not found in the Vite config\n")
30
- process.exit(1)
31
- }
23
+ const { userConfig, plugin, vite } = await loadReactEmailRailsConfig({
24
+ command: "serve",
25
+ mode: "development",
26
+ })
32
27
 
33
28
  // Forward config that affects how components resolve and compile (but not the
34
29
  // host's dev-server plugins, which have global side effects), so dev rendering
35
30
  // stays close to the production email bundle.
36
- const server = await createServer({
37
- configFile: false,
38
- resolve: userConfig.resolve,
39
- define: userConfig.define,
40
- css: userConfig.css,
41
- esbuild: { jsx: "automatic" },
42
- plugins: [emailPlugin],
43
- server: { middlewareMode: true },
44
- appType: "custom",
45
- clearScreen: false,
46
- customLogger: logger,
47
- })
31
+ const server = await createServer(
32
+ isolatedViteConfig(userConfig, vite, {
33
+ configFile: false,
34
+ plugins: [plugin],
35
+ server: { middlewareMode: true },
36
+ appType: "custom",
37
+ clearScreen: false,
38
+ customLogger: logger,
39
+ }),
40
+ )
48
41
 
49
42
  // Render through the same `email` environment the production build uses, so dev
50
43
  // and build resolve and compile components identically.
51
44
  const environment = server.environments.email
52
45
  if (!isRunnableDevEnvironment(environment)) {
53
46
  await server.close()
54
- process.stderr.write("react-email-rails: the email environment is not runnable\n")
55
- process.exit(1)
47
+ fail("react-email-rails: the email environment is not runnable")
56
48
  }
57
49
 
58
50
  try {
package/bin/shared.mjs ADDED
@@ -0,0 +1,76 @@
1
+ import { loadConfigFromFile, mergeConfig } from "vite"
2
+
3
+ const CONFIG_SYMBOL = Symbol.for("react-email-rails.config")
4
+ const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite")
5
+ const EMAIL_VITE_CONFIG_KEYS = [
6
+ "assetsInclude",
7
+ "css",
8
+ "define",
9
+ "esbuild",
10
+ "json",
11
+ "oxc",
12
+ "plugins",
13
+ "resolve",
14
+ ]
15
+
16
+ export async function loadReactEmailRailsConfig({
17
+ command,
18
+ mode,
19
+ root,
20
+ configFile,
21
+ logLevel,
22
+ configLoader,
23
+ }) {
24
+ const loaded = await loadConfigFromFile(
25
+ { command, mode },
26
+ configFile,
27
+ root,
28
+ logLevel,
29
+ undefined,
30
+ configLoader,
31
+ )
32
+ const userConfig = loaded?.config ?? {}
33
+ const plugin = (userConfig.plugins ?? [])
34
+ .flat(Infinity)
35
+ .find((plugin) => plugin?.name === "react-email-rails")
36
+ const metadata = plugin?.[CONFIG_SYMBOL]
37
+
38
+ if (!plugin) fail("react-email-rails: reactEmailRails() plugin not found in the Vite config")
39
+ if (!metadata) fail("react-email-rails: reactEmailRails() plugin metadata not found")
40
+
41
+ return {
42
+ userConfig,
43
+ plugin,
44
+ metadata,
45
+ vite: plugin[VITE_CONFIG_SYMBOL] ?? {},
46
+ }
47
+ }
48
+
49
+ export function isolatedViteConfig(userConfig, emailViteConfig, baseConfig) {
50
+ const userEsbuild =
51
+ userConfig.esbuild && typeof userConfig.esbuild === "object" ? userConfig.esbuild : {}
52
+ const forwarded = {
53
+ assetsInclude: userConfig.assetsInclude,
54
+ resolve: userConfig.resolve,
55
+ define: userConfig.define,
56
+ css: userConfig.css,
57
+ json: userConfig.json,
58
+ oxc: userConfig.oxc,
59
+ esbuild: { ...userEsbuild, jsx: userEsbuild.jsx ?? "automatic" },
60
+ }
61
+
62
+ return mergeConfig({ ...forwarded, ...baseConfig }, pickEmailViteConfig(emailViteConfig))
63
+ }
64
+
65
+ function pickEmailViteConfig(config) {
66
+ return Object.fromEntries(
67
+ EMAIL_VITE_CONFIG_KEYS.flatMap((key) =>
68
+ config && Object.hasOwn(config, key) ? [[key, config[key]]] : [],
69
+ ),
70
+ )
71
+ }
72
+
73
+ export function fail(message) {
74
+ process.stderr.write(`${message}\n`)
75
+ process.exit(1)
76
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Plugin } from "vite";
1
+ import type { Plugin, UserConfig } from "vite";
2
2
  export type EmailsOption = string | {
3
3
  path?: string;
4
4
  extension?: string | string[];
@@ -7,6 +7,10 @@ export type EmailsOption = string | {
7
7
  export type ReactEmailRailsOptions = {
8
8
  emails?: EmailsOption;
9
9
  standalone?: boolean;
10
+ vite?: ReactEmailRailsViteOptions;
11
+ };
12
+ export type ReactEmailRailsViteOptions = Pick<UserConfig, "assetsInclude" | "css" | "define" | "esbuild" | "json" | "plugins" | "resolve"> & {
13
+ oxc?: unknown;
10
14
  };
11
15
  export declare const EMAIL_ENVIRONMENT = "email";
12
16
  export declare function reactEmailRails(options?: ReactEmailRailsOptions): Plugin;
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`;
7
7
  // The dedicated build environment that emits the server-side email bundle.
8
8
  export const EMAIL_ENVIRONMENT = "email";
9
9
  const CONFIG_SYMBOL = Symbol.for("react-email-rails.config");
10
+ const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite");
10
11
  const OUT_DIR = "tmp/react-email-rails";
11
12
  const BUNDLE_FILE = "emails.js";
12
13
  export function reactEmailRails(options = {}) {
@@ -59,13 +60,12 @@ export function reactEmailRails(options = {}) {
59
60
  }
60
61
  },
61
62
  config() {
62
- // Register a dedicated `email` build environment and opt the app into
63
- // building all environments, so a plain `vite build` emits the email
64
- // bundle alongside the client assets no separate build step required.
63
+ // Register a dedicated `email` build environment. The official
64
+ // react-email-rails-build bin opts into building it with an isolated
65
+ // plugin stack so host app plugins cannot break email SSR builds.
65
66
  // The environment is a server consumer. Standalone builds inline Node
66
67
  // dependencies by default so Rails runtime images do not need node_modules.
67
68
  return {
68
- builder: {},
69
69
  environments: {
70
70
  [EMAIL_ENVIRONMENT]: {
71
71
  ...(standalone ? { resolve: { noExternal: true } } : {}),
@@ -98,6 +98,9 @@ export function reactEmailRails(options = {}) {
98
98
  ...metadata,
99
99
  },
100
100
  });
101
+ Object.defineProperty(plugin, VITE_CONFIG_SYMBOL, {
102
+ value: options.vite ?? {},
103
+ });
101
104
  return plugin;
102
105
  }
103
106
  export { RENDER_PROTOCOL_VERSION, VERSION } from "./version.js";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.1.1";
2
2
  export declare const RENDER_PROTOCOL_VERSION = 1;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.1.0";
1
+ export const VERSION = "0.1.1";
2
2
  export const RENDER_PROTOCOL_VERSION = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-rails",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Build and send emails using React and Rails",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "types": "./dist/index.d.ts",
39
39
  "bin": {
40
+ "react-email-rails-build": "./bin/build.mjs",
40
41
  "react-email-rails-config": "./bin/config.mjs",
41
42
  "react-email-rails-dev": "./bin/dev.mjs"
42
43
  },
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Plugin } from "vite"
1
+ import type { Plugin, UserConfig } from "vite"
2
2
 
3
3
  export type EmailsOption =
4
4
  | string
@@ -11,6 +11,14 @@ export type EmailsOption =
11
11
  export type ReactEmailRailsOptions = {
12
12
  emails?: EmailsOption
13
13
  standalone?: boolean
14
+ vite?: ReactEmailRailsViteOptions
15
+ }
16
+
17
+ export type ReactEmailRailsViteOptions = Pick<
18
+ UserConfig,
19
+ "assetsInclude" | "css" | "define" | "esbuild" | "json" | "plugins" | "resolve"
20
+ > & {
21
+ oxc?: unknown
14
22
  }
15
23
 
16
24
  type PluginMetadata = {
@@ -35,6 +43,7 @@ const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`
35
43
  // The dedicated build environment that emits the server-side email bundle.
36
44
  export const EMAIL_ENVIRONMENT = "email"
37
45
  const CONFIG_SYMBOL = Symbol.for("react-email-rails.config")
46
+ const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite")
38
47
  const OUT_DIR = "tmp/react-email-rails"
39
48
  const BUNDLE_FILE = "emails.js"
40
49
 
@@ -98,13 +107,12 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
98
107
  },
99
108
 
100
109
  config() {
101
- // Register a dedicated `email` build environment and opt the app into
102
- // building all environments, so a plain `vite build` emits the email
103
- // bundle alongside the client assets no separate build step required.
110
+ // Register a dedicated `email` build environment. The official
111
+ // react-email-rails-build bin opts into building it with an isolated
112
+ // plugin stack so host app plugins cannot break email SSR builds.
104
113
  // The environment is a server consumer. Standalone builds inline Node
105
114
  // dependencies by default so Rails runtime images do not need node_modules.
106
115
  return {
107
- builder: {},
108
116
  environments: {
109
117
  [EMAIL_ENVIRONMENT]: {
110
118
  ...(standalone ? { resolve: { noExternal: true } } : {}),
@@ -139,6 +147,9 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
139
147
  ...metadata,
140
148
  },
141
149
  })
150
+ Object.defineProperty(plugin, VITE_CONFIG_SYMBOL, {
151
+ value: options.vite ?? {},
152
+ })
142
153
 
143
154
  return plugin
144
155
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.1.0"
1
+ export const VERSION = "0.1.1"
2
2
  export const RENDER_PROTOCOL_VERSION = 1