fix(nextjs): support relative imports in next.config.js (#17127)
This commit is contained in:
parent
9e3596abe5
commit
b6361738a4
@ -443,26 +443,26 @@ describe('Next.js Applications', () => {
|
|||||||
});
|
});
|
||||||
}, 300_000);
|
}, 300_000);
|
||||||
|
|
||||||
it('should create a generate a next.js app with app layout enabled', async () => {
|
it('should copy relative modules needed by the next.config.js file', async () => {
|
||||||
const appName = uniq('app');
|
const appName = uniq('app');
|
||||||
|
|
||||||
runCLI(
|
runCLI(`generate @nx/next:app ${appName} --style=css --no-interactive`);
|
||||||
`generate @nx/next:app ${appName} --style=css --appDir --no-interactive`
|
|
||||||
|
updateFile(`apps/${appName}/redirects.js`, 'module.exports = [];');
|
||||||
|
updateFile(
|
||||||
|
`apps/${appName}/nested/headers.js`,
|
||||||
|
`module.exports = require('./headers-2');`
|
||||||
);
|
);
|
||||||
|
updateFile(`apps/${appName}/nested/headers-2.js`, 'module.exports = [];');
|
||||||
checkFilesExist(`apps/${appName}/app/api/hello/route.ts`);
|
updateFile(`apps/${appName}/next.config.js`, (content) => {
|
||||||
checkFilesExist(`apps/${appName}/app/page.tsx`);
|
return `const redirects = require('./redirects');\nconst headers = require('./nested/headers.js');\n${content}`;
|
||||||
checkFilesExist(`apps/${appName}/app/layout.tsx`);
|
|
||||||
checkFilesExist(`apps/${appName}/app/global.css`);
|
|
||||||
checkFilesExist(`apps/${appName}/app/page.module.css`);
|
|
||||||
|
|
||||||
await checkApp(appName, {
|
|
||||||
checkUnitTest: false,
|
|
||||||
checkLint: false,
|
|
||||||
checkE2E: false,
|
|
||||||
checkExport: false,
|
|
||||||
});
|
});
|
||||||
}, 300_000);
|
|
||||||
|
runCLI(`build ${appName}`);
|
||||||
|
checkFilesExist(`dist/apps/${appName}/redirects.js`);
|
||||||
|
checkFilesExist(`dist/apps/${appName}/nested/headers.js`);
|
||||||
|
checkFilesExist(`dist/apps/${appName}/nested/headers-2.js`);
|
||||||
|
}, 120_000);
|
||||||
|
|
||||||
it('should support --turbo to enable Turbopack', async () => {
|
it('should support --turbo to enable Turbopack', async () => {
|
||||||
const appName = uniq('app');
|
const appName = uniq('app');
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
import { getWithNxContent } from './create-next-config-file';
|
import {
|
||||||
|
ensureFileExtensions,
|
||||||
|
findNextConfigPath,
|
||||||
|
getRelativeFilesToCopy,
|
||||||
|
getRelativeImports,
|
||||||
|
getWithNxContent,
|
||||||
|
} from './create-next-config-file';
|
||||||
import { stripIndents } from '@nx/devkit';
|
import { stripIndents } from '@nx/devkit';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
describe('Next.js config: getWithNxContent', () => {
|
describe('Next.js config: getWithNxContent', () => {
|
||||||
it('should swap distDir and getWithNxContext with static values', () => {
|
it('should swap distDir and getWithNxContext with static values', () => {
|
||||||
const result = getWithNxContent({
|
const result = getWithNxContent({
|
||||||
withNxFile: `with-nx.js`,
|
file: `with-nx.js`,
|
||||||
withNxContent: stripIndents`
|
content: stripIndents`
|
||||||
// SHOULD BE LEFT INTACT
|
// SHOULD BE LEFT INTACT
|
||||||
const constants = require("next/constants");
|
const constants = require("next/constants");
|
||||||
|
|
||||||
@ -56,4 +63,72 @@ describe('Next.js config: getWithNxContent', () => {
|
|||||||
expect(result).toContain(`libsDir: ''`);
|
expect(result).toContain(`libsDir: ''`);
|
||||||
expect(result).not.toContain(`libsDir: workspaceLayout.libsDir()`);
|
expect(result).not.toContain(`libsDir: workspaceLayout.libsDir()`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return relative module paths used in next.config.js when calling getRelativeFilesToCopy', () => {
|
||||||
|
const modulePaths = getRelativeFilesToCopy(
|
||||||
|
findNextConfigPath(join(__dirname, 'test-fixtures/config-js')),
|
||||||
|
join(__dirname, 'test-fixtures/config-js')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(modulePaths).toEqual([
|
||||||
|
'nested/a.cjs',
|
||||||
|
'nested/b.cjs',
|
||||||
|
'nested-c.cjs',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return relative module paths used in next.config.mjs when calling getRelativeFilesToCopy', () => {
|
||||||
|
const modulePaths = getRelativeFilesToCopy(
|
||||||
|
findNextConfigPath(join(__dirname, 'test-fixtures/config-mjs')),
|
||||||
|
join(__dirname, 'test-fixtures/config-mjs')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(modulePaths).toEqual(['a.mjs']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return relative requires when calling getRelativeImports', () => {
|
||||||
|
const result = getRelativeImports({
|
||||||
|
file: 'next.config.js',
|
||||||
|
content: stripIndents`
|
||||||
|
const w = require('@scoped/w');
|
||||||
|
const x = require('x');
|
||||||
|
const y = require("./y");
|
||||||
|
const z = require('./nested/z');
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(['./y', './nested/z']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return relative imports when calling getRelativeImports', () => {
|
||||||
|
const result = getRelativeImports({
|
||||||
|
file: 'next.config.js',
|
||||||
|
content: stripIndents`
|
||||||
|
import { w } from '@scoped/w';
|
||||||
|
import { x } from 'x';
|
||||||
|
import { y } from "./y";
|
||||||
|
import { z } from './nested/z'
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(['./y', './nested/z']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return files with their extensions when calling ensureFileExtensions', () => {
|
||||||
|
const result = ensureFileExtensions(
|
||||||
|
['bar', 'foo', 'faz', 'nested/baz.cjs'],
|
||||||
|
join(__dirname, 'test-fixtures/ensure-exts')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['bar.mjs', 'foo.cjs', 'faz.cjs', 'nested/baz.cjs']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if a path cannot be found when calling ensureFileExtensions', () => {
|
||||||
|
expect(() =>
|
||||||
|
ensureFileExtensions(
|
||||||
|
['not-found'],
|
||||||
|
join(__dirname, 'test-fixtures/ensure-exts')
|
||||||
|
)
|
||||||
|
).toThrow(/Cannot find file "not-found"/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,67 +9,89 @@ import {
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {
|
import {
|
||||||
copyFileSync,
|
copyFileSync,
|
||||||
|
ensureDirSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from 'fs';
|
} from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { dirname, extname, join, relative } from 'path';
|
||||||
|
import { findNodes } from 'nx/src/utils/typescript';
|
||||||
|
|
||||||
import type { NextBuildBuilderOptions } from '../../../utils/types';
|
import type { NextBuildBuilderOptions } from '../../../utils/types';
|
||||||
import { findNodes } from 'nx/src/utils/typescript';
|
|
||||||
|
|
||||||
export function createNextConfigFile(
|
export function createNextConfigFile(
|
||||||
options: NextBuildBuilderOptions,
|
options: NextBuildBuilderOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
) {
|
) {
|
||||||
const nextConfigPath = options.nextConfig
|
const configRelativeToProjectRoot = findNextConfigPath(
|
||||||
? join(context.root, options.nextConfig)
|
options.root,
|
||||||
: join(context.root, options.root, 'next.config.js');
|
// If user passed a config then it is relative to the workspace root, need to normalize it to be relative to the project root.
|
||||||
|
options.nextConfig ? relative(options.root, options.nextConfig) : undefined
|
||||||
|
);
|
||||||
|
const configAbsolutePath = join(options.root, configRelativeToProjectRoot);
|
||||||
|
|
||||||
|
if (!existsSync(configAbsolutePath)) {
|
||||||
|
throw new Error('next.config.js not found');
|
||||||
|
}
|
||||||
|
|
||||||
// Copy config file and our `.nx-helpers` folder to remove dependency on @nrwl/next for production build.
|
// Copy config file and our `.nx-helpers` folder to remove dependency on @nrwl/next for production build.
|
||||||
if (existsSync(nextConfigPath)) {
|
const helpersPath = join(options.outputPath, '.nx-helpers');
|
||||||
const helpersPath = join(options.outputPath, '.nx-helpers');
|
mkdirSync(helpersPath, { recursive: true });
|
||||||
mkdirSync(helpersPath, { recursive: true });
|
copyFileSync(
|
||||||
copyFileSync(
|
join(__dirname, '../../../utils/compose-plugins.js'),
|
||||||
join(__dirname, '../../../utils/compose-plugins.js'),
|
join(helpersPath, 'compose-plugins.js')
|
||||||
join(helpersPath, 'compose-plugins.js')
|
);
|
||||||
);
|
writeFileSync(join(helpersPath, 'with-nx.js'), getWithNxContent());
|
||||||
writeFileSync(join(helpersPath, 'with-nx.js'), getWithNxContent());
|
writeFileSync(
|
||||||
writeFileSync(
|
join(helpersPath, 'compiled.js'),
|
||||||
join(helpersPath, 'compiled.js'),
|
`
|
||||||
`
|
|
||||||
const withNx = require('./with-nx');
|
const withNx = require('./with-nx');
|
||||||
module.exports = withNx;
|
module.exports = withNx;
|
||||||
module.exports.withNx = withNx;
|
module.exports.withNx = withNx;
|
||||||
module.exports.composePlugins = require('./compose-plugins').composePlugins;
|
module.exports.composePlugins = require('./compose-plugins').composePlugins;
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(options.outputPath, 'next.config.js'),
|
join(options.outputPath, configRelativeToProjectRoot),
|
||||||
readFileSync(nextConfigPath)
|
readFileSync(configAbsolutePath)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/["']@nx\/next["']/, `'./.nx-helpers/compiled.js'`)
|
.replace(/["']@nx\/next["']/, `'./.nx-helpers/compiled.js'`)
|
||||||
// TODO(v17): Remove this once users have all migrated to new @nx scope and import from '@nx/next' not the deep import paths.
|
// TODO(v17): Remove this once users have all migrated to new @nx scope and import from '@nx/next' not the deep import paths.
|
||||||
.replace('@nx/next/plugins/with-nx', './.nx-helpers/compiled.js')
|
.replace('@nx/next/plugins/with-nx', './.nx-helpers/compiled.js')
|
||||||
.replace('@nrwl/next/plugins/with-nx', './.nx-helpers/compiled.js')
|
.replace('@nrwl/next/plugins/with-nx', './.nx-helpers/compiled.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find all relative imports needed by next.config.js and copy them to the dist folder.
|
||||||
|
const moduleFilesToCopy = getRelativeFilesToCopy(
|
||||||
|
configRelativeToProjectRoot,
|
||||||
|
options.root
|
||||||
|
);
|
||||||
|
for (const moduleFile of moduleFilesToCopy) {
|
||||||
|
ensureDirSync(dirname(join(context.root, options.outputPath, moduleFile)));
|
||||||
|
copyFileSync(
|
||||||
|
join(context.root, options.root, moduleFile),
|
||||||
|
join(context.root, options.outputPath, moduleFile)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function readSource() {
|
|
||||||
const withNxFile = join(__dirname, '../../../../plugins/with-nx.js');
|
function readSource(getFile: () => string): { file: string; content: string } {
|
||||||
const withNxContent = readFileSync(withNxFile).toString();
|
|
||||||
return {
|
return {
|
||||||
withNxFile,
|
file: getFile(),
|
||||||
withNxContent,
|
content: readFileSync(getFile()).toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exported for testing
|
// Exported for testing
|
||||||
export function getWithNxContent({ withNxFile, withNxContent } = readSource()) {
|
export function getWithNxContent(
|
||||||
|
{ file, content } = readSource(() =>
|
||||||
|
join(__dirname, '../../../../plugins/with-nx.js')
|
||||||
|
)
|
||||||
|
) {
|
||||||
const withNxSource = ts.createSourceFile(
|
const withNxSource = ts.createSourceFile(
|
||||||
withNxFile,
|
file,
|
||||||
withNxContent,
|
content,
|
||||||
ts.ScriptTarget.Latest,
|
ts.ScriptTarget.Latest,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@ -80,7 +102,7 @@ export function getWithNxContent({ withNxFile, withNxContent } = readSource()) {
|
|||||||
(node: ts.FunctionDeclaration) => node.name?.text === 'getWithNxContext'
|
(node: ts.FunctionDeclaration) => node.name?.text === 'getWithNxContext'
|
||||||
);
|
);
|
||||||
if (getWithNxContextDeclaration) {
|
if (getWithNxContextDeclaration) {
|
||||||
withNxContent = applyChangesToString(withNxContent, [
|
content = applyChangesToString(content, [
|
||||||
{
|
{
|
||||||
type: ChangeType.Delete,
|
type: ChangeType.Delete,
|
||||||
start: getWithNxContextDeclaration.getStart(withNxSource),
|
start: getWithNxContextDeclaration.getStart(withNxSource),
|
||||||
@ -99,5 +121,119 @@ export function getWithNxContent({ withNxFile, withNxContent } = readSource()) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return withNxContent;
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findNextConfigPath(
|
||||||
|
dirname: string,
|
||||||
|
userDefinedConfigPath?: string
|
||||||
|
): string {
|
||||||
|
if (userDefinedConfigPath) {
|
||||||
|
const file = userDefinedConfigPath;
|
||||||
|
if (existsSync(file)) return file;
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find the Next.js config file: ${userDefinedConfigPath}. Is the path correct in project.json?`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = ['next.config.js', 'next.config.cjs', 'next.config.mjs'];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (existsSync(join(dirname, candidate))) return candidate;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find any of the following files in your project: ${candidates.join(
|
||||||
|
', '
|
||||||
|
)}. Is this a Next.js project?`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing
|
||||||
|
export function getRelativeFilesToCopy(
|
||||||
|
fileName: string,
|
||||||
|
cwd: string
|
||||||
|
): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const collected = new Set<string>();
|
||||||
|
|
||||||
|
function doCollect(currFile: string): void {
|
||||||
|
// Prevent circular dependencies from causing infinite loop
|
||||||
|
if (seen.has(currFile)) return;
|
||||||
|
seen.add(currFile);
|
||||||
|
|
||||||
|
const absoluteFilePath = join(cwd, currFile);
|
||||||
|
const content = readFileSync(absoluteFilePath).toString();
|
||||||
|
const files = getRelativeImports({ file: currFile, content });
|
||||||
|
const modules = ensureFileExtensions(files, dirname(absoluteFilePath));
|
||||||
|
|
||||||
|
const relativeDirPath = dirname(currFile);
|
||||||
|
|
||||||
|
for (const moduleName of modules) {
|
||||||
|
const relativeModulePath = join(relativeDirPath, moduleName);
|
||||||
|
collected.add(relativeModulePath);
|
||||||
|
doCollect(relativeModulePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doCollect(fileName);
|
||||||
|
|
||||||
|
return Array.from(collected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing
|
||||||
|
export function getRelativeImports({
|
||||||
|
file,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
file: string;
|
||||||
|
content: string;
|
||||||
|
}): string[] {
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
file,
|
||||||
|
content,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const callExpressionsOrImportDeclarations = findNodes(source, [
|
||||||
|
ts.SyntaxKind.CallExpression,
|
||||||
|
ts.SyntaxKind.ImportDeclaration,
|
||||||
|
]) as (ts.CallExpression | ts.ImportDeclaration)[];
|
||||||
|
const modulePaths: string[] = [];
|
||||||
|
for (const node of callExpressionsOrImportDeclarations) {
|
||||||
|
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
|
||||||
|
modulePaths.push(stripOuterQuotes(node.moduleSpecifier.getText(source)));
|
||||||
|
} else {
|
||||||
|
if (node.expression.getText(source) === 'require') {
|
||||||
|
modulePaths.push(stripOuterQuotes(node.arguments[0].getText(source)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modulePaths.filter((path) => path.startsWith('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripOuterQuotes(str: string): string {
|
||||||
|
return str.match(/^["'](.*)["']/)?.[1] ?? str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing
|
||||||
|
export function ensureFileExtensions(
|
||||||
|
files: string[],
|
||||||
|
absoluteDir: string
|
||||||
|
): string[] {
|
||||||
|
const extensions = ['.js', '.cjs', '.mjs'];
|
||||||
|
return files.map((file) => {
|
||||||
|
if (extname(file)) return file;
|
||||||
|
|
||||||
|
const ext = extensions.find((ext) =>
|
||||||
|
existsSync(join(absoluteDir, file + ext))
|
||||||
|
);
|
||||||
|
if (ext) {
|
||||||
|
return file + ext;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find file "${file}" with any of the following extensions: ${extensions.join(
|
||||||
|
', '
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./b');
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
require('./a'); // Circular dependency should be handled
|
||||||
|
require('../nested-c'); // one level up
|
||||||
|
module.exports = {};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
require('./nested/a');
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const a = [];
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import './a.mjs';
|
||||||
|
export default {};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const bar = 'bar';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = 'faz';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
module.exports = 'foo';
|
||||||
Loading…
x
Reference in New Issue
Block a user