Using Codemods
WarpDrive provides automated codemods to help migrate your EmberData application to modern WarpDrive patterns. The @ember-data/codemods package includes tools for transforming models and mixins into schemas, updating legacy store methods, and managing package dependencies.
NOTE
The codemods are under active development. Missing features and bugs are expected - please report any issues you find!
Getting Started
Listing Available Codemods
npx @ember-data/codemods listThis will print the available codemods:
| Codemod | Description |
|---|---|
migrate-to-schema | Migrates EmberData models and mixins to WarpDrive schemas |
legacy-compat-builders | Updates legacy store methods to use store.request and builders |
update-packages | Updates package.json with packages required for WarpDrive migration |
Running a Codemod
npx @ember-data/codemods apply <codemod-name> [options] <target...>To see the options for a specific codemod:
npx @ember-data/codemods apply <codemod-name> --helpmigrate-to-schema
This codemod transforms EmberData models and mixins into WarpDrive's schema format. For each model it generates:
- Schema files - Define the data structure using
LegacyResourceSchema - Type files - TypeScript interfaces for the resource
- Extension files - Preserve computed properties, methods, and other non-data logic
- Trait files - Reusable schema components extracted from mixins
The codemod is non-destructive - original model files are not removed. New files are generated in the app/data/ by default.
Basic Usage
# Transform all models and mixins (looks at ./app by default)
npx @ember-data/codemods apply migrate-to-schema --project-name my-app
# With custom search path
npx @ember-data/codemods apply migrate-to-schema --project-name my-app ./packages/ember-app/app
# Specify the WarpDrive import preset
npx @ember-data/codemods apply migrate-to-schema --project-name my-app --warp-drive-imports legacy| Option | Description |
|---|---|
--project-name <name> | Project name for resolving classic ember module imports (e.g., my-app/models/user) |
--warp-drive-imports <preset> | WarpDrive import preset: legacy (default, @ember-data/*), modern (@warp-drive/*), or mirror (@warp-drive-mirror/*) |
--config <path> | Path to a JSON configuration file |
--skip-processed | Skip files that have already been processed |
--force-typescript | Force all output files to TypeScript (.ts) |
--model-source-dir <path> | Directory containing model files (default: ./app/models) |
--mixin-source-dir <path> | Directory containing mixin files (default: ./app/mixins) |
--output-dir <path> | Output directory for generated schemas (default: ./app/data) |
Configuration
When the defaults aren't enough: For projects with custom base classes, re-exported models, or monorepo structures, the codemod accepts a JSON configuration file:
npx @ember-data/codemods apply migrate-to-schema --config=./codemod.config.jsonTIP
For the full list of available configuration options, see the config type definition and the JSON schema.
Simple Configuration
For most projects, a minimal config is all you need. The warpDriveImports option tells the codemod which package set your app uses for WarpDrive APIs - "legacy" for classic @ember-data/* packages, "modern" for the new @warp-drive/* packages:
{
"projectName": "example-app",
"warpDriveImports": "legacy",
"typeMapping": {
"uuid": "string",
"currency": "number",
"json": "unknown"
}
}Complex Configuration
For projects with custom import sources, intermediate base classes, or monorepo structures where models and mixins live across multiple packages:
{
"projectName": "example-app",
"emberDataImportSource": "@example-org/warp-drive/v1/model",
"resourcesImport": "example-app/data/resources",
"forceTypeScript": true,
"typeMapping": {
"uuid": "string",
"currency": "number",
"json": "unknown"
},
"warpDriveImports": {
"Model": { "imported": "default", "source": "@example-org/warp-drive/v1/model" },
"Type": { "imported": "Type", "source": "@example-org/warp-drive/v1/core-types/symbols" },
"WithLegacy": { "imported": "WithLegacy", "source": "@example-org/warp-drive/v1/model/migration-support" },
"withDefaults": { "imported": "withDefaults", "source": "@example-org/warp-drive/v1/model/migration-support" },
"LegacyResourceSchema": { "imported": "LegacyResourceSchema", "source": "@example-org/warp-drive/v1/core-types/schema/fields" }
},
"intermediateModelPaths": [
"example-app/core/data-field-model",
"@example-org/client-core/mixins/base-model",
"../core/base-model"
],
"additionalMixinSources": [
{
"pattern": "@example-org/core/mixins/*",
"dir": "../../libraries/core/package/src/mixins/*"
}
],
"additionalModelSources": [
{
"pattern": "example-app/core/",
"dir": "./app/core/"
},
{
"pattern": "../core/",
"dir": "./app/core/"
}
]
}Key configuration options:
projectName- The Ember app name, used for resolving classic module imports likeexample-app/models/user.emberDataImportSource/warpDriveImports- Tell the codemod where your app imports EmberData and WarpDrive APIs from, when they differ from the defaults (@ember-data/model,@warp-drive/core, etc.).typeMapping- Maps custom EmberData transform names (e.g.,@attr('uuid')) to TypeScript types for the generated type files.intermediateModelPaths- Import paths of base classes betweenModeland your concrete models. The codemod will analyze these and convert them to traits.importSubstitutes- For base classes whose source can't be analyzed, tells the codemod what trait/extension names to reference.additionalModelSources/additionalMixinSources- Maps import patterns to on-disk directories so the codemod can locate source files that live outside the mainapp/directory (e.g., in a monorepo's shared libraries).
What Gets Generated
Given a model like:
// app/models/user.ts
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';
export default class User extends Model {
declare [Type]: 'user';
@attr('string') declare name: string;
@attr('string') declare email: string;
@belongsTo('company', { async: false, inverse: null })
declare company: Company;
@hasMany('post', { async: true, inverse: 'author' })
declare posts: Post[];
get displayName() {
return this.name || this.email;
}
}The codemod produces:
import { withDefaults } from '@warp-drive/legacy/model/migration-support';
export const UserSchema = withDefaults({
type: 'user',
fields: [
{ kind: 'attribute', name: 'name', type: 'string' },
{ kind: 'attribute', name: 'email', type: 'string' },
{
kind: 'belongsTo',
name: 'company',
type: 'company',
options: { async: false, inverse: null },
},
{
kind: 'hasMany',
name: 'posts',
type: 'post',
options: { async: true, inverse: 'author' },
},
],
objectExtensions: ['user-extension'],
});import type { Type } from '@warp-drive/core/types/symbols';
import { WithLegacy } from '@warp-drive/legacy/model/migration-support';
export interface User {
[Type]: 'user';
name: string;
email: string;
company: Company;
posts: Post[];
}
export type LegacyUser = WithLegacy<User>;import type { LegacyUser } from './type';
export interface UserExtension extends LegacyUser {}
export class UserExtension {
get displayName() {
return this.name || this.email;
}
}
const Registration = {
name: 'user-extension',
kind: 'object',
features: UserExtension,
};
export default Registration;Mixins Become Traits
Mixins are decomposed into trait schemas and extensions:
// app/mixins/timestamped.ts
import Mixin from '@ember/object/mixin';
import { attr } from '@ember-data/model';
export default Mixin.create({
createdAt: attr(),
updatedAt: attr(),
async softDelete() {
// ...
}
});Models that use the mixin will reference the trait and extension by name in their generated schemas:
export const UserSchema = withDefaults({
type: 'user',
fields: [/* ... */],
traits: ['timestamped'],
objectExtensions: ['timestamped-extension', 'user-extension'],
});Caveats
Parent / base classes require manual migration. The codemod does not reliably migrate abstract base classes such as
BaseModelorDataFieldModel. If your app has intermediate classes betweenModeland your concrete models, you should migrate those by hand first and then useimportSubstitutesorintermediateModelPathsin your configuration to tell the codemod how to reference them.Re-exported models from libraries are not migrated. The codemod tries its best to follow imports and locate source files, but models that are re-exported from external packages (e.g.,
import MyModel from '@my-org/shared-models/my-model') cannot have their source analyzed. These will be skipped. UseadditionalModelSourcesto point the codemod at the on-disk location of library code, or migrate those models manually.Only the default export is processed per file. If a file contains multiple classes, only the default export (or
export { X as default }) is analyzed. Additional class declarations in the same file are silently ignored. Split them into separate files before running the codemod.Run the codemod, review the output, and iterate on configuration as needed. Most projects will need at least a minimal config file.
Registering Generated Schemas
After the codemod generates your schema, type, and extension files, you need to register them with the WarpDrive store. You can use import.meta.glob (available in Vite and Embroider) to bulk-load everything from the generated directories instead of manually importing each file.
Loading schemas, traits, and extensions
const schemaModules = import.meta.glob('./data/resources/**/*.schema.ts', { eager: true });
const traitModules = import.meta.glob('./data/traits/**/*.schema.ts', { eager: true });
const extensionModules = import.meta.glob(
['./data/resources/**/*.ext.ts', './data/traits/**/*.ext.ts'],
{ eager: true }
);Each module's default or named export contains the schema/trait/extension object that needs to be registered.
Registering with useLegacyStore
useLegacyStore accepts schemas, traits, and CAUTION_MEGA_DANGER_ZONE_extensions arrays directly:
import { useLegacyStore } from '@warp-drive/legacy';
import { JSONAPICache } from '@warp-drive/json-api';
const schemas = Object.values(import.meta.glob('../data/resources/**/*.schema.ts', { eager: true, import: 'default' }));
const traits = Object.values(import.meta.glob('../data/traits/**/*.schema.ts', { eager: true, import: 'default' }));
const extensions = Object.values(import.meta.glob(
['../data/resources/**/*.ext.ts', '../data/traits/**/*.ext.ts'],
{ eager: true, import: 'default' }
));
export default useLegacyStore({
legacyRequests: true,
cache: JSONAPICache,
schemas,
traits,
CAUTION_MEGA_DANGER_ZONE_extensions: extensions,
});Registering with a custom store
If you are using a custom Store subclass with createSchemaService(), register manually on the SchemaService:
createSchemaService() {
const schema = new SchemaService();
registerDerivations(schema);
schema.registerResources(schemas);
for (const trait of traits) {
schema.registerTrait(trait);
}
for (const extension of extensions) {
schema.CAUTION_MEGA_DANGER_ZONE_registerExtension(extension);
}
return schema;
}legacy-compat-builders
This codemod updates legacy store methods (findAll, findRecord, query, queryRecord, saveRecord) to use store.request with builders from @ember-data/legacy-compat/builders.
Usage
# Transform all files matching the pattern
npx @ember-data/codemods apply legacy-compat-builders './app/**/*.{js,ts}'
# Transform only specific methods
npx @ember-data/codemods apply legacy-compat-builders --methods findRecord query './app/**/*.{js,ts}'
# Dry run
npx @ember-data/codemods apply legacy-compat-builders --dry './app/**/*.{js,ts}'Options
| Option | Description |
|---|---|
-d, --dry | Dry run (no changes made) |
-v, --verbose <level> | Verbosity level (0, 1, 2) |
-l, --log-file [path] | Write logs to a file |
-i, --ignore <pattern...> | Ignore files matching the pattern |
--store-names <name...> | Identifier names for the store (default: ["store"]) |
--methods <name...> | Only transform specific methods |
Examples
findAll
// before
const posts = await store.findAll<Post>('post');
// after
import { findAll } from '@ember-data/legacy-compat/builders';
const { content: posts } = await store.request<Post[]>(findAll<Post>('post'));findRecord
// before
const post = await store.findRecord<Post>({ type: 'post', id: '1' });
// after
import { findRecord } from '@ember-data/legacy-compat/builders';
const { content: post } = await store.request<Post>(findRecord<Post>({ type: 'post', id: '1' }));query
// before
const posts = await store.query<Post>('post', { id: '1' });
// after
import { query } from '@ember-data/legacy-compat/builders';
const { content: posts } = await store.request<Post[]>(query<Post>('post', { id: '1' }));queryRecord
// before
const post = await store.queryRecord<Post>('post', { id: '1' });
// after
import { queryRecord } from '@ember-data/legacy-compat/builders';
const { content: post } = await store.request<Post>(queryRecord<Post>('post', { id: '1' }));saveRecord
// before
const post = store.createRecord<Post>('post', { name: 'Krystan rules, you drool' });
const saved = await store.saveRecord<Post>(post);
// after
import { saveRecord } from '@ember-data/legacy-compat/builders';
const post = store.createRecord<Post>('post', { name: 'Krystan rules, you drool' });
const { content: saved } = await store.request<Post>(saveRecord(post));Caveats
- Calls to legacy store methods that are not awaited will not be transformed. The codemod cannot safely add
awaitsince it doesn't know if consuming code can handle the change. - Exception: In a route's
modelhook, the codemod will transform the call and addawait. store.findRecordcalls with apreloadoption are not transformed, as this option is not supported by the legacy compat builders.- GJS and GTS files are not currently supported.
See the V3/V4 to V5 migration guide for the full migration process including store setup, reactivity configuration, and post-migration cleanup.