DEV Community

KhaledSalem
KhaledSalem

Posted on

Plugins Are Powerful. But They Shouldn’t Become Your Architecture.

Plugin systems are one of the reasons modern frontend tooling became so flexible.

Vite proved this extremely well.

A good plugin system lets a tool support frameworks, experiments, edge cases, and ecosystem creativity without forcing everything into the core.

But there is a point where something changes.

Plugins stop being extensions.

They become architecture.

And once foundational behavior lives across many plugin hooks, the cost is no longer just “install one more package”.

The cost becomes engineering complexity.

The plugin cost is not the package
When people talk about plugins, they usually talk about installation:

npm install plugin-name
Enter fullscreen mode Exit fullscreen mode

But that is not the real cost.

The real cost is:

  • hook ordering
  • duplicated graph knowledge
  • dev/build parity
  • cache invalidation
  • transform boundaries
  • hidden runtime assumptions
  • debugging intermediate output
  • plugin compatibility across versions

A plugin can add a feature.

But if the feature affects the module graph, dependency identity, runtime behavior, or cache correctness, then the build engine needs to understand it deeply.

Not just call a hook and hope the result still makes sense.


Five features Ionify treats as native

Ionify is built around a simple idea:

Foundational build behavior should be understood by the engine, not reconstructed through plugin layers.

Here are five examples.

1. React should not need a plugin to start

In many frontend setups, React support is treated as a plugin concern.

That usually means JSX/TSX transforms, Fast Refresh wiring, runtime overlays, and production stripping need to be coordinated across the dev server and production build.

The engineering cost is not “React support”.

The cost is keeping these pieces consistent:

  • JSX/TSX transform behavior
  • Fast Refresh boundaries
  • error overlay/runtime injection
  • production-only stripping
  • dev/build parity

Ionify handles this natively.

React is not treated as an external behavior bolted onto the build.
The engine understands the React path directly, which means fewer moving parts before the app even starts.


2. Resolution should be one authority
Aliases, workspace paths, tsconfig paths, package exports, browser fields, peer dependency identity — these are not small details.

They define what a module is.

When resolution is spread across plugin layers, different parts of the toolchain can develop slightly different answers to the same question:

What does this import point to?

That creates painful bugs:

  • works in dev, fails in build
  • duplicate copies of a dependency
  • broken peer singletons
  • incorrect workspace resolution
  • alias behavior that differs from TypeScript

Ionify uses a native resolver with support for aliases, tsconfig / jsconfig paths, package exports/imports/browser fields, and workspace-aware ids.

The goal is simple:

One import should have one identity.


3. CommonJS compatibility should not be a guessing game
CommonJS is still everywhere.

The difficult part is not simply “convert CJS to ESM”.

The difficult part is recovering the shape of exports safely enough that browser ESM can consume it predictably.

Plugin-based CJS conversion can become expensive because it sits at the boundary between:

  • dependency optimization
  • dev-time ESM serving
  • production bundling
  • named export recovery
  • default export interop
  • cache correctness

Ionify handles CJS-to-browser-safe ESM natively in Rust, including export analysis and default/named export recovery.

That means CommonJS compatibility becomes part of the engine’s dependency model instead of an after-the-fact transform.


4. Dependency optimization should not be separate from the graph

Dependency optimization is one of the most important parts of modern frontend tooling.

It affects startup time, browser request counts, cache reuse, shared chunks, and vendor behavior.

The hidden cost appears when dependency optimization is treated as a separate pre-step instead of part of the engine’s persistent understanding of the project.

You end up asking:

  • Was this dependency already optimized?
  • Did the graph change?
  • Is this wrapper deterministic?
  • Can this output be reused?
  • Should this vendor pack be shared?
  • Why did this dependency rebuild?

Ionify treats dependency optimization as native graph work.

It has native /@deps optimization, deterministic dependency wrappers, shared chunks, vendor packs, and slimming.

The important part is not just speed.

The important part is that dependency optimization belongs to the same engine that understands the graph


5. CSS should have dev/build parity

CSS looks simple until it touches real applications.
Then you get:

  • PostCSS config
  • CSS Modules tokens
  • ?inline
  • ?raw
  • ?url
  • ?module
  • assets referenced from CSS
  • HMR behavior
  • production extraction
  • class name generation

The engineering cost is making all of that behave the same way in dev and build.

When CSS is handled through disconnected transforms, it is easy for one mode to understand something another mode does not.

Ionify handles core CSS, PostCSS, CSS Modules, and CSS query modes natively.

The goal is not to make CSS magical.
The goal is to make CSS boring.

Boring is good.
Boring means fewer surprises.


And more...

Those are only five examples.
Ionify also handles more build behavior natively, including:

  • static assets
  • images, fonts, SVGs, hashed build assets, and publicDir
  • build precompression with .br and .gz
  • compression manifests and compression CAS
  • SPA history fallback with internal/asset exclusions
  • .env, define, import.meta.env, and process.env.NODE_ENV
  • basic analyzer output through ionify analyze, build.stats.json, graph/deps/manifest authorities
  • manifest-driven ESM federation work, where the graph direction matters

Not every feature belongs in core.
But foundational behavior does.


The real difference: hooks vs understanding

A plugin hook can transform a file.

But the engine still has to answer deeper questions:

  • What changed?
  • What can be reused?
  • What depends on this?
  • Is this output still valid?
  • Is this module the same identity as before?
  • Does dev behavior match build behavior?

If the engine does not understand the feature, the plugin has to simulate that understanding from the outside.

That is where complexity grows.

Ionify’s bet is that many common build features should not be simulated from the outside.

They should be native engine concepts.


This is not anti-plugin

Plugins are still valuable.

There will always be project-specific needs, framework experiments, integrations, and custom behavior that should live outside the core.

But the foundation should be stable.

React startup, module identity, dependency optimization, CommonJS compatibility, CSS behavior, assets, env handling, federation, and build compression are not exotic edge cases.

They are daily build-system work.
Ionify tries to make that work native.
Less hook choreography.
Less duplicated graph logic.
Less rebuilding what the engine should already know.


The question

  • Where do you draw the line?

  • Which build features should be native to the engine, and which ones should stay as plugins?

I’m genuinely curious how other teams think about this.

Top comments (0)