diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index c260e4624f6f0..3eb3ed59858dc 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -21,6 +21,7 @@ import { } from '@nx/e2e/utils'; import * as http from 'http'; import { checkApp } from './utils'; +import { removeSync, mkdirSync } from 'fs-extra'; describe('Next.js Applications', () => { let proj: string; @@ -39,6 +40,13 @@ describe('Next.js Applications', () => { }); it('should generate app + libs', async () => { + // Remove apps/libs folder and use packages. + // Allows us to test other integrated monorepo setup that had a regression. + // See: https://github.com/nrwl/nx/issues/16658 + removeSync(`${tmpProjPath()}/libs`); + removeSync(`${tmpProjPath()}/apps`); + mkdirSync(`${tmpProjPath()}/packages`); + const appName = uniq('app'); const nextLib = uniq('nextlib'); const jsLib = uniq('tslib'); @@ -52,7 +60,7 @@ describe('Next.js Applications', () => { ); // Create file in public that should be copied to dist - updateFile(`apps/${appName}/public/a/b.txt`, `Hello World!`); + updateFile(`packages/${appName}/public/a/b.txt`, `Hello World!`); // Additional assets that should be copied to dist const sharedLib = uniq('sharedLib'); @@ -60,13 +68,13 @@ describe('Next.js Applications', () => { json.targets.build.options.assets = [ { glob: '**/*', - input: `libs/${sharedLib}/src/assets`, + input: `packages/${sharedLib}/src/assets`, output: 'shared/ui', }, ]; return json; }); - updateFile(`libs/${sharedLib}/src/assets/hello.txt`, 'Hello World!'); + updateFile(`packages/${sharedLib}/src/assets/hello.txt`, 'Hello World!'); // create a css file in node_modules so that it can be imported in a lib // to test that it works as expected @@ -76,7 +84,7 @@ describe('Next.js Applications', () => { ); updateFile( - `libs/${jsLib}/src/lib/${jsLib}.ts`, + `packages/${jsLib}/src/lib/${jsLib}.ts`, ` export function jsLib(): string { return 'Hello Nx'; @@ -90,7 +98,7 @@ describe('Next.js Applications', () => { ); updateFile( - `libs/${buildableLib}/src/lib/${buildableLib}.ts`, + `packages/${buildableLib}/src/lib/${buildableLib}.ts`, ` export function buildableLib(): string { return 'Hello Buildable'; @@ -98,11 +106,11 @@ describe('Next.js Applications', () => { ` ); - const mainPath = `apps/${appName}/pages/index.tsx`; + const mainPath = `packages/${appName}/pages/index.tsx`; const content = readFile(mainPath); updateFile( - `apps/${appName}/pages/api/hello.ts`, + `packages/${appName}/pages/api/hello.ts`, ` import { jsLibAsync } from '@${proj}/${jsLib}'; @@ -139,7 +147,7 @@ describe('Next.js Applications', () => { )}` ); - const e2eTestPath = `apps/${appName}-e2e/src/e2e/app.cy.ts`; + const e2eTestPath = `packages/${appName}-e2e/src/e2e/app.cy.ts`; const e2eContent = readFile(e2eTestPath); updateFile( e2eTestPath, @@ -160,12 +168,13 @@ describe('Next.js Applications', () => { checkLint: true, checkE2E: isNotWindows(), checkExport: false, + appsDir: 'packages', }); // public and shared assets should both be copied to dist checkFilesExist( - `dist/apps/${appName}/public/a/b.txt`, - `dist/apps/${appName}/public/shared/ui/hello.txt` + `dist/packages/${appName}/public/a/b.txt`, + `dist/packages/${appName}/public/shared/ui/hello.txt` ); // Check that `nx serve --prod` works with previous production build (e.g. `nx build `). @@ -178,14 +187,9 @@ describe('Next.js Applications', () => { ); // Check that the output is self-contained (i.e. can run with its own package.json + node_modules) - const distPath = joinPathFragments(tmpProjPath(), 'dist/apps', appName); const selfContainedPort = 3000; - const pmc = getPackageManagerCommand(); - runCommand(`${pmc.install}`, { - cwd: distPath, - }); runCLI( - `generate @nx/workspace:run-commands serve-prod --project ${appName} --cwd=dist/apps/${appName} --command="npx next start --port=${selfContainedPort}"` + `generate @nx/workspace:run-commands serve-prod --project ${appName} --cwd=dist/packages/${appName} --command="npx next start --port=${selfContainedPort}"` ); const selfContainedProcess = await runCommandUntil( `run ${appName}:serve-prod`, diff --git a/e2e/next/src/utils.ts b/e2e/next/src/utils.ts index 2a419b95de815..dc4ef27ebf7f3 100644 --- a/e2e/next/src/utils.ts +++ b/e2e/next/src/utils.ts @@ -14,13 +14,15 @@ export async function checkApp( checkLint: boolean; checkE2E: boolean; checkExport: boolean; + appsDir?: string; } ) { + const appsDir = opts.appsDir ?? 'apps'; const buildResult = runCLI(`build ${appName}`); expect(buildResult).toContain(`Compiled successfully`); - checkFilesExist(`dist/apps/${appName}/.next/build-manifest.json`); + checkFilesExist(`dist/${appsDir}/${appName}/.next/build-manifest.json`); - const packageJson = readJson(`dist/apps/${appName}/package.json`); + const packageJson = readJson(`dist/${appsDir}/${appName}/package.json`); expect(packageJson.dependencies.react).toBeDefined(); expect(packageJson.dependencies['react-dom']).toBeDefined(); expect(packageJson.dependencies.next).toBeDefined(); @@ -45,6 +47,6 @@ export async function checkApp( if (opts.checkExport) { runCLI(`export ${appName}`); - checkFilesExist(`dist/apps/${appName}/exported/index.html`); + checkFilesExist(`dist/${appsDir}/${appName}/exported/index.html`); } } diff --git a/packages/js/src/utils/buildable-libs-utils.ts b/packages/js/src/utils/buildable-libs-utils.ts index 86a0e62190103..0801a959fd2fb 100644 --- a/packages/js/src/utils/buildable-libs-utils.ts +++ b/packages/js/src/utils/buildable-libs-utils.ts @@ -13,6 +13,7 @@ import { unlinkSync } from 'fs'; import { output } from 'nx/src/utils/output'; import { isNpmProject } from 'nx/src/project-graph/operators'; import { ensureTypescript } from './typescript/ensure-typescript'; +import { readTsConfigPaths } from './typescript/ts-config'; let tsModule: typeof import('typescript'); @@ -190,40 +191,11 @@ export function computeCompilerOptionsPaths( tsConfig: string | ts.ParsedCommandLine, dependencies: DependentBuildableProjectNode[] ) { - const paths = readPaths(tsConfig) || {}; + const paths = readTsConfigPaths(tsConfig) || {}; updatePaths(dependencies, paths); return paths; } -function readPaths(tsConfig: string | ts.ParsedCommandLine) { - if (!tsModule) { - tsModule = ensureTypescript(); - } - try { - let config: ts.ParsedCommandLine; - if (typeof tsConfig === 'string') { - const configFile = tsModule.readConfigFile( - tsConfig, - tsModule.sys.readFile - ); - config = tsModule.parseJsonConfigFileContent( - configFile.config, - tsModule.sys, - dirname(tsConfig) - ); - } else { - config = tsConfig; - } - if (config.options?.paths) { - return config.options.paths; - } else { - return null; - } - } catch (e) { - return null; - } -} - export function createTmpTsConfig( tsconfigPath: string, workspaceRoot: string, diff --git a/packages/js/src/utils/typescript/ts-config.ts b/packages/js/src/utils/typescript/ts-config.ts index 64c806d99f707..25f2d5869e4e2 100644 --- a/packages/js/src/utils/typescript/ts-config.ts +++ b/packages/js/src/utils/typescript/ts-config.ts @@ -1,6 +1,8 @@ import { offsetFromRoot, Tree, updateJson, workspaceRoot } from '@nx/devkit'; import { existsSync } from 'fs'; import { dirname, join } from 'path'; +import * as ts from 'typescript'; +import { ensureTypescript } from './ensure-typescript'; let tsModule: typeof import('typescript'); @@ -76,3 +78,35 @@ export function addTsConfigPath( return json; }); } + +export function readTsConfigPaths(tsConfig?: string | ts.ParsedCommandLine) { + tsConfig ??= getRootTsConfigPath(); + try { + if (!tsModule) { + tsModule = ensureTypescript(); + } + + let config: ts.ParsedCommandLine; + + if (typeof tsConfig === 'string') { + const configFile = tsModule.readConfigFile( + tsConfig, + tsModule.sys.readFile + ); + config = tsModule.parseJsonConfigFileContent( + configFile.config, + tsModule.sys, + dirname(tsConfig) + ); + } else { + config = tsConfig; + } + if (config.options?.paths) { + return config.options.paths; + } else { + return null; + } + } catch (e) { + return null; + } +} diff --git a/packages/next/plugins/with-nx.spec.ts b/packages/next/plugins/with-nx.spec.ts index f0dfcf7c17e5b..d4da25951b7eb 100644 --- a/packages/next/plugins/with-nx.spec.ts +++ b/packages/next/plugins/with-nx.spec.ts @@ -1,7 +1,7 @@ import { NextConfigComplete } from 'next/dist/server/config-shared'; -import { getNextConfig } from './with-nx'; +import { getAliasForProject, getNextConfig } from './with-nx'; -describe('withNx', () => { +describe('getNextConfig', () => { describe('svgr', () => { it('should be used by default', () => { const config = getNextConfig(); @@ -64,3 +64,55 @@ describe('withNx', () => { }); }); }); + +describe('getAliasForProject', () => { + it('should return the matching alias for a project', () => { + const paths = { + '@x/proj1': ['packages/proj1'], + // customized lookup paths with relative path syntax + '@x/proj2': ['./something-else', './packages/proj2'], + }; + + expect( + getAliasForProject( + { + name: 'proj1', + type: 'lib', + data: { + root: 'packages/proj1', + files: [], + }, + }, + paths + ) + ).toEqual('@x/proj1'); + + expect( + getAliasForProject( + { + name: 'proj2', + type: 'lib', + data: { + root: 'packages/proj2', // relative path + files: [], + }, + }, + paths + ) + ).toEqual('@x/proj2'); + + expect( + getAliasForProject( + { + name: 'no-alias', + type: 'lib', + data: { + root: 'packages/no-alias', + files: [], + }, + }, + paths + ) + ).toEqual(null); + }); +}); diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index 8ce9d5e4d9a96..2d991e049a385 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -5,9 +5,12 @@ import * as path from 'path'; import type { NextConfig } from 'next'; import type { NextConfigFn } from '../src/utils/config'; +import { forNextVersion } from '../src/utils/config'; import type { NextBuildBuilderOptions } from '../src/utils/types'; import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils'; import type { ProjectGraph, ProjectGraphProjectNode, Target } from '@nx/devkit'; +import { readTsConfigPaths } from '@nx/js'; +import { findAllProjectNodeDependencies } from 'nx/src/utils/project-graph-utils'; export interface WithNxOptions extends NextConfig { nx?: { @@ -207,6 +210,22 @@ function withNx( // 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 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); + } + } + }); + const outputDir = `${offsetFromRoot(projectDirectory)}${ options.outputPath }`; @@ -418,10 +437,30 @@ function addNxEnvVariables(config: any) { } } +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; +} + // 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 }; diff --git a/packages/next/src/utils/config.ts b/packages/next/src/utils/config.ts index b1be952f4d079..2586b7d3f0295 100644 --- a/packages/next/src/utils/config.ts +++ b/packages/next/src/utils/config.ts @@ -107,3 +107,12 @@ function isTsRule(r: RuleSetRule): boolean { return r.test.test('a.ts'); } + +// 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(); + } +}