ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
251 lines (217 loc) • 7.73 kB
JavaScript
/**
* 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;