1
0

cli-test-utils.ts 6.3 KB

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