react-email-rails 0.1.0 → 0.1.2

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")) {
@@ -18,45 +19,49 @@ const logger = {
18
19
  hasWarned: false,
19
20
  }
20
21
 
21
- // 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)
22
+ const divertStdout = () => {
23
+ const write = process.stdout.write.bind(process.stdout)
24
+ process.stdout.write = (chunk, encoding, callback) => {
25
+ if (typeof encoding === "function") return process.stderr.write(chunk, encoding)
26
+ return process.stderr.write(chunk, encoding, callback)
27
+ }
28
+ return () => {
29
+ process.stdout.write = write
30
+ }
31
31
  }
32
32
 
33
+ // Load only this plugin and aliases; host dev-server plugins have global side effects.
34
+ const restoreStdout = divertStdout()
35
+ const { userConfig, plugin, vite } = await loadReactEmailRailsConfig({
36
+ command: "serve",
37
+ mode: "development",
38
+ })
39
+
33
40
  // Forward config that affects how components resolve and compile (but not the
34
41
  // host's dev-server plugins, which have global side effects), so dev rendering
35
42
  // 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
- })
43
+ const server = await createServer(
44
+ isolatedViteConfig(userConfig, vite, {
45
+ configFile: false,
46
+ plugins: [plugin],
47
+ server: { middlewareMode: true },
48
+ appType: "custom",
49
+ clearScreen: false,
50
+ customLogger: logger,
51
+ }),
52
+ )
48
53
 
49
54
  // Render through the same `email` environment the production build uses, so dev
50
55
  // and build resolve and compile components identically.
51
56
  const environment = server.environments.email
52
57
  if (!isRunnableDevEnvironment(environment)) {
53
58
  await server.close()
54
- process.stderr.write("react-email-rails: the email environment is not runnable\n")
55
- process.exit(1)
59
+ fail("react-email-rails: the email environment is not runnable")
56
60
  }
57
61
 
58
62
  try {
59
63
  const { run } = await environment.runner.import("virtual:react-email-rails/server")
64
+ restoreStdout()
60
65
  await run()
61
66
  } finally {
62
67
  await server.close()
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
@@ -4,9 +4,11 @@ const VIRTUAL_SERVER = "virtual:react-email-rails/server";
4
4
  const VIRTUAL_MAIN = "virtual:react-email-rails/main";
5
5
  const RESOLVED_SERVER = `\0${VIRTUAL_SERVER}`;
6
6
  const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`;
7
+ const VIRTUAL_MODULE_PATTERN = /virtual:react-email-rails\/(?:server|main)$/;
7
8
  // The dedicated build environment that emits the server-side email bundle.
8
9
  export const EMAIL_ENVIRONMENT = "email";
9
10
  const CONFIG_SYMBOL = Symbol.for("react-email-rails.config");
11
+ const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite");
10
12
  const OUT_DIR = "tmp/react-email-rails";
11
13
  const BUNDLE_FILE = "emails.js";
12
14
  export function reactEmailRails(options = {}) {
@@ -34,41 +36,48 @@ export function reactEmailRails(options = {}) {
34
36
  const globArg = JSON.stringify(globPatterns.length === 1 ? globPatterns[0] : globPatterns);
35
37
  const plugin = {
36
38
  name: "react-email-rails",
37
- resolveId(id) {
38
- if (id === VIRTUAL_SERVER)
39
- return RESOLVED_SERVER;
40
- if (id === VIRTUAL_MAIN)
41
- return RESOLVED_MAIN;
39
+ resolveId: {
40
+ filter: { id: VIRTUAL_MODULE_PATTERN },
41
+ handler(id) {
42
+ if (id === VIRTUAL_SERVER)
43
+ return RESOLVED_SERVER;
44
+ if (id === VIRTUAL_MAIN)
45
+ return RESOLVED_MAIN;
46
+ },
42
47
  },
43
- load(id) {
44
- if (id === RESOLVED_SERVER) {
45
- return [
46
- `import { serve, toComponentName } from "react-email-rails/runtime"`,
47
- `const modules = import.meta.glob(${globArg})`,
48
- `const extensions = ${JSON.stringify(extensions)}`,
49
- `const registry = Object.create(null)`,
50
- `for (const path in modules) {`,
51
- ` const extension = extensions.find((extension) => path.endsWith(extension)) ?? path.slice(path.lastIndexOf("."))`,
52
- ` registry[toComponentName(path, ${JSON.stringify(root)}, extension)] = modules[path]`,
53
- `}`,
54
- `export const run = () => serve(registry)`,
55
- ].join("\n");
56
- }
57
- if (id === RESOLVED_MAIN) {
58
- return `import { run } from ${JSON.stringify(VIRTUAL_SERVER)}\nrun()\n`;
59
- }
48
+ load: {
49
+ filter: { id: VIRTUAL_MODULE_PATTERN },
50
+ handler(id) {
51
+ if (id === RESOLVED_SERVER) {
52
+ return [
53
+ `import { serve, toComponentName } from "react-email-rails/runtime"`,
54
+ `const modules = import.meta.glob(${globArg})`,
55
+ `const extensions = ${JSON.stringify(extensions)}`,
56
+ `const registry = Object.create(null)`,
57
+ `for (const path in modules) {`,
58
+ ` const extension = extensions.find((extension) => path.endsWith(extension)) ?? path.slice(path.lastIndexOf("."))`,
59
+ ` registry[toComponentName(path, ${JSON.stringify(root)}, extension)] = modules[path]`,
60
+ `}`,
61
+ `export const run = () => serve(registry)`,
62
+ ].join("\n");
63
+ }
64
+ if (id === RESOLVED_MAIN) {
65
+ return `import { run } from ${JSON.stringify(VIRTUAL_SERVER)}\nrun()\n`;
66
+ }
67
+ },
60
68
  },
61
- 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.
65
- // The environment is a server consumer. Standalone builds inline Node
66
- // dependencies by default so Rails runtime images do not need node_modules.
69
+ config(_config, env) {
70
+ // Register a dedicated `email` build environment. The official
71
+ // react-email-rails-build bin opts into building it with an isolated
72
+ // plugin stack so host app plugins cannot break email SSR builds.
73
+ // The environment is a server consumer. Production standalone builds inline
74
+ // Node dependencies by default so Rails runtime images do not need
75
+ // node_modules; dev rendering keeps dependencies external for Vite's module
76
+ // runner.
67
77
  return {
68
- builder: {},
69
78
  environments: {
70
79
  [EMAIL_ENVIRONMENT]: {
71
- ...(standalone ? { resolve: { noExternal: true } } : {}),
80
+ ...(standalone && env.command === "build" ? { resolve: { noExternal: true } } : {}),
72
81
  build: {
73
82
  ssr: true,
74
83
  outDir: OUT_DIR,
@@ -98,6 +107,9 @@ export function reactEmailRails(options = {}) {
98
107
  ...metadata,
99
108
  },
100
109
  });
110
+ Object.defineProperty(plugin, VITE_CONFIG_SYMBOL, {
111
+ value: options.vite ?? {},
112
+ });
101
113
  return plugin;
102
114
  }
103
115
  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.2";
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.2";
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.2",
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 { ConfigEnv, 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 = {
@@ -31,10 +39,12 @@ const VIRTUAL_SERVER = "virtual:react-email-rails/server"
31
39
  const VIRTUAL_MAIN = "virtual:react-email-rails/main"
32
40
  const RESOLVED_SERVER = `\0${VIRTUAL_SERVER}`
33
41
  const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`
42
+ const VIRTUAL_MODULE_PATTERN = /virtual:react-email-rails\/(?:server|main)$/
34
43
 
35
44
  // The dedicated build environment that emits the server-side email bundle.
36
45
  export const EMAIL_ENVIRONMENT = "email"
37
46
  const CONFIG_SYMBOL = Symbol.for("react-email-rails.config")
47
+ const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite")
38
48
  const OUT_DIR = "tmp/react-email-rails"
39
49
  const BUNDLE_FILE = "emails.js"
40
50
 
@@ -72,42 +82,49 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
72
82
  const plugin: Plugin = {
73
83
  name: "react-email-rails",
74
84
 
75
- resolveId(id) {
76
- if (id === VIRTUAL_SERVER) return RESOLVED_SERVER
77
- if (id === VIRTUAL_MAIN) return RESOLVED_MAIN
85
+ resolveId: {
86
+ filter: { id: VIRTUAL_MODULE_PATTERN },
87
+ handler(id) {
88
+ if (id === VIRTUAL_SERVER) return RESOLVED_SERVER
89
+ if (id === VIRTUAL_MAIN) return RESOLVED_MAIN
90
+ },
78
91
  },
79
92
 
80
- load(id) {
81
- if (id === RESOLVED_SERVER) {
82
- return [
83
- `import { serve, toComponentName } from "react-email-rails/runtime"`,
84
- `const modules = import.meta.glob(${globArg})`,
85
- `const extensions = ${JSON.stringify(extensions)}`,
86
- `const registry = Object.create(null)`,
87
- `for (const path in modules) {`,
88
- ` const extension = extensions.find((extension) => path.endsWith(extension)) ?? path.slice(path.lastIndexOf("."))`,
89
- ` registry[toComponentName(path, ${JSON.stringify(root)}, extension)] = modules[path]`,
90
- `}`,
91
- `export const run = () => serve(registry)`,
92
- ].join("\n")
93
- }
94
-
95
- if (id === RESOLVED_MAIN) {
96
- return `import { run } from ${JSON.stringify(VIRTUAL_SERVER)}\nrun()\n`
97
- }
93
+ load: {
94
+ filter: { id: VIRTUAL_MODULE_PATTERN },
95
+ handler(id) {
96
+ if (id === RESOLVED_SERVER) {
97
+ return [
98
+ `import { serve, toComponentName } from "react-email-rails/runtime"`,
99
+ `const modules = import.meta.glob(${globArg})`,
100
+ `const extensions = ${JSON.stringify(extensions)}`,
101
+ `const registry = Object.create(null)`,
102
+ `for (const path in modules) {`,
103
+ ` const extension = extensions.find((extension) => path.endsWith(extension)) ?? path.slice(path.lastIndexOf("."))`,
104
+ ` registry[toComponentName(path, ${JSON.stringify(root)}, extension)] = modules[path]`,
105
+ `}`,
106
+ `export const run = () => serve(registry)`,
107
+ ].join("\n")
108
+ }
109
+
110
+ if (id === RESOLVED_MAIN) {
111
+ return `import { run } from ${JSON.stringify(VIRTUAL_SERVER)}\nrun()\n`
112
+ }
113
+ },
98
114
  },
99
115
 
100
- 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.
104
- // The environment is a server consumer. Standalone builds inline Node
105
- // dependencies by default so Rails runtime images do not need node_modules.
116
+ config(_config, env: ConfigEnv) {
117
+ // Register a dedicated `email` build environment. The official
118
+ // react-email-rails-build bin opts into building it with an isolated
119
+ // plugin stack so host app plugins cannot break email SSR builds.
120
+ // The environment is a server consumer. Production standalone builds inline
121
+ // Node dependencies by default so Rails runtime images do not need
122
+ // node_modules; dev rendering keeps dependencies external for Vite's module
123
+ // runner.
106
124
  return {
107
- builder: {},
108
125
  environments: {
109
126
  [EMAIL_ENVIRONMENT]: {
110
- ...(standalone ? { resolve: { noExternal: true } } : {}),
127
+ ...(standalone && env.command === "build" ? { resolve: { noExternal: true } } : {}),
111
128
  build: {
112
129
  ssr: true,
113
130
  outDir: OUT_DIR,
@@ -139,6 +156,9 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
139
156
  ...metadata,
140
157
  },
141
158
  })
159
+ Object.defineProperty(plugin, VITE_CONFIG_SYMBOL, {
160
+ value: options.vite ?? {},
161
+ })
142
162
 
143
163
  return plugin
144
164
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.1.0"
1
+ export const VERSION = "0.1.2"
2
2
  export const RENDER_PROTOCOL_VERSION = 1