ast-utils.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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 = {}, providedTsConfigPath?: string) {
  30. const tsConfigFile = providedTsConfigPath ?? (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. compilerOptions: {
  39. skipLibCheck: true,
  40. },
  41. ...options,
  42. });
  43. project.enableLogging(false);
  44. return { project, tsConfigPath };
  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, filePath: string) {
  117. const template = fs.readFileSync(templatePath, 'utf-8');
  118. try {
  119. const file = project.createSourceFile(filePath, template, {
  120. overwrite: true,
  121. scriptKind: ScriptKind.TS,
  122. });
  123. project.resolveSourceFileDependencies();
  124. return file;
  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. const prefix = parsedPath.dir.startsWith('..') ? '' : './';
  136. return `${prefix}${parsedPath.dir}/${parsedPath.name}`.replace(/\/\//g, '/');
  137. }
  138. export function customizeCreateUpdateInputInterfaces(sourceFile: SourceFile, entityRef: EntityRef) {
  139. const createInputInterface = sourceFile
  140. .getInterface('CreateEntityInput')
  141. ?.rename(`Create${entityRef.name}Input`);
  142. const updateInputInterface = sourceFile
  143. .getInterface('UpdateEntityInput')
  144. ?.rename(`Update${entityRef.name}Input`);
  145. let index = 0;
  146. for (const { name, type, nullable } of entityRef.getProps()) {
  147. if (
  148. type.isBoolean() ||
  149. type.isString() ||
  150. type.isNumber() ||
  151. (type.isObject() && type.getText() === 'Date')
  152. ) {
  153. createInputInterface?.insertProperty(index, {
  154. name,
  155. type: writer => writer.write(type.getText()),
  156. hasQuestionToken: nullable,
  157. });
  158. updateInputInterface?.insertProperty(index + 1, {
  159. name,
  160. type: writer => writer.write(type.getText()),
  161. hasQuestionToken: true,
  162. });
  163. index++;
  164. }
  165. }
  166. if (!entityRef.hasCustomFields()) {
  167. createInputInterface?.getProperty('customFields')?.remove();
  168. updateInputInterface?.getProperty('customFields')?.remove();
  169. }
  170. if (entityRef.isTranslatable()) {
  171. createInputInterface
  172. ?.getProperty('translations')
  173. ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
  174. updateInputInterface
  175. ?.getProperty('translations')
  176. ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
  177. } else {
  178. createInputInterface?.getProperty('translations')?.remove();
  179. updateInputInterface?.getProperty('translations')?.remove();
  180. }
  181. }