react-email-rails 0.6.1 → 0.7.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.
- package/bin/build.mjs +0 -2
- package/bin/config.mjs +0 -0
- package/bin/dev.mjs +0 -4
- package/bin/shared.mjs +5 -4
- package/dist/document.js +0 -17
- package/dist/index.js +17 -10
- package/dist/runtime.js +8 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/document.ts +2 -21
- package/src/index.ts +21 -11
- package/src/runtime.ts +8 -3
- package/src/version.ts +1 -1
package/bin/build.mjs
CHANGED
|
@@ -51,8 +51,6 @@ const { userConfig, plugin, vite } = await loadReactEmailRailsConfig({
|
|
|
51
51
|
configLoader,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
// Same isolation as the dev renderer: keep component resolve/transforms but leave unrelated
|
|
55
|
-
// app plugins out of the email environment so their hooks can't break SSR output.
|
|
56
54
|
const builder = await createBuilder(
|
|
57
55
|
isolatedViteConfig(userConfig, vite, {
|
|
58
56
|
root: root ?? userConfig.root,
|
package/bin/config.mjs
CHANGED
|
File without changes
|
package/bin/dev.mjs
CHANGED
|
@@ -31,14 +31,12 @@ const divertStdout = () => {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Load only this plugin and aliases; host dev-server plugins have global side effects.
|
|
35
34
|
const restoreStdout = divertStdout()
|
|
36
35
|
const { userConfig, plugin, vite } = await loadReactEmailRailsConfig({
|
|
37
36
|
command: "serve",
|
|
38
37
|
mode: "development",
|
|
39
38
|
})
|
|
40
39
|
|
|
41
|
-
// Forward only component resolve/compile config, so dev rendering stays close to the build.
|
|
42
40
|
const server = await createServer(
|
|
43
41
|
isolatedViteConfig(userConfig, vite, {
|
|
44
42
|
configFile: false,
|
|
@@ -50,7 +48,6 @@ const server = await createServer(
|
|
|
50
48
|
}),
|
|
51
49
|
)
|
|
52
50
|
|
|
53
|
-
// Render through the same `email` environment as the production build, so the two match.
|
|
54
51
|
const environment = server.environments.email
|
|
55
52
|
if (!isRunnableDevEnvironment(environment)) {
|
|
56
53
|
await server.close()
|
|
@@ -59,7 +56,6 @@ if (!isRunnableDevEnvironment(environment)) {
|
|
|
59
56
|
|
|
60
57
|
try {
|
|
61
58
|
const { run } = await environment.runner.import("virtual:react-email-rails/server")
|
|
62
|
-
// Restore before run(): serve() re-isolates stdout with its own protocol writer.
|
|
63
59
|
restoreStdout()
|
|
64
60
|
await run()
|
|
65
61
|
} finally {
|
package/bin/shared.mjs
CHANGED
|
@@ -2,10 +2,8 @@ import { loadConfigFromFile, mergeConfig } from "vite"
|
|
|
2
2
|
|
|
3
3
|
import { RENDER_PROTOCOL_VERSION, VERSION } from "../dist/version.js"
|
|
4
4
|
|
|
5
|
-
// Wire contract: must match the Symbol.for(...) keys in src/index.ts.
|
|
6
5
|
const CONFIG_SYMBOL = Symbol.for("react-email-rails.config")
|
|
7
6
|
const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite")
|
|
8
|
-
// Must mirror ReactEmailRailsViteOptions in src/index.ts and the option list in README.md.
|
|
9
7
|
const EMAIL_VITE_CONFIG_KEYS = [
|
|
10
8
|
"assetsInclude",
|
|
11
9
|
"css",
|
|
@@ -17,12 +15,15 @@ const EMAIL_VITE_CONFIG_KEYS = [
|
|
|
17
15
|
"resolve",
|
|
18
16
|
]
|
|
19
17
|
|
|
20
|
-
// Emit the version/protocol handshake and exit on --health. Shared by the build and dev bins.
|
|
21
18
|
export function exitIfHealthCheck() {
|
|
22
19
|
if (!process.argv.includes("--health")) return
|
|
23
20
|
|
|
24
21
|
process.stdout.write(
|
|
25
|
-
JSON.stringify({
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
ok: true,
|
|
24
|
+
protocolVersion: RENDER_PROTOCOL_VERSION,
|
|
25
|
+
packageVersion: VERSION,
|
|
26
|
+
}),
|
|
26
27
|
)
|
|
27
28
|
process.exit(0)
|
|
28
29
|
}
|
package/dist/document.js
CHANGED
|
@@ -2,13 +2,9 @@ import { EmailNode, composeReactEmail } from "@react-email/editor/core";
|
|
|
2
2
|
import { StarterKit } from "@react-email/editor/extensions";
|
|
3
3
|
import { EmailTheming } from "@react-email/editor/plugins";
|
|
4
4
|
import { getSchema, resolveExtensions } from "@tiptap/core";
|
|
5
|
-
// Editor-bundled structural nodes render to null by design. Derive the list from
|
|
6
|
-
// the installed editor package so warning filtering tracks version changes.
|
|
7
5
|
const STRUCTURAL_NODE_TYPES = new Set(resolveExtensions([StarterKit, EmailTheming])
|
|
8
6
|
.filter((extension) => extension.type === "node" && !(extension instanceof EmailNode))
|
|
9
7
|
.map((extension) => extension.name));
|
|
10
|
-
// composeReactEmail renders a node as null when no extension matches or the match isn't an
|
|
11
|
-
// EmailNode; mirror that here so warnings catch the silent case (in-schema node, no email renderer).
|
|
12
8
|
function collectDroppedNodes(document, extensions) {
|
|
13
9
|
const byName = new Map();
|
|
14
10
|
for (const extension of extensions)
|
|
@@ -69,11 +65,7 @@ async function loadRenderMarkdown() {
|
|
|
69
65
|
}
|
|
70
66
|
throw new Error("marked is missing the expected parse export; check the installed version");
|
|
71
67
|
}
|
|
72
|
-
// The schema whitelists nodes and attributes but never validates URI protocols, so a
|
|
73
|
-
// javascript:/data: href on a link or button reaches content_json unchecked. Allow only safe schemes.
|
|
74
68
|
const ALLOWED_URI_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
75
|
-
// Characters browsers ignore when resolving a scheme (so "java\tscript:" runs as javascript:).
|
|
76
|
-
// Built numerically to keep the source free of literal control characters.
|
|
77
69
|
const URI_IGNORED_RANGES = [
|
|
78
70
|
[0x00, 0x20],
|
|
79
71
|
[0xa0, 0xa0],
|
|
@@ -89,12 +81,9 @@ const URI_IGNORED_CHARS = new RegExp("[" +
|
|
|
89
81
|
URI_IGNORED_RANGES.map(([lo, hi]) => escapeCodePoint(lo) + "-" + escapeCodePoint(hi)).join("") +
|
|
90
82
|
"]", "g");
|
|
91
83
|
function hasAllowedUriScheme(uri) {
|
|
92
|
-
// No scheme → relative/anchor/query; nothing to neutralize.
|
|
93
84
|
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(uri.replace(URI_IGNORED_CHARS, ""))?.[1];
|
|
94
85
|
return scheme === undefined || ALLOWED_URI_SCHEMES.has(scheme.toLowerCase());
|
|
95
86
|
}
|
|
96
|
-
// Blank disallowed hrefs (link marks and nodes like button) in place; the tree is fresh
|
|
97
|
-
// toJSON() output, so mutation is safe.
|
|
98
87
|
function neutralizeUnsafeUris(value) {
|
|
99
88
|
if (Array.isArray(value)) {
|
|
100
89
|
for (const item of value)
|
|
@@ -111,7 +100,6 @@ function neutralizeUnsafeUris(value) {
|
|
|
111
100
|
neutralizeUnsafeUris(node.marks);
|
|
112
101
|
neutralizeUnsafeUris(node.content);
|
|
113
102
|
}
|
|
114
|
-
// Both inputs converge on HTML: markdown is rendered first, then parsed like any HTML.
|
|
115
103
|
async function resolveHtmlInput(request, dependencies) {
|
|
116
104
|
const hasHtml = request.html !== undefined;
|
|
117
105
|
const hasMarkdown = request.markdown !== undefined;
|
|
@@ -125,7 +113,6 @@ async function resolveHtmlInput(request, dependencies) {
|
|
|
125
113
|
return request.html;
|
|
126
114
|
}
|
|
127
115
|
export async function composeDocument(request, registry) {
|
|
128
|
-
// Fail legibly if the optional editor peers are present but their shape shifted.
|
|
129
116
|
if (typeof composeReactEmail !== "function" ||
|
|
130
117
|
typeof resolveExtensions !== "function" ||
|
|
131
118
|
typeof getSchema !== "function") {
|
|
@@ -136,15 +123,12 @@ export async function composeDocument(request, registry) {
|
|
|
136
123
|
const extensions = resolveExtensions(renderer.buildExtensions(request.context));
|
|
137
124
|
const schema = getSchema(extensions);
|
|
138
125
|
const warnings = collectDroppedNodes(document, extensions);
|
|
139
|
-
// The minimal editor composeReactEmail reads, built headless (no DOM, no view).
|
|
140
|
-
// state.doc is required: EmailTheming finds the globalContent theme node through it.
|
|
141
126
|
const editor = {
|
|
142
127
|
getJSON: () => document,
|
|
143
128
|
extensionManager: { extensions },
|
|
144
129
|
schema,
|
|
145
130
|
state: { doc: schema.nodeFromJSON(document) },
|
|
146
131
|
};
|
|
147
|
-
// composeReactEmail takes `preview?: string`; omit it rather than pass null.
|
|
148
132
|
const preview = request.preview ?? renderer.getPreview?.(request.context) ?? null;
|
|
149
133
|
const params = preview === null ? { editor } : { editor, preview };
|
|
150
134
|
const { html, text } = await composeReactEmail(params);
|
|
@@ -162,7 +146,6 @@ export async function parseDocument(request, registry, dependencies = {}) {
|
|
|
162
146
|
return { document };
|
|
163
147
|
}
|
|
164
148
|
export function createParseDocument(generateJSON, renderMarkdown) {
|
|
165
|
-
// Omit renderMarkdown entirely when unset; exactOptionalPropertyTypes forbids passing `undefined`.
|
|
166
149
|
const dependencies = renderMarkdown === undefined ? { generateJSON } : { generateJSON, renderMarkdown };
|
|
167
150
|
return (request, registry) => parseDocument(request, registry, dependencies);
|
|
168
151
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,18 +9,12 @@ const VIRTUAL_MAIN = "virtual:react-email-rails/main";
|
|
|
9
9
|
const RESOLVED_SERVER = `\0${VIRTUAL_SERVER}`;
|
|
10
10
|
const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`;
|
|
11
11
|
const VIRTUAL_MODULE_PATTERN = /virtual:react-email-rails\/(?:server|main)$/;
|
|
12
|
-
// The dedicated build environment that emits the server-side email bundle.
|
|
13
12
|
export const EMAIL_ENVIRONMENT = "email";
|
|
14
|
-
// Wire contract: must match the Symbol.for(...) keys the bins read in bin/shared.mjs.
|
|
15
13
|
const CONFIG_SYMBOL = Symbol.for("react-email-rails.config");
|
|
16
14
|
const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite");
|
|
17
|
-
// Must match Ruby's Configuration::BUNDLE_PATH (check_version_sync.rb asserts it).
|
|
18
15
|
const OUT_DIR = "tmp/react-email-rails";
|
|
19
16
|
const BUNDLE_FILE = "emails.js";
|
|
20
17
|
const require = createRequire(import.meta.url);
|
|
21
|
-
// happy-dom (via @tiptap/html) pulls in `ws`, which guards optional native-addon requires behind
|
|
22
|
-
// these flags. Setting them lets a standalone (noExternal) build tree-shake the uninstalled
|
|
23
|
-
// bufferutil/utf-8-validate requires away; ws's pure-JS path is all the HTML parser needs.
|
|
24
18
|
const WS_NATIVE_ADDON_OPT_OUT = {
|
|
25
19
|
"process.env.WS_NO_BUFFER_UTIL": "'1'",
|
|
26
20
|
"process.env.WS_NO_UTF_8_VALIDATE": "'1'",
|
|
@@ -49,6 +43,13 @@ function normalizeSource(option, defaultPath, defaultExtensions) {
|
|
|
49
43
|
const globArg = JSON.stringify(globPatterns.length === 1 ? globPatterns[0] : globPatterns);
|
|
50
44
|
return { path, extensions, ignore, root, globArg };
|
|
51
45
|
}
|
|
46
|
+
function normalizePath(path) {
|
|
47
|
+
return path.replace(/\\/g, "/");
|
|
48
|
+
}
|
|
49
|
+
function isWithinSource(file, root, relativeDir) {
|
|
50
|
+
const base = `${normalizePath(root).replace(/\/+$/, "")}/${relativeDir}/`;
|
|
51
|
+
return normalizePath(file).startsWith(base);
|
|
52
|
+
}
|
|
52
53
|
function optionalPeersAvailable(specifiers) {
|
|
53
54
|
return specifiers.every((specifier) => {
|
|
54
55
|
try {
|
|
@@ -65,6 +66,7 @@ export function reactEmailRails(options = {}) {
|
|
|
65
66
|
const documentSource = options.documents === undefined || options.documents === false
|
|
66
67
|
? null
|
|
67
68
|
: normalizeSource(options.documents === true ? undefined : options.documents, DEFAULT_DOCUMENT_PATH, DEFAULT_DOCUMENT_EXTENSIONS);
|
|
69
|
+
const liveReloadDirs = [emailSource.path, documentSource?.path].filter((path) => Boolean(path));
|
|
68
70
|
const standalone = options.standalone ?? true;
|
|
69
71
|
const plugin = {
|
|
70
72
|
name: "react-email-rails",
|
|
@@ -83,7 +85,6 @@ export function reactEmailRails(options = {}) {
|
|
|
83
85
|
if (id === RESOLVED_SERVER) {
|
|
84
86
|
const lines = [`import { buildRegistry, serve } from "react-email-rails/runtime"`];
|
|
85
87
|
const parserPeersAvailable = documentSource && optionalPeersAvailable(["@tiptap/html", "happy-dom"]);
|
|
86
|
-
// Markdown parsing layers `marked` on top of the HTML parser peers.
|
|
87
88
|
const markdownPeerAvailable = parserPeersAvailable && optionalPeersAvailable(["marked"]);
|
|
88
89
|
if (documentSource) {
|
|
89
90
|
lines.push(parserPeersAvailable
|
|
@@ -109,10 +110,16 @@ export function reactEmailRails(options = {}) {
|
|
|
109
110
|
}
|
|
110
111
|
},
|
|
111
112
|
},
|
|
113
|
+
hotUpdate(options) {
|
|
114
|
+
const root = options.server.config.root;
|
|
115
|
+
if (!liveReloadDirs.some((dir) => isWithinSource(options.file, root, dir)))
|
|
116
|
+
return;
|
|
117
|
+
if (this.environment.name !== "client")
|
|
118
|
+
return;
|
|
119
|
+
this.environment.hot.send({ type: "full-reload" });
|
|
120
|
+
return [];
|
|
121
|
+
},
|
|
112
122
|
config(_config, env) {
|
|
113
|
-
// Dedicated `email` build environment: the react-email-rails-build bin builds it with an
|
|
114
|
-
// isolated plugin stack so host plugins can't break email SSR. Standalone builds inline
|
|
115
|
-
// Node deps (so Rails images need no node_modules); dev keeps them external for the runner.
|
|
116
123
|
return {
|
|
117
124
|
environments: {
|
|
118
125
|
[EMAIL_ENVIRONMENT]: {
|
package/dist/runtime.js
CHANGED
|
@@ -20,8 +20,14 @@ export async function renderEmail(request, registry) {
|
|
|
20
20
|
const mod = typeof loader === "function" ? await loader() : loader;
|
|
21
21
|
const element = React.createElement(mod.default, request.props ?? {});
|
|
22
22
|
return {
|
|
23
|
-
html: await render(element, {
|
|
24
|
-
|
|
23
|
+
html: await render(element, {
|
|
24
|
+
...request.renderOptions?.html,
|
|
25
|
+
plainText: false,
|
|
26
|
+
}),
|
|
27
|
+
text: await render(element, {
|
|
28
|
+
...request.renderOptions?.text,
|
|
29
|
+
plainText: true,
|
|
30
|
+
}),
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
function isDocumentRequest(request) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.7.0";
|
|
2
2
|
export declare const RENDER_PROTOCOL_VERSION = 3;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.7.0";
|
|
2
2
|
export const RENDER_PROTOCOL_VERSION = 3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email-rails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Build and send emails using React and Rails",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
"check:version": "ruby ../scripts/check_version_sync.rb",
|
|
51
51
|
"lint": "oxlint src test bin",
|
|
52
52
|
"format": "oxfmt src test bin",
|
|
53
|
+
"check:format": "oxfmt --check src test bin",
|
|
53
54
|
"typecheck": "tsgo -p tsconfig.json --noEmit",
|
|
54
55
|
"test": "vitest run",
|
|
55
56
|
"sync:version": "ruby ../scripts/sync_version.rb",
|
|
56
|
-
"ci": "pnpm run check:version && pnpm build && pnpm lint && pnpm typecheck && pnpm test",
|
|
57
|
+
"ci": "pnpm run check:version && pnpm run check:format && pnpm build && pnpm lint && pnpm typecheck && pnpm test",
|
|
57
58
|
"prepack": "pnpm run sync:version && pnpm build",
|
|
58
59
|
"prepublishOnly": "pnpm run check:version"
|
|
59
60
|
},
|
package/src/document.ts
CHANGED
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
RenderResult,
|
|
13
13
|
} from "./runtime.js"
|
|
14
14
|
|
|
15
|
-
// Re-exported from runtime.ts (the single source) to keep react-email-rails/document's surface.
|
|
16
15
|
export type { DroppedNode, ParseDocumentRequest, RenderDocumentRequest }
|
|
17
16
|
|
|
18
17
|
export type DocumentRenderer = {
|
|
@@ -21,16 +20,12 @@ export type DocumentRenderer = {
|
|
|
21
20
|
getPreview?: (context: unknown) => string | null
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
// Editor-bundled structural nodes render to null by design. Derive the list from
|
|
25
|
-
// the installed editor package so warning filtering tracks version changes.
|
|
26
23
|
const STRUCTURAL_NODE_TYPES: ReadonlySet<string> = new Set(
|
|
27
24
|
resolveExtensions([StarterKit, EmailTheming])
|
|
28
25
|
.filter((extension) => extension.type === "node" && !(extension instanceof EmailNode))
|
|
29
26
|
.map((extension) => extension.name),
|
|
30
27
|
)
|
|
31
28
|
|
|
32
|
-
// composeReactEmail renders a node as null when no extension matches or the match isn't an
|
|
33
|
-
// EmailNode; mirror that here so warnings catch the silent case (in-schema node, no email renderer).
|
|
34
29
|
function collectDroppedNodes(document: unknown, extensions: Extensions): DroppedNode[] {
|
|
35
30
|
const byName = new Map<string, Extensions[number]>()
|
|
36
31
|
for (const extension of extensions) byName.set(extension.name, extension)
|
|
@@ -62,7 +57,6 @@ export type DocumentRegistry = Record<string, DocumentLoader>
|
|
|
62
57
|
type GenerateJSON = (html: string, extensions: Extensions) => unknown
|
|
63
58
|
type RenderMarkdown = (markdown: string) => string | Promise<string>
|
|
64
59
|
|
|
65
|
-
// Bound at build time by createParseDocument when the peers are bundled; lazy-loaded otherwise.
|
|
66
60
|
type ParseDependencies = {
|
|
67
61
|
generateJSON?: GenerateJSON
|
|
68
62
|
renderMarkdown?: RenderMarkdown
|
|
@@ -120,12 +114,8 @@ async function loadRenderMarkdown(): Promise<RenderMarkdown> {
|
|
|
120
114
|
throw new Error("marked is missing the expected parse export; check the installed version")
|
|
121
115
|
}
|
|
122
116
|
|
|
123
|
-
// The schema whitelists nodes and attributes but never validates URI protocols, so a
|
|
124
|
-
// javascript:/data: href on a link or button reaches content_json unchecked. Allow only safe schemes.
|
|
125
117
|
const ALLOWED_URI_SCHEMES: ReadonlySet<string> = new Set(["http", "https", "mailto", "tel"])
|
|
126
118
|
|
|
127
|
-
// Characters browsers ignore when resolving a scheme (so "java\tscript:" runs as javascript:).
|
|
128
|
-
// Built numerically to keep the source free of literal control characters.
|
|
129
119
|
const URI_IGNORED_RANGES: ReadonlyArray<readonly [number, number]> = [
|
|
130
120
|
[0x00, 0x20],
|
|
131
121
|
[0xa0, 0xa0],
|
|
@@ -145,14 +135,11 @@ const URI_IGNORED_CHARS = new RegExp(
|
|
|
145
135
|
)
|
|
146
136
|
|
|
147
137
|
function hasAllowedUriScheme(uri: string): boolean {
|
|
148
|
-
// No scheme → relative/anchor/query; nothing to neutralize.
|
|
149
138
|
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(uri.replace(URI_IGNORED_CHARS, ""))?.[1]
|
|
150
139
|
|
|
151
140
|
return scheme === undefined || ALLOWED_URI_SCHEMES.has(scheme.toLowerCase())
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
// Blank disallowed hrefs (link marks and nodes like button) in place; the tree is fresh
|
|
155
|
-
// toJSON() output, so mutation is safe.
|
|
156
143
|
function neutralizeUnsafeUris(value: unknown): void {
|
|
157
144
|
if (Array.isArray(value)) {
|
|
158
145
|
for (const item of value) neutralizeUnsafeUris(item)
|
|
@@ -176,7 +163,6 @@ function neutralizeUnsafeUris(value: unknown): void {
|
|
|
176
163
|
neutralizeUnsafeUris(node.content)
|
|
177
164
|
}
|
|
178
165
|
|
|
179
|
-
// Both inputs converge on HTML: markdown is rendered first, then parsed like any HTML.
|
|
180
166
|
async function resolveHtmlInput(
|
|
181
167
|
request: ParseDocumentRequest,
|
|
182
168
|
dependencies: ParseDependencies,
|
|
@@ -199,7 +185,6 @@ export async function composeDocument(
|
|
|
199
185
|
request: RenderDocumentRequest,
|
|
200
186
|
registry: DocumentRegistry,
|
|
201
187
|
): Promise<RenderResult> {
|
|
202
|
-
// Fail legibly if the optional editor peers are present but their shape shifted.
|
|
203
188
|
if (
|
|
204
189
|
typeof composeReactEmail !== "function" ||
|
|
205
190
|
typeof resolveExtensions !== "function" ||
|
|
@@ -218,8 +203,6 @@ export async function composeDocument(
|
|
|
218
203
|
const schema = getSchema(extensions)
|
|
219
204
|
const warnings = collectDroppedNodes(document, extensions)
|
|
220
205
|
|
|
221
|
-
// The minimal editor composeReactEmail reads, built headless (no DOM, no view).
|
|
222
|
-
// state.doc is required: EmailTheming finds the globalContent theme node through it.
|
|
223
206
|
const editor = {
|
|
224
207
|
getJSON: () => document,
|
|
225
208
|
extensionManager: { extensions },
|
|
@@ -227,11 +210,10 @@ export async function composeDocument(
|
|
|
227
210
|
state: { doc: schema.nodeFromJSON(document) },
|
|
228
211
|
} as unknown as Editor
|
|
229
212
|
|
|
230
|
-
// composeReactEmail takes `preview?: string`; omit it rather than pass null.
|
|
231
213
|
const preview = request.preview ?? renderer.getPreview?.(request.context) ?? null
|
|
232
214
|
const params = preview === null ? { editor } : { editor, preview }
|
|
233
|
-
|
|
234
215
|
const { html, text } = await composeReactEmail(params)
|
|
216
|
+
|
|
235
217
|
return warnings.length > 0 ? { html, text, warnings } : { html, text }
|
|
236
218
|
}
|
|
237
219
|
|
|
@@ -243,18 +225,17 @@ export async function parseDocument(
|
|
|
243
225
|
const renderer = await resolveRenderer(request.type, registry)
|
|
244
226
|
const extensions = resolveExtensions(renderer.buildExtensions(request.context))
|
|
245
227
|
const schema = getSchema(extensions)
|
|
246
|
-
|
|
247
228
|
const html = await resolveHtmlInput(request, dependencies)
|
|
248
229
|
const parseHTML = dependencies.generateJSON ?? (await loadGenerateJSON())
|
|
249
230
|
const parsed = parseHTML(html, extensions)
|
|
250
231
|
const document = schema.nodeFromJSON(parsed).toJSON()
|
|
232
|
+
|
|
251
233
|
neutralizeUnsafeUris(document)
|
|
252
234
|
|
|
253
235
|
return { document }
|
|
254
236
|
}
|
|
255
237
|
|
|
256
238
|
export function createParseDocument(generateJSON: GenerateJSON, renderMarkdown?: RenderMarkdown) {
|
|
257
|
-
// Omit renderMarkdown entirely when unset; exactOptionalPropertyTypes forbids passing `undefined`.
|
|
258
239
|
const dependencies: ParseDependencies =
|
|
259
240
|
renderMarkdown === undefined ? { generateJSON } : { generateJSON, renderMarkdown }
|
|
260
241
|
return (request: ParseDocumentRequest, registry: DocumentRegistry): Promise<ParseResult> =>
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type EmailsOption =
|
|
|
12
12
|
|
|
13
13
|
export type ReactEmailRailsOptions = {
|
|
14
14
|
emails?: EmailsOption
|
|
15
|
-
// Editor document renderers, discovered like emails. Off by default.
|
|
16
15
|
documents?: EmailsOption | boolean
|
|
17
16
|
standalone?: boolean
|
|
18
17
|
vite?: ReactEmailRailsViteOptions
|
|
@@ -59,19 +58,13 @@ const RESOLVED_SERVER = `\0${VIRTUAL_SERVER}`
|
|
|
59
58
|
const RESOLVED_MAIN = `\0${VIRTUAL_MAIN}`
|
|
60
59
|
const VIRTUAL_MODULE_PATTERN = /virtual:react-email-rails\/(?:server|main)$/
|
|
61
60
|
|
|
62
|
-
// The dedicated build environment that emits the server-side email bundle.
|
|
63
61
|
export const EMAIL_ENVIRONMENT = "email"
|
|
64
|
-
// Wire contract: must match the Symbol.for(...) keys the bins read in bin/shared.mjs.
|
|
65
62
|
const CONFIG_SYMBOL = Symbol.for("react-email-rails.config")
|
|
66
63
|
const VITE_CONFIG_SYMBOL = Symbol.for("react-email-rails.vite")
|
|
67
|
-
// Must match Ruby's Configuration::BUNDLE_PATH (check_version_sync.rb asserts it).
|
|
68
64
|
const OUT_DIR = "tmp/react-email-rails"
|
|
69
65
|
const BUNDLE_FILE = "emails.js"
|
|
70
66
|
const require = createRequire(import.meta.url)
|
|
71
67
|
|
|
72
|
-
// happy-dom (via @tiptap/html) pulls in `ws`, which guards optional native-addon requires behind
|
|
73
|
-
// these flags. Setting them lets a standalone (noExternal) build tree-shake the uninstalled
|
|
74
|
-
// bufferutil/utf-8-validate requires away; ws's pure-JS path is all the HTML parser needs.
|
|
75
68
|
const WS_NATIVE_ADDON_OPT_OUT = {
|
|
76
69
|
"process.env.WS_NO_BUFFER_UTIL": "'1'",
|
|
77
70
|
"process.env.WS_NO_UTF_8_VALIDATE": "'1'",
|
|
@@ -113,6 +106,15 @@ function normalizeSource(
|
|
|
113
106
|
return { path, extensions, ignore, root, globArg }
|
|
114
107
|
}
|
|
115
108
|
|
|
109
|
+
function normalizePath(path: string): string {
|
|
110
|
+
return path.replace(/\\/g, "/")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isWithinSource(file: string, root: string, relativeDir: string): boolean {
|
|
114
|
+
const base = `${normalizePath(root).replace(/\/+$/, "")}/${relativeDir}/`
|
|
115
|
+
return normalizePath(file).startsWith(base)
|
|
116
|
+
}
|
|
117
|
+
|
|
116
118
|
function optionalPeersAvailable(specifiers: string[]): boolean {
|
|
117
119
|
return specifiers.every((specifier) => {
|
|
118
120
|
try {
|
|
@@ -134,6 +136,9 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
|
|
|
134
136
|
DEFAULT_DOCUMENT_PATH,
|
|
135
137
|
DEFAULT_DOCUMENT_EXTENSIONS,
|
|
136
138
|
)
|
|
139
|
+
const liveReloadDirs = [emailSource.path, documentSource?.path].filter((path): path is string =>
|
|
140
|
+
Boolean(path),
|
|
141
|
+
)
|
|
137
142
|
const standalone = options.standalone ?? true
|
|
138
143
|
|
|
139
144
|
const plugin: Plugin = {
|
|
@@ -154,7 +159,6 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
|
|
|
154
159
|
const lines = [`import { buildRegistry, serve } from "react-email-rails/runtime"`]
|
|
155
160
|
const parserPeersAvailable =
|
|
156
161
|
documentSource && optionalPeersAvailable(["@tiptap/html", "happy-dom"])
|
|
157
|
-
// Markdown parsing layers `marked` on top of the HTML parser peers.
|
|
158
162
|
const markdownPeerAvailable = parserPeersAvailable && optionalPeersAvailable(["marked"])
|
|
159
163
|
|
|
160
164
|
if (documentSource) {
|
|
@@ -191,10 +195,16 @@ export function reactEmailRails(options: ReactEmailRailsOptions = {}): Plugin {
|
|
|
191
195
|
},
|
|
192
196
|
},
|
|
193
197
|
|
|
198
|
+
hotUpdate(options) {
|
|
199
|
+
const root = options.server.config.root
|
|
200
|
+
if (!liveReloadDirs.some((dir) => isWithinSource(options.file, root, dir))) return
|
|
201
|
+
if (this.environment.name !== "client") return
|
|
202
|
+
|
|
203
|
+
this.environment.hot.send({ type: "full-reload" })
|
|
204
|
+
return []
|
|
205
|
+
},
|
|
206
|
+
|
|
194
207
|
config(_config, env: ConfigEnv) {
|
|
195
|
-
// Dedicated `email` build environment: the react-email-rails-build bin builds it with an
|
|
196
|
-
// isolated plugin stack so host plugins can't break email SSR. Standalone builds inline
|
|
197
|
-
// Node deps (so Rails images need no node_modules); dev keeps them external for the runner.
|
|
198
208
|
return {
|
|
199
209
|
environments: {
|
|
200
210
|
[EMAIL_ENVIRONMENT]: {
|
package/src/runtime.ts
CHANGED
|
@@ -47,7 +47,6 @@ export type RenderDocumentRequest = {
|
|
|
47
47
|
preview?: string | null
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// Exactly one of `html` or `markdown` is set.
|
|
51
50
|
export type ParseDocumentRequest = {
|
|
52
51
|
kind: "parse"
|
|
53
52
|
type: string
|
|
@@ -108,8 +107,14 @@ export async function renderEmail(
|
|
|
108
107
|
const element = React.createElement(mod.default, request.props ?? {})
|
|
109
108
|
|
|
110
109
|
return {
|
|
111
|
-
html: await render(element, {
|
|
112
|
-
|
|
110
|
+
html: await render(element, {
|
|
111
|
+
...request.renderOptions?.html,
|
|
112
|
+
plainText: false,
|
|
113
|
+
}),
|
|
114
|
+
text: await render(element, {
|
|
115
|
+
...request.renderOptions?.text,
|
|
116
|
+
plainText: true,
|
|
117
|
+
}),
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.7.0"
|
|
2
2
|
export const RENDER_PROTOCOL_VERSION = 3
|