/** * WARNING: Do not add development dependencies to top-level imports. * Instead, `require` them inline during the build phase. */ import type { NextConfig } from 'next'; import type { NextConfigFn } from '../src/utils/config'; import type { NextBuildBuilderOptions } from '../src/utils/types'; import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils'; import type { ExecutorContext, ProjectGraph, ProjectGraphProjectNode, Target, } from '@nx/devkit'; const baseNXEnvironmentVariables = [ 'NX_BASE', 'NX_CACHE_DIRECTORY', 'NX_CACHE_PROJECT_GRAPH', 'NX_DAEMON', 'NX_DEFAULT_PROJECT', 'NX_HEAD', 'NX_PERF_LOGGING', 'NX_PROFILE', 'NX_PROJECT_GRAPH_CACHE_DIRECTORY', 'NX_INVOKED_BY_RUNNER', // This is from nx cloud runner 'NX_PROJECT_GRAPH_MAX_WORKERS', 'NX_RUNNER', 'NX_SKIP_NX_CACHE', 'NX_TASKS_RUNNER', 'NX_TASKS_RUNNER_DYNAMIC_OUTPUT', 'NX_VERBOSE_LOGGING', 'NX_DRY_RUN', 'NX_INTERACTIVE', 'NX_GENERATE_QUIET', 'NX_PREFER_TS_NODE', 'NX_TASK_TARGET_PROJECT', 'NX_TASK_TARGET_TARGET', 'NX_TASK_TARGET_CONFIGURATION', 'NX_CLI_SET', 'NX_LOAD_DOT_ENV_FILES', 'NX_WORKSPACE_ROOT', 'NX_TASK_HASH', 'NX_NEXT_DIR', 'NX_NEXT_OUTPUT_PATH', 'NX_E2E_RUN_E2E', 'NX_E2E_CI_CACHE_KEY', ]; export interface WithNxOptions extends NextConfig { nx?: { svgr?: boolean; babelUpwardRootMode?: boolean; }; } export interface WithNxContext { workspaceRoot: string; libsDir: string; } function regexEqual(x, y) { return ( x instanceof RegExp && y instanceof RegExp && x.source === y.source && x.global === y.global && x.ignoreCase === y.ignoreCase && x.multiline === y.multiline ); } /** * Do not remove or rename this function. Production builds inline `with-nx.js` file with a replacement * To this function that hard-codes the libsDir. */ function getWithNxContext(): WithNxContext { const { workspaceRoot, workspaceLayout } = require('@nx/devkit'); return { workspaceRoot, libsDir: workspaceLayout().libsDir, }; } function getNxContext( graph: ProjectGraph, target: Target ): { node: ProjectGraphProjectNode; options: NextBuildBuilderOptions; projectName: string; targetName: string; configurationName?: string; } { const { parseTargetString, workspaceRoot } = require('@nx/devkit'); const projectNode = graph.nodes[target.project]; const targetConfig = projectNode.data.targets[target.target]; const targetOptions = targetConfig.options; if (target.configuration) { Object.assign( targetOptions, targetConfig.configurations[target.configuration] ); } const partialExecutorContext: Partial = { projectName: target.project, targetName: target.target, projectGraph: graph, configurationName: target.configuration, root: workspaceRoot, }; if (targetOptions.devServerTarget) { // Executors such as @nx/cypress:cypress define the devServerTarget option. return getNxContext( graph, parseTargetString(targetOptions.devServerTarget, partialExecutorContext) ); } else if (targetOptions.buildTarget) { // Executors such as @nx/next:server or @nx/next:export define the buildTarget option. return getNxContext( graph, parseTargetString(targetOptions.buildTarget, partialExecutorContext) ); } // Default case, return info for current target. // This could be a build using @nx/next:build or run-commands without using our executors. return { node: graph.nodes[target.project], options: targetOptions, projectName: target.project, targetName: target.target, configurationName: target.configuration, }; } /** * Try to read output dir from project, and default to '.next' if executing outside of Nx (e.g. dist is added to a docker image). */ function withNx( _nextConfig = {} as WithNxOptions, context: WithNxContext = getWithNxContext() ): NextConfigFn { return async (phase: string) => { const { PHASE_PRODUCTION_SERVER } = await import('next/constants'); if (phase === PHASE_PRODUCTION_SERVER) { // If we are running an already built production server, just return the configuration. // NOTE: Avoid any `require(...)` or `import(...)` statements here. Development dependencies are not available at production runtime. const { nx, ...validNextConfig } = _nextConfig; return { distDir: '.next', ...validNextConfig, }; } else { const { createProjectGraphAsync, joinPathFragments, offsetFromRoot, workspaceRoot, } = require('@nx/devkit'); // Otherwise, add in webpack and eslint configuration for build or test. let dependencies: DependentBuildableProjectNode[] = []; const graph = await createProjectGraphAsync(); const originalTarget = { project: process.env.NX_TASK_TARGET_PROJECT, target: process.env.NX_TASK_TARGET_TARGET, configuration: process.env.NX_TASK_TARGET_CONFIGURATION, }; const { node: projectNode, options, projectName: project, targetName, configurationName, } = getNxContext(graph, originalTarget); const projectDirectory = projectNode.data.root; if (options.buildLibsFromSource === false && targetName) { const { calculateProjectDependencies, } = require('@nx/js/src/utils/buildable-libs-utils'); const result = calculateProjectDependencies( graph, workspaceRoot, project, targetName, configurationName ); dependencies = result.dependencies; } // Get next config const nextConfig = getNextConfig(_nextConfig, context); // For Next.js 13.1 and greater, make sure workspace libs are transpiled. forNextVersion('>=13.1.0', () => { if (!graph.dependencies[project]) return; const { readTsConfigPaths } = require('@nx/js'); const { findAllProjectNodeDependencies, } = require('nx/src/utils/project-graph-utils'); const paths = readTsConfigPaths(); const deps = findAllProjectNodeDependencies(project); nextConfig.transpilePackages ??= []; for (const dep of deps) { const alias = getAliasForProject(graph.nodes[dep], paths); if (alias) { nextConfig.transpilePackages.push(alias); } } }); // process.env.NX_NEXT_OUTPUT_PATH is set when running @nx/next:build options.outputPath = process.env.NX_NEXT_OUTPUT_PATH || options.outputPath; // outputPath may be undefined if using run-commands or other executors other than @nx/next:build. // In this case, the user should set distDir in their next.config.js. if (options.outputPath) { const outputDir = `${offsetFromRoot(projectDirectory)}${ options.outputPath }`; nextConfig.distDir = nextConfig.distDir && nextConfig.distDir !== '.next' ? joinPathFragments(outputDir, nextConfig.distDir) : joinPathFragments(outputDir, '.next'); } const userWebpackConfig = nextConfig.webpack; const { createWebpackConfig } = require('../src/utils/config'); nextConfig.webpack = (a, b) => createWebpackConfig( workspaceRoot, projectDirectory, options.fileReplacements, options.assets )(userWebpackConfig ? userWebpackConfig(a, b) : a, b); return nextConfig; } }; } export function getNextConfig( nextConfig = {} as WithNxOptions, context: WithNxContext = getWithNxContext() ): NextConfig { // If `next-compose-plugins` is used, the context argument is invalid. if (!context.libsDir || !context.workspaceRoot) { context = getWithNxContext(); } const userWebpack = nextConfig.webpack || ((x) => x); const { nx, ...validNextConfig } = nextConfig; return { eslint: { ignoreDuringBuilds: true, ...(validNextConfig.eslint ?? {}), }, ...validNextConfig, webpack: (config, options) => { /* * Update babel to support our monorepo setup. * The 'upward' mode allows the root babel.config.json and per-project .babelrc files to be picked up. */ if (nx?.babelUpwardRootMode) { options.defaultLoaders.babel.options.babelrc = true; options.defaultLoaders.babel.options.rootMode = 'upward'; } /* * Modify the Next.js webpack config to allow workspace libs to use css modules. * Note: This would be easier if Next.js exposes css-loader and sass-loader on `defaultLoaders`. */ // Include workspace libs in css/sass loaders const includes = [ require('path').join(context.workspaceRoot, context.libsDir), ]; const nextCssLoaders = config.module.rules.find( (rule) => typeof rule.oneOf === 'object' ); // webpack config is not as expected if (!nextCssLoaders) return config; /* * 1. Modify css loader to enable module support for workspace libs */ const nextCssLoader = nextCssLoaders.oneOf.find( (rule) => rule.sideEffects === false && regexEqual(rule.test, /\.module\.css$/) ); // Might not be found if Next.js webpack config changes in the future if (nextCssLoader && nextCssLoader.issuer) { nextCssLoader.issuer.or = nextCssLoader.issuer.and ? nextCssLoader.issuer.and.concat(includes) : includes; delete nextCssLoader.issuer.and; } /* * 2. Modify sass loader to enable module support for workspace libs */ const nextSassLoader = nextCssLoaders.oneOf.find( (rule) => rule.sideEffects === false && regexEqual(rule.test, /\.module\.(scss|sass)$/) ); // Might not be found if Next.js webpack config changes in the future if (nextSassLoader && nextSassLoader.issuer) { nextSassLoader.issuer.or = nextSassLoader.issuer.and ? nextSassLoader.issuer.and.concat(includes) : includes; delete nextSassLoader.issuer.and; } /* * 3. Modify error loader to ignore css modules used by workspace libs */ const nextErrorCssModuleLoader = nextCssLoaders.oneOf.find( (rule) => rule.use && rule.use.loader === 'error-loader' && rule.use.options && (rule.use.options.reason === 'CSS Modules \u001b[1mcannot\u001b[22m be imported from within \u001b[1mnode_modules\u001b[22m.\n' + 'Read more: https://err.sh/next.js/css-modules-npm' || rule.use.options.reason === 'CSS Modules cannot be imported from within node_modules.\nRead more: https://err.sh/next.js/css-modules-npm') ); // Might not be found if Next.js webpack config changes in the future if (nextErrorCssModuleLoader) { nextErrorCssModuleLoader.exclude = includes; } /** * 4. Modify css loader to allow global css from node_modules to be imported from workspace libs */ const nextGlobalCssLoader = nextCssLoaders.oneOf.find((rule) => rule.include?.and?.find((include) => regexEqual(include, /node_modules/) ) ); // Might not be found if Next.js webpack config changes in the future if (nextGlobalCssLoader && nextGlobalCssLoader.issuer) { nextGlobalCssLoader.issuer.or = nextGlobalCssLoader.issuer.and ? nextGlobalCssLoader.issuer.and.concat(includes) : includes; delete nextGlobalCssLoader.issuer.and; } /** * 5. Add env variables prefixed with NX_ */ addNxEnvVariables(config); /** * 6. Add SVGR support if option is on. */ // Default SVGR support to be on for projects. if (nx?.svgr !== false) { config.module.rules.push({ test: /\.svg$/, oneOf: [ // If coming from JS/TS file, then transform into React component using SVGR. { issuer: /\.[jt]sx?$/, use: [ { loader: require.resolve('@svgr/webpack'), options: { svgo: false, titleProp: true, ref: true, }, }, { loader: require.resolve('url-loader'), options: { limit: 10000, // 10kB name: '[name].[hash:7].[ext]', }, }, ], }, // Fallback to plain URL loader if someone just imports the SVG and references it on the tag { loader: require.resolve('url-loader'), options: { limit: 10000, // 10kB name: '[name].[hash:7].[ext]', }, }, ], }); } return userWebpack(config, options); }, }; } function getNonBaseVariables(oldEnv) { return Object.keys(oldEnv).filter( (env) => !baseNXEnvironmentVariables.includes(env) ); } function getNxEnvironmentVariables() { return Object.keys(process.env) .filter((env) => /^NX_/i.test(env)) .reduce((env, key) => { env[key] = process.env[key]; return env; }, {}); } let hasWarnedAboutDeprecatedEnvVariables = false; /** * TODO(v18) * @deprecated Use Next.js 9.4+ built-in support for environment variables. Reference https://nextjs.org/docs/pages/api-reference/next-config-js/env */ function addNxEnvVariables(config: any) { const maybeDefinePlugin = config.plugins?.find((plugin) => { return plugin.definitions?.['process.env.NODE_ENV']; }); if (maybeDefinePlugin) { const env = getNxEnvironmentVariables(); Object.entries(env) .map(([name, value]) => [`process.env.${name}`, `"${value}"`]) .filter(([name]) => !maybeDefinePlugin.definitions[name]) .forEach( ([name, value]) => (maybeDefinePlugin.definitions[name] = value) ); const vars = getNonBaseVariables(env); if (vars.length > 0 && !hasWarnedAboutDeprecatedEnvVariables) { hasWarnedAboutDeprecatedEnvVariables = true; console.warn( `Warning, in Nx 18 environment variables starting with NX_ will not be available in the browser, and currently will not work with @nx/next:server executor.\nPlease rename the following environment variables: ${vars.join( ', ' )} using Next.js' built-in support for environment variables. Reference https://nextjs.org/docs/pages/api-reference/next-config-js/env` ); } } } export function getAliasForProject( node: ProjectGraphProjectNode, paths: Record ): null | string { // Match workspace libs to their alias in tsconfig paths. for (const [alias, lookup] of Object.entries(paths)) { const lookupContainsDepNode = lookup.some( (lookupPath) => lookupPath.startsWith(node?.data?.root) || lookupPath.startsWith('./' + node?.data?.root) ); if (lookupContainsDepNode) { return alias; } } return null; } // Runs a function if the Next.js version satisfies the range. export function forNextVersion(range: string, fn: () => void) { const semver = require('semver'); const nextJsVersion = require('next/package.json').version; if (semver.satisfies(nextJsVersion, range)) { fn(); } } // Support for older generated code: `const withNx = require('@nx/next/plugins/with-nx');` module.exports = withNx; // Support for newer generated code: `const { withNx } = require(...);` module.exports.withNx = withNx; module.exports.getNextConfig = getNextConfig; module.exports.getAliasForProject = getAliasForProject; export { withNx };