shared-prompts.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { cancel, isCancel, multiselect, select, spinner } from '@clack/prompts';
  2. import { ClassDeclaration, Project } from 'ts-morph';
  3. import { addServiceCommand } from '../commands/add/service/add-service';
  4. import { Messages } from '../constants';
  5. import { getPluginClasses, getTsMorphProject, selectTsConfigFile } from '../utilities/ast-utils';
  6. import { pauseForPromptDisplay } from '../utilities/utils';
  7. import { EntityRef } from './entity-ref';
  8. import { ServiceRef } from './service-ref';
  9. import { VendurePluginRef } from './vendure-plugin-ref';
  10. export async function analyzeProject(options: {
  11. providedVendurePlugin?: VendurePluginRef;
  12. cancelledMessage?: string;
  13. }) {
  14. const providedVendurePlugin = options.providedVendurePlugin;
  15. let project = providedVendurePlugin?.classDeclaration.getProject();
  16. let tsConfigPath: string | undefined;
  17. if (!providedVendurePlugin) {
  18. const projectSpinner = spinner();
  19. const tsConfigFile = await selectTsConfigFile();
  20. projectSpinner.start('Analyzing project...');
  21. await pauseForPromptDisplay();
  22. const { project: _project, tsConfigPath: _tsConfigPath } = await getTsMorphProject({}, tsConfigFile);
  23. project = _project;
  24. tsConfigPath = _tsConfigPath;
  25. projectSpinner.stop('Project analyzed');
  26. }
  27. return { project: project as Project, tsConfigPath };
  28. }
  29. export async function selectPlugin(project: Project, cancelledMessage: string): Promise<VendurePluginRef> {
  30. const pluginClasses = getPluginClasses(project);
  31. if (pluginClasses.length === 0) {
  32. cancel(Messages.NoPluginsFound);
  33. process.exit(0);
  34. }
  35. const targetPlugin = await select({
  36. message: 'To which plugin would you like to add the feature?',
  37. options: pluginClasses.map(c => ({
  38. value: c,
  39. label: c.getName() as string,
  40. })),
  41. maxItems: 10,
  42. });
  43. if (isCancel(targetPlugin)) {
  44. cancel(cancelledMessage);
  45. process.exit(0);
  46. }
  47. return new VendurePluginRef(targetPlugin as ClassDeclaration);
  48. }
  49. export async function selectEntity(plugin: VendurePluginRef): Promise<EntityRef> {
  50. const entities = plugin.getEntities();
  51. if (entities.length === 0) {
  52. throw new Error(Messages.NoEntitiesFound);
  53. }
  54. const targetEntity = await select({
  55. message: 'Select an entity',
  56. options: entities
  57. .filter(e => !e.isTranslation())
  58. .map(e => ({
  59. value: e,
  60. label: e.name,
  61. })),
  62. maxItems: 10,
  63. });
  64. if (isCancel(targetEntity)) {
  65. cancel('Cancelled');
  66. process.exit(0);
  67. }
  68. return targetEntity as EntityRef;
  69. }
  70. export async function selectMultiplePluginClasses(
  71. project: Project,
  72. cancelledMessage: string,
  73. ): Promise<VendurePluginRef[]> {
  74. const pluginClasses = getPluginClasses(project);
  75. if (pluginClasses.length === 0) {
  76. cancel(Messages.NoPluginsFound);
  77. process.exit(0);
  78. }
  79. const selectAll = await select({
  80. message: 'To which plugin would you like to add the feature?',
  81. options: [
  82. {
  83. value: 'all',
  84. label: 'All plugins',
  85. },
  86. {
  87. value: 'specific',
  88. label: 'Specific plugins (you will be prompted to select the plugins)',
  89. },
  90. ],
  91. });
  92. if (isCancel(selectAll)) {
  93. cancel(cancelledMessage);
  94. process.exit(0);
  95. }
  96. if (selectAll === 'all') {
  97. return pluginClasses.map(pc => new VendurePluginRef(pc));
  98. }
  99. const targetPlugins = await multiselect({
  100. message: 'Select one or more plugins (use ↑, ↓, space to select)',
  101. options: pluginClasses.map(c => ({
  102. value: c,
  103. label: c.getName() as string,
  104. })),
  105. });
  106. if (isCancel(targetPlugins)) {
  107. cancel(cancelledMessage);
  108. process.exit(0);
  109. }
  110. return (targetPlugins as ClassDeclaration[]).map(pc => new VendurePluginRef(pc));
  111. }
  112. export async function selectServiceRef(
  113. project: Project,
  114. plugin: VendurePluginRef,
  115. canCreateNew = true,
  116. ): Promise<ServiceRef> {
  117. const serviceRefs = getServices(project).filter(sr => {
  118. return sr.classDeclaration
  119. .getSourceFile()
  120. .getDirectoryPath()
  121. .includes(plugin.getSourceFile().getDirectoryPath());
  122. });
  123. if (serviceRefs.length === 0 && !canCreateNew) {
  124. throw new Error(Messages.NoServicesFound);
  125. }
  126. const result = await select({
  127. message: 'Which service contains the business logic for this API extension?',
  128. maxItems: 8,
  129. options: [
  130. ...(canCreateNew
  131. ? [
  132. {
  133. value: 'new',
  134. label: `Create new generic service`,
  135. },
  136. ]
  137. : []),
  138. ...serviceRefs.map(sr => {
  139. const features = sr.crudEntityRef
  140. ? `CRUD service for ${sr.crudEntityRef.name}`
  141. : `Generic service`;
  142. const label = `${sr.name}: (${features})`;
  143. return {
  144. value: sr,
  145. label,
  146. };
  147. }),
  148. ],
  149. });
  150. if (isCancel(result)) {
  151. cancel('Cancelled');
  152. process.exit(0);
  153. }
  154. if (result === 'new') {
  155. return addServiceCommand.run({ type: 'basic', plugin }).then(r => r.serviceRef);
  156. } else {
  157. return result as ServiceRef;
  158. }
  159. }
  160. export function getServices(project: Project): ServiceRef[] {
  161. const servicesSourceFiles = project.getSourceFiles().filter(sf => {
  162. return (
  163. sf.getDirectory().getPath().endsWith('/services') ||
  164. sf.getDirectory().getPath().endsWith('/service')
  165. );
  166. });
  167. return servicesSourceFiles
  168. .flatMap(sf => sf.getClasses())
  169. .filter(classDeclaration => classDeclaration.getDecorator('Injectable'))
  170. .map(classDeclaration => new ServiceRef(classDeclaration));
  171. }