Skip to content

feat(plugin-clean)!: initial release#442

Merged
shellscape merged 6 commits into
mainfrom
feat/plugin-clean/init
May 13, 2026
Merged

feat(plugin-clean)!: initial release#442
shellscape merged 6 commits into
mainfrom
feat/plugin-clean/init

Conversation

@shellscape
Copy link
Copy Markdown
Owner

Component / Package Name:

@jsx-email/plugin-clean

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

List any relevant issue numbers:

N/A

Description

Adds the initial standalone @jsx-email/plugin-clean package at version 2.0.0.

The plugin removes unused CSS selectors and class names from rendered email HTML. It exports a configurable clean(options)
factory and a default plugin export for direct use in the plugins array of a JSX email config.

Included behavior:

  • removes unused CSS selectors from <style> blocks
  • removes unused class names from HTML attributes
  • supports whitelist for selectors that should always be preserved
  • preserves common template backend delimiters like {{ }} and {% %} by default
  • includes default email-client selector preservation
  • preserves standard Outlook conditional comments by default
  • supports class and ID shortening through uglify

This PR intentionally does not integrate the plugin into jsx-email render options or automatic core plugin loading. That
integration will be handled separately.

Docs were added for the new plugin page and plugin sidebar entry, including descriptions for each supported CleanOptions
option.

Comment thread apps/web/src/content/docs/plugins/_meta.json
@shellscape shellscape changed the title feat(plugin-clean): initial release feat(plugin-clean)!: initial release May 13, 2026
@shellscape
Copy link
Copy Markdown
Owner Author

@CharlieHelps in an earlier implementation, you tried to setup the integration with jsx-email with this plugin as a default, builtin plugin. As part of that earlier implementation, you stated that this plugin should be run before other specific plugin operations otherwise the value of this plugin would be moot, given how the minify and inline plugins operate. Break down the order of operations we should follow, and reply with that breakdown. In your reply, detail options for how we could extend the plugin contract to allow plugins to insert themselves before the execution of other plugins. We rarely want to encourage this, due to it leading to obvious chicken and egg situations, but correct me if I'm wrong that this is a rare exception.

@charliecreates
Copy link
Copy Markdown
Contributor

@shellscape You’re right to call this out — this is one of the rare cases where ordering is semantically important.

Order of operations to follow

  1. Render JSX to HTML.
  2. Run beforeRender hooks.
  3. Run internal preprocessors (moveStylerawconditional).
  4. Run clean before inline and before minify.
  5. Run inline.
  6. Run minify.
  7. Run afterRender hooks (e.g. formatting like pretty).

The key reason: clean needs original selectors/class usage to be present. inline removes/rewrites that data (notably <style> and class usage), so running clean after inline makes most of its value moot.

Important note for current implementation

In this PR, plugin-clean is currently afterRender-only, so it can’t be ordered ahead of inline/minify process hooks. If we want it to be effective in the default pipeline, we should move it into the process stage (or otherwise make it run pre-inline).

Options to extend the plugin contract for ordering

  1. Relative ordering metadata (best rare-exception fit)

    • Add contract fields like order.before, order.after, and optional stage.
    • Example: clean declares before: ['inline', 'minify'].
    • Resolve with per-stage topological sort + hard cycle errors.
  2. Priority buckets (simpler, less precise)

    • Add priority (or enforce: 'pre' | 'normal' | 'post').
    • Easy to reason about, but can’t express “before X, after Y” exactly.
  3. Config-level order overrides (no contract churn)

    • Keep plugin interface unchanged; add config APIs to reorder known plugins.
    • Safer globally, but plugin packages can’t self-position.

I agree this should stay rare. Most plugins should remain order-agnostic; this is one of the exceptions where transform semantics make ordering materially important.

@shellscape
Copy link
Copy Markdown
Owner Author

@CharlieHelps if we change this plugin from using the afterRender hook to the beforeRender hook, would that resolve the ordering issue with core plugins and this plugin?

@charliecreates
Copy link
Copy Markdown
Contributor

Yes — switching plugin-clean from afterRender to beforeRender should resolve the ordering issue with core plugins.

I checked the current pipeline and it runs beforeRender hooks first, then process hooks, then afterRender hooks. Since core inline/minify are process hooks (and pretty is afterRender), moving clean to beforeRender puts it ahead of those core transforms regardless of plugin array order.

One caveat: beforeRender runs before internal HTML transforms, so clean won’t be operating on the fully post-processed HTML anymore. I’d add/adjust an integration test to confirm that behavior still matches what you want.

@shellscape
Copy link
Copy Markdown
Owner Author

@CharlieHelps change the plugin to use beforeRender and adjust tests.

@charliecreates
Copy link
Copy Markdown
Contributor

Implemented this on the PR.

  • Switched @jsx-email/plugin-clean to use the beforeRender plugin hook.
  • Updated plugin-clean tests to validate behavior via beforeRender.
  • Commit: a629b3e

Checks run:

  • CI=true corepack pnpm exec moon repo:build.deps
  • CI=true corepack pnpm exec moon plugin-clean:test

@shellscape shellscape merged commit 3cad1e7 into main May 13, 2026
4 checks passed
@shellscape shellscape deleted the feat/plugin-clean/init branch May 13, 2026 18:11
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.

2 participants