Skip to content

Commit 277a195

Browse files
chore(codemods): add codemods guide documentation (#10501)
* fix(codemods): centralize defaults, improve error visibility, remove hardcoded path - Centralize default values (inputDir, outputDir, modelSourceDir, etc.) in config.ts and make them required on FinalOptions so downstream code reads them directly without scattered fallbacks - Promote extension creation failure from log.warn to log.error so failures are visible at default log levels - Remove hardcoded /app/core/ intermediate model check — users should configure additionalModelSources instead * docs(codemods): Add using codemods guide * fix(codemods): use 'modern' warpdrive imports by default fix: use LegacyTrait interface for traits instead of LegacyResourceSchema * chore(codemods): add a TODO note for POJO extensions
1 parent d992b2e commit 277a195

17 files changed

Lines changed: 773 additions & 209 deletions

File tree

guides/migrating/codemods.md

Lines changed: 495 additions & 0 deletions
Large diffs are not rendered by default.

packages/codemods/src/cli/apply.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ function createMigrateToSchemaCommand(applyCommand: Command) {
5050

5151
command
5252
.addOption(new Option('--config <path>', 'Path to configuration file'))
53+
.addOption(new Option('--project-name <name>', 'Project name for resolving classic ember module imports'))
54+
.addOption(
55+
new Option(
56+
'--warp-drive-imports <preset>',
57+
'WarpDrive import preset: "legacy" for @ember-data/*, "modern" for @warp-drive/*, "mirror" for @warp-drive-mirror/*'
58+
).choices(['legacy', 'modern', 'mirror'])
59+
)
5360
.addOption(new Option('--skip-processed', 'Skip files that have already been processed'))
5461
.addOption(new Option('--force-typescript', 'Force all output files to TypeScript (.ts)'))
5562
.addOption(new Option('--model-source-dir <path>', 'Directory containing model files').default('./app/models'))
@@ -136,6 +143,10 @@ async function handleMigrateToSchema(
136143
...(options.mixinsOnly !== undefined && { mixinsOnly: Boolean(options.mixinsOnly) }),
137144
...(options.skipProcessed !== undefined && { skipProcessed: Boolean(options.skipProcessed) }),
138145
...(options.forceTypescript !== undefined && { forceTypeScript: Boolean(options.forceTypescript) }),
146+
...(options.projectName !== undefined && { projectName: String(options.projectName) }),
147+
...(options.warpDriveImports !== undefined && {
148+
warpDriveImports: options.warpDriveImports as 'legacy' | 'modern' | 'mirror',
149+
}),
139150
modelSourceDir: String(options.modelSourceDir || './app/models'),
140151
mixinSourceDir: String(options.mixinSourceDir || './app/mixins'),
141152
outputDir: String(options.outputDir || './app/schemas'),

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,6 @@ function isIntermediateModel(
6565
}
6666
}
6767
}
68-
69-
// Also check if it's in app/core
70-
if (filePath.includes('/app/core/')) {
71-
return true;
72-
}
7368
}
7469
}
7570

@@ -267,7 +262,6 @@ export class Codemod {
267262
}
268263

269264
async findModels() {
270-
// TODO: || './app/models'
271265
if (!this.finalOptions.modelSourceDir) {
272266
throw new Error('`options.modelSourceDir` must be specified before looking for files');
273267
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export const DEFAULT_RESOURCES_DIR = './app/data/resources';
22
export const DEFAULT_TRAITS_DIR = './app/data/traits';
3+
export const DEFAULT_INPUT_DIR = './app';
4+
export const DEFAULT_OUTPUT_DIR = './app/schemas';
5+
export const DEFAULT_MODEL_SOURCE_DIR = './app/models';
6+
export const DEFAULT_MIXIN_SOURCE_DIR = './app/mixins';
7+
export const DEFAULT_WARP_DRIVE_IMPORTS = 'modern' as const;
38

49
export interface PackageImport {
510
imported: string;
@@ -37,25 +42,29 @@ interface TransformPackageImports {
3742
Type: PackageImport;
3843
WithLegacy: PackageImport;
3944
LegacyResourceSchema: PackageImport;
45+
LegacyTrait: PackageImport;
4046
}
4147

4248
export const LegacyPackageImports = {
4349
Model: { imported: 'default', source: '@ember-data/model' },
4450
Type: { imported: 'Type', source: '@warp-drive/core-types/symbols' },
4551
WithLegacy: { imported: 'WithLegacy', source: '@ember-data/model/migration-support' },
4652
LegacyResourceSchema: { imported: 'LegacyResourceSchema', source: '@warp-drive/core-types/schema/fields' },
53+
LegacyTrait: { imported: 'LegacyTrait', source: '@warp-drive/core-types/schema/fields' },
4754
} satisfies TransformPackageImports;
4855
export const ModernPackageImports = {
4956
Model: { imported: 'Model', source: '@warp-drive/legacy/model' },
5057
Type: { imported: 'Type', source: '@warp-drive/core/types/symbols' },
5158
WithLegacy: { imported: 'WithLegacy', source: '@warp-drive/legacy/model/migration-support' },
5259
LegacyResourceSchema: { imported: 'LegacyResourceSchema', source: '@warp-drive/core/types/schema/fields' },
60+
LegacyTrait: { imported: 'LegacyTrait', source: '@warp-drive/core/types/schema/fields' },
5361
} satisfies TransformPackageImports;
5462
export const MirrorPackageImports = {
5563
Model: { imported: 'Model', source: '@warp-drive-mirror/legacy/model' },
5664
Type: { imported: 'Type', source: '@warp-drive-mirror/core/types/symbols' },
5765
WithLegacy: { imported: 'WithLegacy', source: '@warp-drive-mirror/legacy/model/migration-support' },
5866
LegacyResourceSchema: { imported: 'LegacyResourceSchema', source: '@warp-drive-mirror/core/types/schema/fields' },
67+
LegacyTrait: { imported: 'LegacyTrait', source: '@warp-drive-mirror/core/types/schema/fields' },
5968
} satisfies TransformPackageImports;
6069

6170
export function getConfiguredImport(
@@ -210,4 +219,10 @@ export interface MigrateOptions extends Partial<TransformOptions> {
210219
mixinSourceDir?: string;
211220
}
212221

213-
export type FinalOptions = TransformOptions & MigrateOptions & { kind: 'finalized' };
222+
export type FinalOptions = TransformOptions &
223+
MigrateOptions &
224+
Required<Pick<TransformOptions, 'modelSourceDir' | 'mixinSourceDir' | 'warpDriveImports' | 'projectName'>> &
225+
Required<Pick<MigrateOptions, 'inputDir'>> & {
226+
kind: 'finalized';
227+
outputDir: string;
228+
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ function resolveMixinPath(
192192
options: FinalOptions
193193
): string | null {
194194
try {
195-
const mixinSourceDir = resolve(options.mixinSourceDir || './app/mixins');
195+
const mixinSourceDir = resolve(options.mixinSourceDir);
196196
const config = getImportSourceConfig('mixin', options);
197197

198198
// Handle relative paths - must be within mixin source directory

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import { type InstanciatedLogger, logger } from '../../../utils/logger.js';
55
import type { SkippedFile, TransformerResult } from '../codemod.js';
66
import { Codemod } from '../codemod.js';
77
import type { FinalOptions, MigrateOptions, TransformOptions } from '../config.js';
8-
import { DEFAULT_RESOURCES_DIR, DEFAULT_TRAITS_DIR } from '../config.js';
8+
import {
9+
DEFAULT_INPUT_DIR,
10+
DEFAULT_MIXIN_SOURCE_DIR,
11+
DEFAULT_MODEL_SOURCE_DIR,
12+
DEFAULT_OUTPUT_DIR,
13+
DEFAULT_RESOURCES_DIR,
14+
DEFAULT_TRAITS_DIR,
15+
DEFAULT_WARP_DRIVE_IMPORTS,
16+
} from '../config.js';
917
import { toArtifacts as mixinToArtifacts } from '../processors/mixin.js';
1018
import { processIntermediateModelsToTraits, toArtifacts as modelToArtifacts } from '../processors/model.js';
1119
import type { SchemaArtifact, SchemaArtifactRegistry } from '../utils/artifact.js';
@@ -121,9 +129,9 @@ function getRelativePathFromAdditionalSources(
121129
/**
122130
* Get the relative path for a mixin file, handling both local and external mixins
123131
*/
124-
function getRelativePathForMixin(filePath: string, options: TransformOptions): string {
132+
function getRelativePathForMixin(filePath: string, options: FinalOptions): string {
125133
// First, try to get relative path from the main mixin source directory
126-
const mixinSourceDir = resolve(options.mixinSourceDir || './app/mixins');
134+
const mixinSourceDir = resolve(options.mixinSourceDir);
127135
if (filePath.startsWith(mixinSourceDir)) {
128136
return filePath.replace(mixinSourceDir, '').replace(/^\//, '');
129137
}
@@ -154,9 +162,9 @@ function getRelativePathForMixin(filePath: string, options: TransformOptions): s
154162
/**
155163
* Calculate relative path for model-based artifacts (schema, resource-type)
156164
*/
157-
function getRelativePathForModel(filePath: string, options: TransformOptions): string {
165+
function getRelativePathForModel(filePath: string, options: FinalOptions): string {
158166
// Try standard model source directory first
159-
let relativePath = filePath.replace(resolve(options.modelSourceDir || './app/models'), '');
167+
let relativePath = filePath.replace(resolve(options.modelSourceDir), '');
160168

161169
// If not in standard directory, check additionalModelSources
162170
if (relativePath === filePath) {
@@ -210,7 +218,7 @@ function buildOutputFileName(
210218
/**
211219
* Get the output directory for an artifact type
212220
*/
213-
function getOutputDirectory(artifactType: string, options: TransformOptions): string {
221+
function getOutputDirectory(artifactType: string, options: FinalOptions): string {
214222
const config = ARTIFACT_CONFIG[artifactType as ArtifactType] ?? DEFAULT_FALLBACK_CONFIG;
215223
return options[config.directoryKey] ?? config.defaultDir;
216224
}
@@ -221,7 +229,7 @@ function getOutputDirectory(artifactType: string, options: TransformOptions): st
221229
function getArtifactOutputPath(
222230
artifact: Artifact,
223231
filePath: string,
224-
options: TransformOptions
232+
options: FinalOptions
225233
): { outputDir: string; outputPath: string } {
226234
const config = ARTIFACT_CONFIG[artifact.type as ArtifactType] ?? DEFAULT_FALLBACK_CONFIG;
227235
const outputDir = getOutputDirectory(artifact.type, options);
@@ -367,21 +375,21 @@ function processFiles({ registry, finalOptions, log }: ProcessFilesOptions): Pro
367375
export async function runMigration(options: MigrateOptions): Promise<void> {
368376
const finalOptions: FinalOptions = {
369377
kind: 'finalized',
370-
inputDir: options.inputDir || './app',
371-
outputDir: options.outputDir || './app/schemas',
378+
inputDir: options.inputDir || DEFAULT_INPUT_DIR,
379+
outputDir: options.outputDir || DEFAULT_OUTPUT_DIR,
372380
dryRun: options.dryRun || false,
373381
verbose: options.verbose || false,
374-
warpDriveImports: options.warpDriveImports || 'legacy',
375-
modelSourceDir: options.modelSourceDir || './app/models',
376-
mixinSourceDir: options.mixinSourceDir || './app/mixins',
382+
warpDriveImports: options.warpDriveImports || DEFAULT_WARP_DRIVE_IMPORTS,
383+
modelSourceDir: options.modelSourceDir || DEFAULT_MODEL_SOURCE_DIR,
384+
mixinSourceDir: options.mixinSourceDir || DEFAULT_MIXIN_SOURCE_DIR,
377385
projectName: options.projectName || '',
378386
...options,
379387
};
380388

381389
const log = logger.for('migrate-to-schema');
382390
log.info(`🚀 Starting schema migration...`);
383-
log.info(`📁 Input directory: ${resolve(finalOptions.inputDir || './app')}`);
384-
log.info(`📁 Output directory: ${resolve(finalOptions.outputDir || './app/schemas')}`);
391+
log.info(`📁 Input directory: ${resolve(finalOptions.inputDir)}`);
392+
log.info(`📁 Output directory: ${resolve(finalOptions.outputDir)}`);
385393

386394
const codemod = new Codemod(log, finalOptions);
387395

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface ConfigOptions {
77
debug?: boolean;
88
forceTypeScript?: boolean;
99
mirror?: boolean;
10+
projectName?: string;
11+
warpDriveImports?: 'legacy' | 'modern' | 'mirror';
1012
emberDataImportSource?: string;
1113
intermediateModelPaths?: string[] | string;
1214
modelImportSource?: string;

packages/codemods/src/schema-migration/utils/extension-generation.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,16 @@ export function generateExtensionCode(
303303

304304
const objectCode = `export const ${config.identifiers.extension} = {\n${properties}\n};`;
305305
const registrationBlock = generateRegistrationBlock(config.name, config.identifiers.extension!);
306+
const migrationNote = `// TODO: migrate this extension to a class so that TypeScript declaration merging works.\n// Object extensions do not support interface merging.\n`;
306307

307308
if (config.extensionIsTyped && typeToExtend) {
308309
const importStatement = interfaceImportPath
309310
? `import type { ${typeToExtend} } from '${interfaceImportPath}';\n\n`
310311
: '';
311-
return `${importStatement}export interface ${config.identifiers.extension} extends ${typeToExtend} {}\n\n${objectCode}\n\n${registrationBlock}`;
312+
return `${importStatement}export interface ${config.identifiers.extension} extends ${typeToExtend} {}\n\n${migrationNote}${objectCode}\n\n${registrationBlock}`;
312313
}
313314

314-
return `${objectCode}\n\n${registrationBlock}`;
315+
return `${migrationNote}${objectCode}\n\n${registrationBlock}`;
315316
}
316317

317318
/**
@@ -622,7 +623,7 @@ export function createExtensionFromOriginalFile(
622623
suggestedFileName: extFileName,
623624
};
624625
} catch (error) {
625-
log.warn(`Error creating extension from original file: ${String(error)}`);
626+
log.error(`❌ Error creating extension for '${filePath}': ${String(error)}`);
626627
return null;
627628
}
628629
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,8 @@ function generateSchemaDeclaration(config: ArtifactConfig, schemaObject: Record<
692692
jsonString = jsonString.replace(new RegExp(`'${SCHEMA_OPTION_REF_PREFIX}([^']+)'`, 'g'), '$1');
693693

694694
if (config.schemaIsTyped) {
695-
return `const ${config.identifiers.schema} = ${jsonString} satisfies LegacyResourceSchema;`;
695+
const satisfiesType = config.type === 'trait' ? 'LegacyTrait' : 'LegacyResourceSchema';
696+
return `const ${config.identifiers.schema} = ${jsonString} satisfies ${satisfiesType};`;
696697
} else {
697698
return `const ${config.identifiers.schema} = ${jsonString};`;
698699
}
@@ -865,7 +866,8 @@ export function generateMergedSchemaCode(opts: MergedSchemaOptions): GeneratedSc
865866
};
866867

867868
if (!opts.options?.disableTypescriptSchemas) {
868-
const importLocation = getConfiguredImport(opts.options, 'LegacyResourceSchema');
869+
const schemaTypeKey = config.type === 'trait' ? 'LegacyTrait' : 'LegacyResourceSchema';
870+
const importLocation = getConfiguredImport(opts.options, schemaTypeKey);
869871
parts.schemaImports = `import type { ${importLocation.imported} } from '${importLocation.source}';\n`;
870872
}
871873

0 commit comments

Comments
 (0)