Skip to content

Commit

Permalink
fix(nextjs): add workspace dependencies to transpilePackages automati…
Browse files Browse the repository at this point in the history
…cally (#16774)
  • Loading branch information
jaysoo authored May 4, 2023
1 parent c15c4dd commit 8d35eda
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 51 deletions.
36 changes: 20 additions & 16 deletions e2e/next/src/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand All @@ -52,21 +60,21 @@ 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');
updateProjectConfig(appName, (json) => {
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
Expand All @@ -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';
Expand All @@ -90,19 +98,19 @@ describe('Next.js Applications', () => {
);

updateFile(
`libs/${buildableLib}/src/lib/${buildableLib}.ts`,
`packages/${buildableLib}/src/lib/${buildableLib}.ts`,
`
export function buildableLib(): string {
return 'Hello Buildable';
};
`
);

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}';
Expand Down Expand Up @@ -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,
Expand All @@ -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 <app> --prod` works with previous production build (e.g. `nx build <app>`).
Expand All @@ -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`,
Expand Down
8 changes: 5 additions & 3 deletions e2e/next/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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`);
}
}
32 changes: 2 additions & 30 deletions packages/js/src/utils/buildable-libs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions packages/js/src/utils/typescript/ts-config.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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;
}
}
56 changes: 54 additions & 2 deletions packages/next/plugins/with-nx.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
});
});
39 changes: 39 additions & 0 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

This comment has been minimized.

Copy link
@roderik

roderik May 8, 2023

This import is not inlined in the generated compiled js version of this file, breaking running the dist folder

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?: {
Expand Down Expand Up @@ -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
}`;
Expand Down Expand Up @@ -418,10 +437,30 @@ function addNxEnvVariables(config: any) {
}
}

export function getAliasForProject(
node: ProjectGraphProjectNode,
paths: Record<string, string[]>
): 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 };
9 changes: 9 additions & 0 deletions packages/next/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

1 comment on commit 8d35eda

@vercel
Copy link

@vercel vercel bot commented on 8d35eda May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-git-master-nrwl.vercel.app
nx.dev
nx-dev-nrwl.vercel.app
nx-five.vercel.app

Please sign in to comment.