Skip to content

refactor(jsx-email): use rehype rawPlugin to hoist <Raw> content#320

Merged
shellscape merged 5 commits into
mainfrom
ai-319-refactor-use-rehype-plugin-to-hoist-raw-com
Oct 16, 2025
Merged

refactor(jsx-email): use rehype rawPlugin to hoist <Raw> content#320
shellscape merged 5 commits into
mainfrom
ai-319-refactor-use-rehype-plugin-to-hoist-raw-com

Conversation

@charliecreates

@charliecreates charliecreates Bot commented Oct 16, 2025

Copy link
Copy Markdown
Contributor

Component / Package Name:

jsx-email

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

If yes, please include "BREAKING CHANGES:" in the first commit message body, followed by a description of what is breaking.

List any relevant issue numbers:

resolves #319

Description

Refactors the renderer to hoist <Raw> component content using a dedicated rehype plugin, mirroring the existing move-style pattern. This removes the brittle post-stringify replacement and performs the transformation during AST processing instead.

Key points:

  • Add getRawPlugin() in packages/jsx-email/src/renderer/raw.ts that:
    • finds <jsx-email-raw> elements,
    • extracts the inner HTML comment payload,
    • unescapes it via unescapeForRawComponent(), and
    • replaces the wrapper with a raw node so content is injected verbatim during stringify.
  • Wire the plugin into the rehype processor in packages/jsx-email/src/renderer/render.ts next to the existing move plugin.
  • Remove the post-stringify regex for <jsx-email-raw>…</jsx-email-raw> replacement.
  • Strengthen HAST typings (model the raw node locally; avoid any casts) and keep existing escaping helpers intact.
  • Behavior is unchanged for consumers; this is an internal refactor focused on clarity and maintainability.

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Registering rawPlugin before callProcessHook changes what downstream plugins see (now raw nodes, previously wrapper elements), which can be a backward-compatibility risk.
  • The transform uses multiple any casts that can be avoided with stronger HAST typings, improving safety and maintainability.
  • If the comment payload is missing, the plugin leaves <jsx-email-raw> wrappers in the output; dropping the wrapper would be safer to prevent leaking non-standard tags.
Additional notes (1)
  • Maintainability | packages/jsx-email/src/renderer/raw.ts:44-58
    The comment extraction and replacement logic works, but it relies on multiple any casts. This hides potential mistakes and makes the transform harder to maintain. You can tighten the types by leveraging Comment and Parent from hast and a type guard for the comment node, avoiding as any for parent and the child search.
Summary of changes
  • Introduced an async getRawPlugin() rehype plugin in raw.ts to replace <jsx-email-raw> wrapper elements with raw nodes containing unescaped HTML from the inner comment payload.
  • Wired the new plugin into the rehype pipeline in render.ts (after movePlugin).
  • Removed the post-stringify regex that previously handled <jsx-email-raw> replacement; the plugin now performs this during AST processing.
  • Kept existing escapeForRawComponent()/unescapeForRawComponent() helpers and overall rendering behavior intact, including allowDangerousHtml in rehype-stringify.

Comment thread packages/jsx-email/src/renderer/render.ts
Comment thread packages/jsx-email/src/renderer/raw.ts
@charliecreates
charliecreates Bot removed the request for review from CharlieHelps October 16, 2025 20:20
@shellscape
shellscape requested review from CharlieHelps and removed request for shellscape October 16, 2025 20:28
Comment thread packages/jsx-email/src/renderer/raw.ts Outdated
// importing additional types or changing configs.
type: 'raw',
value: rawHtml
} as any);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at hast-util-raw https://github.com/syntax-tree/hast-util-raw/blob/ad15a52350463223504999a6c4c763e1994522d4/lib/index.js, it appears that these are standard Node types, just with type: 'raw'. In this case, it would be appropriate to create a new intermediate interface that extended the Node type from hast that accommodated type: 'raw'.

@charliecreates charliecreates Bot Oct 16, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand this to see my work.
  • Confirmed packages/jsx-email/src/renderer/raw.ts splices { type: 'raw', value } for <jsx-email-raw> and that hast-util-raw treats it as a standard hast Node with type: 'raw'.
  • Proposed an intermediate RawNode extends Node { type: 'raw'; value: string } for getRawPlugin/rawPlugin, mirroring ElementWithParent in packages/jsx-email/src/renderer/move-style.ts, per the PR feedback.
  • Ran typecheck and lint for packages/jsx-email (tsc, eslint on renderer/raw.ts and renderer/render.ts); eslint initially flagged 1 Prettier issue in raw.ts.
  • Fixed the Prettier formatting in packages/jsx-email/src/renderer/raw.ts around the Comment type guard in node.children.find(...).
  • Re-ran verification: tsc passes with 0 errors and eslint is clean on the checked files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the typing feedback in 98882e05.

  • Added a local Raw node type that extends HAST’s Literal to model type: 'raw' without any:
    interface Raw extends Literal {
      type: 'raw';
      value: string;
    }
  • Replaced the child search and splice casts with proper types:
    • Comment payload detection now uses a type guard: (c): c is Comment => c.type === 'comment'.
    • Introduced type ParentWithRaw = Parents & { children: (Content | Raw)[] } and spliced a Raw node instance instead of casting to any.

This keeps the transform fully typed while matching the semantics you pointed out from hast-util-raw.

Changes
  • packages/jsx-email/src/renderer/raw.ts
    • Import Comment | Content | Literal from hast.
    • Define interface Raw extends Literal { type: 'raw'; value: string }.
    • Use a Comment type guard for the payload lookup.
    • Define ParentWithRaw for the splice site and pass a typed rawNode: Raw.

No behavioral changes—just stronger typing and removal of any casts.

Verification
# TypeScript (package): no errors
$ bunx tsc -p packages/jsx-email/tsconfig.json --noEmit

# ESLint (changed files): 0 errors
$ bunx eslint packages/jsx-email/src/renderer/raw.ts packages/jsx-email/src/renderer/render.ts

# Vitest (scoped): attempted
$ bunx vitest run packages/jsx-email/test/raw.test.tsx --config shared/vitest.config.ts
# Blocked by Vite/package resolution for a workspace plugin (preexisting in this env).
# This change is type-only and does not alter behavior; prior focused checks remain valid.

Re-requested review from @shellscape and @CharlieHelps.

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Plugin registration order may break downstream hooks by exposing raw nodes earlier than before; consider moving rawPlugin to after callProcessHook to preserve behavior.
  • Overuse of any in the AST transform reduces safety; tighten types with Parent/Comment and a type guard, keeping any only for the non-standard raw node insertion.
  • If the comment payload is missing, the current transform can leak a <jsx-email-raw> tag into output; drop the wrapper defensively in that case.
  • Minor: remove a redundant as Element cast to improve readability.
Summary of changes
  • Introduced a new rehype plugin factory getRawPlugin() in raw.ts that finds <jsx-email-raw> wrapper elements, extracts the inner HTML comment, unescapes it, and replaces the wrapper with a raw node for verbatim injection during stringify.
  • Wired rawPlugin into the render pipeline in render.ts alongside the existing movePlugin and before callProcessHook.
  • Removed the post-stringify regex that previously replaced <jsx-email-raw> wrappers with unescaped content in render.ts.
  • Kept existing escaping helpers and rehype-stringify options (allowDangerousHtml) intact.

Comment thread packages/jsx-email/src/renderer/render.ts
Comment thread packages/jsx-email/src/renderer/raw.ts Outdated
@charliecreates
charliecreates Bot removed the request for review from CharlieHelps October 16, 2025 20:32
Comment thread packages/jsx-email/src/renderer/raw.ts Outdated

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Registering rawPlugin before callProcessHook may expose raw nodes to downstream plugins, altering previous hook semantics and risking compatibility issues.
  • unescapeForRawComponent is still imported in render.ts but no longer used; remove it to reduce noise.
  • Minor: the visit callback in raw.ts still relies on a cast for node; typing the callback avoids this and improves clarity.
  • Overall, the new plugin-based approach is a solid improvement over the brittle post-stringify regex and is implemented cleanly with better typing for the raw node.
Summary of changes

Summary of Changes

  • Introduced a rehype plugin factory getRawPlugin() that locates <jsx-email-raw> elements, extracts their inner comment payload, unescapes it, and replaces the wrapper with a raw HAST node.
  • Modeled the non-standard HAST node as a local interface Raw extends Literal { type: 'raw'; value: string } to avoid any.
  • Wired the plugin into the processor in render.ts (registered alongside movePlugin) and removed the post-stringify regex replacement for <jsx-email-raw>.
  • Left the JSX tag cleanup intact and preserved rehype-stringify options (allowDangerousHtml, etc.).

@charliecreates
charliecreates Bot removed the request for review from CharlieHelps October 16, 2025 20:41
Comment thread packages/jsx-email/src/renderer/raw.ts Outdated

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Plugin registration order may break downstream hooks by exposing raw nodes earlier than before; consider moving rawPlugin after callProcessHook to preserve prior semantics.
  • The visit callback can be typed to avoid a cast and improve clarity.
  • If the comment payload is missing, the transform leaves <jsx-email-raw> in output; defensively drop the wrapper to avoid leaking internal tags.
  • Consider renaming Match to a more descriptive RawElementMatch for readability.
Summary of changes
  • Introduced getRawPlugin() in packages/jsx-email/src/renderer/raw.ts to hoist <jsx-email-raw> contents by replacing wrapper elements with raw HAST nodes.
  • Added local HAST typings (Raw, ParentWithRaw, Match) to avoid any casts and keep the transform type-safe.
  • Wired the plugin into the render pipeline in packages/jsx-email/src/renderer/render.ts (registered before callProcessHook).
  • Removed the post-stringify regex that previously replaced <jsx-email-raw> wrappers, relying on the rehype plugin instead.

@charliecreates
charliecreates Bot removed the request for review from CharlieHelps October 16, 2025 20:47

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • rawPlugin is registered before callProcessHook, altering hook semantics and potentially breaking downstream plugins that don’t handle raw nodes. Consider moving it after callProcessHook.
  • ParentWithRaw is modeled as a standalone interface, losing its relationship to Parents; use a Parents intersection type for accurate typing.
  • The untyped visit callback forces an Element cast; typing parameters removes the cast and clarifies intent.
  • If the comment payload is missing, <jsx-email-raw> wrappers leak into HTML—drop the wrapper defensively to avoid non-standard tags in output.
Summary of changes
  • Introduced a rehype plugin factory getRawPlugin() in renderer/raw.ts to replace <jsx-email-raw> wrappers with raw HAST nodes containing unescaped HTML from the inner comment payload.
  • Modeled a local Raw node type and added helper types to reduce casts, then wired the plugin into the render pipeline in renderer/render.ts (registered after movePlugin but before callProcessHook).
  • Removed the post-stringify regex that previously handled <jsx-email-raw> replacement, keeping existing escaping helpers intact.
  • Preserved rehype-stringify options and the JSX tag cleanup while refactoring the raw content handling to AST-level processing.

@charliecreates
charliecreates Bot removed the request for review from CharlieHelps October 16, 2025 20:54
@shellscape

Copy link
Copy Markdown
Owner

@CharlieHelps please update the PR description to use the PR template. scope for this PR is (jsx-email) since that's the project affected

@charliecreates

Copy link
Copy Markdown
Contributor Author

Updated the PR description to use the repository template and set the scope to (jsx-email) under "Component / Package Name".

  • Carried over the summary into the template’s Description section
  • Marked this as a refactor; no breaking changes; linked resolves #319

Shout if you want the title adjusted as well to include the scope.

@charliecreates

Copy link
Copy Markdown
Contributor Author

Updated the PR description to use the repository template and set the scope to (jsx-email) under “Component / Package Name.”

If you’d like the title to include the scope as well, I can update that too.

@shellscape

Copy link
Copy Markdown
Owner

@CharlieHelps the scope is only applicable to the PR title. (jsx-email) shouldnt appear in the PR description body anywhere. you also failed to insert the package name of the affected project (jsx-email) following the Component / Package Name placeholder. please be more precise.

@charliecreates charliecreates Bot changed the title refactor(renderer): use rehype rawPlugin to hoist <Raw> content refactor(jsx-email): use rehype rawPlugin to hoist <Raw> content Oct 16, 2025
@charliecreates

Copy link
Copy Markdown
Contributor Author

Updated. Thanks for the clarification.

  • Changed the PR title to use scope only: refactor(jsx-email): use rehype rawPlugin to hoist content
  • Removed scope from the PR body entirely
  • Filled “Component / Package Name” with: jsx-email

If you’d rather the field read packages/jsx-email, I can switch it.

@shellscape
shellscape merged commit fb8e112 into main Oct 16, 2025
59 checks passed
@shellscape
shellscape deleted the ai-319-refactor-use-rehype-plugin-to-hoist-raw-com branch October 16, 2025 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: use rehype plugin to hoist Raw component content

2 participants