Przeglądaj źródła

feat(cli): Allow new entity features to be selected

Michael Bromley 1 rok temu
rodzic
commit
74c69dd2e8

+ 98 - 8
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);
+    }
+}

+ 4 - 1
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(),

+ 5 - 26
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],
     });

+ 29 - 0
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<ScaffoldEntity>, HasCustomFields
+{
+    constructor(input?: DeepPartial<Translation<ScaffoldTranslation>>) {
+        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;
+}

+ 18 - 4
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<ScaffoldEntity>) {
         super(input);
     }
 
     @Column()
-    name: string;
+    code: string;
 
     @Column(type => ScaffoldEntityCustomFields)
     customFields: ScaffoldEntityCustomFields;
+
+    localizedName: LocaleString;
+
+    @OneToMany(type => ScaffoldTranslation, translation => translation.base, { eager: true })
+    translations: Array<Translation<ScaffoldEntity>>;
 }

+ 1 - 1
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');

+ 30 - 1
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()