From 9dc20042dc6f85706359c3a299788a23cdac4545 Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Tue, 17 Feb 2026 16:05:53 +0100 Subject: [PATCH 1/4] feat(codemods): implemented Typescript interface merging proposal --- .../src/schema-migration/processors/mixin.ts | 4 +- .../src/schema-migration/processors/model.ts | 36 +-- .../src/schema-migration/utils/ast-utils.ts | 6 +- .../utils/extension-generation.ts | 19 +- .../utils/schema-generation.ts | 42 ++- .../migrate-to-schema.test.ts.snap | 242 +++++++++++++++--- .../intermediate-models.test.ts | 1 - .../migrate-to-schema.test.ts | 79 ++++-- .../tests/schema-migration/test-helpers.ts | 6 +- .../mixin-to-schema.test.ts.snap | 223 +++++++++++----- .../model-to-schema.test.ts.snap | 148 +++++++++-- .../transforms/mixin-to-schema.test.ts | 33 ++- .../transforms/model-to-schema.test.ts | 35 +-- .../schema-migration/utils/ast-utils.test.ts | 2 - .../schema-migration/utils/config.test.ts | 2 +- .../utils/generate-config.test.ts | 1 - 16 files changed, 641 insertions(+), 238 deletions(-) diff --git a/packages/codemods/src/schema-migration/processors/mixin.ts b/packages/codemods/src/schema-migration/processors/mixin.ts index 57c3c0e017b..94462ad1007 100644 --- a/packages/codemods/src/schema-migration/processors/mixin.ts +++ b/packages/codemods/src/schema-migration/processors/mixin.ts @@ -231,7 +231,7 @@ function generateMixinArtifacts( collectTraitImports(extendedTraits, imports, options); - const traitSchemaName = traitInterfaceName; + const traitSchemaName = `${toPascalCase(baseName)}Schema`; const traitInternalName = pascalToKebab(mixinName); const traitSchemaObject = buildTraitSchemaObject(traitFields as SchemaField[], extendedTraits, { name: traitInternalName, @@ -264,7 +264,7 @@ function generateMixinArtifacts( filePath, source, baseName, - `${mixinName}Extension`, + `${toPascalCase(mixinName)}Extension`, extensionProperties, options, traitInterfaceName, diff --git a/packages/codemods/src/schema-migration/processors/model.ts b/packages/codemods/src/schema-migration/processors/model.ts index a6e8f7dabf3..fad705f647a 100644 --- a/packages/codemods/src/schema-migration/processors/model.ts +++ b/packages/codemods/src/schema-migration/processors/model.ts @@ -54,7 +54,7 @@ import { NODE_KIND_METHOD_DEFINITION, NODE_KIND_PROPERTY_IDENTIFIER, } from '../utils/code-processing.js'; -import { appendExtensionSignatureType, createExtensionFromOriginalFile } from '../utils/extension-generation.js'; +import { createExtensionFromOriginalFile } from '../utils/extension-generation.js'; import type { ParsedFile } from '../utils/file-parser.js'; import { isClassMethodSyntax } from '../utils/file-parser.js'; import { replaceWildcardPattern } from '../utils/path-utils.js'; @@ -294,7 +294,7 @@ function extractHeritageInfo( const mixinImports = getMixinImports(root, options); mixinTraits.push(...extractMixinTraits(heritageClause, root, mixinImports, options)); - const mixinExts = extractMixinExtensions(heritageClause, root, mixinImports, filePath, options); + const mixinExts = extractMixinExtensions(filePath, options); mixinExtensions.push(...mixinExts); if (options?.intermediateModelPaths && options.intermediateModelPaths.length > 0) { @@ -649,6 +649,7 @@ function generateRegularModelArtifacts( const schemaObject = buildLegacySchemaObject(baseName, schemaFields, mixinTraits, mixinExtensions, isFragment); // Generate merged schema code (schema + types in one file) + const extensionName = extensionProperties.length > 0 ? `${modelName}Extension` : undefined; const mergedSchemaCode = generateMergedSchemaCode({ baseName, interfaceName: modelName, @@ -659,6 +660,7 @@ function generateRegularModelArtifacts( imports: schemaImports, isTypeScript, options, + extensionName, }); artifacts.push({ @@ -668,8 +670,7 @@ function generateRegularModelArtifacts( suggestedFileName: `${baseName}.schema${originalExtension}`, }); - // Create extension artifact preserving original file content - const modelInterfaceName = modelName; + const modelInterfaceName = `${modelName}Trait`; const modelImportPath = options?.resourcesImport ? `${options.resourcesImport}/${baseName}.schema` : `../resources/${baseName}.schema`; @@ -692,15 +693,6 @@ function generateRegularModelArtifacts( artifacts.push(extensionArtifact); } - // Create extension signature type alias if there are extension properties - log.debug( - `Extension properties length: ${extensionProperties.length}, extensionArtifact exists: ${!!extensionArtifact}` - ); - log.debug(`Extension properties: ${JSON.stringify(extensionProperties.map((p) => p.name))}`); - if (extensionProperties.length > 0 && extensionArtifact) { - appendExtensionSignatureType(extensionArtifact, modelName); - } - return artifacts; } @@ -919,9 +911,6 @@ function generateIntermediateModelTraitArtifacts( ); if (extensionArtifact) { artifacts.push(extensionArtifact); - - // Create extension signature type alias if there are extension properties - appendExtensionSignatureType(extensionArtifact, traitPascalName); } } @@ -1689,17 +1678,9 @@ function extractMixinTraits( } /** - * Extract mixin extensions for a model file - * Uses the pre-computed modelToMixinsMap to look up which mixins this model uses, - * then checks the mixinExtensionCache to get extension names for mixins with extension properties + * Get mixin extension names based on model imports. */ -function extractMixinExtensions( - _heritageClause: SgNode, - _root: SgNode, - _mixinImports: Map, - filePath: string, - options?: TransformOptions -): string[] { +function extractMixinExtensions(filePath: string, options?: TransformOptions): string[] { const mixinExtensions: string[] = []; const modelMixins = options?.modelToMixinsMap?.get(filePath); @@ -1711,7 +1692,8 @@ function extractMixinExtensions( // Check the mixinExtensionCache to see if this mixin has an extension const cacheEntry = mixinExtensionCache.get(mixinFilePath); if (cacheEntry?.hasExtension && cacheEntry.extensionName) { - mixinExtensions.push(cacheEntry.extensionName); + const basePart = cacheEntry.extensionName.replace(/Extension$/, ''); + mixinExtensions.push(`${toPascalCase(basePart)}Extension`); } } diff --git a/packages/codemods/src/schema-migration/utils/ast-utils.ts b/packages/codemods/src/schema-migration/utils/ast-utils.ts index 6550b3e360c..1adc0aa458a 100644 --- a/packages/codemods/src/schema-migration/utils/ast-utils.ts +++ b/packages/codemods/src/schema-migration/utils/ast-utils.ts @@ -96,8 +96,4 @@ export { } from './import-utils.js'; // Re-export from extension-generation -export { - generateExtensionCode, - createExtensionFromOriginalFile, - appendExtensionSignatureType, -} from './extension-generation.js'; +export { generateExtensionCode, createExtensionFromOriginalFile } from './extension-generation.js'; diff --git a/packages/codemods/src/schema-migration/utils/extension-generation.ts b/packages/codemods/src/schema-migration/utils/extension-generation.ts index 34be7d8f631..b99d9702fe0 100644 --- a/packages/codemods/src/schema-migration/utils/extension-generation.ts +++ b/packages/codemods/src/schema-migration/utils/extension-generation.ts @@ -123,7 +123,7 @@ function shouldKeepExported(exportNode: SgNode): boolean { * This only removes fragment imports since they're not needed in schema-record */ function removeUnnecessaryImports(source: string, options?: TransformOptions): string { - const linesToRemove = ['ember-data-model-fragments/attributes']; + const linesToRemove = ['ember-data-model-fragments/attributes', '@ember/object/mixin', '/mixins/']; const lines = source.split('\n'); const filteredLines = lines.filter((line) => { @@ -284,23 +284,6 @@ function updateRelativeImportsForExtensions( return result; } -/** - * Append extension signature type alias to an extension artifact - * This creates a TypeScript type alias like: export type UserExtensionSignature = typeof UserExtension; - */ -export function appendExtensionSignatureType(extensionArtifact: TransformArtifact, entityName: string): void { - const isTypeScript = extensionArtifact.suggestedFileName.endsWith('.ts'); - if (!isTypeScript) { - return; - } - - const signatureType = `${entityName}ExtensionSignature`; - const className = `${entityName}Extension`; - const signatureCode = `export type ${signatureType} = typeof ${className};`; - - extensionArtifact.code += '\n\n' + signatureCode; -} - /** * Create extension artifact by modifying the original file using AST * This preserves all imports, comments, and structure while replacing the class/export diff --git a/packages/codemods/src/schema-migration/utils/schema-generation.ts b/packages/codemods/src/schema-migration/utils/schema-generation.ts index ac274514177..7c3b7776ef5 100644 --- a/packages/codemods/src/schema-migration/utils/schema-generation.ts +++ b/packages/codemods/src/schema-migration/utils/schema-generation.ts @@ -127,7 +127,7 @@ export function buildLegacySchemaObject( }; if (mixinTraits.length > 0) { - legacySchema.traits = mixinTraits; + legacySchema.traits = [...mixinTraits]; } if (mixinExtensions.length > 0 || isFragment) { @@ -512,6 +512,8 @@ export interface MergedSchemaOptions { isTypeScript: boolean; /** Transform options */ options?: TransformOptions; + /** Extension name (e.g., 'UserExtension') -> when set with traits, triggers composite interface pattern */ + extensionName?: string; } /** @@ -607,10 +609,30 @@ function generateInterfaceOnly( * This creates a single .schema.js or .schema.ts file with everything needed */ export function generateMergedSchemaCode(opts: MergedSchemaOptions): string { - const { schemaName, interfaceName, schemaObject, properties, traits = [], imports = new Set(), isTypeScript } = opts; + const { + baseName, + schemaName, + interfaceName, + schemaObject, + properties, + traits = [], + imports = new Set(), + isTypeScript, + options, + extensionName, + } = opts; + + const useComposite = isTypeScript && Boolean(extensionName) && traits.length > 0; const sections: string[] = []; + if (useComposite) { + const extensionImportPath = options?.resourcesImport + ? `${options.resourcesImport}/${baseName}.ext` + : `../resources/${baseName}.ext`; + imports.add(`type { ${extensionName} } from '${extensionImportPath}'`); + } + // Generate imports section (only for TypeScript) if (isTypeScript) { const importsCode = generateTypeScriptImports(imports); @@ -627,8 +649,20 @@ export function generateMergedSchemaCode(opts: MergedSchemaOptions): string { sections.push(`\nexport default ${schemaName};`); // Generate interface (only for TypeScript) - if (isTypeScript) { - // Build extends clause from traits + if (useComposite) { + // Composite pattern: field interface is {Name}Trait, composite is {Name} + const fieldInterfaceName = `${interfaceName}Trait`; + const fieldInterfaceCode = generateInterfaceOnly(fieldInterfaceName, properties); + sections.push(''); + sections.push(fieldInterfaceCode); + + // Composite interface merges field interface, extension, and trait interfaces + const traitInterfaces = traits.map(traitNameToInterfaceName); + const compositeExtends = [fieldInterfaceName, extensionName, ...traitInterfaces].join(', '); + sections.push(''); + sections.push(`export interface ${interfaceName} extends ${compositeExtends} {}`); + } else { + // Standard pattern: single interface with optional trait extends let extendsClause: string | undefined; if (traits.length > 0) { const traitInterfaces = traits.map(traitNameToInterfaceName); diff --git a/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap index 77e1b91f666..51c8156493e 100644 --- a/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap @@ -1,5 +1,138 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`migrate-to-schema batch operation > README example: model with mixin, belongsTo, hasMany, and extension methods > readme_example 1`] = ` +{ + "resources/": "__dir__", + "resources/user.ext.ts": " +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; + +import type { UserTrait } from 'test-app/data/resources/user.schema'; + +export interface UserExtension extends UserTrait {} + +export class UserExtension { + get displayName() { + return this.name || this.email; + } + + async updateProfile(data) { + this.setProperties(data); + return this.save(); + } +}", + "resources/user.schema.ts": " +import type { Type } from '@ember-data/core-types/symbols'; +import type { Company } from 'test-app/data/resources/company.schema'; +import type { Project } from 'test-app/data/resources/project.schema'; +import type { AsyncHasMany } from '@ember-data/model'; +import type { TimestampableTrait } from 'test-app/data/traits/timestampable.schema'; +import type { UserExtension } from 'test-app/data/resources/user.ext'; +const UserSchema = { + 'type': 'user', + 'legacy': true, + 'identity': { + 'kind': '@id', + 'name': 'id' + }, + 'fields': [ + { + 'kind': 'attribute', + 'name': 'name', + 'type': 'string' + }, + { + 'kind': 'attribute', + 'name': 'email', + 'type': 'string' + }, + { + 'kind': 'belongsTo', + 'name': 'company', + 'type': 'company', + 'options': { + 'async': false + } + }, + { + 'kind': 'hasMany', + 'name': 'projects', + 'type': 'project', + 'options': { + 'async': true + } + } + ], + 'traits': [ + 'timestampable' + ], + 'objectExtensions': [ + 'TimestampableExtension' + ] +} as const; + +export default UserSchema; + +export interface UserTrait { + readonly [Type]: 'user'; + readonly name: string | null; + readonly email: string | null; + readonly company: Company | null; + readonly projects: AsyncHasMany; +} + +export interface User extends UserTrait, UserExtension, TimestampableTrait {}", + "traits/": "__dir__", + "traits/timestampable.ext.ts": " +import Mixin from '@ember/object/mixin'; +import { attr } from '@ember-data/model'; + +import type { TimestampableTrait } from 'test-app/data/traits/timestampable.schema'; + +export interface TimestampableExtension extends TimestampableTrait {} + +export const TimestampableExtension = { + timeSince() { + return Date.now() - this.updatedAt; + } +};", + "traits/timestampable.schema.ts": " +const TimestampableSchema = { + 'name': 'timestampable', + 'mode': 'legacy', + 'fields': [ + { + 'name': 'createdAt', + 'kind': 'attribute', + 'type': 'date' + }, + { + 'name': 'updatedAt', + 'kind': 'attribute', + 'type': 'date' + } + ] +} as const; + +export default TimestampableSchema; + +export interface TimestampableTrait { + createdAt: Date | null; + updatedAt: Date | null; +}", +} +`; + +exports[`migrate-to-schema batch operation > README example: model with mixin, belongsTo, hasMany, and extension methods > readme_example_structure 1`] = ` +[ + "resources/", + "resources/user.ext.ts", + "resources/user.schema.ts", + "traits/", + "traits/timestampable.ext.ts", + "traits/timestampable.schema.ts", +] +`; + exports[`migrate-to-schema batch operation > colocates type files with their corresponding schemas and traits > nested directory files 1`] = ` { "resources/": "__dir__", @@ -59,7 +192,7 @@ export interface NestedModel { "traits/": "__dir__", "traits/admin/": "__dir__", "traits/admin/connected.schema.ts": " -const ConnectedTrait = { +const ConnectedSchema = { 'name': 'connected', 'mode': 'legacy', 'fields': [ @@ -71,7 +204,7 @@ const ConnectedTrait = { ] } as const; -export default ConnectedTrait; +export default ConnectedSchema; export interface ConnectedTrait { commonField: string | null; @@ -177,7 +310,7 @@ export interface AuditedRecord { }", "traits/": "__dir__", "traits/auditable.schema.ts": " -const AuditableTrait = { +const AuditableSchema = { 'name': 'auditable', 'mode': 'legacy', 'fields': [ @@ -194,7 +327,7 @@ const AuditableTrait = { ] } as const; -export default AuditableTrait; +export default AuditableSchema; export interface AuditableTrait { auditStatus: string | null; @@ -295,7 +428,7 @@ export interface User { }", "traits/": "__dir__", "traits/workstreamable.schema.ts": " -const WorkstreamableTrait = { +const WorkstreamableSchema = { 'name': 'workstreamable', 'mode': 'legacy', 'fields': [ @@ -307,7 +440,7 @@ const WorkstreamableTrait = { ] } as const; -export default WorkstreamableTrait; +export default WorkstreamableSchema; export interface WorkstreamableTrait { workstreamType: string | null; @@ -361,17 +494,15 @@ exports[`migrate-to-schema batch operation > generates multiple artifacts when p "resources/company.ext.ts": " import Model, { attr, hasMany } from '@ember-data/model'; -import type { Company } from 'test-app/data/resources/company.schema'; +import type { CompanyTrait } from 'test-app/data/resources/company.schema'; -export interface CompanyExtension extends Company {} +export interface CompanyExtension extends CompanyTrait {} export class CompanyExtension { get userCount() { return this.users.length; } -} - -export type CompanyExtensionSignature = typeof CompanyExtension;", +}", "resources/company.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { User } from 'test-app/data/resources/user.schema'; @@ -457,17 +588,15 @@ exports[`migrate-to-schema batch operation > generates schema and type artifacts "resources/user.ext.ts": " import Model, { attr, belongsTo } from '@ember-data/model'; -import type { User } from 'test-app/data/resources/user.schema'; +import type { UserTrait } from 'test-app/data/resources/user.schema'; -export interface UserExtension extends User {} +export interface UserExtension extends UserTrait {} export class UserExtension { get displayName() { return this.name || this.email; } -} - -export type UserExtensionSignature = typeof UserExtension;", +}", "resources/user.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { Company } from 'test-app/data/resources/company.schema'; @@ -557,7 +686,7 @@ export interface TestModel extends ExternalTrait, LocalTrait { }", "traits/": "__dir__", "traits/external-mixin.schema.ts": " -const ExternalMixinTrait = { +const ExternalMixinSchema = { 'name': 'external-mixin', 'mode': 'legacy', 'fields': [ @@ -569,13 +698,13 @@ const ExternalMixinTrait = { ] } as const; -export default ExternalMixinTrait; +export default ExternalMixinSchema; export interface ExternalMixinTrait { externalField: string | null; }", "traits/local-mixin.schema.ts": " -const LocalMixinTrait = { +const LocalMixinSchema = { 'name': 'local-mixin', 'mode': 'legacy', 'fields': [ @@ -587,7 +716,7 @@ const LocalMixinTrait = { ] } as const; -export default LocalMixinTrait; +export default LocalMixinSchema; export interface LocalMixinTrait { localField: string | null; @@ -613,7 +742,6 @@ exports[`migrate-to-schema batch operation > handles mixed js and ts files corre "resources/": "__dir__", "resources/js-model-with-mixin.ext.js": " import Model, { attr } from '@ember-data/model'; -import JsMixin from '../../mixins/js-mixin'; export class JsModelWithMixinExtension { get displayName() { @@ -640,7 +768,12 @@ const JsModelWithMixinSchema = { ] }; -export default JsModelWithMixinSchema;", +export default JsModelWithMixinSchema; + +export interface JsModelWithMixin extends JsTrait { + readonly [Type]: 'js-model-with-mixin'; + readonly name: string | null; +}", "resources/ts-model-with-mixin.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { TsTrait } from 'test-app/data/traits/ts.schema'; @@ -662,7 +795,7 @@ const TsModelWithMixinSchema = { 'ts' ], 'objectExtensions': [ - 'tsMixinExtension' + 'TsmixinExtension' ] } as const; @@ -674,7 +807,7 @@ export interface TsModelWithMixin extends TsTrait { }", "traits/": "__dir__", "traits/js-mixin.schema.js": " -const JsMixinTrait = { +const JsMixinSchema = { 'name': 'js-mixin', 'mode': 'legacy', 'fields': [ @@ -686,22 +819,26 @@ const JsMixinTrait = { ] }; -export default JsMixinTrait;", +export default JsMixinSchema; + +export interface JsMixinTrait { + createdAt: Date | null; +}", "traits/ts-mixin.ext.ts": " import Mixin from '@ember/object/mixin'; import { attr } from '@ember-data/model'; import type { TsMixinTrait } from 'test-app/data/traits/ts-mixin.schema'; -export interface tsMixinExtension extends TsMixinTrait {} +export interface TsmixinExtension extends TsMixinTrait {} -export const tsMixinExtension = { +export const TsmixinExtension = { toggleEnabled() { this.set('isEnabled', !this.isEnabled); } };", "traits/ts-mixin.schema.ts": " -const TsMixinTrait = { +const TsMixinSchema = { 'name': 'ts-mixin', 'mode': 'legacy', 'fields': [ @@ -713,7 +850,7 @@ const TsMixinTrait = { ] } as const; -export default TsMixinTrait; +export default TsMixinSchema; export interface TsMixinTrait { isEnabled: boolean | null; @@ -792,7 +929,7 @@ export interface User { }", "traits/": "__dir__", "traits/workstreamable.schema.ts": " -const WorkstreamableTrait = { +const WorkstreamableSchema = { 'name': 'workstreamable', 'mode': 'legacy', 'fields': [ @@ -804,7 +941,7 @@ const WorkstreamableTrait = { ] } as const; -export default WorkstreamableTrait; +export default WorkstreamableSchema; export interface WorkstreamableTrait { workstreamType: string | null; @@ -849,7 +986,7 @@ const TestModelSchema = { 'workstreamable' ], 'objectExtensions': [ - 'workstreamableExtension' + 'WorkstreamableExtension' ] } as const; @@ -891,13 +1028,13 @@ import { attr } from '@ember-data/model'; import type { WorkstreamableTrait } from 'test-app/data/traits/workstreamable.schema'; -export interface workstreamableExtension extends WorkstreamableTrait {} +export interface WorkstreamableExtension extends WorkstreamableTrait {} -export const workstreamableExtension = { +export const WorkstreamableExtension = { imAnObjectExtensionNow() {} };", "traits/workstreamable.schema.ts": " -const WorkstreamableTrait = { +const WorkstreamableSchema = { 'name': 'workstreamable', 'mode': 'legacy', 'fields': [ @@ -909,7 +1046,7 @@ const WorkstreamableTrait = { ] } as const; -export default WorkstreamableTrait; +export default WorkstreamableSchema; export interface WorkstreamableTrait { workstreamType: string | null; @@ -977,7 +1114,32 @@ const BaseModelWithMethodsTrait = { ] }; -export default BaseModelWithMethodsTrait;", +export default BaseModelWithMethodsTrait; + +export interface BaseModelWithMethodsTrait { + id: string | null; + readonly baseField: string | null; + readonly isNew: boolean; + readonly hasDirtyAttributes: boolean; + readonly isDeleted: boolean; + readonly isSaving: boolean; + readonly isValid: boolean; + readonly isError: boolean; + readonly isLoaded: boolean; + readonly isEmpty: boolean; + save: (options?: Record) => Promise; + reload: (options?: Record) => Promise; + deleteRecord: () => void; + unloadRecord: () => void; + destroyRecord: (options?: Record) => Promise; + rollbackAttributes: () => void; + belongsTo: (propertyName: string) => BelongsToReference; + hasMany: (propertyName: string) => HasManyReference; + serialize: (options?: Record) => unknown; + readonly errors: Errors; + readonly adapterError: Error | null; + readonly isReloading: boolean; +}", } `; @@ -1005,7 +1167,11 @@ const CustomSelectOptionSchema = { ] }; -export default CustomSelectOptionSchema;", +export default CustomSelectOptionSchema; + +export interface CustomSelectOption { + readonly [Type]: 'custom-select-option'; +}", } `; diff --git a/tests/codemods/tests/schema-migration/intermediate-models.test.ts b/tests/codemods/tests/schema-migration/intermediate-models.test.ts index a6c21a7e7af..df2753ea702 100644 --- a/tests/codemods/tests/schema-migration/intermediate-models.test.ts +++ b/tests/codemods/tests/schema-migration/intermediate-models.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; diff --git a/tests/codemods/tests/schema-migration/migrate-to-schema.test.ts b/tests/codemods/tests/schema-migration/migrate-to-schema.test.ts index 94f6aeea401..0215b85c5b8 100644 --- a/tests/codemods/tests/schema-migration/migrate-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/migrate-to-schema.test.ts @@ -1,10 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { MigrateOptions } from '../../../../packages/codemods/src/schema-migration/codemod.js'; +import type { MigrateOptions } from '@ember-data/codemods/schema-migration/config.js'; + import { runMigration } from '../../../../packages/codemods/src/schema-migration/tasks/migrate.js'; import { collectFilesSnapshot, collectFileStructure, prepareFiles } from './test-helpers.ts'; @@ -22,7 +22,6 @@ describe('migrate-to-schema batch operation', () => { traitsDir: join(tempDir, 'app/data/traits'), modelSourceDir: join(tempDir, 'app/models'), mixinSourceDir: join(tempDir, 'app/mixins'), - appImportPrefix: 'test-app', resourcesImport: 'test-app/data/resources', traitsImport: 'test-app/data/traits', modelImportSource: 'test-app/models', @@ -779,9 +778,9 @@ export default class TestModel extends BaseModel { "resources/typed.ext.ts": " import BaseModel from 'test-app/models/base-model'; - import type { Typed } from 'test-app/data/resources/typed.schema'; + import type { TypedTrait } from 'test-app/data/resources/typed.schema'; - export interface TypedExtension extends Typed {} + export interface TypedExtension extends TypedTrait {} export class TypedExtension { @attr('string') declare name: string | null @@ -791,12 +790,11 @@ export default class TestModel extends BaseModel { @hasMany('tag', { async: true, inverse: null }) declare tags: unknown - } - - export type TypedExtensionSignature = typeof TypedExtension;", + }", "resources/typed.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { StaticBaseModelTraitTrait } from 'test-app/data/traits/static-base-model-trait.schema'; + import type { TypedExtension } from 'test-app/data/resources/typed.ext'; const TypedSchema = { 'type': 'typed', 'legacy': true, @@ -815,9 +813,11 @@ export default class TestModel extends BaseModel { export default TypedSchema; - export interface Typed extends StaticBaseModelTraitTrait { + export interface TypedTrait { readonly [Type]: 'typed'; - }", + } + + export interface Typed extends TypedTrait, TypedExtension, StaticBaseModelTraitTrait {}", "traits/": "__dir__", } `); @@ -856,9 +856,9 @@ export default class TestModel extends BaseModel { "resources/typed.ext.ts": " import BaseModel from 'test-app/models/base-model.js'; - import type { Typed } from 'test-app/data/resources/typed.schema'; + import type { TypedTrait } from 'test-app/data/resources/typed.schema'; - export interface TypedExtension extends Typed {} + export interface TypedExtension extends TypedTrait {} export class TypedExtension { @attr('string') declare name: string | null @@ -868,12 +868,11 @@ export default class TestModel extends BaseModel { @hasMany('tag', { async: true, inverse: null }) declare tags: unknown - } - - export type TypedExtensionSignature = typeof TypedExtension;", + }", "resources/typed.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { StaticBaseModelTraitTrait } from 'test-app/data/traits/static-base-model-trait.schema'; + import type { TypedExtension } from 'test-app/data/resources/typed.ext'; const TypedSchema = { 'type': 'typed', 'legacy': true, @@ -892,14 +891,60 @@ export default class TestModel extends BaseModel { export default TypedSchema; - export interface Typed extends StaticBaseModelTraitTrait { + export interface TypedTrait { readonly [Type]: 'typed'; - }", + } + + export interface Typed extends TypedTrait, TypedExtension, StaticBaseModelTraitTrait {}", "traits/": "__dir__", } `); }); + it('README example: model with mixin, belongsTo, hasMany, and extension methods', async () => { + prepareFiles(tempDir, { + 'app/models/user.ts': ` +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import Timestampable from '../mixins/timestampable'; + +export default class User extends Model.extend(Timestampable) { + @attr('string') declare name: string; + @attr('string') declare email: string; + @belongsTo('company', { async: false }) declare company: Company; + @hasMany('project', { async: true }) declare projects: Project[]; + + get displayName() { + return this.name || this.email; + } + + async updateProfile(data) { + this.setProperties(data); + return this.save(); + } +} +`, + 'app/mixins/timestampable.ts': ` +import Mixin from '@ember/object/mixin'; +import { attr } from '@ember-data/model'; + +export default Mixin.create({ + createdAt: attr('date'), + updatedAt: attr('date'), + + timeSince() { + return Date.now() - this.updatedAt; + } +}); +`, + }); + + await runMigration(options); + + const dataDir = join(tempDir, 'app/data'); + expect(collectFileStructure(dataDir)).toMatchSnapshot('readme_example_structure'); + expect(collectFilesSnapshot(dataDir)).toMatchSnapshot('readme_example'); + }); + it('regression: ember-data import without default import is respected', async () => { prepareFiles(tempDir, { 'app/models/typed.ts': ` diff --git a/tests/codemods/tests/schema-migration/test-helpers.ts b/tests/codemods/tests/schema-migration/test-helpers.ts index 4f8b00e26f7..9f7ee619766 100644 --- a/tests/codemods/tests/schema-migration/test-helpers.ts +++ b/tests/codemods/tests/schema-migration/test-helpers.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join, relative } from 'path'; -import type { TransformOptions } from '../../../../packages/codemods/src/schema-migration/utils/ast-utils.js'; +import type { TransformOptions } from '@ember-data/codemods/schema-migration/config.js'; export function prepareFiles(baseDir: string, files: Record) { for (const [key, content] of Object.entries(files)) { @@ -79,8 +79,8 @@ export const DEFAULT_TEST_OPTIONS: TransformOptions = { testMode: true, // Configure mixin sources for test patterns additionalMixinSources: [ - { pattern: 'app/mixins/', name: 'app mixins' }, - { pattern: '../mixins/', name: 'relative mixins' }, + { dir: 'app/mixins/', pattern: 'app/mixins/' }, + { dir: '../mixins/', pattern: '../mixins/' }, ], }; diff --git a/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap index 0612d5bdc95..3488991ee45 100644 --- a/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates only trait artifact when mixin has only data fields > data-only trait type interface 1`] = ` -"const SimpleTrait = { +"const SimpleSchema = { 'name': 'simple', 'mode': 'legacy', 'fields': [ @@ -21,7 +21,12 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default SimpleTrait;" +export default SimpleSchema; + +export interface SimpleTrait { + title: string | null; + author: Promise; +}" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates trait and extension artifacts when mixin has computed properties and methods > mixin extension code 1`] = ` @@ -29,7 +34,7 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen import { attr } from '@ember-data/model'; import { computed } from '@ember/object'; -export const nameableExtension = { +export const NameableExtension = { displayName: computed('name', function() { return \`Name: \${this.name}\`; }), @@ -40,7 +45,7 @@ export const nameableExtension = { `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates trait and extension artifacts when mixin has computed properties and methods > mixin trait type interface 1`] = ` -"const NameableTrait = { +"const NameableSchema = { 'name': 'nameable', 'mode': 'legacy', 'fields': [ @@ -52,11 +57,15 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default NameableTrait;" +export default NameableSchema; + +export interface NameableTrait { + name: string | null; +}" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates trait artifact with merged types for basic mixins > basic trait type interface 1`] = ` -"const FileableTrait = { +"const FileableSchema = { 'name': 'fileable', 'mode': 'legacy', 'fields': [ @@ -85,11 +94,17 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default FileableTrait;" +export default FileableSchema; + +export interface FileableTrait { + files: HasMany; + name: string | null; + isActive: boolean | null; +}" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > handles custom type mappings in mixin trait type interfaces > mixin custom type mappings interface 1`] = ` -"const TypedTrait = { +"const TypedSchema = { 'name': 'typed', 'mode': 'legacy', 'fields': [ @@ -111,7 +126,13 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default TypedTrait;" +export default TypedSchema; + +export interface TypedTrait { + id: string | null; + amount: number | null; + metadata: Record | null; +}" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > collects the real fileable mixin shape into trait and extension artifacts > artifact metadata 1`] = ` @@ -122,12 +143,12 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > collects "type": "resource-type-stub", }, { - "name": "FileableTrait", + "name": "FileableSchema", "suggestedFileName": "fileable.schema.js", "type": "trait", }, { - "name": "fileableExtension", + "name": "FileableExtension", "suggestedFileName": "fileable.ext.js", "type": "trait-extension", }, @@ -144,7 +165,7 @@ import { attr, hasMany } from '@ember-data/model'; import { sortBy } from 'soxhub-client/utils/sort-by'; -export const fileableExtension = { +export const FileableExtension = { sortedFiles: sortBy('files', 'createdAt:desc'), hasFiles: arrayHasLength('files'), numFiles: readOnly('files.length'), @@ -160,7 +181,7 @@ export const fileableExtension = { `; exports[`mixin-to-schema transform (artifacts) > basic functionality > collects the real fileable mixin shape into trait and extension artifacts > trait code 1`] = ` -"const FileableTrait = { +"const FileableSchema = { 'name': 'fileable', 'mode': 'legacy', 'fields': [ @@ -185,14 +206,19 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > collects ] }; -export default FileableTrait;" +export default FileableSchema; + +export interface FileableTrait { + files: HasMany; + showFilesRequiringReviewError: boolean | null; +}" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > converts mixin with no trait fields to extension artifact > code 1`] = ` "import Mixin from '@ember/object/mixin'; import { computed } from '@ember/object'; -export const noTraitsExtension = { +export const NotraitsExtension = { complexMethod() { return 'processed'; }, computedValue: computed(function() { return 'computed'; }) };" @@ -201,12 +227,12 @@ export const noTraitsExtension = { exports[`mixin-to-schema transform (artifacts) > basic functionality > converts mixin with no trait fields to extension artifact > metadata 1`] = ` [ { - "name": "NoTraitsTrait", + "name": "NoTraitsSchema", "suggestedFileName": "no-traits.schema.js", "type": "trait", }, { - "name": "noTraitsExtension", + "name": "NotraitsExtension", "suggestedFileName": "no-traits.ext.js", "type": "trait-extension", }, @@ -218,7 +244,7 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > preserves import { computed } from '@ember/object'; import { service } from '@ember/service'; -export const plannableExtension = { +export const PlannableExtension = { library: service('library'), projectPlans: computed('_modelName', 'intId', 'library.projectPlans.[]', function () { return this.get('library.projectPlans') @@ -231,12 +257,12 @@ export const plannableExtension = { exports[`mixin-to-schema transform (artifacts) > basic functionality > preserves newlines and tabs in extension artifact properties without escaping > metadata 1`] = ` [ { - "name": "PlannableTrait", + "name": "PlannableSchema", "suggestedFileName": "plannable.schema.js", "type": "trait", }, { - "name": "plannableExtension", + "name": "PlannableExtension", "suggestedFileName": "plannable.ext.js", "type": "trait-extension", }, @@ -261,7 +287,7 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > produces "type": "resource-type-stub", }, { - "name": "FileableTrait", + "name": "FileableSchema", "suggestedFileName": "fileable.schema.js", "type": "trait", }, @@ -269,7 +295,7 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > produces `; exports[`mixin-to-schema transform (artifacts) > basic functionality > supports alias of Mixin import and still produces a trait artifact > code 1`] = ` -"const AliasedTrait = { +"const AliasedSchema = { 'name': 'aliased', 'mode': 'legacy', 'fields': [ @@ -281,13 +307,17 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > supports ] }; -export default AliasedTrait;" +export default AliasedSchema; + +export interface AliasedTrait { + name: string | null; +}" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > supports alias of Mixin import and still produces a trait artifact > metadata 1`] = ` [ { - "name": "AliasedTrait", + "name": "AliasedSchema", "suggestedFileName": "aliased.schema.js", "type": "trait", }, @@ -309,7 +339,7 @@ export interface File { "type": "resource-type-stub", }, { - "code": "const CustomSourceTrait = { + "code": "const CustomSourceSchema = { 'name': 'custom-source', 'mode': 'legacy', 'fields': [ @@ -326,8 +356,13 @@ export interface File { ] }; -export default CustomSourceTrait;", - "name": "CustomSourceTrait", +export default CustomSourceSchema; + +export interface CustomSourceTrait { + name: string | null; + files: HasMany; +}", + "name": "CustomSourceSchema", "suggestedFileName": "custom-source.schema.js", "type": "trait", }, @@ -336,10 +371,10 @@ export default CustomSourceTrait;", import { attr, hasMany } from '@my-custom/model'; import { computed } from '@ember/object'; -export const customSourceExtension = { +export const CustomsourceExtension = { customProp: computed('name', function() { return this.name; }) };", - "name": "customSourceExtension", + "name": "CustomsourceExtension", "suggestedFileName": "custom-source.ext.js", "type": "trait-extension", }, @@ -361,7 +396,7 @@ export interface File { "type": "resource-type-stub", }, { - "code": "const RenamedMixedSourcesTrait = { + "code": "const RenamedMixedSourcesSchema = { 'name': 'renamed-mixed-sources', 'mode': 'legacy', 'fields': [ @@ -373,8 +408,12 @@ export interface File { ] }; -export default RenamedMixedSourcesTrait;", - "name": "RenamedMixedSourcesTrait", +export default RenamedMixedSourcesSchema; + +export interface RenamedMixedSourcesTrait { + files: HasMany; +}", + "name": "RenamedMixedSourcesSchema", "suggestedFileName": "renamed-mixed-sources.schema.js", "type": "trait", }, @@ -383,10 +422,10 @@ export default RenamedMixedSourcesTrait;", import { hasMany as many } from '@ember-data/model'; import { attr as attribute } from '@unsupported/source'; -export const renamedMixedSourcesExtension = { +export const RenamedmixedsourcesExtension = { name: attribute('string') };", - "name": "renamedMixedSourcesExtension", + "name": "RenamedmixedsourcesExtension", "suggestedFileName": "renamed-mixed-sources.ext.js", "type": "trait-extension", }, @@ -396,7 +435,7 @@ export const renamedMixedSourcesExtension = { exports[`mixin-to-schema transform (artifacts) > import validation > handles CLI option name conversion from kebab-case to camelCase (trait artifact) 1`] = ` [ { - "code": "const CliOptionTrait = { + "code": "const CliOptionSchema = { 'name': 'cli-option', 'mode': 'legacy', 'fields': [ @@ -408,8 +447,12 @@ exports[`mixin-to-schema transform (artifacts) > import validation > handles CLI ] }; -export default CliOptionTrait;", - "name": "CliOptionTrait", +export default CliOptionSchema; + +export interface CliOptionTrait { + name: string | null; +}", + "name": "CliOptionSchema", "suggestedFileName": "cli-option.schema.js", "type": "trait", }, @@ -443,7 +486,7 @@ export interface User { "type": "resource-type-stub", }, { - "code": "const AliasedImportsTrait = { + "code": "const AliasedImportsSchema = { 'name': 'aliased-imports', 'mode': 'legacy', 'fields': [ @@ -465,8 +508,14 @@ export interface User { ] }; -export default AliasedImportsTrait;", - "name": "AliasedImportsTrait", +export default AliasedImportsSchema; + +export interface AliasedImportsTrait { + name: string | null; + files: HasMany; + owner: User | null; +}", + "name": "AliasedImportsSchema", "suggestedFileName": "aliased-imports.schema.js", "type": "trait", }, @@ -476,7 +525,7 @@ export default AliasedImportsTrait;", exports[`mixin-to-schema transform (artifacts) > import validation > ignores decorators from unsupported import sources (only attr recognized) 1`] = ` [ { - "code": "const UnsupportedSourceTrait = { + "code": "const UnsupportedSourceSchema = { 'name': 'unsupported-source', 'mode': 'legacy', 'fields': [ @@ -488,8 +537,12 @@ exports[`mixin-to-schema transform (artifacts) > import validation > ignores dec ] }; -export default UnsupportedSourceTrait;", - "name": "UnsupportedSourceTrait", +export default UnsupportedSourceSchema; + +export interface UnsupportedSourceTrait { + name: string | null; +}", + "name": "UnsupportedSourceSchema", "suggestedFileName": "unsupported-source.schema.js", "type": "trait", }, @@ -499,11 +552,11 @@ import { attr } from '@ember-data/model'; import { hasMany } from '@unsupported/source'; import { computed } from '@ember/object'; -export const unsupportedSourceExtension = { +export const UnsupportedsourceExtension = { files: hasMany('file'), customProp: computed('name', function() { return this.name; }) };", - "name": "unsupportedSourceExtension", + "name": "UnsupportedsourceExtension", "suggestedFileName": "unsupported-source.ext.js", "type": "trait-extension", }, @@ -517,11 +570,11 @@ exports[`mixin-to-schema transform (artifacts) > import validation > only proces "type": "resource-type-stub", }, { - "name": "DefaultSourceTrait", + "name": "DefaultSourceSchema", "type": "trait", }, { - "name": "defaultSourceExtension", + "name": "DefaultsourceExtension", "type": "trait-extension", }, ] @@ -536,7 +589,7 @@ export interface File { // Stub: properties will be populated when the actual resource type is generated } ", - "const DefaultSourceTrait = { + "const DefaultSourceSchema = { 'name': 'default-source', 'mode': 'legacy', 'fields': [ @@ -553,12 +606,17 @@ export interface File { ] }; -export default DefaultSourceTrait;", +export default DefaultSourceSchema; + +export interface DefaultSourceTrait { + name: string | null; + files: HasMany; +}", "import Mixin from '@ember/object/mixin'; import { attr, hasMany } from '@ember-data/model'; import { computed } from '@ember/object'; -export const defaultSourceExtension = { +export const DefaultsourceExtension = { customProp: computed('name', function() { return this.name; }) };", ] @@ -579,7 +637,7 @@ export interface User { "type": "resource-type-stub", }, { - "code": "const BelongsToTrait = { + "code": "const BelongsToSchema = { 'name': 'belongs-to', 'mode': 'legacy', 'fields': [ @@ -594,8 +652,12 @@ export interface User { ] }; -export default BelongsToTrait;", - "name": "BelongsToTrait", +export default BelongsToSchema; + +export interface BelongsToTrait { + owner: Promise; +}", + "name": "BelongsToSchema", "suggestedFileName": "belongs-to.schema.js", "type": "trait", }, @@ -605,14 +667,17 @@ export default BelongsToTrait;", exports[`mixin-to-schema transform (artifacts) > import validation > produces an extension artifact when no valid EmberData imports are found 1`] = ` [ { - "code": "const NoValidImportsTrait = { + "code": "const NoValidImportsSchema = { 'name': 'no-valid-imports', 'mode': 'legacy', 'fields': [] }; -export default NoValidImportsTrait;", - "name": "NoValidImportsTrait", +export default NoValidImportsSchema; + +export interface NoValidImportsTrait { +}", + "name": "NoValidImportsSchema", "suggestedFileName": "no-valid-imports.schema.js", "type": "trait", }, @@ -621,11 +686,11 @@ export default NoValidImportsTrait;", import { computed } from '@ember/object'; import { attr } from '@unsupported/source'; -export const noValidImportsExtension = { +export const NovalidimportsExtension = { name: attr('string'), customProp: computed('name', function() { return this.name; }) };", - "name": "noValidImportsExtension", + "name": "NovalidimportsExtension", "suggestedFileName": "no-valid-imports.ext.js", "type": "trait-extension", }, @@ -647,7 +712,7 @@ export interface File { "type": "resource-type-stub", }, { - "code": "const AuditboardSourceTrait = { + "code": "const AuditboardSourceSchema = { 'name': 'auditboard-source', 'mode': 'legacy', 'fields': [ @@ -664,8 +729,13 @@ export interface File { ] }; -export default AuditboardSourceTrait;", - "name": "AuditboardSourceTrait", +export default AuditboardSourceSchema; + +export interface AuditboardSourceTrait { + name: string | null; + files: HasMany; +}", + "name": "AuditboardSourceSchema", "suggestedFileName": "auditboard-source.schema.js", "type": "trait", }, @@ -674,10 +744,10 @@ export default AuditboardSourceTrait;", import { attr, hasMany } from '@auditboard/warp-drive/v1/model'; import { computed } from '@ember/object'; -export const auditboardSourceExtension = { +export const AuditboardsourceExtension = { customProp: computed('name', function() { return this.name; }) };", - "name": "auditboardSourceExtension", + "name": "AuditboardsourceExtension", "suggestedFileName": "auditboard-source.ext.js", "type": "trait-extension", }, @@ -692,7 +762,7 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr "type": "resource-type-stub", }, { - "name": "FileableTrait", + "name": "FileableSchema", "suggestedFileName": "fileable.schema.js", "type": "trait", }, @@ -700,7 +770,7 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr `; exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces trait with extended traits when using createWithMixins > inheritance trait code 1`] = ` -"const FileableTrait = { +"const FileableSchema = { 'name': 'fileable', 'mode': 'legacy', 'fields': [ @@ -724,11 +794,16 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default FileableTrait;" +export default FileableSchema; + +export interface FileableTrait extends BaseModelTrait, TimestampTrait { + description: string | null; + files: HasMany; +}" `; exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces trait with single extended trait > single inheritance trait code 1`] = ` -"const DescribableTrait = { +"const DescribableSchema = { 'name': 'describable', 'mode': 'legacy', 'fields': [ @@ -743,11 +818,15 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default DescribableTrait;" +export default DescribableSchema; + +export interface DescribableTrait extends BaseModelTrait { + description: string | null; +}" `; exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces trait without traits property when no inheritance > no inheritance trait code 1`] = ` -"const DescribableTrait = { +"const DescribableSchema = { 'name': 'describable', 'mode': 'legacy', 'fields': [ @@ -759,5 +838,9 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default DescribableTrait;" +export default DescribableSchema; + +export interface DescribableTrait { + description: string | null; +}" `; diff --git a/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap index b5c8b170867..131ed1fd2ed 100644 --- a/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap @@ -36,7 +36,13 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default ProcessedModelSchema;" +export default ProcessedModelSchema; + +export interface ProcessedModel { + readonly [Type]: 'processed-model'; + readonly name: string | null; + readonly content: string | null; +}" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > generates schema with merged types for basic models > basic schema with merged types 1`] = ` @@ -80,7 +86,15 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default UserSchema;" +export default UserSchema; + +export interface User { + readonly [Type]: 'user'; + readonly name: string | null; + readonly isActive: boolean | null; + readonly company: Company | null; + readonly projects: AsyncHasMany; +}" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > handles custom type mappings in merged schema files > custom type mappings in merged schema 1`] = ` @@ -110,7 +124,14 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default TypedModelSchema;" +export default TypedModelSchema; + +export interface TypedModel { + readonly [Type]: 'typed-model'; + readonly id: string | null; + readonly amount: number | null; + readonly metadata: Record | null; +}" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > handles relationship types correctly in merged schema files > relationship types in merged schema 1`] = ` @@ -162,7 +183,16 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default RelationshipModelSchema;" +export default RelationshipModelSchema; + +export interface RelationshipModel { + readonly [Type]: 'relationship-model'; + readonly name: string | null; + readonly owner: User | null; + readonly company: Promise; + readonly attachments: HasMany; + readonly tags: AsyncHasMany; +}" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > uses unknown type for unsupported transforms > unknown types in merged schema 1`] = ` @@ -192,7 +222,14 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > use ] }; -export default UnknownTypesModelSchema;" +export default UnknownTypesModelSchema; + +export interface UnknownTypesModel { + readonly [Type]: 'unknown-types-model'; + readonly customField: unknown | null; + readonly anotherField: unknown | null; + readonly knownField: string | null; +}" `; exports[`model-to-schema transform (artifacts) > basic functionality > handles complex field options correctly > complex field options 1`] = ` @@ -243,7 +280,15 @@ exports[`model-to-schema transform (artifacts) > basic functionality > handles c ] }; -export default ComplexModelSchema;" +export default ComplexModelSchema; + +export interface ComplexModel { + readonly [Type]: 'complex-model'; + readonly name: string; + readonly birthDate: Date | null; + readonly owner: Promise; + readonly attachments: HasMany; +}" `; exports[`model-to-schema transform (artifacts) > basic functionality > handles model with mixins > schema with mixins 1`] = ` @@ -272,16 +317,22 @@ exports[`model-to-schema transform (artifacts) > basic functionality > handles m ] }; -export default DocumentSchema;" +export default DocumentSchema; + +export interface Document extends FileableTrait, TimestampableTrait { + readonly [Type]: 'document'; + readonly title: string | null; + readonly content: string | null; +}" `; exports[`model-to-schema transform (artifacts) > basic functionality > preserves TypeScript syntax in extension properties > typescript extension 1`] = ` "import Model, { attr } from '@ember-data/model'; import { service } from '@ember/service'; -import type { TypedModel } from 'test-app/data/resources/typed-model.schema'; +import type { TypedModelTrait } from 'test-app/data/resources/typed-model.schema'; -export interface TypedModelExtension extends TypedModel {} +export interface TypedModelExtension extends TypedModelTrait {} export class TypedModelExtension { @service declare router: RouterService @@ -295,9 +346,7 @@ export class TypedModelExtension { get computedValue(): string { return \`Processed: \${this.name}\`; } -} - -export type TypedModelExtensionSignature = typeof TypedModelExtension;" +}" `; exports[`model-to-schema transform (artifacts) > basic functionality > produces schema and extension artifacts for basic model > artifact metadata 1`] = ` @@ -377,7 +426,16 @@ exports[`model-to-schema transform (artifacts) > basic functionality > produces ] }; -export default UserSchema;" +export default UserSchema; + +export interface User { + readonly [Type]: 'user'; + readonly name: string | null; + readonly email: string | null; + readonly isActive: boolean | null; + readonly company: Company | null; + readonly projects: AsyncHasMany; +}" `; exports[`model-to-schema transform (artifacts) > basic functionality > supports alternate import sources > custom import source 1`] = ` @@ -405,7 +463,13 @@ exports[`model-to-schema transform (artifacts) > basic functionality > supports ] }; -export default CustomModelSchema;" +export default CustomModelSchema; + +export interface CustomModel { + readonly [Type]: 'custom-model'; + readonly name: string | null; + readonly items: HasMany; +}" `; exports[`model-to-schema transform (artifacts) > custom type mappings > applies custom type mappings to attribute types > custom type mappings in merged schema 1`] = ` @@ -435,7 +499,14 @@ exports[`model-to-schema transform (artifacts) > custom type mappings > applies ] }; -export default CustomTypesModelSchema;" +export default CustomTypesModelSchema; + +export interface CustomTypesModel { + readonly [Type]: 'custom-types-model'; + readonly id: string | null; + readonly createdAt: Date | null; + readonly price: number | null; +}" `; exports[`model-to-schema transform (artifacts) > custom type mappings > falls back to unknown for unmapped custom types > unknown fallback for unmapped types 1`] = ` @@ -460,7 +531,13 @@ exports[`model-to-schema transform (artifacts) > custom type mappings > falls ba ] }; -export default UnmappedTypesModelSchema;" +export default UnmappedTypesModelSchema; + +export interface UnmappedTypesModel { + readonly [Type]: 'unmapped-types-model'; + readonly field1: unknown | null; + readonly field2: unknown | null; +}" `; exports[`model-to-schema transform (artifacts) > edge cases > handles aliased imports correctly > aliased imports 1`] = ` @@ -485,7 +562,13 @@ exports[`model-to-schema transform (artifacts) > edge cases > handles aliased im ] }; -export default AliasedModelSchema;" +export default AliasedModelSchema; + +export interface AliasedModel { + readonly [Type]: 'aliased-model'; + readonly name: string | null; + readonly items: HasMany; +}" `; exports[`model-to-schema transform (artifacts) > edge cases > preserves complex object literal options > complex options 1`] = ` @@ -511,7 +594,12 @@ exports[`model-to-schema transform (artifacts) > edge cases > preserves complex ] }; -export default ComplexOptionsModelSchema;" +export default ComplexOptionsModelSchema; + +export interface ComplexOptionsModel { + readonly [Type]: 'complex-options-model'; + readonly owner: Promise; +}" `; exports[`model-to-schema transform (artifacts) > mirror flag > uses configured emberDataImportSource for HasMany types in merged schema > custom EmberData source for HasMany types in merged schema 1`] = ` @@ -555,7 +643,15 @@ exports[`model-to-schema transform (artifacts) > mirror flag > uses configured e ] }; -export default RelationshipModelSchema;" +export default RelationshipModelSchema; + +export interface RelationshipModel { + readonly [Type]: 'relationship-model'; + readonly name: string | null; + readonly tags: HasMany; + readonly projects: AsyncHasMany; + readonly owner: User | null; +}" `; exports[`model-to-schema transform (artifacts) > mixin handling > extracts mixin names and converts them to trait references > single mixin schema 1`] = ` @@ -578,7 +674,12 @@ exports[`model-to-schema transform (artifacts) > mixin handling > extracts mixin ] }; -export default DocumentSchema;" +export default DocumentSchema; + +export interface Document extends FileableTrait { + readonly [Type]: 'document'; + readonly title: string | null; +}" `; exports[`model-to-schema transform (artifacts) > mixin handling > handles multiple mixins correctly > multiple mixins schema 1`] = ` @@ -603,5 +704,10 @@ exports[`model-to-schema transform (artifacts) > mixin handling > handles multip ] }; -export default ComplexDocumentSchema;" +export default ComplexDocumentSchema; + +export interface ComplexDocument extends FileableTrait, TimestampableTrait, AuditableTrait { + readonly [Type]: 'complex-document'; + readonly title: string | null; +}" `; diff --git a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts index 5c0bcfd8e7e..10a7fa1bdbd 100644 --- a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { FinalOptions } from '@ember-data/codemods/schema-migration/codemod.js'; +import type { FinalOptions } from '@ember-data/codemods/schema-migration/config.js'; import { toArtifacts } from '../../../../../packages/codemods/src/schema-migration/processors/mixin.ts'; import { parseFile } from '../../../../../packages/codemods/src/schema-migration/utils/file-parser.js'; @@ -24,7 +23,6 @@ describe('mixin-to-schema transform (artifacts)', () => { traitsDir: join(tempDir, 'app/data/traits'), modelSourceDir: join(tempDir, 'app/models'), mixinSourceDir: join(tempDir, 'app/mixins'), - appImportPrefix: 'test-app', resourcesImport: 'test-app/data/resources', traitsImport: 'test-app/data/traits', modelImportSource: 'test-app/models', @@ -52,14 +50,17 @@ export default Mixin.create({});`; const trait = artifacts.find((a) => a.type === 'trait'); expect(trait).toMatchInlineSnapshot(` { - "code": "const EmptyTrait = { + "code": "const EmptySchema = { 'name': 'empty', 'mode': 'legacy', 'fields': [] }; - export default EmptyTrait;", - "name": "EmptyTrait", + export default EmptySchema; + + export interface EmptyTrait { + }", + "name": "EmptySchema", "suggestedFileName": "empty.schema.js", "type": "trait", } @@ -85,7 +86,7 @@ export default Mixin.create({ const extension = artifacts.find((a) => a.type === 'trait-extension'); expect(trait).toMatchInlineSnapshot(` { - "code": "const FileableTrait = { + "code": "const FileableSchema = { 'name': 'fileable', 'mode': 'legacy', 'fields': [ @@ -114,8 +115,14 @@ export default Mixin.create({ ] }; - export default FileableTrait;", - "name": "FileableTrait", + export default FileableSchema; + + export interface FileableTrait { + files: HasMany; + name: string | null; + isActive: boolean | null; + }", + "name": "FileableSchema", "suggestedFileName": "fileable.schema.js", "type": "trait", } @@ -126,10 +133,10 @@ export default Mixin.create({ import Mixin from '@ember/object/mixin'; import { computed } from '@ember/object'; - export const fileableExtension = { + export const FileableExtension = { titleCaseName: computed('name', function () { return (this.name || '').toUpperCase(); }) };", - "name": "fileableExtension", + "name": "FileableExtension", "suggestedFileName": "fileable.ext.js", "type": "trait-extension", } @@ -763,10 +770,10 @@ export default Mixin.create({ });`; options = { + kind: 'finalized', modelImportSource: 'test-app/models', resourcesImport: 'test-app/data/resources', resourcesDir: './test-output/resources', - appImportPrefix: 'test-app', verbose: false, debug: false, }; @@ -805,10 +812,10 @@ export default Mixin.create({ });`; options = { + kind: 'finalized', modelImportSource: 'test-app/models', resourcesImport: 'test-app/data/resources', resourcesDir: './test-output/resources', - appImportPrefix: 'test-app', verbose: false, debug: false, }; diff --git a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts index 575b36a2c25..1f35b7bb453 100644 --- a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ import { describe, expect, it } from 'vitest'; import transform, { toArtifacts } from '../../../../../packages/codemods/src/schema-migration/processors/model.js'; @@ -82,8 +81,8 @@ export default class Document extends Model.extend(FileableMixin, TimestampableM expect(artifacts).toHaveLength(2); const schema = artifacts.find((a) => a.type === 'schema'); - expect(schema?.code).toContain('fileable'); - expect(schema?.code).toContain('timestampable'); + expect(schema?.code).toContain('Fileable'); + expect(schema?.code).toContain('Timestampable'); expect(schema?.code).toMatchSnapshot('schema with mixins'); }); @@ -853,7 +852,12 @@ export default class TestModel extends Model.extend(WorkstreamableMixin) { ] }; - export default TestModelSchema;" + export default TestModelSchema; + + export interface TestModel extends WorkstreamableTrait { + readonly [Type]: 'test-model'; + readonly workstreamable: Workstreamable | null; + }" `); }); }); @@ -1120,7 +1124,12 @@ export default class Translatable extends Model { ] }; - export default TestModelSchema;", + export default TestModelSchema; + + export interface TestModel { + readonly [Type]: 'test-model'; + readonly name: string | null; + }", "name": "TestModelSchema", "suggestedFileName": "test-model.schema.js", "type": "schema", @@ -1170,17 +1179,15 @@ export default class Amendment extends Model { const INTERNAL_HELPER = 'helper'; - import type { Amendment } from 'test-app/data/resources/amendment.schema'; + import type { AmendmentTrait } from 'test-app/data/resources/amendment.schema'; - export interface AmendmentExtension extends Amendment {} + export interface AmendmentExtension extends AmendmentTrait {} export class AmendmentExtension { get changes(): DisplayableChange[] { return []; } - } - - export type AmendmentExtensionSignature = typeof AmendmentExtension;" + }" `); }); @@ -1252,17 +1259,15 @@ export default class Task extends Model { export type Priority = 'low' | 'medium' | 'high'; - import type { Task } from 'test-app/data/resources/task.schema'; + import type { TaskTrait } from 'test-app/data/resources/task.schema'; - export interface TaskExtension extends Task {} + export interface TaskExtension extends TaskTrait {} export class TaskExtension { get config(): Config { return { enabled: true, threshold: 100 }; } - } - - export type TaskExtensionSignature = typeof TaskExtension;" + }" `); }); }); diff --git a/tests/codemods/tests/schema-migration/utils/ast-utils.test.ts b/tests/codemods/tests/schema-migration/utils/ast-utils.test.ts index 4f48f5d1ddd..70a64680c38 100644 --- a/tests/codemods/tests/schema-migration/utils/ast-utils.test.ts +++ b/tests/codemods/tests/schema-migration/utils/ast-utils.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ - import { describe, expect, it } from 'vitest'; import type { PropertyInfo } from '../../../../../packages/codemods/src/schema-migration/utils/ast-utils.js'; diff --git a/tests/codemods/tests/schema-migration/utils/config.test.ts b/tests/codemods/tests/schema-migration/utils/config.test.ts index 35033792aaf..b1e988d1059 100644 --- a/tests/codemods/tests/schema-migration/utils/config.test.ts +++ b/tests/codemods/tests/schema-migration/utils/config.test.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; diff --git a/tests/codemods/tests/schema-migration/utils/generate-config.test.ts b/tests/codemods/tests/schema-migration/utils/generate-config.test.ts index 2dd78ad68d6..043c9f6f374 100644 --- a/tests/codemods/tests/schema-migration/utils/generate-config.test.ts +++ b/tests/codemods/tests/schema-migration/utils/generate-config.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ import { existsSync, mkdirSync, rmSync } from 'fs'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; From 6743076baeeedb321059db1469f52f91baffdc1a Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Wed, 18 Feb 2026 12:17:27 +0100 Subject: [PATCH 2/4] chore(codemods): fix pnpm lint errors --- packages/codemods/package.json | 1 + packages/codemods/src/legacy-compat-builders/options.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 0f7cbb7af27..8c1609d1ee3 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -12,6 +12,7 @@ "bin": "dist/index.js", "scripts": { "build:cli": "bun build src/cli/index.ts --target node --outdir dist --sourcemap", + "build:pkg": "tsc", "prepack": "pnpm run build:cli", "lint": "eslint . --quiet --cache --cache-strategy=content" }, diff --git a/packages/codemods/src/legacy-compat-builders/options.ts b/packages/codemods/src/legacy-compat-builders/options.ts index 1cbfd7e41e1..508fbfa659d 100644 --- a/packages/codemods/src/legacy-compat-builders/options.ts +++ b/packages/codemods/src/legacy-compat-builders/options.ts @@ -1,4 +1,4 @@ -import type { SharedCodemodOptions } from '../utils/options.js'; +import type { SharedCodemodOptions } from '../cli/index.js'; import type { LegacyStoreMethod } from './config.js'; export interface Options extends SharedCodemodOptions { From 26ba68bc9a3f99ea5ab897c220079ebaeb3a650e Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Wed, 18 Feb 2026 17:48:34 +0100 Subject: [PATCH 3/4] fix(codemods): don't generate interfaces for javascript files --- .../utils/schema-generation.ts | 45 ++--- .../migrate-to-schema.test.ts.snap | 46 +---- .../mixin-to-schema.test.ts.snap | 119 ++---------- .../model-to-schema.test.ts.snap | 179 ++++++------------ .../transforms/mixin-to-schema.test.ts | 13 +- .../transforms/model-to-schema.test.ts | 45 +++-- 6 files changed, 134 insertions(+), 313 deletions(-) diff --git a/packages/codemods/src/schema-migration/utils/schema-generation.ts b/packages/codemods/src/schema-migration/utils/schema-generation.ts index 7c3b7776ef5..2ddfa0a0b1d 100644 --- a/packages/codemods/src/schema-migration/utils/schema-generation.ts +++ b/packages/codemods/src/schema-migration/utils/schema-generation.ts @@ -648,30 +648,31 @@ export function generateMergedSchemaCode(opts: MergedSchemaOptions): string { // Generate default export sections.push(`\nexport default ${schemaName};`); - // Generate interface (only for TypeScript) - if (useComposite) { - // Composite pattern: field interface is {Name}Trait, composite is {Name} - const fieldInterfaceName = `${interfaceName}Trait`; - const fieldInterfaceCode = generateInterfaceOnly(fieldInterfaceName, properties); - sections.push(''); - sections.push(fieldInterfaceCode); - - // Composite interface merges field interface, extension, and trait interfaces - const traitInterfaces = traits.map(traitNameToInterfaceName); - const compositeExtends = [fieldInterfaceName, extensionName, ...traitInterfaces].join(', '); - sections.push(''); - sections.push(`export interface ${interfaceName} extends ${compositeExtends} {}`); - } else { - // Standard pattern: single interface with optional trait extends - let extendsClause: string | undefined; - if (traits.length > 0) { + if (isTypeScript) { + if (useComposite) { + // Composite pattern: field interface is {Name}Trait, composite is {Name} + const fieldInterfaceName = `${interfaceName}Trait`; + const fieldInterfaceCode = generateInterfaceOnly(fieldInterfaceName, properties); + sections.push(''); + sections.push(fieldInterfaceCode); + + // Composite interface merges field interface, extension, and trait interfaces const traitInterfaces = traits.map(traitNameToInterfaceName); - extendsClause = traitInterfaces.join(', '); - } + const compositeExtends = [fieldInterfaceName, extensionName, ...traitInterfaces].join(', '); + sections.push(''); + sections.push(`export interface ${interfaceName} extends ${compositeExtends} {}`); + } else { + // Standard pattern: single interface with optional trait extends + let extendsClause: string | undefined; + if (traits.length > 0) { + const traitInterfaces = traits.map(traitNameToInterfaceName); + extendsClause = traitInterfaces.join(', '); + } - const interfaceCode = generateInterfaceOnly(interfaceName, properties, extendsClause); - sections.push(''); - sections.push(interfaceCode); + const interfaceCode = generateInterfaceOnly(interfaceName, properties, extendsClause); + sections.push(''); + sections.push(interfaceCode); + } } return sections.join('\n'); diff --git a/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap index 51c8156493e..2cbaead829d 100644 --- a/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/__snapshots__/migrate-to-schema.test.ts.snap @@ -768,12 +768,7 @@ const JsModelWithMixinSchema = { ] }; -export default JsModelWithMixinSchema; - -export interface JsModelWithMixin extends JsTrait { - readonly [Type]: 'js-model-with-mixin'; - readonly name: string | null; -}", +export default JsModelWithMixinSchema;", "resources/ts-model-with-mixin.schema.ts": " import type { Type } from '@ember-data/core-types/symbols'; import type { TsTrait } from 'test-app/data/traits/ts.schema'; @@ -819,11 +814,7 @@ const JsMixinSchema = { ] }; -export default JsMixinSchema; - -export interface JsMixinTrait { - createdAt: Date | null; -}", +export default JsMixinSchema;", "traits/ts-mixin.ext.ts": " import Mixin from '@ember/object/mixin'; import { attr } from '@ember-data/model'; @@ -1114,32 +1105,7 @@ const BaseModelWithMethodsTrait = { ] }; -export default BaseModelWithMethodsTrait; - -export interface BaseModelWithMethodsTrait { - id: string | null; - readonly baseField: string | null; - readonly isNew: boolean; - readonly hasDirtyAttributes: boolean; - readonly isDeleted: boolean; - readonly isSaving: boolean; - readonly isValid: boolean; - readonly isError: boolean; - readonly isLoaded: boolean; - readonly isEmpty: boolean; - save: (options?: Record) => Promise; - reload: (options?: Record) => Promise; - deleteRecord: () => void; - unloadRecord: () => void; - destroyRecord: (options?: Record) => Promise; - rollbackAttributes: () => void; - belongsTo: (propertyName: string) => BelongsToReference; - hasMany: (propertyName: string) => HasManyReference; - serialize: (options?: Record) => unknown; - readonly errors: Errors; - readonly adapterError: Error | null; - readonly isReloading: boolean; -}", +export default BaseModelWithMethodsTrait;", } `; @@ -1167,11 +1133,7 @@ const CustomSelectOptionSchema = { ] }; -export default CustomSelectOptionSchema; - -export interface CustomSelectOption { - readonly [Type]: 'custom-select-option'; -}", +export default CustomSelectOptionSchema;", } `; diff --git a/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap index 3488991ee45..61ec38533f1 100644 --- a/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/transforms/__snapshots__/mixin-to-schema.test.ts.snap @@ -21,12 +21,7 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default SimpleSchema; - -export interface SimpleTrait { - title: string | null; - author: Promise; -}" +export default SimpleSchema;" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates trait and extension artifacts when mixin has computed properties and methods > mixin extension code 1`] = ` @@ -57,11 +52,7 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default NameableSchema; - -export interface NameableTrait { - name: string | null; -}" +export default NameableSchema;" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > generates trait artifact with merged types for basic mixins > basic trait type interface 1`] = ` @@ -94,13 +85,7 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default FileableSchema; - -export interface FileableTrait { - files: HasMany; - name: string | null; - isActive: boolean | null; -}" +export default FileableSchema;" `; exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > handles custom type mappings in mixin trait type interfaces > mixin custom type mappings interface 1`] = ` @@ -126,13 +111,7 @@ exports[`mixin-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default TypedSchema; - -export interface TypedTrait { - id: string | null; - amount: number | null; - metadata: Record | null; -}" +export default TypedSchema;" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > collects the real fileable mixin shape into trait and extension artifacts > artifact metadata 1`] = ` @@ -206,12 +185,7 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > collects ] }; -export default FileableSchema; - -export interface FileableTrait { - files: HasMany; - showFilesRequiringReviewError: boolean | null; -}" +export default FileableSchema;" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > converts mixin with no trait fields to extension artifact > code 1`] = ` @@ -307,11 +281,7 @@ exports[`mixin-to-schema transform (artifacts) > basic functionality > supports ] }; -export default AliasedSchema; - -export interface AliasedTrait { - name: string | null; -}" +export default AliasedSchema;" `; exports[`mixin-to-schema transform (artifacts) > basic functionality > supports alias of Mixin import and still produces a trait artifact > metadata 1`] = ` @@ -356,12 +326,7 @@ export interface File { ] }; -export default CustomSourceSchema; - -export interface CustomSourceTrait { - name: string | null; - files: HasMany; -}", +export default CustomSourceSchema;", "name": "CustomSourceSchema", "suggestedFileName": "custom-source.schema.js", "type": "trait", @@ -408,11 +373,7 @@ export interface File { ] }; -export default RenamedMixedSourcesSchema; - -export interface RenamedMixedSourcesTrait { - files: HasMany; -}", +export default RenamedMixedSourcesSchema;", "name": "RenamedMixedSourcesSchema", "suggestedFileName": "renamed-mixed-sources.schema.js", "type": "trait", @@ -447,11 +408,7 @@ exports[`mixin-to-schema transform (artifacts) > import validation > handles CLI ] }; -export default CliOptionSchema; - -export interface CliOptionTrait { - name: string | null; -}", +export default CliOptionSchema;", "name": "CliOptionSchema", "suggestedFileName": "cli-option.schema.js", "type": "trait", @@ -508,13 +465,7 @@ export interface User { ] }; -export default AliasedImportsSchema; - -export interface AliasedImportsTrait { - name: string | null; - files: HasMany; - owner: User | null; -}", +export default AliasedImportsSchema;", "name": "AliasedImportsSchema", "suggestedFileName": "aliased-imports.schema.js", "type": "trait", @@ -537,11 +488,7 @@ exports[`mixin-to-schema transform (artifacts) > import validation > ignores dec ] }; -export default UnsupportedSourceSchema; - -export interface UnsupportedSourceTrait { - name: string | null; -}", +export default UnsupportedSourceSchema;", "name": "UnsupportedSourceSchema", "suggestedFileName": "unsupported-source.schema.js", "type": "trait", @@ -606,12 +553,7 @@ export interface File { ] }; -export default DefaultSourceSchema; - -export interface DefaultSourceTrait { - name: string | null; - files: HasMany; -}", +export default DefaultSourceSchema;", "import Mixin from '@ember/object/mixin'; import { attr, hasMany } from '@ember-data/model'; import { computed } from '@ember/object'; @@ -652,11 +594,7 @@ export interface User { ] }; -export default BelongsToSchema; - -export interface BelongsToTrait { - owner: Promise; -}", +export default BelongsToSchema;", "name": "BelongsToSchema", "suggestedFileName": "belongs-to.schema.js", "type": "trait", @@ -673,10 +611,7 @@ exports[`mixin-to-schema transform (artifacts) > import validation > produces an 'fields': [] }; -export default NoValidImportsSchema; - -export interface NoValidImportsTrait { -}", +export default NoValidImportsSchema;", "name": "NoValidImportsSchema", "suggestedFileName": "no-valid-imports.schema.js", "type": "trait", @@ -729,12 +664,7 @@ export interface File { ] }; -export default AuditboardSourceSchema; - -export interface AuditboardSourceTrait { - name: string | null; - files: HasMany; -}", +export default AuditboardSourceSchema;", "name": "AuditboardSourceSchema", "suggestedFileName": "auditboard-source.schema.js", "type": "trait", @@ -794,12 +724,7 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default FileableSchema; - -export interface FileableTrait extends BaseModelTrait, TimestampTrait { - description: string | null; - files: HasMany; -}" +export default FileableSchema;" `; exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces trait with single extended trait > single inheritance trait code 1`] = ` @@ -818,11 +743,7 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default DescribableSchema; - -export interface DescribableTrait extends BaseModelTrait { - description: string | null; -}" +export default DescribableSchema;" `; exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces trait without traits property when no inheritance > no inheritance trait code 1`] = ` @@ -838,9 +759,5 @@ exports[`mixin-to-schema transform (artifacts) > mixin inheritance > produces tr ] }; -export default DescribableSchema; - -export interface DescribableTrait { - description: string | null; -}" +export default DescribableSchema;" `; diff --git a/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap b/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap index 131ed1fd2ed..b041f885a2d 100644 --- a/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap +++ b/tests/codemods/tests/schema-migration/transforms/__snapshots__/model-to-schema.test.ts.snap @@ -36,13 +36,7 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default ProcessedModelSchema; - -export interface ProcessedModel { - readonly [Type]: 'processed-model'; - readonly name: string | null; - readonly content: string | null; -}" +export default ProcessedModelSchema;" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > generates schema with merged types for basic models > basic schema with merged types 1`] = ` @@ -86,15 +80,7 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > gen ] }; -export default UserSchema; - -export interface User { - readonly [Type]: 'user'; - readonly name: string | null; - readonly isActive: boolean | null; - readonly company: Company | null; - readonly projects: AsyncHasMany; -}" +export default UserSchema;" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > handles custom type mappings in merged schema files > custom type mappings in merged schema 1`] = ` @@ -124,14 +110,7 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default TypedModelSchema; - -export interface TypedModel { - readonly [Type]: 'typed-model'; - readonly id: string | null; - readonly amount: number | null; - readonly metadata: Record | null; -}" +export default TypedModelSchema;" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > handles relationship types correctly in merged schema files > relationship types in merged schema 1`] = ` @@ -183,16 +162,7 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > han ] }; -export default RelationshipModelSchema; - -export interface RelationshipModel { - readonly [Type]: 'relationship-model'; - readonly name: string | null; - readonly owner: User | null; - readonly company: Promise; - readonly attachments: HasMany; - readonly tags: AsyncHasMany; -}" +export default RelationshipModelSchema;" `; exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > uses unknown type for unsupported transforms > unknown types in merged schema 1`] = ` @@ -222,14 +192,48 @@ exports[`model-to-schema transform (artifacts) > TypeScript type artifacts > use ] }; -export default UnknownTypesModelSchema; +export default UnknownTypesModelSchema;" +`; -export interface UnknownTypesModel { - readonly [Type]: 'unknown-types-model'; - readonly customField: unknown | null; - readonly anotherField: unknown | null; - readonly knownField: string | null; -}" +exports[`model-to-schema transform (artifacts) > basic functionality > handles Typescript model with mixins > schema with mixins 1`] = ` +"import type { Type } from '@ember-data/core-types/symbols'; +import type { FileableTrait } from '../traits/fileable.schema'; +import type { TimestampableTrait } from '../traits/timestampable.schema'; +import type { DocumentExtension } from 'test-app/data/resources/document.ext'; +const DocumentSchema = { + 'type': 'document', + 'legacy': true, + 'identity': { + 'kind': '@id', + 'name': 'id' + }, + 'fields': [ + { + 'kind': 'attribute', + 'name': 'title', + 'type': 'string' + }, + { + 'kind': 'attribute', + 'name': 'content', + 'type': 'string' + } + ], + 'traits': [ + 'fileable', + 'timestampable' + ] +} as const; + +export default DocumentSchema; + +export interface DocumentTrait { + readonly [Type]: 'document'; + readonly title: string | null; + readonly content: string | null; +} + +export interface Document extends DocumentTrait, DocumentExtension, FileableTrait, TimestampableTrait {}" `; exports[`model-to-schema transform (artifacts) > basic functionality > handles complex field options correctly > complex field options 1`] = ` @@ -280,15 +284,7 @@ exports[`model-to-schema transform (artifacts) > basic functionality > handles c ] }; -export default ComplexModelSchema; - -export interface ComplexModel { - readonly [Type]: 'complex-model'; - readonly name: string; - readonly birthDate: Date | null; - readonly owner: Promise; - readonly attachments: HasMany; -}" +export default ComplexModelSchema;" `; exports[`model-to-schema transform (artifacts) > basic functionality > handles model with mixins > schema with mixins 1`] = ` @@ -317,13 +313,7 @@ exports[`model-to-schema transform (artifacts) > basic functionality > handles m ] }; -export default DocumentSchema; - -export interface Document extends FileableTrait, TimestampableTrait { - readonly [Type]: 'document'; - readonly title: string | null; - readonly content: string | null; -}" +export default DocumentSchema;" `; exports[`model-to-schema transform (artifacts) > basic functionality > preserves TypeScript syntax in extension properties > typescript extension 1`] = ` @@ -426,16 +416,7 @@ exports[`model-to-schema transform (artifacts) > basic functionality > produces ] }; -export default UserSchema; - -export interface User { - readonly [Type]: 'user'; - readonly name: string | null; - readonly email: string | null; - readonly isActive: boolean | null; - readonly company: Company | null; - readonly projects: AsyncHasMany; -}" +export default UserSchema;" `; exports[`model-to-schema transform (artifacts) > basic functionality > supports alternate import sources > custom import source 1`] = ` @@ -463,13 +444,7 @@ exports[`model-to-schema transform (artifacts) > basic functionality > supports ] }; -export default CustomModelSchema; - -export interface CustomModel { - readonly [Type]: 'custom-model'; - readonly name: string | null; - readonly items: HasMany; -}" +export default CustomModelSchema;" `; exports[`model-to-schema transform (artifacts) > custom type mappings > applies custom type mappings to attribute types > custom type mappings in merged schema 1`] = ` @@ -499,14 +474,7 @@ exports[`model-to-schema transform (artifacts) > custom type mappings > applies ] }; -export default CustomTypesModelSchema; - -export interface CustomTypesModel { - readonly [Type]: 'custom-types-model'; - readonly id: string | null; - readonly createdAt: Date | null; - readonly price: number | null; -}" +export default CustomTypesModelSchema;" `; exports[`model-to-schema transform (artifacts) > custom type mappings > falls back to unknown for unmapped custom types > unknown fallback for unmapped types 1`] = ` @@ -531,13 +499,7 @@ exports[`model-to-schema transform (artifacts) > custom type mappings > falls ba ] }; -export default UnmappedTypesModelSchema; - -export interface UnmappedTypesModel { - readonly [Type]: 'unmapped-types-model'; - readonly field1: unknown | null; - readonly field2: unknown | null; -}" +export default UnmappedTypesModelSchema;" `; exports[`model-to-schema transform (artifacts) > edge cases > handles aliased imports correctly > aliased imports 1`] = ` @@ -562,13 +524,7 @@ exports[`model-to-schema transform (artifacts) > edge cases > handles aliased im ] }; -export default AliasedModelSchema; - -export interface AliasedModel { - readonly [Type]: 'aliased-model'; - readonly name: string | null; - readonly items: HasMany; -}" +export default AliasedModelSchema;" `; exports[`model-to-schema transform (artifacts) > edge cases > preserves complex object literal options > complex options 1`] = ` @@ -594,12 +550,7 @@ exports[`model-to-schema transform (artifacts) > edge cases > preserves complex ] }; -export default ComplexOptionsModelSchema; - -export interface ComplexOptionsModel { - readonly [Type]: 'complex-options-model'; - readonly owner: Promise; -}" +export default ComplexOptionsModelSchema;" `; exports[`model-to-schema transform (artifacts) > mirror flag > uses configured emberDataImportSource for HasMany types in merged schema > custom EmberData source for HasMany types in merged schema 1`] = ` @@ -643,15 +594,7 @@ exports[`model-to-schema transform (artifacts) > mirror flag > uses configured e ] }; -export default RelationshipModelSchema; - -export interface RelationshipModel { - readonly [Type]: 'relationship-model'; - readonly name: string | null; - readonly tags: HasMany; - readonly projects: AsyncHasMany; - readonly owner: User | null; -}" +export default RelationshipModelSchema;" `; exports[`model-to-schema transform (artifacts) > mixin handling > extracts mixin names and converts them to trait references > single mixin schema 1`] = ` @@ -674,12 +617,7 @@ exports[`model-to-schema transform (artifacts) > mixin handling > extracts mixin ] }; -export default DocumentSchema; - -export interface Document extends FileableTrait { - readonly [Type]: 'document'; - readonly title: string | null; -}" +export default DocumentSchema;" `; exports[`model-to-schema transform (artifacts) > mixin handling > handles multiple mixins correctly > multiple mixins schema 1`] = ` @@ -704,10 +642,5 @@ exports[`model-to-schema transform (artifacts) > mixin handling > handles multip ] }; -export default ComplexDocumentSchema; - -export interface ComplexDocument extends FileableTrait, TimestampableTrait, AuditableTrait { - readonly [Type]: 'complex-document'; - readonly title: string | null; -}" +export default ComplexDocumentSchema;" `; diff --git a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts index 10a7fa1bdbd..26bfa768eef 100644 --- a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts @@ -56,10 +56,7 @@ export default Mixin.create({});`; 'fields': [] }; - export default EmptySchema; - - export interface EmptyTrait { - }", + export default EmptySchema;", "name": "EmptySchema", "suggestedFileName": "empty.schema.js", "type": "trait", @@ -115,13 +112,7 @@ export default Mixin.create({ ] }; - export default FileableSchema; - - export interface FileableTrait { - files: HasMany; - name: string | null; - isActive: boolean | null; - }", + export default FileableSchema;", "name": "FileableSchema", "suggestedFileName": "fileable.schema.js", "type": "trait", diff --git a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts index 1f35b7bb453..98181bda27f 100644 --- a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts @@ -59,7 +59,7 @@ export default class SimpleModel extends Model { expect(artifacts[0]?.name).toBe('SimpleModelSchema'); }); - it('handles model with mixins', () => { + it('handles Typescript model with mixins', () => { const input = `import Model, { attr } from '@ember-data/model'; import FileableMixin from 'app/mixins/fileable'; import TimestampableMixin from 'app/mixins/timestampable'; @@ -74,7 +74,7 @@ export default class Document extends Model.extend(FileableMixin, TimestampableM }`; const artifacts = toArtifacts( - parseFile('app/models/document.js', input, DEFAULT_TEST_OPTIONS), + parseFile('app/models/document.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); // Schema now includes merged types, so we have: schema (with types) + extension @@ -86,6 +86,33 @@ export default class Document extends Model.extend(FileableMixin, TimestampableM expect(schema?.code).toMatchSnapshot('schema with mixins'); }); + it('handles model with mixins', () => { + const input = `import Model, { attr } from '@ember-data/model'; +import FileableMixin from 'app/mixins/fileable'; +import TimestampableMixin from 'app/mixins/timestampable'; + +export default class Document extends Model.extend(FileableMixin, TimestampableMixin) { + @attr('string') title; + @attr('string') content; + + get wordCount() { + return (this.content || '').split(' ').length; + } +}`; + + const artifacts = toArtifacts( + parseFile('app/models/document.js', input, DEFAULT_TEST_OPTIONS), + DEFAULT_TEST_OPTIONS + ); + // Schema now includes merged types, so we have: schema (with types) + extension + expect(artifacts).toHaveLength(2); + + const schema = artifacts.find((a) => a.type === 'schema'); + expect(schema?.code).toContain('fileable'); + expect(schema?.code).toContain('timestampable'); + expect(schema?.code).toMatchSnapshot('schema with mixins'); + }); + it('supports alternate import sources', () => { const input = `import Model, { attr, hasMany } from '@auditboard/warp-drive/v1/model'; @@ -852,12 +879,7 @@ export default class TestModel extends Model.extend(WorkstreamableMixin) { ] }; - export default TestModelSchema; - - export interface TestModel extends WorkstreamableTrait { - readonly [Type]: 'test-model'; - readonly workstreamable: Workstreamable | null; - }" + export default TestModelSchema;" `); }); }); @@ -1124,12 +1146,7 @@ export default class Translatable extends Model { ] }; - export default TestModelSchema; - - export interface TestModel { - readonly [Type]: 'test-model'; - readonly name: string | null; - }", + export default TestModelSchema;", "name": "TestModelSchema", "suggestedFileName": "test-model.schema.js", "type": "schema", From 104b089ded0326566b514cf9e58c47fcb58c1764 Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Tue, 24 Feb 2026 14:38:29 +0100 Subject: [PATCH 4/4] feat(codemod): log what files were skipped for what reason --- .../codemods/src/schema-migration/codemod.ts | 60 +++++++++--- .../src/schema-migration/processors/mixin.ts | 29 +++--- .../src/schema-migration/processors/model.ts | 8 +- .../src/schema-migration/tasks/migrate.ts | 55 +++++++---- .../transforms/mixin-to-schema.test.ts | 67 ++++++------- .../transforms/model-to-schema.test.ts | 94 +++++++++++-------- 6 files changed, 189 insertions(+), 124 deletions(-) diff --git a/packages/codemods/src/schema-migration/codemod.ts b/packages/codemods/src/schema-migration/codemod.ts index 4e278ec2052..fd127f2246b 100644 --- a/packages/codemods/src/schema-migration/codemod.ts +++ b/packages/codemods/src/schema-migration/codemod.ts @@ -6,6 +6,7 @@ import { basename, extname, join, resolve } from 'path'; import { InstanciatedLogger } from '../../utils/logger.js'; import type { FinalOptions } from './config.js'; import { analyzeModelMixinUsage } from './processors/mixin-analyzer.js'; +import type { TransformArtifact } from './utils/ast-utils.js'; import type { ParsedFile } from './utils/file-parser.js'; import { parseFile } from './utils/file-parser.js'; import { FILE_EXTENSION_REGEX, TRAILING_SINGLE_WILDCARD_REGEX, TRAILING_WILDCARD_REGEX } from './utils/string.js'; @@ -13,6 +14,28 @@ import { FILE_EXTENSION_REGEX, TRAILING_SINGLE_WILDCARD_REGEX, TRAILING_WILDCARD export type Filename = string; export type InputFile = { path: string; code: string }; +export type SkipReason = + | 'dts-file' + | 'file-not-found' + | 'already-processed' + | 'intermediate-model' + | 'parse-error' + | 'invalid-model' + | 'not-mixin-file-type' + | 'mixin-not-connected' + | 'empty-artifacts'; + +export interface SkippedFile { + file: string; + reason: SkipReason; + phase: 'discovery' | 'parsing' | 'generation'; +} + +export interface TransformerResult { + artifacts: TransformArtifact[]; + skipReason?: SkipReason; +} + /** * Check if a file path matches any intermediate model path */ @@ -66,25 +89,26 @@ function expandGlobPattern(dir: string): string { async function findFiles( sources: string[], - predicate: (file: string) => boolean, + predicate: (file: string) => SkipReason | null, finalOptions: FinalOptions, logger: InstanciatedLogger -): Promise<{ output: InputFile[]; skipped: string[]; errors: Error[] }> { +): Promise<{ output: InputFile[]; skipped: SkippedFile[]; errors: Error[] }> { const output: InputFile[] = []; const errors: Error[] = []; - const skipped: string[] = []; + const skipped: SkippedFile[] = []; for (const source of sources) { try { const files = await glob(source); for (const file of files) { - if (predicate(file)) { + const skipReason = predicate(file); + if (skipReason === null) { const content = await readFile(file, 'utf-8'); output.push({ path: file, code: content }); } else { - skipped.push(file); + skipped.push({ file, reason: skipReason, phase: 'discovery' }); } } @@ -107,7 +131,7 @@ export class Input { mixins: Map = new Map(); parsedModels: Map = new Map(); parsedMixins: Map = new Map(); - skipped: string[] = []; + skipped: SkippedFile[] = []; errors: Error[] = []; } @@ -150,7 +174,6 @@ export class Codemod { let modelsParsed = 0; let mixinsParsed = 0; - let parseErrors = 0; for (const [filePath, inputFile] of this.input.models) { try { @@ -159,7 +182,7 @@ export class Codemod { modelsParsed++; } catch (error) { this.logger.error(`āŒ Error parsing model ${filePath}: ${String(error)}`); - parseErrors++; + this.input.skipped.push({ file: filePath, reason: 'parse-error', phase: 'parsing' }); } } @@ -170,10 +193,11 @@ export class Codemod { mixinsParsed++; } catch (error) { this.logger.error(`āŒ Error parsing mixin ${filePath}: ${String(error)}`); - parseErrors++; + this.input.skipped.push({ file: filePath, reason: 'parse-error', phase: 'parsing' }); } } + const parseErrors = this.input.skipped.filter((s) => s.reason === 'parse-error').length; this.logger.info(`āœ… Parsed ${modelsParsed} models and ${mixinsParsed} mixins (${parseErrors} errors).`); } @@ -208,11 +232,14 @@ export class Codemod { const models = await findFiles( fileSources, (file) => { - return ( - existsSync(file) && - (!this.finalOptions.skipProcessed || !isAlreadyProcessed(file)) && - !isIntermediateModel(file, this.finalOptions.intermediateModelPaths, this.finalOptions.additionalModelSources) - ); + if (file.endsWith('.d.ts')) return 'dts-file'; + if (!existsSync(file)) return 'file-not-found'; + if (this.finalOptions.skipProcessed && isAlreadyProcessed(file)) return 'already-processed'; + if ( + isIntermediateModel(file, this.finalOptions.intermediateModelPaths, this.finalOptions.additionalModelSources) + ) + return 'intermediate-model'; + return null; }, this.finalOptions, this.logger @@ -242,7 +269,10 @@ export class Codemod { const models = await findFiles( fileSources, (file) => { - return existsSync(file) && (!this.finalOptions.skipProcessed || !isAlreadyProcessed(file)); + if (file.endsWith('.d.ts')) return 'dts-file'; + if (!existsSync(file)) return 'file-not-found'; + if (this.finalOptions.skipProcessed && isAlreadyProcessed(file)) return 'already-processed'; + return null; }, this.finalOptions, this.logger diff --git a/packages/codemods/src/schema-migration/processors/mixin.ts b/packages/codemods/src/schema-migration/processors/mixin.ts index 94462ad1007..dd2a775fa18 100644 --- a/packages/codemods/src/schema-migration/processors/mixin.ts +++ b/packages/codemods/src/schema-migration/processors/mixin.ts @@ -4,6 +4,7 @@ import { existsSync } from 'fs'; import { join } from 'path'; import { logger } from '../../../utils/logger.js'; +import type { TransformerResult } from '../codemod.js'; import type { TransformOptions } from '../config.js'; import type { ExtractedType, PropertyInfo, SchemaField, TransformArtifact } from '../utils/ast-utils.js'; import { @@ -129,12 +130,12 @@ export default function transform(filePath: string, source: string, options: Tra * This does not modify the original source. The CLI can use this to write * files to the requested output directories. */ -export function toArtifacts(parsedFile: ParsedFile, options: TransformOptions): TransformArtifact[] { +export function toArtifacts(parsedFile: ParsedFile, options: TransformOptions): TransformerResult { const { path: filePath, source, baseName, camelName: mixinName } = parsedFile; if (parsedFile.fileType !== 'mixin') { log.debug('Not a mixin file, returning empty artifacts'); - return []; + return { artifacts: [], skipReason: 'not-mixin-file-type' }; } const traitFields = parsedFile.fields.map((f) => ({ @@ -161,19 +162,21 @@ export function toArtifacts(parsedFile: ParsedFile, options: TransformOptions): if (!isConnectedToModel) { log.debug(`Skipping ${mixinName}: not connected to any models`); - return []; + return { artifacts: [], skipReason: 'mixin-not-connected' }; } - return generateMixinArtifacts( - filePath, - source, - baseName, - mixinName, - traitFields, - extensionProperties, - extendedTraits, - options - ); + return { + artifacts: generateMixinArtifacts( + filePath, + source, + baseName, + mixinName, + traitFields, + extensionProperties, + extendedTraits, + options + ), + }; } /** diff --git a/packages/codemods/src/schema-migration/processors/model.ts b/packages/codemods/src/schema-migration/processors/model.ts index fad705f647a..c5375f0214a 100644 --- a/packages/codemods/src/schema-migration/processors/model.ts +++ b/packages/codemods/src/schema-migration/processors/model.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { logger } from '../../../utils/logger.js'; -import type { Filename } from '../codemod.js'; +import type { Filename, TransformerResult } from '../codemod.js'; import type { TransformOptions } from '../config.js'; import type { ExtractedType, SchemaField, TransformArtifact } from '../utils/ast-utils.js'; import { @@ -696,15 +696,15 @@ function generateRegularModelArtifacts( return artifacts; } -export function toArtifacts(parsedFile: ParsedFile, options: TransformOptions): TransformArtifact[] { +export function toArtifacts(parsedFile: ParsedFile, options: TransformOptions): TransformerResult { log.debug(`=== DEBUG: Processing ${parsedFile.path} ===`); const analysis = analyzeModelFromParsed(parsedFile, options); if (!analysis.isValid) { log.debug('Model analysis failed, skipping artifact generation'); - return []; + return { artifacts: [], skipReason: 'invalid-model' }; } - return generateRegularModelArtifacts(parsedFile.path, parsedFile.source, analysis, options); + return { artifacts: generateRegularModelArtifacts(parsedFile.path, parsedFile.source, analysis, options) }; } /** diff --git a/packages/codemods/src/schema-migration/tasks/migrate.ts b/packages/codemods/src/schema-migration/tasks/migrate.ts index 91d90dd7b5f..8543ddfe6f8 100644 --- a/packages/codemods/src/schema-migration/tasks/migrate.ts +++ b/packages/codemods/src/schema-migration/tasks/migrate.ts @@ -2,6 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { basename, dirname, join, resolve } from 'path'; import { InstanciatedLogger, logger } from '../../../utils/logger.js'; +import type { SkippedFile, TransformerResult } from '../codemod.js'; import { Codemod } from '../codemod.js'; import type { FinalOptions, MigrateOptions, TransformOptions } from '../config.js'; import { toArtifacts as mixinToArtifacts } from '../processors/mixin.js'; @@ -32,7 +33,7 @@ interface Artifact { interface ProcessingResult { processed: number; - skipped: string[]; + skipped: SkippedFile[]; errors: string[]; } @@ -277,7 +278,7 @@ function writeIntermediateArtifacts(artifacts: Artifact[], finalOptions: FinalOp } } -type ArtifactTransformer = (parsedFile: ParsedFile, options: TransformOptions) => Artifact[]; +type ArtifactTransformer = (parsedFile: ParsedFile, options: TransformOptions) => TransformerResult; interface ProcessFilesOptions { parsedFiles: Map; @@ -292,8 +293,8 @@ interface ProcessFilesOptions { */ function processFiles({ parsedFiles, transformer, finalOptions, log }: ProcessFilesOptions): ProcessingResult { let processed = 0; - const skipped = []; - const errors = []; + const skipped: SkippedFile[] = []; + const errors: string[] = []; for (const [filePath, parsedFile] of parsedFiles) { try { @@ -301,12 +302,12 @@ function processFiles({ parsedFiles, transformer, finalOptions, log }: ProcessFi log.debug(`šŸ”„ Processing: ${filePath}`); } - const artifacts = transformer(parsedFile, finalOptions); + const result = transformer(parsedFile, finalOptions); - if (artifacts.length > 0) { + if (result.artifacts.length > 0) { processed++; - for (const artifact of artifacts) { + for (const artifact of result.artifacts) { const { outputPath } = getArtifactOutputPath(artifact, filePath, finalOptions); writeArtifact(artifact, outputPath, { @@ -316,7 +317,7 @@ function processFiles({ parsedFiles, transformer, finalOptions, log }: ProcessFi }); } } else { - skipped.push(filePath); + skipped.push({ file: filePath, reason: result.skipReason ?? 'empty-artifacts', phase: 'generation' }); } } catch (error) { errors.push(filePath); @@ -436,22 +437,38 @@ export async function runMigration(options: MigrateOptions): Promise { log, }); - // Aggregate results + // Aggregate all skipped files from every phase + const allSkipped: SkippedFile[] = [...codemod.input.skipped, ...modelResults.skipped, ...mixinResults.skipped]; + const processed = modelResults.processed + mixinResults.processed; - const skipped = modelResults.skipped.length + mixinResults.skipped.length; const errors = modelResults.errors.length + mixinResults.errors.length; - log.info(`\nāœ… Migration complete!`); - log.info(` šŸ“Š Processed: ${processed}`); - log.info( - ` ā­ļø Skipped: ${skipped} - Mixins: ${mixinResults.skipped.length}, Models: ${modelResults.skipped.length}` - ); + const dtsFiles = allSkipped.filter((s) => s.reason === 'dts-file'); + const nonDtsSkipped = allSkipped.filter((s) => s.reason !== 'dts-file'); + + const phaseGroups = new Map(); + for (const entry of nonDtsSkipped) { + let group = phaseGroups.get(entry.phase); + if (!group) { + group = []; + phaseGroups.set(entry.phase, group); + } + group.push(entry); + } - if (options.verbose) { - log.warn( - `Skipped:\n Mixins:\n ${mixinResults.skipped.join(', ')}\n Models: ${modelResults.skipped.join(', ')}` - ); + if (phaseGroups.size > 0) { + log.warn('\nWarning! the following files were not transformed:'); + for (const [phase, files] of phaseGroups) { + log.warn(`\n(${phase})`); + for (const x of files) { + log.warn(x.file); + } + } } + + log.info(`\nāœ… Migration complete!`); + log.info(` šŸ“Š Processed: ${processed}`); + log.info(` ā­ļø Skipped: ${allSkipped.length} (${dtsFiles.length} .d.ts files)`); if (errors > 0) { log.info(` āŒ Errors: ${errors} files`); } diff --git a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts index 26bfa768eef..4d50f44f2f7 100644 --- a/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/mixin-to-schema.test.ts @@ -44,7 +44,7 @@ describe('mixin-to-schema transform (artifacts)', () => { export default Mixin.create({});`; - const artifacts = toArtifacts(parseFile('app/mixins/empty.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/empty.js', input, options), options); expect(artifacts).toHaveLength(1); const trait = artifacts.find((a) => a.type === 'trait'); @@ -76,7 +76,7 @@ export default Mixin.create({ titleCaseName: computed('name', function () { return (this.name || '').toUpperCase(); }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); expect(artifacts).toHaveLength(3); // trait, extension, and resource-type-stub for 'file' const trait = artifacts.find((a) => a.type === 'trait'); @@ -140,7 +140,7 @@ import { attr } from '@ember-data/model'; export default MyMixin.create({ name: attr('string') });`; - const artifacts = toArtifacts(parseFile('app/mixins/aliased.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/aliased.js', input, options), options); expect( artifacts.map((a) => ({ type: a.type, name: a.name, suggestedFileName: a.suggestedFileName })) ).toMatchSnapshot('metadata'); @@ -154,7 +154,7 @@ import { hasMany } from '@ember-data/model'; const Fileable = Mixin.create({ files: hasMany('file', { async: false }) }); export default Fileable;`; - const artifacts = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); expect( artifacts.map((a) => ({ type: a.type, name: a.name, suggestedFileName: a.suggestedFileName })) ).toMatchSnapshot('metadata'); @@ -166,7 +166,7 @@ export default Fileable;`; export default SomethingElse.create({ name: attr('string') });`; - const artifacts = toArtifacts(parseFile('app/mixins/not-ember-mixin.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/not-ember-mixin.js', input, options), options); expect(artifacts).toHaveLength(0); }); @@ -179,7 +179,7 @@ export default Mixin.create({ computedValue: computed(function() { return 'computed'; }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/no-traits.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/no-traits.js', input, options), options); expect( artifacts.map((a) => ({ type: a.type, name: a.name, suggestedFileName: a.suggestedFileName })) ).toMatchSnapshot('metadata'); @@ -201,7 +201,7 @@ export default Mixin.create({ }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/plannable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/plannable.js', input, options), options); expect( artifacts.map((a) => ({ type: a.type, name: a.name, suggestedFileName: a.suggestedFileName })) ).toMatchSnapshot('metadata'); @@ -237,7 +237,7 @@ export default Mixin.create({ }, });`; - const artifacts = toArtifacts(parseFile('apps/client/app/mixins/fileable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('apps/client/app/mixins/fileable.js', input, options), options); expect(artifacts).toHaveLength(3); // Trait, extension, and resource-type-stub for 'file' expect( artifacts.map((a) => ({ type: a.type, suggestedFileName: a.suggestedFileName, name: a.name })) @@ -263,7 +263,7 @@ export default Mixin.create({ customProp: computed('name', function() { return this.name; }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/default-source.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/default-source.js', input, options), options); expect(artifacts.map((a) => ({ type: a.type, name: a.name }))).toMatchSnapshot('artifact types'); expect(artifacts.map((a) => a.code)).toMatchSnapshot('generated code'); }); @@ -283,7 +283,7 @@ export default Mixin.create({ ...options, emberDataImportSource: '@my-custom/model', }; - const artifacts = toArtifacts(parseFile('app/mixins/custom-source.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/custom-source.js', input, opts), opts); expect(artifacts).toMatchSnapshot(); }); @@ -302,7 +302,7 @@ export default Mixin.create({ ...options, emberDataImportSource: '@auditboard/warp-drive/v1/model', }; - const artifacts = toArtifacts(parseFile('app/mixins/auditboard-source.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/auditboard-source.js', input, opts), opts); expect(artifacts).toMatchSnapshot(); }); @@ -318,7 +318,7 @@ export default Mixin.create({ customProp: computed('name', function() { return this.name; }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/unsupported-source.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/unsupported-source.js', input, options), options); expect(artifacts).toMatchSnapshot(); }); @@ -332,7 +332,7 @@ export default Mixin.create({ owner: oneRelation('user') });`; - const artifacts = toArtifacts(parseFile('app/mixins/aliased-imports.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/aliased-imports.js', input, options), options); expect(artifacts).toMatchSnapshot(); }); @@ -346,7 +346,7 @@ export default Mixin.create({ name: attribute('string') // Should be ignored, treated as regular function call });`; - const artifacts = toArtifacts(parseFile('app/mixins/renamed-mixed-sources.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/renamed-mixed-sources.js', input, options), options); expect(artifacts).toMatchSnapshot(); }); @@ -360,7 +360,7 @@ export default Mixin.create({ customProp: computed('name', function() { return this.name; }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/no-valid-imports.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/no-valid-imports.js', input, options), options); expect(artifacts).toMatchSnapshot(); }); @@ -372,7 +372,7 @@ export default Mixin.create({ owner: belongsTo('user', { async: true }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/belongs-to.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/belongs-to.js', input, options), options); expect(artifacts).toMatchSnapshot(); }); @@ -383,7 +383,7 @@ export default class MyClass { name = 'test'; }`; - const artifacts = toArtifacts(parseFile('app/mixins/not-a-mixin.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/not-a-mixin.js', input, options), options); expect(artifacts).toHaveLength(0); }); @@ -399,7 +399,7 @@ export default Mixin.create({ ...options, emberDataImportSource: '@my-custom/model', }; - const artifacts = toArtifacts(parseFile('app/mixins/cli-option.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/cli-option.js', input, opts), opts); expect(artifacts).toMatchSnapshot(); }); }); @@ -415,7 +415,7 @@ export default Mixin.create({ isActive: attr('boolean', { defaultValue: false }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); // Should have trait and resource-type-stub for 'file' (no extension if no computed/methods) expect(artifacts).toHaveLength(2); @@ -443,7 +443,7 @@ export default Mixin.create({ } });`; - const artifacts = toArtifacts(parseFile('app/mixins/nameable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/nameable.js', input, options), options); // Should have trait and extension artifacts (types merged into trait) expect(artifacts).toHaveLength(2); @@ -467,7 +467,7 @@ export default Mixin.create({ author: belongsTo('user', { async: true }) });`; - const artifacts = toArtifacts(parseFile('app/mixins/simple.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/simple.js', input, options), options); // Should have trait and resource-type-stub for 'user' (no extension for data-only mixins) expect(artifacts).toHaveLength(2); @@ -494,7 +494,7 @@ export default Mixin.create({ }; const opts = { ...options, typeMapping: customTypeMappings }; - const artifacts = toArtifacts(parseFile('app/mixins/typed.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/typed.js', input, opts), opts); const trait = artifacts.find((a) => a.type === 'trait'); expect(trait?.code).toMatchSnapshot('mixin custom type mappings interface'); @@ -534,7 +534,7 @@ export default BaseModelMixin; ...options, appImportPrefix: 'test-app', }; - const artifacts = toArtifacts(parseFile('/app/mixins/base-model.ts', mixinSource, opts), opts); + const { artifacts } = toArtifacts(parseFile('/app/mixins/base-model.ts', mixinSource, opts), opts); // Should find both trait fields and extension properties expect(artifacts.length).toBeGreaterThan(0); @@ -592,7 +592,7 @@ export default BaseModelMixin; ...options, appImportPrefix: 'test-app', }; - const artifacts = toArtifacts(parseFile('/app/mixins/base-model.ts', mixinSource, opts), opts); + const { artifacts } = toArtifacts(parseFile('/app/mixins/base-model.ts', mixinSource, opts), opts); expect(artifacts.length).toBeGreaterThan(0); @@ -626,7 +626,7 @@ const NestedCastMixin = Mixin.create({ export default NestedCastMixin; `.trim(); - const artifacts = toArtifacts(parseFile('/app/mixins/nested-cast.ts', mixinSource, options), options); + const { artifacts } = toArtifacts(parseFile('/app/mixins/nested-cast.ts', mixinSource, options), options); expect(artifacts.length).toBeGreaterThan(0); @@ -648,11 +648,14 @@ export default Mixin.create({ // Test with mirror flag const mirrorOpts = { ...options, mirror: true }; - const artifactsMirror = toArtifacts(parseFile('app/mixins/basic.js', input, mirrorOpts), mirrorOpts); + const { artifacts: artifactsMirror } = toArtifacts( + parseFile('app/mixins/basic.js', input, mirrorOpts), + mirrorOpts + ); const traitMirror = artifactsMirror.find((a) => a.type === 'trait'); // Test without mirror flag - const artifactsRegular = toArtifacts(parseFile('app/mixins/basic.js', input, options), options); + const { artifacts: artifactsRegular } = toArtifacts(parseFile('app/mixins/basic.js', input, options), options); const traitRegular = artifactsRegular.find((a) => a.type === 'trait'); // Mixins themselves don't generate @warp-drive imports, so they should be the same @@ -676,7 +679,7 @@ export default Mixin.createWithMixins(BaseModelMixin, TimestampMixin, { ...options, appImportPrefix: 'test-app', }; - const artifacts = toArtifacts(parseFile('app/mixins/fileable.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/fileable.js', input, opts), opts); // Should produce trait and resource-type-stub for 'file' (no extension since no methods/computed properties) expect(artifacts).toHaveLength(2); @@ -716,7 +719,7 @@ export default Mixin.createWithMixins(BaseModelMixin, { ...options, appImportPrefix: 'test-app', }; - const artifacts = toArtifacts(parseFile('app/mixins/describable.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/mixins/describable.js', input, opts), opts); const trait = artifacts.find((a) => a.type === 'trait'); @@ -737,7 +740,7 @@ export default Mixin.create({ description: attr('string') });`; - const artifacts = toArtifacts(parseFile('app/mixins/describable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/describable.js', input, options), options); const trait = artifacts.find((a) => a.type === 'trait'); @@ -769,7 +772,7 @@ export default Mixin.create({ debug: false, }; - const artifacts = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/fileable.js', input, options), options); // Should have trait (with merged types) and resource-type-stub artifacts expect(artifacts).toHaveLength(3); // trait, file stub, user stub @@ -811,7 +814,7 @@ export default Mixin.create({ debug: false, }; - const artifacts = toArtifacts(parseFile('app/mixins/commentable.js', input, options), options); + const { artifacts } = toArtifacts(parseFile('app/mixins/commentable.js', input, options), options); // Should have trait (with merged types) and multiple resource-type-stub artifacts expect(artifacts.length).toBeGreaterThanOrEqual(4); // trait + 3 stubs diff --git a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts index 98181bda27f..96c85f43039 100644 --- a/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts +++ b/tests/codemods/tests/schema-migration/transforms/model-to-schema.test.ts @@ -25,7 +25,10 @@ export default class User extends Model { } }`; - const artifacts = toArtifacts(parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS); + const { artifacts } = toArtifacts( + parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), + DEFAULT_TEST_OPTIONS + ); // Schema now includes merged types, so we have: schema (with types) + extension expect(artifacts).toHaveLength(2); @@ -49,7 +52,7 @@ export default class SimpleModel extends Model { @attr('number') count; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/simple-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -73,7 +76,7 @@ export default class Document extends Model.extend(FileableMixin, TimestampableM } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/document.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -100,7 +103,7 @@ export default class Document extends Model.extend(FileableMixin, TimestampableM } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/document.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -124,7 +127,7 @@ export default class CustomModel extends Model { const opts = createTestOptions({ emberDataImportSource: '@auditboard/warp-drive/v1/model', }); - const artifacts = toArtifacts(parseFile('app/models/custom-model.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/custom-model.js', input, opts), opts); // Schema now includes merged types, so we only have 1 artifact for data-only models expect(artifacts).toHaveLength(1); expect(artifacts[0]?.type).toBe('schema'); @@ -141,7 +144,7 @@ export default class ComplexModel extends Model { @hasMany('file', { async: false, inverse: null, as: 'fileable' }) attachments; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/complex-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -169,7 +172,7 @@ export default class TypedModel extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/typed-model.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -187,7 +190,7 @@ export default class ProjectPlan extends Model { @attr('string') title; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/project-plan.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -212,7 +215,7 @@ export default class FragmentModel extends Model { @fragment('address') address; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/fragment-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -242,7 +245,7 @@ export default class Address extends Fragment { @attr('string') zip; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/address.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -279,7 +282,7 @@ export default class Address extends BaseFragment { const opts = createTestOptions({ intermediateFragmentPaths: ['./base-fragment', 'base-fragment'], }); - const artifacts = toArtifacts(parseFile('app/models/address.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/address.js', input, opts), opts); // Schema now includes merged types, so we only have 1 artifact for data-only models expect(artifacts).toHaveLength(1); @@ -307,7 +310,7 @@ export default class Address extends BaseFragment { const opts = createTestOptions({ intermediateFragmentPaths: ['codemod/models/base-fragment'], }); - const artifacts = toArtifacts(parseFile('/Users/test/codemod/models/address.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('/Users/test/codemod/models/address.js', input, opts), opts); // Schema now includes merged types, so we only have 1 artifact for data-only models expect(artifacts).toHaveLength(1); @@ -337,7 +340,7 @@ export default class FragmentArrayModel extends Model { @fragmentArray('address') addresses; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/fragment-array-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -368,7 +371,7 @@ export default class ArrayModel extends Model { @array() tags; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/array-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -400,7 +403,7 @@ export default class NotAModel extends Component { @attr('string') name; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/components/not-a-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -415,7 +418,7 @@ export default class NotExtendingModel extends EmberObject { @attr('string') name; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/not-extending-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -428,7 +431,7 @@ export default class NotExtendingModel extends EmberObject { export default class EmptyModel extends Model { }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/empty-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -446,7 +449,7 @@ export default class AliasedModel extends Model { @manyRelation('item') items; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/aliased-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -464,7 +467,7 @@ export default class MixedSourceModel extends Model { @customDecorator items; // Should be ignored and moved to extension }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/mixed-source-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -496,7 +499,7 @@ export default class AuditBoardModel extends BaseModel.extend(BaseModelMixin) { @attr('number') id; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/auditboard-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -516,7 +519,7 @@ export default class ComplexOptionsModel extends Model { }) owner; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/complex-options-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -535,7 +538,7 @@ export default class Document extends Model.extend(FileableMixin) { @attr('string') title; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/document.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -556,7 +559,7 @@ export default class ComplexDocument extends Model.extend(FileableMixin, Timesta @attr('string') title; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/complex-document.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -579,7 +582,10 @@ export default class User extends Model { @hasMany('project', { async: true }) projects; }`; - const artifacts = toArtifacts(parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS); + const { artifacts } = toArtifacts( + parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), + DEFAULT_TEST_OPTIONS + ); // Types are now merged into schema, so we only have 1 artifact for data-only models expect(artifacts).toHaveLength(1); @@ -607,7 +613,7 @@ export default class ProcessedModel extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/processed-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -641,7 +647,7 @@ export default class TypedModel extends Model { }; const opts = createTestOptions({ typeMapping: customTypeMappings }); - const artifacts = toArtifacts(parseFile('app/models/typed-model.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/typed-model.js', input, opts), opts); // Types are now merged into schema const schema = artifacts.find((a) => a.type === 'schema'); @@ -659,7 +665,7 @@ export default class RelationshipModel extends Model { @hasMany('tag', { async: true }) tags; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/relationship-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -678,7 +684,7 @@ export default class UnknownTypesModel extends Model { @attr('string') knownField; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/unknown-types-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -707,7 +713,7 @@ export default class CustomTypesModel extends Model { }; const opts = createTestOptions({ typeMapping: customTypeMappings }); - const artifacts = toArtifacts(parseFile('app/models/custom-types-model.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/custom-types-model.js', input, opts), opts); // Types are now merged into schema const schema = artifacts.find((a) => a.type === 'schema'); @@ -722,7 +728,7 @@ export default class UnmappedTypesModel extends Model { @attr('another-unknown') field2; }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/unmapped-types-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -748,7 +754,7 @@ export default class RelationshipModel extends Model { const opts = createTestOptions({ emberDataImportSource: '@auditboard/warp-drive/v1/model', }); - const artifacts = toArtifacts(parseFile('app/models/relationship-model.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/relationship-model.js', input, opts), opts); // Types are now merged into schema const schema = artifacts.find((a) => a.type === 'schema'); @@ -846,7 +852,7 @@ export default class TestModel extends Model.extend(WorkstreamableMixin) { // Mark workstreamable as a connected mixin so it imports from traits modelConnectedMixins: new Set(['app/mixins/workstreamable.js']), }); - const artifacts = toArtifacts(parseFile('app/models/test-model.js', input, opts), opts); + const { artifacts } = toArtifacts(parseFile('app/models/test-model.js', input, opts), opts); const schemaType = artifacts.find((a) => a.type === 'schema'); @@ -906,7 +912,10 @@ export default class User extends Model { } }`; - const artifacts = toArtifacts(parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS); + const { artifacts } = toArtifacts( + parseFile('app/models/user.js', input, DEFAULT_TEST_OPTIONS), + DEFAULT_TEST_OPTIONS + ); const schema = artifacts.find((a) => a.type === 'schema'); // Schema should not contain utility functions @@ -934,7 +943,7 @@ export default class Product extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/product.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -965,7 +974,7 @@ export default class Product extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/product.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -1004,7 +1013,7 @@ export default class Translatable extends Model { }, }); - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('client-core/models/translatable.js', input, optionsWithMapping), optionsWithMapping ); @@ -1040,7 +1049,7 @@ export default class Translatable extends Model { }, }); - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('client-core/package/src/models/translatable.js', input, optionsWithMapping), optionsWithMapping ); @@ -1089,7 +1098,7 @@ export default class Translatable extends Model { } `; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/test-model.js', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -1177,7 +1186,7 @@ export default class Amendment extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/amendment.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -1228,7 +1237,7 @@ export default class Amendment extends Model { } }`; - const artifacts = toArtifacts( + const { artifacts } = toArtifacts( parseFile('app/models/amendment.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS ); @@ -1262,7 +1271,10 @@ export default class Task extends Model { } }`; - const artifacts = toArtifacts(parseFile('app/models/task.ts', input, DEFAULT_TEST_OPTIONS), DEFAULT_TEST_OPTIONS); + const { artifacts } = toArtifacts( + parseFile('app/models/task.ts', input, DEFAULT_TEST_OPTIONS), + DEFAULT_TEST_OPTIONS + ); const extension = artifacts.find((a) => a.type === 'resource-extension'); expect(extension?.code).toMatchInlineSnapshot(` "import Model, { attr } from '@ember-data/model';