"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.allCssTests = exports.nextHandler = void 0; const tslib_1 = require("tslib"); const debug_1 = tslib_1.__importDefault(require("debug")); const module_1 = tslib_1.__importDefault(require("module")); const fs = tslib_1.__importStar(require("fs")); const path = tslib_1.__importStar(require("path")); const sourceRelativeWebpackModules_1 = require("./sourceRelativeWebpackModules"); const debug = (0, debug_1.default)('cypress:webpack-dev-server-fresh:nextHandler'); async function nextHandler(devServerConfig) { const webpackConfig = await loadWebpackConfig(devServerConfig); debug('resolved next.js webpack config %o', webpackConfig); checkSWC(webpackConfig, devServerConfig.cypressConfig); watchEntryPoint(webpackConfig); allowGlobalStylesImports(webpackConfig); changeNextCachePath(webpackConfig); return { frameworkConfig: webpackConfig, sourceWebpackModulesResult: sourceNextWebpackDeps(devServerConfig) }; } exports.nextHandler = nextHandler; /** * Acquire the modules needed to load the Next webpack config. We are using Next's APIs to grab the webpackConfig * but since this is in the binary, we have to `require.resolve` them from the projectRoot * `loadConfig` acquires the next.config.js * `getNextJsBaseWebpackConfig` acquires the webpackConfig dependent on the next.config.js */ function getNextJsPackages(devServerConfig) { var _a, _b, _c, _d, _e; const resolvePaths = { paths: [devServerConfig.cypressConfig.projectRoot] }; const packages = {}; try { const loadConfigPath = require.resolve('next/dist/server/config', resolvePaths); packages.loadConfig = require(loadConfigPath).default; } catch (e) { throw new Error(`Failed to load "next/dist/server/config" with error: ${(_a = e.message) !== null && _a !== void 0 ? _a : e}`); } try { const getNextJsBaseWebpackConfigPath = require.resolve('next/dist/build/webpack-config', resolvePaths); packages.getNextJsBaseWebpackConfig = require(getNextJsBaseWebpackConfigPath).default; } catch (e) { throw new Error(`Failed to load "next/dist/build/webpack-config" with error: ${(_b = e.message) !== null && _b !== void 0 ? _b : e}`); } try { const loadJsConfigPath = require.resolve('next/dist/build/load-jsconfig', resolvePaths); packages.nextLoadJsConfig = require(loadJsConfigPath).default; } catch (e) { throw new Error(`Failed to load "next/dist/build/load-jsconfig" with error: ${(_c = e.message) !== null && _c !== void 0 ? _c : e}`); } // Does not exist prior to Next 13. try { const getUtilsPath = require.resolve('next/dist/build/utils', resolvePaths); packages.getSupportedBrowsers = (_d = require(getUtilsPath).getSupportedBrowsers) !== null && _d !== void 0 ? _d : (() => Promise.resolve([])); } catch (e) { throw new Error(`Failed to load "next/dist/build/utils" with error: ${(_e = e.message) !== null && _e !== void 0 ? _e : e}`); } return packages; } /** * Types for `getNextJsBaseWebpackConfig` based on version: * - v11.1.4 [ dir: string, options: { buildId: string config: NextConfigComplete dev?: boolean isServer?: boolean pagesDir: string target?: string reactProductionProfiling?: boolean entrypoints: WebpackEntrypoints rewrites: CustomRoutes['rewrites'] isDevFallback?: boolean runWebpackSpan: Span } ] * - v12.0.0 = Same as v11.1.4 * - v12.1.6 [ dir: string, options: { buildId: string config: NextConfigComplete compilerType: 'client' | 'server' | 'edge-server' dev?: boolean entrypoints: webpack5.EntryObject hasReactRoot: boolean isDevFallback?: boolean pagesDir: string reactProductionProfiling?: boolean rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span target?: string } ] * - v13.0.0 [ dir: string, options: { buildId: string config: NextConfigComplete compilerType: CompilerNameValues dev?: boolean entrypoints: webpack.EntryObject hasReactRoot: boolean isDevFallback?: boolean pagesDir?: string reactProductionProfiling?: boolean rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span target?: string appDir?: string middlewareMatchers?: MiddlewareMatcher[] } ] * - v13.0.1 [ dir: string, options: { buildId: string config: NextConfigComplete compilerType: CompilerNameValues dev?: boolean entrypoints: webpack.EntryObject isDevFallback?: boolean pagesDir?: string reactProductionProfiling?: boolean rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span target?: string appDir?: string middlewareMatchers?: MiddlewareMatcher[] } ] * - v13.2.1 [ dir: string, options: { buildId: string config: NextConfigComplete compilerType: CompilerNameValues dev?: boolean entrypoints: webpack.EntryObject isDevFallback?: boolean pagesDir?: string reactProductionProfiling?: boolean rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span target?: string appDir?: string middlewareMatchers?: MiddlewareMatcher[] noMangling?: boolean jsConfig: any resolvedBaseUrl: string | undefined supportedBrowsers: string[] | undefined clientRouterFilters?: { staticFilter: ReturnType< import('../shared/lib/bloom-filter').BloomFilter['export'] > dynamicFilter: ReturnType< import('../shared/lib/bloom-filter').BloomFilter['export'] > } } ] */ async function loadWebpackConfig(devServerConfig) { const { loadConfig, getNextJsBaseWebpackConfig, nextLoadJsConfig, getSupportedBrowsers } = getNextJsPackages(devServerConfig); const nextConfig = await loadConfig('development', devServerConfig.cypressConfig.projectRoot); const runWebpackSpan = getRunWebpackSpan(devServerConfig); const reactVersion = getReactVersion(devServerConfig.cypressConfig.projectRoot); const jsConfigResult = await (nextLoadJsConfig === null || nextLoadJsConfig === void 0 ? void 0 : nextLoadJsConfig(devServerConfig.cypressConfig.projectRoot, nextConfig)); const supportedBrowsers = await getSupportedBrowsers(devServerConfig.cypressConfig.projectRoot, true, nextConfig); const webpackConfig = await getNextJsBaseWebpackConfig(devServerConfig.cypressConfig.projectRoot, Object.assign(Object.assign({ buildId: `@cypress/react-${Math.random().toString()}`, config: nextConfig, dev: true, pagesDir: await findPagesDir(devServerConfig.cypressConfig.projectRoot), entrypoints: {}, rewrites: { fallback: [], afterFiles: [], beforeFiles: [] } }, runWebpackSpan), { // Client webpack config for Next.js <= 12.1.5 isServer: false, // Client webpack config for Next.js > 12.1.5 compilerType: 'client', // Required for Next.js > 13 hasReactRoot: reactVersion === 18, // Required for Next.js > 13.2.0 to respect TS/JS config jsConfig: jsConfigResult.jsConfig, // Required for Next.js > 13.2.0 to respect tsconfig.compilerOptions.baseUrl resolvedBaseUrl: jsConfigResult.resolvedBaseUrl, // Added in Next.js 13, passed via `...info`: https://github.com/vercel/next.js/pull/45637/files supportedBrowsers })); return webpackConfig; } /** * Check if Next is using the SWC compiler. Compilation will fail if user has `nodeVersion: "bundled"` set * due to SWC certificate issues. */ function checkSWC(webpackConfig, cypressConfig) { var _a, _b; const hasSWCLoader = (_b = (_a = webpackConfig.module) === null || _a === void 0 ? void 0 : _a.rules) === null || _b === void 0 ? void 0 : _b.some((rule) => { var _a; return typeof rule !== 'string' && ((_a = rule.oneOf) === null || _a === void 0 ? void 0 : _a.some((oneOf) => { var _a; return ((_a = oneOf.use) === null || _a === void 0 ? void 0 : _a.loader) === 'next-swc-loader'; })); }); // "resolvedNodePath" is only set when using the user's Node.js, which is required to compile Next.js with SWC optimizations // If it is not set, they have either explicitly set "nodeVersion" to "bundled" or are are using Cypress < 9.0.0 where it was set to "bundled" by default if (hasSWCLoader && cypressConfig.nodeVersion === 'bundled') { throw new Error(`Cypress cannot compile your Next.js application when "nodeVersion" is set to "bundled". Please remove this option from your Cypress configuration file.`); } return false; } const exists = async (file) => { try { await fs.promises.access(file, fs.constants.F_OK); return true; } catch (_) { return false; } }; /** * Next allows the `pages` directory to be located at either * `${projectRoot}/pages` or `${projectRoot}/src/pages`. * If neither is found, return projectRoot */ async function findPagesDir(projectRoot) { // prioritize ./pages over ./src/pages let pagesDir = path.join(projectRoot, 'pages'); if (await exists(pagesDir)) { return pagesDir; } pagesDir = path.join(projectRoot, 'src', 'pages'); if (await exists(pagesDir)) { return pagesDir; } return projectRoot; } // Starting with v11.1.1, a trace is required. // 'next/dist/telemetry/trace/trace' only exists since v10.0.9 // and our peerDeps support back to v8 so try-catch this import // Starting from 12.0 trace is now located in 'next/dist/trace/trace' function getRunWebpackSpan(devServerConfig) { let trace; try { try { const traceImportPath = require.resolve('next/dist/telemetry/trace/trace', { paths: [devServerConfig.cypressConfig.projectRoot] }); trace = require(traceImportPath).trace; return { runWebpackSpan: trace('cypress') }; } catch (_) { // @ts-ignore const traceImportPath = require.resolve('next/dist/trace/trace', { paths: [devServerConfig.cypressConfig.projectRoot] }); trace = require(traceImportPath).trace; return { runWebpackSpan: trace('cypress') }; } } catch (_) { return {}; } } const originalModuleLoad = module_1.default._load; function sourceNextWebpackDeps(devServerConfig) { const framework = (0, sourceRelativeWebpackModules_1.sourceFramework)(devServerConfig); const webpack = sourceNextWebpack(devServerConfig, framework); const webpackDevServer = (0, sourceRelativeWebpackModules_1.sourceWebpackDevServer)(devServerConfig, framework); const htmlWebpackPlugin = (0, sourceRelativeWebpackModules_1.sourceHtmlWebpackPlugin)(devServerConfig, framework, webpack); return { framework, webpack, webpackDevServer, htmlWebpackPlugin, }; } function sourceNextWebpack(devServerConfig, framework) { const searchRoot = framework.importPath; debug('NextWebpack: Attempting to load NextWebpack from %s', searchRoot); let webpackJsonPath; const webpack = {}; try { webpackJsonPath = require.resolve('next/dist/compiled/webpack/package.json', { paths: [searchRoot], }); } catch (e) { debug('NextWebpack: Failed to load NextWebpack - %s', e); throw e; } // Next 11 allows the choice of webpack@4 or webpack@5, depending on the "webpack5" property in their next.config.js // The webpackModule.init" for Next 11 returns a webpack@4 or webpack@4 compiler instance based on this boolean let webpack5 = true; const importPath = path.join(path.dirname(webpackJsonPath), 'webpack.js'); const webpackModule = require(importPath); try { const nextConfig = require(path.resolve(devServerConfig.cypressConfig.projectRoot, 'next.config.js')); debug('NextWebpack: next.config.js found - %o', nextConfig); if (nextConfig.webpack5 === false) { webpack5 = false; } } catch (e) { // No next.config.js, assume webpack 5 } debug('NextWebpack: webpack5 - %s', webpack5); webpackModule.init(webpack5); const packageJson = require(webpackJsonPath); webpack.importPath = importPath; // The package.json of "next/dist/compiled/webpack/package.json" has no version so we supply the version for later use webpack.packageJson = Object.assign(Object.assign({}, packageJson), { version: webpack5 ? '5' : '4' }); webpack.module = webpackModule.webpack; webpack.majorVersion = (0, sourceRelativeWebpackModules_1.getMajorVersion)(webpack.packageJson, [4, 5]); debug('NextWebpack: Successfully loaded NextWebpack - %o', webpack); module_1.default._load = function (request, parent, isMain) { // Next with webpack@4 doesn't ship certain dependencies that HtmlWebpackPlugin requires, so we patch the resolution through to our bundled version if ((request === 'webpack' || request.startsWith('webpack/')) && webpack.majorVersion === 4) { const resolvePath = require.resolve(request, { paths: [(0, sourceRelativeWebpackModules_1.cypressWebpackPath)(devServerConfig)], }); debug('NextWebpack: Module._load for webpack@4 - %s', resolvePath); return originalModuleLoad(resolvePath, parent, isMain); } if (request === 'webpack' || request.startsWith('webpack/')) { const resolvePath = require.resolve(request, { paths: [framework.importPath], }); debug('NextWebpack: Module._load - %s', resolvePath); return originalModuleLoad(resolvePath, parent, isMain); } return originalModuleLoad(request, parent, isMain); }; return webpack; } // Next webpack compiler ignored watching any node_modules changes, but we need to watch // for changes to 'dist/browser.js' in order to detect new specs that have been added function watchEntryPoint(webpackConfig) { if (webpackConfig.watchOptions && Array.isArray(webpackConfig.watchOptions.ignored)) { webpackConfig.watchOptions = Object.assign(Object.assign({}, webpackConfig.watchOptions), { ignored: [...webpackConfig.watchOptions.ignored.filter((pattern) => !/node_modules/.test(pattern)), '**/node_modules/!(@cypress/webpack-dev-server/dist/browser.js)**'] }); debug('found options next.js watchOptions.ignored %O', webpackConfig.watchOptions.ignored); } } // We are matching the Next.js regex rules exactly. If we were writing our own loader, we could // condense these regex rules into a single rule but we need the regex.source to be identical to what // we get from Next.js webpack config // see: https://github.com/vercel/next.js/blob/20486c159d8538a337da6b07b0b4490a3a0d6b91/packages/next/build/webpack/config/blocks/css/index.ts#L18 const globalCssRe = [/(? component. // We want users to be able to import the global styles into their component support file so we // delete the "issuer" from the rules that process css/scss files. // see: https://github.com/cypress-io/cypress/issues/22525 // Motivated by: https://github.com/bem/next-global-css function allowGlobalStylesImports(webpackConfig) { var _a; const rules = ((_a = webpackConfig.module) === null || _a === void 0 ? void 0 : _a.rules) || []; for (const rule of rules) { if (typeof rule !== 'string' && rule.oneOf) { for (const oneOf of rule.oneOf) { if (oneOf.test && exports.allCssTests.some((re) => { var _a; return re.source === ((_a = oneOf.test) === null || _a === void 0 ? void 0 : _a.source); })) { delete oneOf.issuer; } } } } } // Our modifications of the Next webpack config can corrupt the cache used for local development. // We separate the cache used for CT from the normal cache (".next/cache/webpack" -> ".next/cache/cypress-webpack") so they don't interfere with each other function changeNextCachePath(webpackConfig) { if (typeof webpackConfig.cache === 'object' && ('cacheDirectory' in webpackConfig.cache) && webpackConfig.cache.cacheDirectory) { const { cacheDirectory } = webpackConfig.cache; webpackConfig.cache.cacheDirectory = cacheDirectory.replace(/webpack$/, 'cypress-webpack'); debug('Changing Next cache path from %s to %s', cacheDirectory, webpackConfig.cache.cacheDirectory); } } function getReactVersion(projectRoot) { try { const reactPackageJsonPath = require.resolve('react/package.json', { paths: [projectRoot] }); const { version } = require(reactPackageJsonPath); return Number(version.split('.')[0]); } catch (e) { debug('Failed to source react with error: ', e); } }