Start Date Release Date Release Versions PR link Tracking Link Stage Teams
2/13/2025
Accepted
  • CLI
  • Data
  • Framework
  • Learning
  • Steering
  • TypeScript

<-- Replace "RFC title" with the title of your RFC -->

WarpDrive Package Unification

Summary

Restructure the existing @ember-data/* and @warp-drive/* packages in order to simplify the installation and configuration story while maintaining the benefits of a multi-package architecture.

Required packages for a fully functional ember.js experience would be:

  • @warp-drive/core (all the universal basics)
  • @warp-drive/ember (reactivity and inspector integration, ember specific components)
  • @warp-drive/json-api (cache implementation)

Additional packages that applications may choose to utilize would be:

  • @warp-drive/utilities
  • @warp-drive/experiments
  • @warp-drive/legacy

A few packages would be deprecated, overall existing packages would remain and could still be individually installed and composed if desired.

Motivation

WarpDrive is designed to be highly flexible and composable, with consuming applications able to pick-and-choose combinations of packages alongside BYO/3rd-party implementations of interfaces.

WarpDrive packages generally try to follow a variation of the single-responsibility-principle: while their surface area is larger than a single class or function, they tend to represent a specific architectural boundary or concept and a group of tightly related primitives.

These are all good things, but it also makes the learning, onboarding and package.json maintenance experience much harder once an application ejects from using the single ember-data package to gain access to new features and abilities.

We want to rebalance the project, holding on to what has been great about small composable packages while simplifying learning, onboarding and maintenance.

Now, as we rebrand to WarpDrive, seems an ideal opportunity to rebalance in this way as it gives us the opportunity to update names throughout.

Detailed design

The World Today

Currently, EmberData/WarpDrive publishes 18 primary source-code packages.

  • @ember-data/active-record
  • @ember-data/adapter
  • @ember-data/debug
  • @ember-data/graph
  • @ember-data/json-api
  • @ember-data/legacy-compat
  • @ember-data/model
  • @ember-data/request
  • @ember-data/request-utils
  • @ember-data/rest
  • @ember-data/serializer
  • @ember-data/store
  • @ember-data/tracking
  • @warp-drive/build-config
  • @warp-drive/core-types
  • @warp-drive/ember
  • @warp-drive/experiments
  • @warp-drive/schema-record

As well as the "meta" package which bundles together the needed dependencies and configures them for the "legacy" EmberData experience.

  • ember-data

We publish mirror and types packages for each of the primary 18 source-code repositories and the meta package (for a total of 57 published artifacts currently)

[Info] Mirror packages allow for two-versions of the library to be used in a codebase simultaneously. Types packages allow older (pre 4.13/5.3) releases to consume the types from later releases as a way of getting a slightly better form of typescript support than what was offered by the DefinitelyTyped project.

We additionally publish a number of packages for tooling:

  • eslint-plugin-ember-data (empty, reserved for security)
  • eslint-plugin-warp-drive
  • warp-drive (cli tool)

And as well we have multiple packages "under development", meaning they are currently unpublished but could become public in the future.

  • @warp-drive/holodeck
  • @warp-drive/schema
  • @warp-drive/diagnostic

A final consideration: we plan to add packages for integrations with other ecosystems as we expand the WarpDrive universe, these would be analogous to @warp-drive/ember

  • @warp-drive/{angular|vue|svelte|solidjs|etc}

In short, lots of packages. So how do we clean this up and unify them?

The World Tomorrow

We will introduce four new packages 🙈

  • @warp-drive/core
  • @warp-drive/json-api
  • @warp-drive/utilities
  • @warp-drive/legacy

Each of these four will also have a mirror (but not a types) equivalent. These four packages alongside the exising packages @warp-drive/ember and @warp-drive/experiments for a total of 6 packages would become the maximal set of packages needing to be installed and configured by any Ember application.

For applications written with other frameworks, replace @warp-drive/ember with the ecosystem specific package and remove @warp-drive/legacy which can only be used with Ember for a maximum of 5 packages.

None of the existing packages will cease to exist

At least, not right away. A few of them will be restructured to support the the ideas outlined below describing what belongs in each of these 6, and two of them will be deprecated.

Retaining the actual package boundaries will allow us to continue to enforce strong boundaries between primitives, continue to offer applications looking to push boundaries the ability to compose an experience using a different combination of packages as desired, and give us the ability to bring new ideas into the library and move old ideas out of the library seamlessly.

But much like users of ember-data rarely had to consider where the source-code actually lived (it has not been in ember-data since 3.11), want future users to be able to get up and running with warp-drive with as few barriers as practical.

Lets look at what goes into each of the 6.

@warp-drive/core

The primary package – what someone could install and use and need nothing else to achieve the most basic WarpDrive experience – is @warp-drive/core.

Unless otherwise specified, all packages moved into core are "clean" re-exports, meaning that the only thing necessary to change from the existing package to the new package is to change the package name and add the appropriate sub-path.

E.G.

-import { Type } from '@warp-drive/core-types/symbols';
+import { Type } from '@warp-drive/core/types/symbols';
-import { setConfig } from '@warp-drive/build-config';
+import { setConfig } from '@warp-drive/core/build-config';

This package is subject to the following constraints:

  • it can contain and depend upon nothing framework specific
  • it must contain everything required to setup and use a basic WarpDrive experience (managed requests)
  • it should contain anything needed to go beyond the basic experience IF it would be usable by any framework and with any desired API.

With this in mind: lets look at what packages move into core:

  • @warp-drive/core-types{/*} => @warp-drive/core/types{/*}

This library which provides symbols and types that all (or most) other WarpDrive packages rely upon.

  • @warp-drive/build-config{/*} => @warp-drive/core/build-config{/*}

This package provides our macros build-plugin configuration – allowing apps to utilize support for feature flags, deprecation removal, debug logging, and environment based debugging assistance.

  • @ember-data/request => @warp-drive/core/request (named exports only)

As the RequestManager is the heart of the basic experience, it will become a named import from the root (see below). The remaining request utilities such as the promise cache and request specific types will be available from the /request path.

We do not retain the request/fetch subpath (see next item).

  • @ember-data/request/fetch => @warp-drive/core

The Fetch handler becomes a named import from the core path, this simplifies the imports when doing the most common configuration.

- import RequestManager from '@ember-data/request';
- import Fetch from '@ember-data/request/fetch';
+ import { RequestManager, Fetch } from '@warp-drive/core';
  • @ember-data/store{/*} => @warp-drive/core/store{/*}

The lone exception in the store package is that the Store itself will become a named export in @warp-drive/core. We thinking giving RequestManager and Store the same level of precedence is important as the former is the heart of the basic experience and the latter the heart of the advanced experience.

  • @ember-data/graph{/*} => @warp-drive/core/graph{/*}

Note: this package still does not have any public APIs, we believe we will be able to mark it public once schema-record reaches feature-complete status.

This package is in many ways a "utility" package for building a robust cache implementation offering ORM-like capabilities for use with the Store. As it is intended for use by any Cache implementation (not just the JSON:API cache) and because it is sufficiently advanced in ways Cache implementations are unlikely to want to attempt to replicate, we provide it from core and rely on tree-shaking to remove it if the cache implementation does not import and use it.

  • @ember-data/tracking (gets deprecated)
  • @warp-drive/signals (gets added)
  • @warp-drive/signals{/*} => @warp-drive/core/signals{/*}

This move is the most chaotic one but we believe if we make it now it'll only affect the small number of apps that have manually configured all of the packages and their required peers, and we think we can do this move in a non-breaking way even for them.

Historically, @ember-data/tracking (and using TC39 terminology) this package has provided a signal and computed implementation as well as a batching mechanism for use by reactive primitives in other packages. By encapsulating our reactivity needs in this way, we've prepared ourselves to enable swaping out the underlying implementation as desired.

Today, this package uses @tracked and @cached as its implementations of signal and computed under the hood (handwave, its slightly more complicated than that), but we want to make the precise mechanism configurable.

We would keep the basic infrastructure, decorators and utils in a new @warp-drive/signals package which would then require being configured for a specific reactivity implementation (such as any of the many signals implementations available today).

We would move the ember-specific configuration code into the @warp-drive/ember package, and duplicate it in @ember-data/tracking with a deprecation to preserve existing behaviors.

  • @warp-drive/schema-record{/*} => @warp-drive/core/reactive

This package enables applications to use deeply-reactive objects to access the data in the cache based upon a provided schema. It is the long-term replacement for @ember-data/model. We believe the SchemaRecord paradigm is flexible and powerful enough that even though we will retain the hook-based configuration for instantiating records we find it unlikely alternative record implementations will be built. By retaining the hook, should we (or someone else) decide to build an alternative, this code will be tree-shaken. That said, we believe this primitive is core to the WarpDrive experience.

@warp-drive/json-api

This package will absorb the cache implementation from @ember-data/json-api (but not the request builders).

Every app that wants to go beyond the basic RequestManager experience and utilize a Store will need a Cache. We hope someday to see more cache implementations built, for now this is the only one officially available.

@warp-drive/ember

This package stays the same but gains features from two other packages.

From @ember-data/tracking it will absorb the responsibility to configure the reactivity system to use ember's signals implementation (@tracked).

From @ember-data/debug it will absorb the repsonsibility of providing support for the ember-inspector, for as long as the library still integrates with ember-inspector. We may provide our own browser extension or "pluggable panel" in the future, at which point the proper home for extension support might get reconsidered. @ember-data/debug thus becomes deprecated.

Both of these changes mean that once deprecation cycles are complete, @warp-drive/ember becomes a required package for using EmberData/WarpDrive in an Ember application.

@warp-drive/utilities

This package reconstitutes all or parts of four current packages:

  • @ember-data/rest/request{/*} => @warp-drive/utilities/rest{/*}
  • @ember-data/active-record/request{/*} => @warp-drive/utilities/active-record{/*}
  • @ember-data/json-api/request{/*} => @warp-drive/utilities/json-api{/*}
  • @ember-data/request-utils{/*} => @warp-drive/utilities/request{/*}

The primary exception is that the basic CachePolicy will move into core and be imported via

import { DefaultCachePolicy } from '@warp-drive/core/store';

We are making this move because we feel it is approaching the level of implementation that most apps will likely use it instead of authoring their own.

@warp-drive/experiments

This package remains unchanged. It represents unstable experiments that we hope to eventually bring into the core experience.

@warp-drive/legacy

This package exists somewhat-temporarily to make maintaining the legacy ember-data experience easier. It comprises of four packages:

  • @ember-data/adapter{/*} => @warp-drive/legacy/adapter{/*}
  • @ember-data/serializer{/*} => @warp-drive/legacy/serializer{/*}
  • @ember-data/model{/*} => @warp-drive/legacy/model{/*}
  • @ember-data/legacy-compat{/*} => @warp-drive/legacy/legacy-compat{/*}

Configuring an Application

With all of the above changes in mind, here is what configuring an Ember application for the recommended experience would look like:

/app/services/store.ts

import { RequestManager, Store, Fetch } from '@warp-drive/core';
import { CacheHandler, DefaultCachePolicy, SchemaService } from '@warp-drive/core/store';
import { JSONAPICache } from '@warp-drive/json-api';
import { instantiateRecord, teardownRecord, type SchemaRecord} from '@warp-drive/core/reactive';

import type { CacheCapabilitiesManager } from '@warp-drive/core/store/types';
import type { StableRecordIdentifier } from '@warp-drive/core/types';

export default class AppStore extends Store {
  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  cachePolicy = new DefaultCachePolicy({
    apiHardExpires: 5 * 60 * 1000 // 5min
    apiSoftExpires: 1 * 60 * 1000 // 1min
  });

  createSchemaService() {
    return new SchemaService();
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: StableRecordIdentifier, createArgs?: Record<string, unknown>): SchemaRecord {
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: SchemaRecord): void {
    return teardownRecord(record);
  }
}

/ember-cli-build.js

'use strict';

const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { maybeEmbroider } = require('@embroider/test-setup');

module.exports = async function (defaults) {
  let app = new EmberApp(defaults, {});

  const { setConfig } = await import('@warp-drive/build-config');
  setConfig(app, __dirname, {
    // the version of WarpDrive this file was generated with,
    // can be updated along with WarpDrive as long as deprecations
    // are resolved.
    compatWith: '5.4',
    debug: {
      // activate flags for logging here
    },
    deprecations: {
      // specify deprecated features to remove here
    }
  });

  return maybeEmbroider(app);
};

The below file would only be updated if @warp-drive/utilities was selected for use:

/app/app.ts

import Application from '@ember/application';
import compatModules from '@embroider/virtual/compat-modules';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
+import { setBuildURLConfig } from '@warp-drive/utilities';
import config from './config/environment';

+setBuildURLConfig({
+  host: '/',
+  namespace: 'api',
+});

export default class App extends Application {
  modulePrefix = config.modulePrefix;
  podModulePrefix = config.podModulePrefix;
  Resolver = Resolver.withModules(compatModules);
}

loadInitializers(App, config.modulePrefix, compatModules);

/package.json

{
  "dependencies": {
+    "@warp-drive/core": "5.4.0",
+    "@warp-drive/ember": "5.4.0",
+    "@warp-drive/json-api": "5.4.0"
  }
}

/tsconfig.json

No changes are required to tsconfig, as we will ship types as stable when installing WarpDrive via this mechanism. If for some reason we are unable to:

 {
   "compilerOptions": {
+     "types": [
+       "ember-source/types",
+       "@warp-drive/core/preview-types",
+       "@warp-drive/ember/preview-types",
+       "@warp-drive/json-api/preview-types",
+    ]
   }
 }

Codemod / Lint Rules for the Migration

This can become the recommended experience for new apps without any codemods or lint rules provided documentation is updated.

In the interest of moving folks to this experience: we should consider a lint rule with an autofix that changes paths similar to when we first split ember and ember-data into their multi-package architecture.

We should also consider a lightweight codemod that uninstalls any WarpDrive/EmberData packages that are present and installs the appropriate packages from the set of 6, updating import paths in the process.

Likely we can safely automate this for any app using 4.13 or 5.3+.

Polaris / V2 App Blueprint

We would remove ember-data from the default blueprint and add the file changes above as shown. This takes the place of an automated installer script (which we still want to do). We can replace these changes with a script that prompts the user for selections once we have the tooling for doing so.

Guides & ApiDocs

The guides will need to be updated to reflect the WarpDrive terminology and new import locations. They are already in need of a refresh to align with modern best-practices as we push towards delivering the Polaris experience for WarpDrive, and this can be done all at once.

We will need a solution for ApiDocs. While the ApiDocs have been able to handle multiple packages, we would want to document these APIs via their locations in the new packages instead, which means dropping quite a lot of packages from being contained in the ApiDocs which has url concerns.

The intent is to have tooling that "re-exports" docs from the original packages at from the new home as well to avoid things "Store" being imported from "@warp-drive/core" but documented via "@ember-data/store".

We could have that tool produce an artifact that contains information about both locations for use by the docs: e.g. something along the lines of:

const classDoc = {
  name: 'Store',
  export: 'Store',
  module: '@warp-drive/core',
  location: 'packages/core/src/index.ts',
  upstream: {
    name: 'Store',
    export: 'default',
    module: '@ember-data/store',
    location: 'packages/store/src/index.ts',
  },
  tags: [],
  description: [],
  // ... etc.
}

Lockstep Versioning

We will continue to publish these packages in lockstep with each other. Specifically this means that peers and dependencies are pinned to the version(s) published in the same release.

We will also continue to follow the general rule of thumb we've had with WarpDrive/EmberData packages to this point: newer packages begin at 0.0.0, progress to 0.X.0 when mostly stable, and then jump to match the overall project version once stable. This way even new packages come to have identical versions once stability is reached, making version management easier.

Future Deprecation cycles

This package configuration provides an avenue for cleaner deprecation cycles in the future for major concepts.

When something at the package-level of scope is removed from the core experience, we can do a two-stage deprecation.

In stage-1, the imports in /core are deprecated and instead users must install and import from /legacy. In stage-2, the feature in /legacy is deprecated. This would spread this sort of "concept removal" deprecation across two majors by design, enabling us to keep our preferred path of long-tail deprecation support while still signifying in a clear way which patterns are the current happy path.

Drawbacks

As far as I am aware, nearly everyone has expresed a desire to simplify the config in some way, though ideas on how have varied.

If there is a reason to not do this it is about future-unknowns.

We learned with ember-data just how hard it could be to remove bad concepts from the library and architecture - a pain which drove us to split into packages for easier isolation, iteration and replacement in the first place.

By recombining at all we risk having this happen again as these packages will be broader in focus than what we have arrived at today. However, we do not intend to drop the use of individual packages, but rather recombine their exports into an ideal place for consumers, so I suspect this risk is comparatively low.

Alternatives

The impact of not doing this will be a cost payed by both maintainers and consumers. Maintainers will have to write extra tooling to help consumers maintain their projects, and spend more time debugging and answering questions around installation and configuration.

Consumers will feel frustrated with not being able to quickly get an updated version, or working installation.

An alternative is to return to a single package. In addition to ember-data, the team also owns the warp-drive npm package. This would enable us to do something along the lines of re-exporting every package under a sub-path. That would look something like this:

warp-drive
  /store
  /request
  /request-utils
  /tracking
  /adapter
  /serializer
  /schema-record
  /legacy-compat
  /model
  /ember
  /serializer
  /debug
  /build-config
  /core-types
  /json-api
  /rest
  /active-record
  /experiments

We feel this approach comes with several drawbacks.

First, it makes it harder to understand which packages are part of the recommended experience, vs which are being phased out.

Second, it means that code from all packages is available for import and intellisense, increasing the odds of developers making mistakes. Often those mistakes are easy to miss like importing the same token (findRecord) from legacy-compat or active-record instead of json-api.

Third: with the rise in AI assisted coding, having those packages and types present in a codebase also increases the risk of AI suggesting code utilizing them.

Lastly: we think we can use this opportunity to re-organize the mental model and reduce the number of concepts and terms developers need to be aware of when using WarpDrive.

In short, the original motivating factors for splitting into many-packages instead of one-package remain unchanged.

Unresolved Questions

Are there more import paths that we should shift the locations of?

For instance, these two types will be imported by nearly every store configuration:

import type { CacheCapabilitiesManager } from '@warp-drive/core/store/types';
import type { StableRecordIdentifier } from '@warp-drive/core/types';

Many types in core/store/types are specific to the store and are store-specific variations of signatures in core/types, but there are a few exceptions to this and CacheCapabilitiesManager is one of them. Maybe it and a few others make the move.

Similarly, the types for Document/RecordArray/Collection etc come from the store today (and are then repurposed for HasMany and similar), but these are types for reactive objects, and as such it may be best to move them into /reactive.

Because types support is still canary for WarpDrive, we do not have to answer this question via RFC and can answer it through iteration. But if we find other non-type imports that make sense to move we should call them out here.