gather-user-responses.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import { select, text } from '@clack/prompts';
  2. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
  3. import { randomBytes } from 'crypto';
  4. import fs from 'fs-extra';
  5. import Handlebars from 'handlebars';
  6. import path from 'path';
  7. import { checkCancel, isDockerAvailable } from './helpers';
  8. import { DbType, FileSources, PackageManager, UserResponses } from './types';
  9. interface PromptAnswers {
  10. dbType: DbType;
  11. dbHost: string | symbol;
  12. dbPort: string | symbol;
  13. dbName: string | symbol;
  14. dbSchema?: string | symbol;
  15. dbUserName: string | symbol;
  16. dbPassword: string | symbol;
  17. dbSSL?: boolean | symbol;
  18. superadminIdentifier: string | symbol;
  19. superadminPassword: string | symbol;
  20. populateProducts: boolean | symbol;
  21. }
  22. /* eslint-disable no-console */
  23. export async function getQuickStartConfiguration(
  24. root: string,
  25. packageManager: PackageManager,
  26. ): Promise<UserResponses> {
  27. // First we want to detect whether Docker is running
  28. const { result: dockerStatus } = await isDockerAvailable();
  29. let usePostgres: boolean;
  30. switch (dockerStatus) {
  31. case 'running':
  32. usePostgres = true;
  33. break;
  34. case 'not-found':
  35. usePostgres = false;
  36. break;
  37. case 'not-running': {
  38. let useSqlite = false;
  39. let dockerIsNowRunning = false;
  40. do {
  41. const useSqliteResponse = await select({
  42. message: 'We could not automatically start Docker. How should we proceed?',
  43. options: [
  44. { label: `Let's use SQLite as the database`, value: true },
  45. { label: 'I have manually started Docker', value: false },
  46. ],
  47. initialValue: true,
  48. });
  49. checkCancel(useSqlite);
  50. useSqlite = useSqliteResponse as boolean;
  51. if (useSqlite === false) {
  52. const { result: dockerStatusManual } = await isDockerAvailable();
  53. dockerIsNowRunning = dockerStatusManual === 'running';
  54. }
  55. } while (dockerIsNowRunning !== true && useSqlite === false);
  56. usePostgres = !useSqlite;
  57. break;
  58. }
  59. }
  60. const quickStartAnswers: PromptAnswers = {
  61. dbType: usePostgres ? 'postgres' : 'sqlite',
  62. dbHost: usePostgres ? 'localhost' : '',
  63. dbPort: usePostgres ? '6543' : '',
  64. dbName: usePostgres ? 'vendure' : '',
  65. dbUserName: usePostgres ? 'vendure' : '',
  66. dbPassword: usePostgres ? randomBytes(16).toString('base64url') : '',
  67. dbSchema: usePostgres ? 'public' : '',
  68. populateProducts: true,
  69. superadminIdentifier: SUPER_ADMIN_USER_IDENTIFIER,
  70. superadminPassword: SUPER_ADMIN_USER_PASSWORD,
  71. };
  72. const responses = {
  73. ...(await generateSources(root, quickStartAnswers, packageManager)),
  74. dbType: quickStartAnswers.dbType,
  75. populateProducts: quickStartAnswers.populateProducts as boolean,
  76. superadminIdentifier: quickStartAnswers.superadminIdentifier as string,
  77. superadminPassword: quickStartAnswers.superadminPassword as string,
  78. };
  79. return responses;
  80. }
  81. /**
  82. * Prompts the user to determine how the new Vendure app should be configured.
  83. */
  84. export async function getManualConfiguration(
  85. root: string,
  86. packageManager: PackageManager,
  87. ): Promise<UserResponses> {
  88. const dbType = (await select({
  89. message: 'Which database are you using?',
  90. options: [
  91. { label: 'MySQL', value: 'mysql' },
  92. { label: 'MariaDB', value: 'mariadb' },
  93. { label: 'Postgres', value: 'postgres' },
  94. { label: 'SQLite', value: 'sqlite' },
  95. ],
  96. initialValue: 'sqlite' as DbType,
  97. })) as DbType;
  98. checkCancel(dbType);
  99. const hasConnection = dbType !== 'sqlite';
  100. const dbHost = hasConnection
  101. ? await text({
  102. message: "What's the database host address?",
  103. initialValue: 'localhost',
  104. })
  105. : '';
  106. checkCancel(dbHost);
  107. const dbPort = hasConnection
  108. ? await text({
  109. message: 'What port is the database listening on?',
  110. initialValue: defaultDBPort(dbType).toString(),
  111. })
  112. : '';
  113. checkCancel(dbPort);
  114. const dbName = hasConnection
  115. ? await text({
  116. message: "What's the name of the database?",
  117. initialValue: 'vendure',
  118. })
  119. : '';
  120. checkCancel(dbName);
  121. const dbSchema =
  122. dbType === 'postgres'
  123. ? await text({
  124. message: "What's the schema name we should use?",
  125. initialValue: 'public',
  126. })
  127. : '';
  128. checkCancel(dbSchema);
  129. const dbSSL =
  130. dbType === 'postgres'
  131. ? await select({
  132. message:
  133. 'Use SSL to connect to the database? (only enable if your database provider supports SSL)',
  134. options: [
  135. { label: 'no', value: false },
  136. { label: 'yes', value: true },
  137. ],
  138. initialValue: false,
  139. })
  140. : false;
  141. checkCancel(dbSSL);
  142. const dbUserName = hasConnection
  143. ? await text({
  144. message: "What's the database user name?",
  145. })
  146. : '';
  147. checkCancel(dbUserName);
  148. const dbPassword = hasConnection
  149. ? await text({
  150. message: "What's the database password?",
  151. defaultValue: '',
  152. })
  153. : '';
  154. checkCancel(dbPassword);
  155. const superadminIdentifier = await text({
  156. message: 'What identifier do you want to use for the superadmin user?',
  157. initialValue: SUPER_ADMIN_USER_IDENTIFIER,
  158. });
  159. checkCancel(superadminIdentifier);
  160. const superadminPassword = await text({
  161. message: 'What password do you want to use for the superadmin user?',
  162. initialValue: SUPER_ADMIN_USER_PASSWORD,
  163. });
  164. checkCancel(superadminPassword);
  165. const populateProducts = await select({
  166. message: 'Populate with some sample product data?',
  167. options: [
  168. { label: 'yes', value: true },
  169. { label: 'no', value: false },
  170. ],
  171. initialValue: true,
  172. });
  173. checkCancel(populateProducts);
  174. const answers: PromptAnswers = {
  175. dbType,
  176. dbHost,
  177. dbPort,
  178. dbName,
  179. dbSchema,
  180. dbUserName,
  181. dbPassword,
  182. dbSSL,
  183. superadminIdentifier,
  184. superadminPassword,
  185. populateProducts,
  186. };
  187. return {
  188. ...(await generateSources(root, answers, packageManager)),
  189. dbType,
  190. populateProducts: answers.populateProducts as boolean,
  191. superadminIdentifier: answers.superadminIdentifier as string,
  192. superadminPassword: answers.superadminPassword as string,
  193. };
  194. }
  195. /**
  196. * Returns mock "user response" without prompting, for use in CI
  197. */
  198. export async function getCiConfiguration(
  199. root: string,
  200. packageManager: PackageManager,
  201. ): Promise<UserResponses> {
  202. const ciAnswers = {
  203. dbType: 'sqlite' as const,
  204. dbHost: '',
  205. dbPort: '',
  206. dbName: 'vendure',
  207. dbUserName: '',
  208. dbPassword: '',
  209. populateProducts: true,
  210. superadminIdentifier: SUPER_ADMIN_USER_IDENTIFIER,
  211. superadminPassword: SUPER_ADMIN_USER_PASSWORD,
  212. };
  213. return {
  214. ...(await generateSources(root, ciAnswers, packageManager)),
  215. dbType: ciAnswers.dbType,
  216. populateProducts: ciAnswers.populateProducts,
  217. superadminIdentifier: ciAnswers.superadminIdentifier,
  218. superadminPassword: ciAnswers.superadminPassword,
  219. };
  220. }
  221. /**
  222. * Create the server index, worker and config source code based on the options specified by the CLI prompts.
  223. */
  224. async function generateSources(
  225. root: string,
  226. answers: PromptAnswers,
  227. packageManager: PackageManager,
  228. ): Promise<FileSources> {
  229. const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName);
  230. /**
  231. * Helper to escape single quotes only. Used when generating the config file since e.g. passwords
  232. * might use special chars (`< > ' "` etc) which Handlebars would be default convert to HTML entities.
  233. * Instead, we disable escaping and use this custom helper to escape only the single quote character.
  234. */
  235. Handlebars.registerHelper('escapeSingle', (aString: unknown) => {
  236. return typeof aString === 'string' ? aString.replace(/'/g, "\\'") : aString;
  237. });
  238. const templateContext = {
  239. ...answers,
  240. dbType: answers.dbType === 'sqlite' ? 'better-sqlite3' : answers.dbType,
  241. name: path.basename(root),
  242. isSQLite: answers.dbType === 'sqlite',
  243. requiresConnection: answers.dbType !== 'sqlite',
  244. cookieSecret: randomBytes(16).toString('base64url'),
  245. };
  246. async function createSourceFile(filename: string, noEscape = false): Promise<string> {
  247. const template = await fs.readFile(assetPath(filename), 'utf-8');
  248. return Handlebars.compile(template, { noEscape })(templateContext);
  249. }
  250. return {
  251. indexSource: await createSourceFile('index.hbs'),
  252. indexWorkerSource: await createSourceFile('index-worker.hbs'),
  253. configSource: await createSourceFile('vendure-config.hbs', true),
  254. envSource: await createSourceFile('.env.hbs', true),
  255. envDtsSource: await createSourceFile('environment.d.hbs', true),
  256. readmeSource: await createSourceFile('readme.hbs'),
  257. dockerfileSource: await createSourceFile('Dockerfile.hbs'),
  258. dockerComposeSource: await createSourceFile('docker-compose.hbs'),
  259. };
  260. }
  261. function defaultDBPort(dbType: DbType): number {
  262. switch (dbType) {
  263. case 'mysql':
  264. case 'mariadb':
  265. return 3306;
  266. case 'postgres':
  267. return 5432;
  268. default:
  269. return 3306;
  270. }
  271. }