Skip to content

Commit

Permalink
feat(cli): Allow new entity features to be selected
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 3, 2024
1 parent e18784f commit 74c69dd
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 41 deletions.
106 changes: 98 additions & 8 deletions packages/cli/src/commands/add/entity/add-entity.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,6 +15,11 @@ export interface AddEntityTemplateContext {
entity: {
className: string;
fileName: string;
translationFileName: string;
features: {
customFields: boolean;
translatable: boolean;
};
};
}

Expand All @@ -22,24 +28,108 @@ 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();

if (!providedVendurePlugin) {
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import path from 'path';
import { Project } from 'ts-morph';
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';

Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 18 additions & 4 deletions packages/cli/src/commands/add/entity/templates/entity.template.ts
Original file line number Diff line number Diff line change
@@ -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>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
31 changes: 30 additions & 1 deletion packages/cli/src/utilities/vendure-plugin-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ClassDeclaration } from 'ts-morph';
import { ClassDeclaration, Node, SyntaxKind } from 'ts-morph';
import { isLiteralExpression } from 'typescript';

import { AdminUiExtensionTypeName } from '../constants';

Expand All @@ -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()
Expand Down

0 comments on commit 74c69dd

Please sign in to comment.