ast-utils.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { cancel, isCancel, log, select } from '@clack/prompts';
  2. import fs from 'fs-extra';
  3. import path from 'node:path';
  4. import { Directory, Node, Project, ProjectOptions, ScriptKind, SourceFile } from 'ts-morph';
  5. import { defaultManipulationSettings } from '../constants';
  6. import { EntityRef } from '../shared/entity-ref';
  7. export async function selectTsConfigFile() {
  8. const tsConfigFiles = fs.readdirSync(process.cwd()).filter(f => /^tsconfig.*\.json$/.test(f));
  9. if (tsConfigFiles.length === 0) {
  10. throw new Error('No tsconfig files found in current directory');
  11. }
  12. if (tsConfigFiles.length === 1) {
  13. return tsConfigFiles[0];
  14. }
  15. const selectedConfigFile = await select({
  16. message: 'Multiple tsconfig files found. Select one:',
  17. options: tsConfigFiles.map(c => ({
  18. value: c,
  19. label: path.basename(c),
  20. })),
  21. maxItems: 10,
  22. });
  23. if (isCancel(selectedConfigFile)) {
  24. cancel();
  25. process.exit(0);
  26. }
  27. return selectedConfigFile as string;
  28. }
  29. export function getTsMorphProject(options: ProjectOptions = {}) {
  30. const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
  31. if (!fs.existsSync(tsConfigPath)) {
  32. throw new Error('No tsconfig.json found in current directory');
  33. }
  34. const project = new Project({
  35. tsConfigFilePath: tsConfigPath,
  36. manipulationSettings: defaultManipulationSettings,
  37. skipFileDependencyResolution: true,
  38. compilerOptions: {
  39. skipLibCheck: true,
  40. },
  41. ...options,
  42. });
  43. project.enableLogging(false);
  44. return project;
  45. }
  46. export function getPluginClasses(project: Project) {
  47. const sourceFiles = project.getSourceFiles();
  48. const pluginClasses = sourceFiles
  49. .flatMap(sf => {
  50. return sf.getClasses();
  51. })
  52. .filter(c => {
  53. const hasPluginDecorator = c.getModifiers().find(m => {
  54. return Node.isDecorator(m) && m.getName() === 'VendurePlugin';
  55. });
  56. return !!hasPluginDecorator;
  57. });
  58. return pluginClasses;
  59. }
  60. export function addImportsToFile(
  61. sourceFile: SourceFile,
  62. options: {
  63. moduleSpecifier: string | SourceFile;
  64. namedImports?: string[];
  65. namespaceImport?: string;
  66. order?: number;
  67. },
  68. ) {
  69. const moduleSpecifier = getModuleSpecifierString(options.moduleSpecifier, sourceFile);
  70. const existingDeclaration = sourceFile.getImportDeclaration(
  71. declaration => declaration.getModuleSpecifier().getLiteralValue() === moduleSpecifier,
  72. );
  73. if (!existingDeclaration) {
  74. const importDeclaration = sourceFile.addImportDeclaration({
  75. moduleSpecifier,
  76. ...(options.namespaceImport ? { namespaceImport: options.namespaceImport } : {}),
  77. ...(options.namedImports ? { namedImports: options.namedImports } : {}),
  78. });
  79. if (options.order != null) {
  80. importDeclaration.setOrder(options.order);
  81. }
  82. } else {
  83. if (
  84. options.namespaceImport &&
  85. !existingDeclaration.getNamespaceImport() &&
  86. !existingDeclaration.getDefaultImport()
  87. ) {
  88. existingDeclaration.setNamespaceImport(options.namespaceImport);
  89. }
  90. if (options.namedImports) {
  91. const existingNamedImports = existingDeclaration.getNamedImports();
  92. for (const namedImport of options.namedImports) {
  93. if (!existingNamedImports.find(ni => ni.getName() === namedImport)) {
  94. existingDeclaration.addNamedImport(namedImport);
  95. }
  96. }
  97. }
  98. }
  99. }
  100. function getModuleSpecifierString(moduleSpecifier: string | SourceFile, sourceFile: SourceFile): string {
  101. if (typeof moduleSpecifier === 'string') {
  102. return moduleSpecifier;
  103. }
  104. return getRelativeImportPath({ from: sourceFile, to: moduleSpecifier });
  105. }
  106. export function getRelativeImportPath(locations: {
  107. from: SourceFile | Directory;
  108. to: SourceFile | Directory;
  109. }): string {
  110. const fromPath =
  111. locations.from instanceof SourceFile ? locations.from.getFilePath() : locations.from.getPath();
  112. const toPath = locations.to instanceof SourceFile ? locations.to.getFilePath() : locations.to.getPath();
  113. const fromDir = /\.[a-z]+$/.test(fromPath) ? path.dirname(fromPath) : fromPath;
  114. return convertPathToRelativeImport(path.relative(fromDir, toPath));
  115. }
  116. export function createFile(project: Project, templatePath: string) {
  117. const template = fs.readFileSync(templatePath, 'utf-8');
  118. const tempFilePath = path.join('/.vendure-cli-temp/', path.basename(templatePath));
  119. try {
  120. return project.createSourceFile(path.join('/.vendure-cli-temp/', tempFilePath), template, {
  121. overwrite: true,
  122. scriptKind: ScriptKind.TS,
  123. });
  124. } catch (e: any) {
  125. log.error(e.message);
  126. process.exit(1);
  127. }
  128. }
  129. function convertPathToRelativeImport(filePath: string): string {
  130. // Normalize the path separators
  131. const normalizedPath = filePath.replace(/\\/g, '/');
  132. // Remove the file extension
  133. const parsedPath = path.parse(normalizedPath);
  134. return `./${parsedPath.dir}/${parsedPath.name}`.replace(/\/\//g, '/');
  135. }
  136. export function customizeCreateUpdateInputInterfaces(sourceFile: SourceFile, entityRef: EntityRef) {
  137. const createInputInterface = sourceFile
  138. .getInterface('CreateEntityInput')
  139. ?.rename(`Create${entityRef.name}Input`);
  140. const updateInputInterface = sourceFile
  141. .getInterface('UpdateEntityInput')
  142. ?.rename(`Update${entityRef.name}Input`);
  143. let index = 0;
  144. for (const { name, type, nullable } of entityRef.getProps()) {
  145. if (
  146. type.isBoolean() ||
  147. type.isString() ||
  148. type.isNumber() ||
  149. (type.isObject() && type.getText() === 'Date')
  150. ) {
  151. createInputInterface?.insertProperty(index, {
  152. name,
  153. type: writer => writer.write(type.getText()),
  154. hasQuestionToken: nullable,
  155. });
  156. updateInputInterface?.insertProperty(index + 1, {
  157. name,
  158. type: writer => writer.write(type.getText()),
  159. hasQuestionToken: true,
  160. });
  161. index++;
  162. }
  163. }
  164. if (!entityRef.hasCustomFields()) {
  165. createInputInterface?.getProperty('customFields')?.remove();
  166. updateInputInterface?.getProperty('customFields')?.remove();
  167. }
  168. if (entityRef.isTranslatable()) {
  169. createInputInterface
  170. ?.getProperty('translations')
  171. ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
  172. updateInputInterface
  173. ?.getProperty('translations')
  174. ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
  175. } else {
  176. createInputInterface?.getProperty('translations')?.remove();
  177. updateInputInterface?.getProperty('translations')?.remove();
  178. }
  179. }