-
-
Notifications
You must be signed in to change notification settings - Fork 409
WarpDrive: A framework-agnostic build plugin for WarpDrive #1233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,382 @@ | ||
| --- | ||
| stage: accepted | ||
| start-date: 2026-09-02T00:00:00.000Z | ||
| release-date: | ||
| release-versions: | ||
| teams: | ||
| - data | ||
| prs: | ||
| accepted: https://github.com/emberjs/rfcs/pull/1233 | ||
| project-link: | ||
| suite: | ||
| --- | ||
|
|
||
| # A Framework-Agnostic Build Plugin for WarpDrive | ||
|
|
||
| ## Summary | ||
|
|
||
| WarpDrive (EmberData) gains a first-class bundler plugin, imported from | ||
| `@warp-drive/core/build-plugin`, that replaces its current babel-based build configuration. | ||
| One plugin works in Vite, Rollup, Rolldown, Webpack, Rspack, and esbuild, serving Ember, | ||
| React, Vue, Svelte, and Angular apps. It removes `@embroider/macros` and babel from | ||
| WarpDrive's app-facing build path, reduces setup to one line of bundler config, and | ||
| guarantees a single shared configuration no matter how many copies of WarpDrive packages | ||
| exist in an app's dependency tree. The existing `setConfig` options are accepted unchanged; | ||
| the babel path is deprecated on a staged timeline and removed in a future major. | ||
|
|
||
| ## Motivation | ||
|
|
||
| WarpDrive instruments its source with build-time flags: deprecation stripping keyed on | ||
| `compatWith`, canary feature flags, dev-only assertions, and toggleable debug logging. Today, | ||
| applying an app's configuration to those flags requires a babel pipeline: `setConfig()` feeds | ||
| `@embroider/macros`, and the app's babel config must include the embroider macros plugins, | ||
| `babel-plugin-debug-macros`, and (for app-side flag use) WarpDrive's own transform set. Four | ||
| problems motivate replacing this: | ||
|
|
||
| **1. Supporting more frameworks.** WarpDrive now ships bindings for React, Vue, and Svelte | ||
| alongside Ember. Those ecosystems do not have babel in their default toolchains — Vite uses | ||
| esbuild/oxc, Angular uses its own compiler. Today a React app must disable its native | ||
| transforms and adopt babel *solely to configure WarpDrive*. A bundler plugin meets every | ||
| framework where it already is. | ||
|
|
||
| **2. Simplifying configuration overall.** The current Ember setup threads one config object | ||
| through several coupled pieces: `buildMacros({ configure: ... })`, `setConfig()`, | ||
| `...macros()`, `...Macros.babelMacros`, and a `babel-plugin-debug-macros` entry — each a | ||
| chance to wire something in the wrong order or miss a piece entirely (the widely-copied | ||
| "Simple Config" recipe silently omits the transforms that compile app-side flag imports). | ||
| The replacement is a single plugin call carrying the same options object. | ||
|
|
||
| **3. Avoiding a forced babel pass.** Requiring babel is a real cost even where babel exists: | ||
| every file WarpDrive ships must flow through the app's babel pipeline in `node_modules`, and | ||
| apps must maintain marker-based include lists to make that happen. The plugin transforms only | ||
| the files that need it, using a native-speed parser, with no babel dependency. | ||
|
|
||
| **4. Avoiding collisions with `@embroider/macros`.** When an app's dependency tree | ||
| accidentally contains more than one copy of `@embroider/macros`, each copy holds its own | ||
| config store — under `buildMacros()` the copies never coordinate — so WarpDrive's config can | ||
| silently fail to reach the copy compiling its files, producing wrong or missing stripping | ||
| with no error. The plugin owns its own coordination (detailed below) and, by consuming | ||
| WarpDrive's macros itself before babel runs, removes WarpDrive from embroider's blast radius | ||
| entirely — while leaving other addons' embroider usage untouched. | ||
|
|
||
| The expected outcome: every supported framework configures WarpDrive with one plugin line; | ||
| Ember apps keep their existing options unchanged; the config-collision failure mode becomes | ||
| impossible or loud; and `@embroider/macros` plus babel leave WarpDrive's dependency graph on | ||
| a published timeline. | ||
|
|
||
| ## Detailed design | ||
|
|
||
| ### The plugin | ||
|
|
||
| The implementation lives in `@warp-drive/build-config` (the package that already owns | ||
| `setConfig` and the flag definitions) and is re-exported as `@warp-drive/core/build-plugin`. | ||
| It is **one plugin**, built on [unplugin](https://unplugin.unjs.io): a single instance whose | ||
| per-bundler adapters are property accesses, so there is one import path for every bundler and | ||
| no per-framework plugin packages. | ||
|
|
||
| ```js | ||
| import { warpDrive } from '@warp-drive/core/build-plugin'; | ||
|
|
||
| warpDrive.vite(options); // Vite (Ember via embroider, React, Vue, Svelte, SolidStart, Astro, Nuxt) | ||
| warpDrive.webpack(options); // Webpack (Next.js, Angular's legacy builder) | ||
| warpDrive.esbuild(options); // esbuild (Angular's current builder, via custom-esbuild) | ||
| warpDrive.rollup(options); // also .rolldown(), .rspack(), .rsbuild() | ||
| ``` | ||
|
|
||
| The options are the same `WarpDriveConfig` object `setConfig` accepts today — `compatWith`, | ||
| `deprecations`, `features`, `debug`, `polyfillUUID`, `includeDataAdapterInProduction` — with | ||
| identical semantics and identical environment-variable handling (`EMBER_ENV`, `NODE_ENV`, | ||
| `IS_TESTING`, `WARP_DRIVE_FEATURE_OVERRIDE`, and friends). No option is renamed. The plugin | ||
| and `setConfig` share one implementation of config resolution, so they cannot drift. | ||
|
|
||
| ### Using the new API | ||
|
|
||
| **Ember (embroider + Vite):** | ||
|
|
||
| ```js | ||
| // vite.config.mjs | ||
| import { ember, extensions } from '@embroider/vite'; | ||
| import { warpDrive } from '@warp-drive/core/build-plugin'; | ||
|
|
||
| export default { | ||
| plugins: [ | ||
| ...ember(), | ||
| warpDrive.vite({ compatWith: '5.7' }), | ||
| ], | ||
| }; | ||
| ``` | ||
|
|
||
| WarpDrive-related entries in `babel.config.mjs` are no longer needed (see Migration). Babel | ||
| remains for Ember's own needs — decorators, templates — untouched. | ||
|
|
||
| **Ember (classic ember-cli): no change.** `setConfig(app, __dirname, config)` in | ||
| `ember-cli-build.js` remains the entire user surface, in 5.x via today's pipeline and after | ||
| the transition via a WarpDrive-provided babel bridge that the addon wires up automatically. | ||
|
|
||
| **React (plain Vite)** — shown to make the framework-agnostic claim concrete: | ||
|
|
||
| ```js | ||
| // vite.config.mjs | ||
| import react from '@vitejs/plugin-react'; | ||
| import { warpDrive } from '@warp-drive/core/build-plugin'; | ||
|
|
||
| export default { | ||
| plugins: [react(), warpDrive.vite({ compatWith: '5.7' })], | ||
| }; | ||
| // No babel config. No esbuild:false workaround. WarpDrive was the only reason either existed. | ||
| ``` | ||
|
|
||
| **Using flags in your own app code** works with zero additional configuration. The same | ||
| booleans WarpDrive's source uses are available to apps, compiled by the same plugin: | ||
|
|
||
| ```ts | ||
| import { DEBUG } from '@warp-drive/core/build-config/env'; | ||
| import { assert } from '@warp-drive/core/build-config/macros'; | ||
|
|
||
| if (DEBUG) { | ||
| // stripped from production builds | ||
| } | ||
| assert('expected a store', isStore(candidate)); // stripped from production builds | ||
| ``` | ||
|
|
||
| The plugin recognizes these by their import specifiers — which are WarpDrive-owned module | ||
| names — so it cannot affect any other import in app code. | ||
|
|
||
| ### What the plugin does | ||
|
|
||
| Three transforms, applied only to files that import the relevant modules (a cheap | ||
| string-marker filter, evaluated natively by the bundler where supported, skips everything | ||
| else): | ||
|
|
||
| 1. **Published WarpDrive packages.** WarpDrive's published code carries its flags as | ||
| `@embroider/macros` expressions (`macroCondition(getGlobalConfig().WarpDrive...)`). The | ||
| plugin evaluates these against the app's config and prunes dead branches — the same | ||
| stripping the embroider babel plugin performs today, from the same config values. This | ||
| works against already-published versions: no library upgrade is required to adopt the | ||
| plugin. | ||
| Scoping is by the owning package's `package.json` name (`@warp-drive/*`, `@ember-data/*`, | ||
| `ember-data`), so embroider macros in any other package are never touched. | ||
| 2. **Flag imports in app code** (the example above), replaced with constant values; dead | ||
| branches are removed in production by the plugin or the app's minifier. | ||
| 3. **`deprecate`/`warn` from `@ember/debug`** inside WarpDrive's published files — the job | ||
| `babel-plugin-debug-macros` does today. In Ember apps these are left untouched so | ||
| `registerDeprecationHandler` and `expectDeprecation` keep working; in non-Ember apps they | ||
| are backed by a console shim in development and stripped in production. | ||
|
|
||
| Runtime-toggleable debug logging is preserved exactly: in dev and test builds, logging | ||
| branches remain and are gated at runtime, so `setWarpDriveLogging({ LOG_REQUESTS: true })` | ||
| in the console keeps working without a rebuild; in production builds unconfigured logging | ||
| compiles to zero bytes. | ||
|
|
||
| In a later major (see the schedule under "Deprecating the old API"), WarpDrive's published | ||
| output stops carrying | ||
| `@embroider/macros` expressions at all, switching to plain flag imports with working runtime | ||
| defaults — at which point a build with no plugin configured still runs correctly (as an | ||
| unoptimized development-flavored build that logs a one-time warning), and `@embroider/macros` | ||
| leaves WarpDrive's dependencies. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. once this is achieved, WarpDrive itself can simplify its own build process,
|
||
|
|
||
| ### One config, no matter how many copies | ||
|
|
||
| A real dependency tree can contain several copies of the plugin's own package and many copies | ||
| of WarpDrive libraries, across bundler worker threads and processes. The design guarantees | ||
| they all apply one configuration: | ||
|
|
||
| - **Within a process**, every copy converges on a single registry stored on `globalThis` | ||
| under `Symbol.for('warp-drive.build-store')` — a key that is identical across all copies | ||
| and versions. (Embroider's equivalent handshake is keyed by object identity, which | ||
| independently-created instances never share; that is the root of today's duplicate-copy | ||
| failure.) The registry holds only plain JSON data with an explicit protocol version, so | ||
| version-skewed copies interoperate or fail loudly, never silently. | ||
| - **Registering the same config twice is the normal case** (that is how copies converge). | ||
| Registering a *different* config is a build error that names both sources and the first | ||
| differing keys: | ||
|
|
||
| ``` | ||
| [WarpDrive::build] Conflicting WarpDrive build configs for app '/srv/app'. | ||
| First set from ember-cli-build.js via setConfig() with compatWith: '4.12'; | ||
| then from vite.config.mjs via warpDrive.vite() with compatWith: '5.6'. | ||
| WarpDrive config must be identical everywhere it is declared. | ||
| Differing keys: compatWith, deprecations.DEPRECATE_TRACKING_PACKAGE. | ||
| ``` | ||
|
|
||
| - **Across threads and processes**, which share no `globalThis`, the guarantee is | ||
| determinism: the resolved config is a pure function of the plugin options and environment | ||
| variables, so every worker that evaluates the same bundler config derives the same result. | ||
| Where config travels as data (loader options), it carries a hash that the receiving side | ||
| verifies, turning environment drift into a diagnosable error instead of divergent output. | ||
| - **Library copies need no coordination at build time** — they are inert files, each | ||
| transformed with the same config regardless of which physical copy it is. | ||
|
|
||
| ### Coexistence with `@embroider/macros` | ||
|
|
||
| During migration, an app may have both the plugin and an embroider babel pass wired. This is | ||
| safe in both orders: | ||
|
|
||
| - The plugin runs ahead of babel (`enforce: 'pre'`). After it transforms a WarpDrive file, no | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note we don't need |
||
| `@embroider/macros` imports remain in it. The embroider babel plugin scopes all of its work | ||
| to references of those imports, so it provably no-ops on the plugin's output — and | ||
| continues to process every *other* package's macros exactly as before. | ||
| - If a misconfigured pipeline runs embroider first, `setConfig` (which now also feeds the | ||
| plugin's registry, and continues to feed embroider's) ensures embroider inlines the same | ||
| values the plugin would have; the plugin then finds nothing left to do. | ||
|
|
||
| This ordering property is also the migration mechanism: **adopting the plugin is an | ||
| insertion, not a swap.** Adding the plugin immediately takes over WarpDrive's files; removing | ||
| the old babel entries becomes optional cleanup rather than a coordinated step. | ||
|
|
||
| ### Migrating | ||
|
|
||
| **Before** (Ember, embroider + Vite): | ||
|
|
||
| ```js | ||
| // babel.config.mjs | ||
| import { buildMacros } from '@embroider/macros/babel'; | ||
| import { setConfig } from '@warp-drive/core/build-config'; | ||
| import { macros } from '@warp-drive/core/build-config/babel-macros'; | ||
|
|
||
| const Macros = buildMacros({ | ||
| configure: (config) => { | ||
| setConfig(config, { compatWith: '5.7' }); | ||
| }, | ||
| }); | ||
|
|
||
| export default { | ||
| plugins: [ | ||
| ...macros(), | ||
| ['babel-plugin-debug-macros', { /* ... */ }, 'ember-data-macros'], | ||
| ...Macros.babelMacros, | ||
| // ...decorators, templates, etc. | ||
| ], | ||
| }; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should also show the migration of the currently recommended shorthand |
||
| ``` | ||
|
|
||
| **After:** | ||
|
|
||
| ```js | ||
| // vite.config.mjs — one added line | ||
| import { ember, extensions } from '@embroider/vite'; | ||
| import { warpDrive } from '@warp-drive/core/build-plugin'; | ||
| export default { | ||
| plugins: [...ember(), warpDrive.vite({ compatWith: '5.7' })], | ||
| }; | ||
|
|
||
| // babel.config.mjs — WarpDrive entries deleted; only decorators/templates remain. | ||
| // If other addons in the app use @embroider/macros, keep buildMacros for them; | ||
| // it will no longer process WarpDrive's files either way. | ||
| ``` | ||
|
|
||
| The options object moves verbatim from `setConfig` to the plugin call. Apps that keep both | ||
| temporarily get identical output (same config, either order) or a loud conflict error if the | ||
| two ever disagree — never silent divergence. | ||
|
|
||
| Classic ember-cli apps migrate by doing nothing: `setConfig(app, __dirname, config)` is | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we were to shift off of embroider/macros as our publishing format, classic apps would necessarily need to either add a new thing into their build (updated babel plugin perhaps) or move off of the classic build system. |
||
| unchanged. | ||
|
|
||
| ### Deprecating the old API | ||
|
|
||
| The babel path — `babelPlugin()`, `buildMacros()` + `setConfig()` wiring, `macros()`, and the | ||
| `babel-plugin-debug-macros` entry — is deprecated on this schedule (the 3-arg classic | ||
| `setConfig(app, __dirname, config)` form is *not* deprecated; it becomes classic Ember's way | ||
| of passing options to the plugin): | ||
|
|
||
| 1. **Next 5.x minor:** plugin ships; docs recommend it everywhere except classic ember-cli. | ||
| The babel path is fully supported and prints nothing. | ||
| 2. **Following 5.x minor:** the babel path prints a one-time build notice (info level, not a | ||
| deprecation) pointing at the migration guide. | ||
| 3. **Next major (6.0):** the babel path issues a formal build-time deprecation: | ||
|
|
||
| ``` | ||
| DEPRECATION [warp-drive.legacy-babel-config]: Configuring WarpDrive through babel | ||
| (babelPlugin(), buildMacros() + setConfig(), macros(), or babel-plugin-debug-macros) | ||
| is deprecated. Add the WarpDrive build plugin to your bundler config instead — it | ||
| replaces all of these entries and accepts the same options: | ||
|
|
||
| // vite.config.mjs | ||
| import { warpDrive } from '@warp-drive/core/build-plugin'; | ||
| plugins: [...ember(), warpDrive.vite({ compatWith: '5.7' })] | ||
|
|
||
| Then remove the WarpDrive entries from your babel config. | ||
| Migration guide: https://docs.warp-drive.io/guides/build-plugin-migration | ||
| [deprecation id: warp-drive.legacy-babel-config, since: 6.0, until: 7.0] | ||
| ``` | ||
|
|
||
| 4. **Following major (7.0):** the babel path is removed. A babel *bridge* plugin (wrapping | ||
| the same transform core, no embroider involved) remains available indefinitely for | ||
| pipelines that genuinely only have babel. | ||
|
|
||
| Also at 6.0, WarpDrive's published output switches to the plain-flag format and | ||
| `@embroider/macros` is removed from every WarpDrive package's dependencies — ending the | ||
| duplicate-copy hazard for Ember apps at the root. | ||
|
|
||
| ### Ecosystem implications | ||
|
|
||
| - **Addons** consuming WarpDrive flags in their own code get compiled by the app's plugin the | ||
| same way app code does; addons that use `@embroider/macros` for their own purposes are | ||
| unaffected. | ||
| - **Ember Inspector / debuggability:** unchanged; `includeDataAdapterInProduction` and the | ||
| runtime logging toggles behave identically. | ||
| - **Engines / SSR / FastBoot:** the plugin is build-time only; output semantics match the | ||
| current pipeline. | ||
| - **Blueprints:** the app blueprint's WarpDrive/EmberData wiring updates to the plugin recipe. | ||
| - **Lint rules:** none required. | ||
| - **IDE support:** flag imports are real modules with real types; nothing changes. | ||
|
|
||
| ## How we teach this | ||
|
|
||
| Teach it as "the WarpDrive build plugin" — a continuation of the existing concept that | ||
| WarpDrive has build configuration, relocated from babel to the bundler. The guides' | ||
| setup page reduces from three paradigm-dependent recipes to one line per bundler, with | ||
| classic ember-cli documented as "no change." The `WarpDriveConfig` options reference is | ||
| already written and applies as-is. | ||
|
|
||
| For existing users, the migration guide is the before/after shown above plus one rule of | ||
| thumb: *put the plugin ahead of babel; delete the babel entries when convenient.* For new | ||
| users, the plugin recipe is strictly simpler than what it replaces, and non-Ember framework | ||
| docs no longer need to explain babel at all. | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| - **Install weight:** the plugin adds `unplugin` and `oxc-parser` (a native-binary parser | ||
| with wasm fallback) to `@warp-drive/build-config`'s dependencies, which every consumer | ||
| installs transitively. These are node-only, never bundled, and deduped, but they are real | ||
| bytes and CI surface. | ||
| - **Two supported paths during the transition** (plugin and babel) means dual documentation | ||
| and dual testing until 7.0. | ||
| - **Weak hosts have caveats:** esbuild's plugin model limits coexistence with other | ||
| transform plugins (relevant to Angular's builder), and Turbopack supports only a | ||
| loader-shaped bridge with user-maintained file globs. Both degrade to correct-but- | ||
| unoptimized behavior rather than breakage, but the support tiers must be documented | ||
| honestly. | ||
| - **Reimplementation risk:** the plugin evaluates the macro expressions WarpDrive publishes, | ||
| a job embroider's babel plugin does today. The expression grammar is closed and small | ||
| (WarpDrive's own publish step is its only author), and it is locked down by golden tests | ||
| against real published artifacts, but it is code WarpDrive now owns. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| - **Stay on `@embroider/macros` + babel.** Rejected: it makes babel a hard requirement in | ||
| ecosystems that have moved off it, and the duplicate-copy config hazard is structural. | ||
| - **Ship a babel plugin instead of a bundler plugin.** Simpler to build, but fails the | ||
| primary motivation — non-babel toolchains — and keeps WarpDrive's files flowing through | ||
| app babel pipelines. | ||
| - **Per-framework plugin packages** (`@warp-drive/vite-plugin`, etc.). Rejected: unplugin | ||
| provides all per-bundler adapters from one implementation; separate packages would | ||
| multiply the version-skew and duplicate-copy surface this RFC works to eliminate. | ||
| - **A new standalone package for the plugin.** Rejected in favor of housing it in | ||
| `@warp-drive/build-config` (re-exported from `@warp-drive/core`): the plugin lives beside | ||
| the config code it shares, and consumers need no new dependency. | ||
| - **Do nothing for non-Ember frameworks** and document babel workarounds. Rejected: the | ||
| workarounds (disabling native TS/JSX transforms to insert babel) are the worst part of the | ||
| current non-Ember experience. | ||
|
|
||
| ## Unresolved questions | ||
|
|
||
| - The classic ember-cli story at 6.0 relies on the addon automatically injecting the babel | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another unresolved q: whether the under-development AQL and DSL build plugins ought to be combined into this. It's less clear that's possible due to how they produce virtual imports from directory and file scans. |
||
| bridge into the app's babel options; this is gated behind a flag for the full 6.0 beta | ||
| cycle, with a documented manual fallback if it proves unreliable across ember-cli-babel | ||
| versions and engines setups. | ||
| - Default flag values for builds that never ran the plugin (post-6.0): lenient | ||
| test-friendly defaults keep runtime log toggling alive but relax a duplicate-copy runtime | ||
| guard; strict defaults invert the trade. | ||
| - Whether `@warp-drive/core/build-plugin` should also be exposed under the `ember-data` | ||
| package name for apps that consume WarpDrive exclusively through it. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have 2 options here:
we would maintain the published version using @embroider/macros statements until the existing babel config approach is deprecated and removed
since the babel config uses a util imported from warp-drive, we could swap it to process these via a babel-shim for the new plugin and migrate our publishing format to something with zero risk of confusion for @embroider/macros