diff --git a/packages/cli/src/commands/add/entity/add-entity.ts b/packages/cli/src/commands/add/entity/add-entity.ts index a25c1f394a..535aaa5bc5 100644 --- a/packages/cli/src/commands/add/entity/add-entity.ts +++ b/packages/cli/src/commands/add/entity/add-entity.ts @@ -1,6 +1,7 @@ -import { outro } from '@clack/prompts'; +import { cancel, isCancel, multiselect, outro } from '@clack/prompts'; import { paramCase } from 'change-case'; import path from 'path'; +import { ClassDeclaration, StructureKind, SyntaxKind } from 'ts-morph'; import { analyzeProject, getCustomEntityName, selectPlugin } from '../../../shared/shared-prompts'; import { createFile } from '../../../utilities/ast-utils'; @@ -14,6 +15,11 @@ export interface AddEntityTemplateContext { entity: { className: string; fileName: string; + translationFileName: string; + features: { + customFields: boolean; + translatable: boolean; + }; }; } @@ -22,20 +28,48 @@ export async function addEntity(providedVendurePlugin?: VendurePluginRef) { const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage)); const customEntityName = await getCustomEntityName(cancelledMessage); + + const features = await multiselect({ + message: 'Entity features (use ↑, ↓, space to select)', + required: false, + initialValues: ['customFields'], + options: [ + { + label: 'Custom fields', + value: 'customFields', + hint: 'Adds support for custom fields on this entity', + }, + { + label: 'Translatable', + value: 'translatable', + hint: 'Adds support for localized properties on this entity', + }, + ], + }); + if (isCancel(features)) { + cancel(cancelledMessage); + process.exit(0); + } + const context: AddEntityTemplateContext = { entity: { className: customEntityName, fileName: paramCase(customEntityName) + '.entity', + translationFileName: paramCase(customEntityName) + '-translation.entity', + features: { + customFields: features.includes('customFields'), + translatable: features.includes('translatable'), + }, }, }; - const entitiesDir = path.join(vendurePlugin.getPluginDir().getPath(), 'entities'); - const entityFile = createFile(project, path.join(__dirname, 'templates/entity.template.ts')); - entityFile.move(path.join(entitiesDir, `${context.entity.fileName}.ts`)); - entityFile.getClasses()[0].rename(`${context.entity.className}CustomFields`); - entityFile.getClasses()[1].rename(context.entity.className); - - addEntityToPlugin(vendurePlugin.classDeclaration, entityFile); + const { entityClass, translationClass } = createEntity(vendurePlugin, context); + addEntityToPlugin(vendurePlugin, entityClass); + entityClass.getSourceFile().organizeImports(); + if (context.entity.features.translatable) { + addEntityToPlugin(vendurePlugin, translationClass); + translationClass.getSourceFile().organizeImports(); + } await project.save(); @@ -43,3 +77,59 @@ export async function addEntity(providedVendurePlugin?: VendurePluginRef) { outro('✅ Done!'); } } + +function createEntity(plugin: VendurePluginRef, context: AddEntityTemplateContext) { + const entitiesDir = path.join(plugin.getPluginDir().getPath(), 'entities'); + const entityFile = createFile( + plugin.getSourceFile().getProject(), + path.join(__dirname, 'templates/entity.template.ts'), + ); + const translationFile = createFile( + plugin.getSourceFile().getProject(), + path.join(__dirname, 'templates/entity-translation.template.ts'), + ); + entityFile.move(path.join(entitiesDir, `${context.entity.fileName}.ts`)); + translationFile.move(path.join(entitiesDir, `${context.entity.translationFileName}.ts`)); + + const entityClass = entityFile.getClass('ScaffoldEntity')?.rename(context.entity.className); + const customFieldsClass = entityFile + .getClass('ScaffoldEntityCustomFields') + ?.rename(`${context.entity.className}CustomFields`); + const translationClass = translationFile + .getClass('ScaffoldTranslation') + ?.rename(`${context.entity.className}Translation`); + const translationCustomFieldsClass = translationFile + .getClass('ScaffoldEntityCustomFieldsTranslation') + ?.rename(`${context.entity.className}CustomFieldsTranslation`); + + if (!context.entity.features.customFields) { + // Remove custom fields from entity + customFieldsClass?.remove(); + translationCustomFieldsClass?.remove(); + removeCustomFieldsFromClass(entityClass); + removeCustomFieldsFromClass(translationClass); + } + if (!context.entity.features.translatable) { + // Remove translatable fields from entity + translationClass?.remove(); + entityClass?.getProperty('localizedName')?.remove(); + entityClass?.getProperty('translations')?.remove(); + removeImplementsFromClass('Translatable', entityClass); + translationFile.delete(); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { entityClass: entityClass!, translationClass: translationClass! }; +} + +function removeCustomFieldsFromClass(entityClass?: ClassDeclaration) { + entityClass?.getProperty('customFields')?.remove(); + removeImplementsFromClass('HasCustomFields', entityClass); +} + +function removeImplementsFromClass(implementsName: string, entityClass?: ClassDeclaration) { + const index = entityClass?.getImplements().findIndex(i => i.getText() === implementsName) ?? -1; + if (index > -1) { + entityClass?.removeImplements(index); + } +} diff --git a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts b/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts index c20d69e83a..ce5fe3a394 100644 --- a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts +++ b/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import path from 'path'; import { Project } from 'ts-morph'; import { describe, expect, it } from 'vitest'; @@ -5,6 +6,7 @@ import { describe, expect, it } from 'vitest'; import { defaultManipulationSettings } from '../../../../../constants'; import { createFile, getPluginClasses } from '../../../../../utilities/ast-utils'; import { expectSourceFileContentToMatch } from '../../../../../utilities/testing-utils'; +import { VendurePluginRef } from '../../../../../utilities/vendure-plugin-ref'; import { addEntityToPlugin } from './add-entity-to-plugin'; @@ -19,7 +21,8 @@ describe('addEntityToPlugin', () => { const entityTemplatePath = path.join(__dirname, '../../templates/entity.template.ts'); const entityFile = createFile(project, entityTemplatePath); entityFile.move(path.join(__dirname, 'fixtures', 'entity.ts')); - addEntityToPlugin(pluginClasses[0], entityFile); + const entityClass = entityFile.getClass('ScaffoldEntity'); + addEntityToPlugin(new VendurePluginRef(pluginClasses[0]), entityClass!); expectSourceFileContentToMatch( pluginClasses[0].getSourceFile(), diff --git a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts b/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts index 17c25d60fb..15aba69cd9 100644 --- a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts +++ b/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts @@ -1,37 +1,16 @@ -import { ClassDeclaration, Node, SourceFile, SyntaxKind } from 'ts-morph'; +import { ClassDeclaration } from 'ts-morph'; import { addImportsToFile } from '../../../../../utilities/ast-utils'; +import { VendurePluginRef } from '../../../../../utilities/vendure-plugin-ref'; -export function addEntityToPlugin(pluginClass: ClassDeclaration, entitySourceFile: SourceFile) { - const pluginDecorator = pluginClass.getDecorator('VendurePlugin'); - if (!pluginDecorator) { - throw new Error('Could not find VendurePlugin decorator'); - } - const pluginOptions = pluginDecorator.getArguments()[0]; - if (!pluginOptions) { - throw new Error('Could not find VendurePlugin options'); - } - const entityClass = entitySourceFile.getClasses().find(c => !c.getName()?.includes('CustomFields')); +export function addEntityToPlugin(plugin: VendurePluginRef, entityClass: ClassDeclaration) { if (!entityClass) { throw new Error('Could not find entity class'); } const entityClassName = entityClass.getName() as string; - if (Node.isObjectLiteralExpression(pluginOptions)) { - const entityProperty = pluginOptions.getProperty('entities'); - if (entityProperty) { - const entitiesArray = entityProperty.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression); - if (entitiesArray) { - entitiesArray.addElement(entityClassName); - } - } else { - pluginOptions.addPropertyAssignment({ - name: 'entities', - initializer: `[${entityClassName}]`, - }); - } - } + plugin.addEntity(entityClassName); - addImportsToFile(pluginClass.getSourceFile(), { + addImportsToFile(plugin.classDeclaration.getSourceFile(), { moduleSpecifier: entityClass.getSourceFile(), namedImports: [entityClassName], }); diff --git a/packages/cli/src/commands/add/entity/templates/entity-translation.template.ts b/packages/cli/src/commands/add/entity/templates/entity-translation.template.ts new file mode 100644 index 0000000000..c942176cff --- /dev/null +++ b/packages/cli/src/commands/add/entity/templates/entity-translation.template.ts @@ -0,0 +1,29 @@ +import { LanguageCode } from '@vendure/common/lib/generated-types'; +import { DeepPartial } from '@vendure/common/lib/shared-types'; +import { HasCustomFields, Translation, VendureEntity } from '@vendure/core'; +import { Column, Entity, Index, ManyToOne } from 'typeorm'; + +import { ScaffoldEntity } from './entity.template'; + +export class ScaffoldEntityCustomFieldsTranslation {} + +@Entity() +export class ScaffoldTranslation + extends VendureEntity + implements Translation, HasCustomFields +{ + constructor(input?: DeepPartial>) { + super(input); + } + + @Column('varchar') languageCode: LanguageCode; + + @Column() localizedName: string; + + @Index() + @ManyToOne(type => ScaffoldEntity, base => base.translations, { onDelete: 'CASCADE' }) + base: ScaffoldEntity; + + @Column(type => ScaffoldEntityCustomFieldsTranslation) + customFields: ScaffoldEntityCustomFieldsTranslation; +} diff --git a/packages/cli/src/commands/add/entity/templates/entity.template.ts b/packages/cli/src/commands/add/entity/templates/entity.template.ts index b9739491a9..6484566d2a 100644 --- a/packages/cli/src/commands/add/entity/templates/entity.template.ts +++ b/packages/cli/src/commands/add/entity/templates/entity.template.ts @@ -1,17 +1,31 @@ -import { VendureEntity, DeepPartial, HasCustomFields } from '@vendure/core'; -import { Entity, Column } from 'typeorm'; +import { + DeepPartial, + HasCustomFields, + LocaleString, + Translatable, + Translation, + VendureEntity, +} from '@vendure/core'; +import { Column, Entity, OneToMany } from 'typeorm'; + +import { ScaffoldTranslation } from './entity-translation.template'; export class ScaffoldEntityCustomFields {} @Entity() -export class ScaffoldEntity extends VendureEntity implements HasCustomFields { +export class ScaffoldEntity extends VendureEntity implements Translatable, HasCustomFields { constructor(input?: DeepPartial) { super(input); } @Column() - name: string; + code: string; @Column(type => ScaffoldEntityCustomFields) customFields: ScaffoldEntityCustomFields; + + localizedName: LocaleString; + + @OneToMany(type => ScaffoldTranslation, translation => translation.base, { eager: true }) + translations: Array>; } diff --git a/packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.ts b/packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.ts index e9f058c717..510469d0db 100644 --- a/packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.ts +++ b/packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.ts @@ -35,7 +35,7 @@ export function updateAdminUiPluginInit( .formatText(); } else { const computeFnCall = appProperty.getFirstChildByKind(SyntaxKind.CallExpression); - if (computeFnCall?.getType().getSymbol()?.getName() === AdminUiAppConfigName) { + if (computeFnCall?.getType().getText().includes(AdminUiAppConfigName)) { const arg = computeFnCall.getArguments()[0]; if (arg && Node.isObjectLiteralExpression(arg)) { const extensionsProp = arg.getProperty('extensions'); diff --git a/packages/cli/src/utilities/vendure-plugin-ref.ts b/packages/cli/src/utilities/vendure-plugin-ref.ts index b3c2c40a18..b9d63ecd5e 100644 --- a/packages/cli/src/utilities/vendure-plugin-ref.ts +++ b/packages/cli/src/utilities/vendure-plugin-ref.ts @@ -1,4 +1,5 @@ -import { ClassDeclaration } from 'ts-morph'; +import { ClassDeclaration, Node, SyntaxKind } from 'ts-morph'; +import { isLiteralExpression } from 'typescript'; import { AdminUiExtensionTypeName } from '../constants'; @@ -17,6 +18,34 @@ export class VendurePluginRef { return this.classDeclaration.getSourceFile().getDirectory(); } + getMetadataOptions() { + const pluginDecorator = this.classDeclaration.getDecorator('VendurePlugin'); + if (!pluginDecorator) { + throw new Error('Could not find VendurePlugin decorator'); + } + const pluginOptions = pluginDecorator.getArguments()[0]; + if (!pluginOptions || !Node.isObjectLiteralExpression(pluginOptions)) { + throw new Error('Could not find VendurePlugin options'); + } + return pluginOptions; + } + + addEntity(entityClassName: string) { + const pluginOptions = this.getMetadataOptions(); + const entityProperty = pluginOptions.getProperty('entities'); + if (entityProperty) { + const entitiesArray = entityProperty.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression); + if (entitiesArray) { + entitiesArray.addElement(entityClassName); + } + } else { + pluginOptions.addPropertyAssignment({ + name: 'entities', + initializer: `[${entityClassName}]`, + }); + } + } + hasUiExtensions(): boolean { return !!this.classDeclaration .getStaticProperties()