"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 = [/(?<!\.module)\.css$/, /(?<!\.module)\.(scss|sass)$/];
const globalCssModulesRe = [/\.module\.css$/, /\.module\.(scss|sass)$/];
exports.allCssTests = [...globalCssRe, ...globalCssModulesRe];
// Next does not allow global styles to be loaded outside of the main <App /> 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);
    }
}
