Skip to content

Commit d532625

Browse files
committed
refactor(codemods): simplify TypeScript output config options
Replace `disableTypescriptSchemas` and `disableMissingTypeAutoGen` with `forceTypeScript`. JS files now produce plain JS output by default; set `forceTypeScript: true` to generate TypeScript schemas, types, and extensions from JavaScript sources. `combineSchemasAndTypes` defaults to `true` in `runMigration`.
1 parent 39ca04e commit d532625

23 files changed

Lines changed: 222 additions & 174 deletions

guides/migrating/codemods.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ This codemod transforms EmberData models and mixins into WarpDrive's schema form
5050

5151
The codemod is **non-destructive** - original model files are not removed. New files are generated in the `app/data/` by default.
5252

53+
> [!WARNING]
54+
> On large projects the codemod's AST parsing may exceed the default stack size, causing a segfault. If you hit this, raise the limit before running:
55+
> ```bash
56+
> ulimit -s unlimited
57+
> ```
58+
5359
### Basic Usage
5460
5561
```bash
@@ -70,6 +76,7 @@ npx @ember-data/codemods apply migrate-to-schema --project-name my-app --warp-dr
7076
| `--config <path>` | Path to a JSON configuration file |
7177
| `--skip-processed` | Skip files that have already been processed |
7278
| `--force-typescript` | Force all output files to TypeScript (`.ts`) |
79+
| `--separate-types` | Emit a separate `.type.ts` file alongside each `.schema` file. By default, type interfaces are combined into the `.schema` file |
7380
| `--model-source-dir <path>` | Directory containing model files (default: `./app/models`) |
7481
| `--mixin-source-dir <path>` | Directory containing mixin files (default: `./app/mixins`) |
7582
| `--output-dir <path>` | Output directory for generated schemas (default: `./app/data`) |
@@ -151,6 +158,8 @@ Key configuration options:
151158

152159
- **`projectName`** - The Ember app name, used for resolving classic module imports like `example-app/models/user`.
153160
- **`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.).
161+
- **`forceTypeScript`** - When `true`, all output is `.ts` with type interfaces, even when source files are JavaScript. When `false` (default), `.js` sources produce plain `.js` output without type annotations.
162+
- **`combineSchemasAndTypes`** - When `false`, emit a separate `.type.ts` file alongside each `.schema` file. By default (`true`), type interfaces are merged into the `.schema` file.
154163
- **`typeMapping`** - Maps custom EmberData transform names (e.g., `@attr('uuid')`) to TypeScript types for the generated type files.
155164
- **`intermediateModelPaths`** - Import paths of base classes between `Model` and your concrete models. The codemod will analyze these and convert them to traits.
156165
- **`importSubstitutes`** - For base classes whose source can't be analyzed, tells the codemod what trait/extension names to reference.

packages/codemods/src/cli/apply.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function createMigrateToSchemaCommand(applyCommand: Command) {
5959
)
6060
.addOption(new Option('--skip-processed', 'Skip files that have already been processed'))
6161
.addOption(new Option('--force-typescript', 'Force all output files to TypeScript (.ts)'))
62+
.addOption(new Option('--separate-types', 'Generate .type.ts files'))
6263
.addOption(new Option('--model-source-dir <path>', 'Directory containing model files').default('./app/models'))
6364
.addOption(new Option('--mixin-source-dir <path>', 'Directory containing mixin files').default('./app/mixins'))
6465
.addOption(new Option('--output-dir <path>', 'Output directory for generated schemas').default('./app/data'))
@@ -134,7 +135,6 @@ async function handleMigrateToSchema(
134135
}
135136

136137
const cliOptions: ConfigOptions = {
137-
...(options as ConfigOptions),
138138
inputDir,
139139
...(options.dry !== undefined && { dryRun: Boolean(options.dry) }),
140140
...(options.verbose !== undefined && { verbose: options.verbose === '1' || options.verbose === '2' }),
@@ -143,6 +143,7 @@ async function handleMigrateToSchema(
143143
...(options.mixinsOnly !== undefined && { mixinsOnly: Boolean(options.mixinsOnly) }),
144144
...(options.skipProcessed !== undefined && { skipProcessed: Boolean(options.skipProcessed) }),
145145
...(options.forceTypescript !== undefined && { forceTypeScript: Boolean(options.forceTypescript) }),
146+
...(options.separateTypes !== undefined && { combineSchemasAndTypes: !options.separateTypes }),
146147
...(options.projectName !== undefined && { projectName: String(options.projectName) }),
147148
...(options.warpDriveImports !== undefined && {
148149
warpDriveImports: options.warpDriveImports as 'legacy' | 'modern' | 'mirror',

packages/codemods/src/schema-migration/config-schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
"description": "Force all output files to be TypeScript (.ts) even when input files are JavaScript (.js).",
3737
"default": false
3838
},
39+
"combineSchemasAndTypes": {
40+
"type": "boolean",
41+
"description": "Combine schema and type interface into a single file. When true, each model produces one .schema file. Set to false to emit a separate .type.ts file.",
42+
"default": true
43+
},
3944
"mirror": {
4045
"type": "boolean",
4146
"description": "Use @warp-drive-mirror instead of @warp-drive for imports",

packages/codemods/src/schema-migration/config.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export function getConfiguredImport(
9898
} else if (importName === 'AsyncHasMany' || importName === 'HasMany') {
9999
// typically these imports are from the same source as model
100100
return { imported: importName, source: packageImports.Model.source, isType: true };
101+
} else if (importName === 'LegacyTrait') {
102+
const schemaImport = packageImports['LegacyResourceSchema' as keyof typeof packageImports];
103+
if (schemaImport) {
104+
return { imported: 'LegacyTrait', source: schemaImport.source, isType: true };
105+
}
106+
return { imported: 'LegacyTrait', source: ModernPackageImports.LegacyTrait.source, isType: true };
101107
} else {
102108
return undefined;
103109
}
@@ -123,20 +129,27 @@ export interface TransformOptions {
123129
* import paths from other project files. Defaults to false.
124130
*/
125131
projectImportsUseExtensions?: boolean;
126-
/** Combine schemas and types into a single file. By default these will be in separate files */
127-
combineSchemasAndTypes?: boolean;
128-
/** By default, schemas will be output in TS files even when generated from untyped models. */
129-
disableTypescriptSchemas?: boolean;
130-
/** Force all output files to be TypeScript (.ts) even when input files are JavaScript (.js). */
131-
forceTypeScript?: boolean;
132132
/**
133-
* By default, the codemod will attempt to generate TypeScript types for models that don't
134-
* have them by analyzing the model file and various transforms that are in use.
133+
* Combine schemas and types into a single file. Defaults to true.
134+
*
135+
* When true, each model/mixin produces a single `.schema` file containing
136+
* both the schema object and the TypeScript type interface.
135137
*
136-
* We heavily discourage turning this off as field level documentation comments are
137-
* associated to the type artifact, not the schema.
138+
* Set to false to emit a separate `.type.ts` file alongside the `.schema` file.
138139
*/
139-
disableMissingTypeAutoGen?: boolean;
140+
combineSchemasAndTypes: boolean;
141+
/**
142+
* Force all output files to be TypeScript (.ts) even when input files are JavaScript (.js).
143+
* Defaults to false.
144+
*
145+
* When false, output file extensions match the source file:
146+
* - `.js` source files produce `.schema.js` and `.ext.js` (no type interfaces)
147+
* - `.ts` source files produce `.schema.ts` and `.ext.ts` (with type interfaces)
148+
*
149+
* When true, all output is `.ts` regardless of source language, and type
150+
* interfaces are generated even for JavaScript source files.
151+
*/
152+
forceTypeScript?: boolean;
140153
/**
141154
* By default, the codemod will insert useful comments
142155
* to the generated types for inline/editor documentation
@@ -230,7 +243,17 @@ export interface MigrateOptions extends Partial<TransformOptions> {
230243

231244
export type FinalOptions = TransformOptions &
232245
MigrateOptions &
233-
Required<Pick<TransformOptions, 'modelSourceDir' | 'mixinSourceDir' | 'warpDriveImports' | 'projectName'>> &
246+
Required<
247+
Pick<
248+
TransformOptions,
249+
| 'modelSourceDir'
250+
| 'mixinSourceDir'
251+
| 'warpDriveImports'
252+
| 'projectName'
253+
| 'forceTypeScript'
254+
| 'combineSchemasAndTypes'
255+
>
256+
> &
234257
Required<Pick<MigrateOptions, 'inputDir'>> & {
235258
kind: 'finalized';
236259
outputDir: string;

packages/codemods/src/schema-migration/processors/mixin.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from 'path';
33

44
import { logger } from '../../../utils/logger.js';
55
import type { TransformerResult } from '../codemod.js';
6-
import type { TransformOptions } from '../config.js';
6+
import type { FinalOptions } from '../config.js';
77
import type { SchemaArtifact, SchemaArtifactRegistry } from '../utils/artifact.js';
88
import { createTraitArtifactConfig, isConnectedToModel as isConnectedToModelInRegistry } from '../utils/artifact.js';
99
import type { PropertyInfo, SchemaField, TransformArtifact } from '../utils/ast-utils.js';
@@ -27,7 +27,7 @@ const log = logger.for('mixin-processor');
2727
*/
2828
function ensureResourceTypeFileExists(
2929
modelType: string,
30-
options: TransformOptions,
30+
options: FinalOptions,
3131
artifacts: TransformArtifact[]
3232
): boolean {
3333
const pascalCaseType = toPascalCase(modelType);
@@ -81,7 +81,7 @@ export interface ${typeName} {
8181
*/
8282
export function toArtifacts(
8383
entity: SchemaArtifact,
84-
options: TransformOptions,
84+
options: FinalOptions,
8585
registry: SchemaArtifactRegistry = new Map()
8686
): TransformerResult {
8787
const parsedFile = entity.parsedFile;
@@ -145,7 +145,7 @@ function generateMixinArtifacts(
145145
traitFields: Array<{ name: string; kind: string; type?: string; options?: Record<string, unknown> }>,
146146
extensionProperties: PropertyInfo[],
147147
extendedTraits: string[],
148-
options: TransformOptions,
148+
options: FinalOptions,
149149
registry: SchemaArtifactRegistry
150150
): TransformArtifact[] {
151151
const artifacts: TransformArtifact[] = [];

packages/codemods/src/schema-migration/processors/model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'path';
44

55
import { logger } from '../../../utils/logger.js';
66
import type { TransformerResult } from '../codemod.js';
7-
import { getConfiguredImport, type TransformOptions } from '../config.js';
7+
import { getConfiguredImport, type FinalOptions, type TransformOptions } from '../config.js';
88
import type { SchemaArtifactRegistry } from '../utils/artifact.js';
99
import { createResourceArtifactConfig, createTraitArtifactConfig, SchemaArtifact } from '../utils/artifact.js';
1010
import type { ExtractedType, SchemaField, TransformArtifact } from '../utils/ast-utils.js';
@@ -308,7 +308,7 @@ export function processIntermediateModelsToTraits(
308308
intermediateModelPaths: string[],
309309
additionalModelSources: Array<{ pattern: string; dir: string }> | undefined,
310310
additionalMixinSources: Array<{ pattern: string; dir: string }> | undefined,
311-
options: TransformOptions,
311+
options: FinalOptions,
312312
registry: SchemaArtifactRegistry
313313
): { artifacts: TransformArtifact[]; errors: string[] } {
314314
const artifacts: TransformArtifact[] = [];
@@ -612,7 +612,7 @@ function generateRegularModelArtifacts(
612612

613613
export function toArtifacts(
614614
entity: SchemaArtifact,
615-
options: TransformOptions,
615+
options: FinalOptions,
616616
registry: SchemaArtifactRegistry = new Map()
617617
): TransformerResult {
618618
log.debug(`=== DEBUG: Processing ${entity.path} ===`);

packages/codemods/src/schema-migration/tasks/migrate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { basename, dirname, join, resolve } from 'path';
44
import { type InstanciatedLogger, logger } from '../../../utils/logger.js';
55
import type { SkippedFile, TransformerResult } from '../codemod.js';
66
import { Codemod } from '../codemod.js';
7-
import type { FinalOptions, MigrateOptions, TransformOptions } from '../config.js';
7+
import type { FinalOptions, MigrateOptions } from '../config.js';
88
import {
99
DEFAULT_INPUT_DIR,
1010
DEFAULT_MIXIN_SOURCE_DIR,
@@ -311,7 +311,7 @@ function writeIntermediateArtifacts(
311311

312312
type ArtifactTransformer = (
313313
entity: SchemaArtifact,
314-
options: TransformOptions,
314+
options: FinalOptions,
315315
registry: SchemaArtifactRegistry
316316
) => TransformerResult;
317317

@@ -384,6 +384,8 @@ export async function runMigration(options: MigrateOptions): Promise<void> {
384384
mixinSourceDir: options.mixinSourceDir || DEFAULT_MIXIN_SOURCE_DIR,
385385
projectName: options.projectName || '',
386386
...options,
387+
combineSchemasAndTypes: options.combineSchemasAndTypes ?? true,
388+
forceTypeScript: options.forceTypeScript ?? false,
387389
};
388390

389391
const log = logger.for('migrate-to-schema');

packages/codemods/src/schema-migration/utils/artifact.ts

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -203,31 +203,28 @@ export function createTraitArtifactConfig(
203203
hasExtensionProperties: boolean,
204204
isTypeScript: boolean
205205
): ArtifactConfig {
206-
const hasTypes = isTypeScript || !options.disableMissingTypeAutoGen;
207-
const schemaIsTyped = (hasTypes && options.combineSchemasAndTypes) || !options.disableTypescriptSchemas;
208-
const extensionIsTyped = isTypeScript;
209206
const hasExtension = hasExtensionProperties;
210207

211208
return {
212209
type: 'trait',
213210
name,
214211
isFragment: false,
215-
hasTypes,
216-
schemaIsTyped,
217-
extensionIsTyped,
212+
hasTypes: isTypeScript,
213+
schemaIsTyped: isTypeScript,
214+
extensionIsTyped: isTypeScript,
218215
hasExtension,
219216
identifiers: {
220217
schema: deriveTraitSchemaName(name),
221-
fieldsInterface: hasTypes ? deriveTraitInterfaceName(name) : null,
222-
type: hasTypes ? classified : null,
218+
fieldsInterface: isTypeScript ? deriveTraitInterfaceName(name) : null,
219+
type: isTypeScript ? classified : null,
223220
extension: hasExtension ? deriveTraitExtensionName(name) : null,
224-
extensionAlias: hasTypes && hasExtension ? `${classified}TraitWithExtensions` : null,
221+
extensionAlias: isTypeScript && hasExtension ? `${classified}TraitWithExtensions` : null,
225222
},
226223
traits: traits.map((trait) => ({
227224
name: trait,
228225
identifiers: {
229-
fieldsInterface: hasTypes ? deriveTraitInterfaceName(trait) : null,
230-
extension: hasTypes ? deriveTraitExtensionName(trait) : null,
226+
fieldsInterface: isTypeScript ? deriveTraitInterfaceName(trait) : null,
227+
extension: isTypeScript ? deriveTraitExtensionName(trait) : null,
231228
},
232229
})),
233230
};
@@ -246,43 +243,29 @@ export function createResourceArtifactConfig(
246243
): ArtifactConfig {
247244
const name = analysis.baseName;
248245
const classified = analysis.modelName;
249-
250-
/**
251-
* types are required IF the model was typed OR
252-
* the options don't disable automatic type generation
253-
* for untyped models
254-
*/
255-
const hasTypes = modelWasTyped || !options.disableMissingTypeAutoGen;
256-
const schemaIsTyped = (hasTypes && options.combineSchemasAndTypes) || !options.disableTypescriptSchemas;
257-
const extensionIsTyped = modelWasTyped;
258-
259-
/**
260-
* an extension is required IF
261-
* we have a trait OR we have our own extension
262-
*/
263246
const hasExtension =
264247
analysis.mixinTraits.length > 0 || analysis.mixinExtensions.length > 0 || analysis.extensionProperties.length > 0;
265248

266249
return {
267250
type: 'resource',
268251
name,
269252
isFragment: analysis.isFragment ?? false,
270-
hasTypes,
271-
schemaIsTyped,
272-
extensionIsTyped,
253+
hasTypes: modelWasTyped,
254+
schemaIsTyped: modelWasTyped,
255+
extensionIsTyped: modelWasTyped,
273256
hasExtension,
274257
identifiers: {
275258
schema: deriveResourceSchemaName(name),
276-
fieldsInterface: hasTypes ? `${classified}Resource` : null,
277-
type: hasTypes ? classified : null,
259+
fieldsInterface: modelWasTyped ? `${classified}Resource` : null,
260+
type: modelWasTyped ? classified : null,
278261
extension: hasExtension ? deriveResourceExtensionName(name) : null,
279-
extensionAlias: hasTypes && hasExtension ? `${classified}WithExtensions` : null,
262+
extensionAlias: modelWasTyped && hasExtension ? `${classified}WithExtensions` : null,
280263
},
281264
traits: analysis.mixinTraits.map((trait) => ({
282265
name: trait,
283266
identifiers: {
284-
fieldsInterface: hasTypes ? deriveTraitInterfaceName(trait) : null,
285-
extension: hasTypes ? deriveTraitExtensionName(trait) : null,
267+
fieldsInterface: modelWasTyped ? deriveTraitInterfaceName(trait) : null,
268+
extension: modelWasTyped ? deriveTraitExtensionName(trait) : null,
286269
},
287270
})),
288271
};

packages/codemods/src/schema-migration/utils/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface ConfigOptions {
66
verbose?: boolean;
77
debug?: boolean;
88
forceTypeScript?: boolean;
9+
combineSchemasAndTypes?: boolean;
910
mirror?: boolean;
1011
projectName?: string;
1112
warpDriveImports?: 'legacy' | 'modern' | 'mirror';

packages/codemods/src/schema-migration/utils/import-utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export function generateWarpDriveTypeImport(
8484
* When `hasTypes` is `false` the trait never produced a `.type` file,
8585
* so the suffix is always `.schema` regardless of config.
8686
*/
87-
export function resolveTraitImportPath(baseName: string, options?: TransformOptions, hasTypes = true): string {
88-
const suffix = !options?.combineSchemasAndTypes && hasTypes ? 'type' : 'schema';
89-
const base = options?.traitsImport ?? '../traits';
87+
export function resolveTraitImportPath(baseName: string, options: TransformOptions, hasTypes = true): string {
88+
const suffix = !options.combineSchemasAndTypes && hasTypes ? 'type' : 'schema';
89+
const base = options.traitsImport ?? '../traits';
9090
return `${base}/${baseName}.${suffix}`;
9191
}
9292

@@ -95,7 +95,7 @@ export function resolveTraitImportPath(baseName: string, options?: TransformOpti
9595
* e.g., generateTraitImport('shareable', options) returns:
9696
* "type { ShareableTrait } from '../traits/shareable.type'"
9797
*/
98-
export function generateTraitImport(traitName: string, options?: TransformOptions): string {
98+
export function generateTraitImport(traitName: string, options: TransformOptions): string {
9999
const traitTypeName = deriveTraitInterfaceName(traitName);
100100
return `type { ${traitTypeName} } from '${resolveTraitImportPath(traitName, options)}'`;
101101
}
@@ -188,8 +188,8 @@ export function transformModelToResourceImport(
188188

189189
let useTypeFile = false;
190190
if (!options.combineSchemasAndTypes) {
191-
const isTargetTyped = modelEntity ? modelEntity.parsedFile.isTypeScript : false;
192-
useTypeFile = isTargetTyped || !options.disableMissingTypeAutoGen;
191+
const isTargetTyped = modelEntity ? modelEntity.parsedFile.isTypeScript : true;
192+
useTypeFile = isTargetTyped;
193193
}
194194

195195
const typeFileName = useTypeFile ? `${relatedType}.type${ext}` : `${relatedType}.schema${ext}`;

0 commit comments

Comments
 (0)