UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

251 lines (217 loc) 7.73 kB
/** * ctrl.shift.left Next.js Adapter * * This adapter provides specialized functionality for working with Next.js projects. * It handles the unique aspects of Next.js folder structure, import paths, and testing requirements. */ const fs = require('fs'); const path = require('path'); const NextJSAdapter = { /** * Detect if the current project is a Next.js application * @returns {Object} Detection info with structure details */ detectFramework: () => { const hasApp = fs.existsSync('./app'); const hasPages = fs.existsSync('./pages'); const hasNextConfig = fs.existsSync('./next.config.js') || fs.existsSync('./next.config.mjs'); return { isNextJs: hasApp || hasPages || hasNextConfig, hasAppRouter: hasApp, hasPagesRouter: hasPages, hasNextConfig }; }, /** * Get typical component paths for a Next.js application * @returns {Array<string>} Array of potential component paths */ getComponentPaths: () => { const nextInfo = NextJSAdapter.detectFramework(); if (nextInfo.hasAppRouter) { return [ './app/components', './app/ui', './components', './src/components' ]; } if (nextInfo.hasPagesRouter) { return [ './components', './src/components', './pages/components' ]; } // Default paths if structure cannot be determined return ['./components', './src/components']; }, /** * Get typical API endpoint paths for a Next.js application * @returns {Array<string>} Array of potential API paths */ getApiPaths: () => { const nextInfo = NextJSAdapter.detectFramework(); if (nextInfo.hasAppRouter) { return ['./app/api']; } if (nextInfo.hasPagesRouter) { return ['./pages/api']; } // Default paths if structure cannot be determined return ['./app/api', './pages/api']; }, /** * Adjust test content to work with Next.js structure * @param {string} testContent Original test content * @param {string} componentPath Path to the component being tested * @returns {string} Adjusted test content */ adjustTestOutput: (testContent, componentPath) => { // Fix import paths for Next.js components let adjusted = testContent.replace( /import\s+(\w+)\s+from\s+['"](.+)['"];/g, (match, componentName, importPath) => { if (importPath.startsWith('../') || importPath.startsWith('./')) { // Convert relative paths to Next.js path aliases const nextInfo = NextJSAdapter.detectFramework(); if (nextInfo.hasAppRouter) { // For app router we replace '../components' with '@/components' return `import ${componentName} from '${importPath.replace(/^\.\.\//, '@/')}';`; } } return match; } ); // Add necessary mocks for Next.js features if (testContent.includes('useRouter') || componentPath.includes('/pages/')) { adjusted = adjusted.replace( /(import.+?from ['"]@testing-library\/react['"];)/, '$1\nimport { useRouter } from "next/router";\n\n// Mock Next.js router\njest.mock("next/router", () => ({\n useRouter: jest.fn()\n}));\n' ); adjusted = adjusted.replace( /(beforeEach\(\(\) => \{)/, '$1\n // Setup router mock\n useRouter.mockImplementation(() => ({\n push: jest.fn(),\n pathname: "/",\n query: {}\n }));\n' ); } // Add special handling for server components if necessary if (componentPath.includes('/app/') && !testContent.includes("'use client'")) { // Check if the component has 'use client' directive try { const componentContent = fs.readFileSync(componentPath, 'utf8'); if (!componentContent.includes("'use client'") && !componentContent.includes('"use client"')) { // This is a server component, add special handling adjusted = adjusted.replace( /(describe\(['"].+?['"]\s*,\s*\(\) => \{)/, '$1\n // Note: Testing a React Server Component\n // Some interactions may not be possible in a server component\n' ); } } catch (error) { // If can't read file, assume it's a client component } } return adjusted; }, /** * Generate appropriate Jest configuration for Next.js * @returns {Object} Jest configuration object */ generateJestConfig: () => { return { testEnvironment: 'jsdom', testPathIgnorePatterns: ['/node_modules/', '/.next/'], collectCoverage: true, collectCoverageFrom: [ 'app/**/*.{js,jsx,ts,tsx}', 'pages/**/*.{js,jsx,ts,tsx}', 'components/**/*.{js,jsx,ts,tsx}', '!**/*.d.ts', '!**/node_modules/**', ], moduleNameMapper: { // Handle CSS imports (with CSS modules) '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', // Handle CSS imports (without CSS modules) '^.+\\.(css|sass|scss)$': '<rootDir>/__mocks__/styleMock.js', // Handle image imports '^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$': '<rootDir>/__mocks__/fileMock.js', // Handle module path aliases '^@/components/(.*)$': '<rootDir>/components/$1', '^@/app/(.*)$': '<rootDir>/app/$1', '^@/pages/(.*)$': '<rootDir>/pages/$1', }, setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testMatch: [ '**/__tests__/**/*.{js,jsx,ts,tsx}', '**/*.{spec,test}.{js,jsx,ts,tsx}', ], transform: { '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], }, transformIgnorePatterns: [ '/node_modules/', '^.+\\.module\\.(css|sass|scss)$', ], }; }, /** * Generate Jest setup file content for Next.js * @returns {string} Content for jest.setup.js file */ generateJestSetupContent: () => { return ` // Learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; // Mock next/image jest.mock('next/image', () => ({ __esModule: true, default: (props) => { // eslint-disable-next-line @next/next/no-img-element return <img {...props} src={props.src} alt={props.alt} />; }, })); // Mock next/link jest.mock('next/link', () => { return ({ children, href }) => { return <a href={href}>{children}</a>; }; }); `; }, /** * Set up necessary files for Next.js testing configuration * @param {string} projectRoot Root directory of the project */ setupTestingFiles: (projectRoot) => { // Create mock files for Next.js testing const mockDirs = path.join(projectRoot, '__mocks__'); if (!fs.existsSync(mockDirs)) { fs.mkdirSync(mockDirs, { recursive: true }); } // Create style mock fs.writeFileSync( path.join(mockDirs, 'styleMock.js'), 'module.exports = {};' ); // Create file mock fs.writeFileSync( path.join(mockDirs, 'fileMock.js'), 'module.exports = "test-file-stub";' ); // Create Jest setup file fs.writeFileSync( path.join(projectRoot, 'jest.setup.js'), NextJSAdapter.generateJestSetupContent() ); // Create Jest config if it doesn't exist const jestConfigPath = path.join(projectRoot, 'jest.config.js'); if (!fs.existsSync(jestConfigPath)) { const jestConfig = NextJSAdapter.generateJestConfig(); fs.writeFileSync( jestConfigPath, `module.exports = ${JSON.stringify(jestConfig, null, 2)};` ); } } }; module.exports = NextJSAdapter;