cli-test-utils.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { spawn } from 'child_process';
  2. import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
  3. import { tmpdir } from 'os';
  4. import { join } from 'path';
  5. export interface CliTestProject {
  6. projectDir: string;
  7. cleanup: () => void;
  8. writeFile: (relativePath: string, content: string) => void;
  9. readFile: (relativePath: string) => string;
  10. fileExists: (relativePath: string) => boolean;
  11. runCliCommand: (
  12. args: string[],
  13. options?: { expectError?: boolean },
  14. ) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
  15. }
  16. /**
  17. * Creates a temporary test project for CLI testing
  18. */
  19. export function createTestProject(projectName: string = 'test-project'): CliTestProject {
  20. const projectDir = join(
  21. tmpdir(),
  22. 'vendure-cli-e2e',
  23. `${projectName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
  24. );
  25. // Create project directory
  26. mkdirSync(projectDir, { recursive: true });
  27. // Create basic package.json
  28. const packageJson = {
  29. name: projectName,
  30. version: '1.0.0',
  31. private: true,
  32. dependencies: {
  33. '@vendure/core': '3.3.7',
  34. '@vendure/common': '3.3.7',
  35. },
  36. devDependencies: {
  37. typescript: '5.8.2',
  38. },
  39. };
  40. writeFileSync(join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
  41. // Create basic vendure-config.ts
  42. const vendureConfig = `
  43. import { VendureConfig } from '@vendure/core';
  44. export const config: VendureConfig = {
  45. apiOptions: {
  46. port: 3000,
  47. adminApiPath: 'admin-api',
  48. shopApiPath: 'shop-api',
  49. },
  50. authOptions: {
  51. tokenMethod: ['bearer', 'cookie'],
  52. superadminCredentials: {
  53. identifier: 'superadmin',
  54. password: 'superadmin',
  55. },
  56. cookieOptions: {
  57. secret: 'cookie-secret',
  58. },
  59. },
  60. dbConnectionOptions: {
  61. type: 'sqlite',
  62. database: ':memory:',
  63. synchronize: true,
  64. },
  65. };
  66. `;
  67. writeFileSync(join(projectDir, 'vendure-config.ts'), vendureConfig);
  68. return {
  69. projectDir,
  70. cleanup: () => {
  71. try {
  72. rmSync(projectDir, { recursive: true, force: true });
  73. } catch (error) {
  74. // Ignore cleanup errors - use stderr to avoid no-console lint error
  75. const errorMessage = error instanceof Error ? error.message : String(error);
  76. process.stderr.write(`Failed to cleanup test project at ${projectDir}: ${errorMessage}\n`);
  77. }
  78. },
  79. writeFile: (relativePath: string, content: string) => {
  80. const fullPath = join(projectDir, relativePath);
  81. const dir = join(fullPath, '..');
  82. mkdirSync(dir, { recursive: true });
  83. writeFileSync(fullPath, content);
  84. },
  85. readFile: (relativePath: string) => {
  86. return readFileSync(join(projectDir, relativePath), 'utf-8');
  87. },
  88. fileExists: (relativePath: string) => {
  89. return existsSync(join(projectDir, relativePath));
  90. },
  91. runCliCommand: async (args: string[], options: { expectError?: boolean } = {}) => {
  92. return new Promise((resolve, reject) => {
  93. // Use the built CLI from the dist directory
  94. const cliPath = join(__dirname, '..', 'dist', 'cli.js');
  95. const child = spawn('node', [cliPath, ...args], {
  96. cwd: projectDir,
  97. env: {
  98. ...process.env,
  99. // Ensure we don't inherit any CLI environment variables
  100. VENDURE_RUNNING_IN_CLI: undefined,
  101. },
  102. stdio: ['pipe', 'pipe', 'pipe'],
  103. });
  104. let stdout = '';
  105. let stderr = '';
  106. child.stdout?.on('data', data => {
  107. stdout += data.toString();
  108. });
  109. child.stderr?.on('data', data => {
  110. stderr += data.toString();
  111. });
  112. child.on('close', code => {
  113. const exitCode = code ?? 0;
  114. if (!options.expectError && exitCode !== 0) {
  115. reject(new Error(`CLI command failed with exit code ${exitCode}. stderr: ${stderr}`));
  116. } else {
  117. resolve({ stdout, stderr, exitCode });
  118. }
  119. });
  120. child.on('error', error => {
  121. reject(error);
  122. });
  123. // Send empty stdin to handle any prompts (for non-interactive testing)
  124. child.stdin?.end();
  125. });
  126. },
  127. };
  128. }
  129. /**
  130. * Creates a tsconfig.json with the problematic patterns that caused the original bug
  131. */
  132. export function createProblematicTsConfig(): string {
  133. return `{
  134. // TypeScript configuration file with comments
  135. "compilerOptions": {
  136. "target": "ES2021",
  137. "module": "commonjs",
  138. "lib": ["ES2021"],
  139. "outDir": "./dist",
  140. "rootDir": "./src",
  141. "baseUrl": "./",
  142. /* Path mappings that contain /* patterns in strings - this was breaking the old regex implementation */
  143. "paths": {
  144. "@/vdb/*": ["./node_modules/@vendure/dashboard/src/lib/*"],
  145. "@/components/*": ["src/components/*"],
  146. "@/api/*": ["./api/*/index.ts"],
  147. "@/plugins/*": ["./src/plugins/*/src/index.ts"],
  148. "special": "path/with/*/wildcards"
  149. },
  150. "strict": true,
  151. "esModuleInterop": true,
  152. "skipLibCheck": true,
  153. "forceConsistentCasingInFileNames": true,
  154. "experimentalDecorators": true,
  155. "emitDecoratorMetadata": true
  156. },
  157. "include": ["src/**/*", "vendure-config.ts"],
  158. "exclude": ["node_modules", "dist"]
  159. }`;
  160. }
  161. /**
  162. * Waits for a condition to be true with timeout
  163. */
  164. export async function waitFor(
  165. condition: () => boolean | Promise<boolean>,
  166. timeoutMs: number = 5000,
  167. intervalMs: number = 100,
  168. ): Promise<void> {
  169. const startTime = Date.now();
  170. while (Date.now() - startTime < timeoutMs) {
  171. if (await condition()) {
  172. return;
  173. }
  174. await new Promise(resolve => setTimeout(resolve, intervalMs));
  175. }
  176. throw new Error(`Condition not met within ${timeoutMs}ms`);
  177. }