ast-utils.ts 6.9 KB

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