new-plugin.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { cancel, intro, isCancel, multiselect, outro, text } from '@clack/prompts';
  2. import { camelCase, constantCase, paramCase, pascalCase } from 'change-case';
  3. import * as fs from 'fs-extra';
  4. import path from 'path';
  5. import { getCustomEntityName } from '../../../shared/shared-prompts';
  6. import { renderEntity } from '../../../shared/shared-scaffold/entity';
  7. import { Scaffolder } from '../../../utilities/scaffolder';
  8. import { renderAdminResolver, renderAdminResolverWithEntity } from './scaffold/api/admin.resolver';
  9. import { renderApiExtensions } from './scaffold/api/api-extensions';
  10. import { renderShopResolver, renderShopResolverWithEntity } from './scaffold/api/shop.resolver';
  11. import { renderConstants } from './scaffold/constants';
  12. import { renderPlugin } from './scaffold/plugin';
  13. import { renderService, renderServiceWithEntity } from './scaffold/services/service';
  14. import { renderTypes } from './scaffold/types';
  15. import { GeneratePluginOptions, NewPluginTemplateContext } from './types';
  16. const cancelledMessage = 'Plugin setup cancelled.';
  17. export async function newPlugin() {
  18. const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
  19. intro('Scaffolding a new Vendure plugin!');
  20. if (!options.name) {
  21. const name = await text({
  22. message: 'What is the name of the plugin?',
  23. initialValue: '',
  24. validate: input => {
  25. if (!/^[a-z][a-z-0-9]+$/.test(input)) {
  26. return 'The plugin name must be lowercase and contain only letters, numbers and dashes';
  27. }
  28. },
  29. });
  30. if (isCancel(name)) {
  31. cancel(cancelledMessage);
  32. process.exit(0);
  33. } else {
  34. options.name = name;
  35. }
  36. }
  37. const features = await multiselect({
  38. message: 'Which features would you like to include? (use ↑, ↓, space to select)',
  39. options: [
  40. { value: 'customEntity', label: 'Custom entity' },
  41. { value: 'apiExtensions', label: 'GraphQL API extensions' },
  42. ],
  43. required: false,
  44. });
  45. if (Array.isArray(features)) {
  46. options.withCustomEntity = features.includes('customEntity');
  47. options.withApiExtensions = features.includes('apiExtensions');
  48. }
  49. if (options.withCustomEntity) {
  50. options.customEntityName = await getCustomEntityName(cancelledMessage);
  51. }
  52. const pluginDir = getPluginDirName(options.name);
  53. const confirmation = await text({
  54. message: 'Plugin location',
  55. initialValue: pluginDir,
  56. placeholder: '',
  57. validate: input => {
  58. if (fs.existsSync(input)) {
  59. return `A directory named "${input}" already exists. Please specify a different directory.`;
  60. }
  61. },
  62. });
  63. if (isCancel(confirmation)) {
  64. cancel(cancelledMessage);
  65. process.exit(0);
  66. } else {
  67. options.pluginDir = confirmation;
  68. generatePlugin(options);
  69. }
  70. }
  71. export function generatePlugin(options: GeneratePluginOptions) {
  72. const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
  73. const normalizedName = nameWithoutPlugin + '-plugin';
  74. const templateContext: NewPluginTemplateContext = {
  75. ...options,
  76. pluginName: pascalCase(normalizedName),
  77. pluginInitOptionsName: constantCase(normalizedName) + '_OPTIONS',
  78. service: {
  79. className: pascalCase(nameWithoutPlugin) + 'Service',
  80. instanceName: camelCase(nameWithoutPlugin) + 'Service',
  81. fileName: paramCase(nameWithoutPlugin) + '.service',
  82. },
  83. entity: {
  84. className: options.customEntityName,
  85. instanceName: camelCase(options.customEntityName),
  86. fileName: paramCase(options.customEntityName) + '.entity',
  87. },
  88. };
  89. const scaffolder = new Scaffolder<NewPluginTemplateContext>();
  90. scaffolder.addFile(renderPlugin, paramCase(nameWithoutPlugin) + '.plugin.ts');
  91. scaffolder.addFile(renderTypes, 'types.ts');
  92. scaffolder.addFile(renderConstants, 'constants.ts');
  93. if (options.withApiExtensions) {
  94. scaffolder.addFile(renderApiExtensions, 'api/api-extensions.ts');
  95. if (options.withCustomEntity) {
  96. scaffolder.addFile(renderShopResolverWithEntity, 'api/shop.resolver.ts');
  97. scaffolder.addFile(renderAdminResolverWithEntity, 'api/admin.resolver.ts');
  98. } else {
  99. scaffolder.addFile(renderShopResolver, 'api/shop.resolver.ts');
  100. scaffolder.addFile(renderAdminResolver, 'api/admin.resolver.ts');
  101. }
  102. }
  103. if (options.withCustomEntity) {
  104. scaffolder.addFile(renderEntity, `entities/${templateContext.entity.fileName}.ts`);
  105. scaffolder.addFile(renderServiceWithEntity, `services/${templateContext.service.fileName}.ts`);
  106. } else {
  107. scaffolder.addFile(renderService, `services/${templateContext.service.fileName}.ts`);
  108. }
  109. const pluginDir = options.pluginDir;
  110. scaffolder.createScaffold({
  111. dir: pluginDir,
  112. context: templateContext,
  113. });
  114. outro('✅ Plugin scaffolding complete!');
  115. }
  116. function getPluginDirName(name: string) {
  117. const cwd = process.cwd();
  118. const pathParts = cwd.split(path.sep);
  119. const currentlyInPluginsDir = pathParts[pathParts.length - 1] === 'plugins';
  120. const currentlyInRootDir = fs.pathExistsSync(path.join(cwd, 'package.json'));
  121. const nameWithoutPlugin = name.replace(/-?plugin$/i, '');
  122. if (currentlyInPluginsDir) {
  123. return path.join(cwd, paramCase(nameWithoutPlugin));
  124. }
  125. if (currentlyInRootDir) {
  126. return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
  127. }
  128. return path.join(cwd, paramCase(nameWithoutPlugin));
  129. }