feat(nextjs): update Next.js to 13.4.1 and default to App Router for new apps (#16948)

This commit is contained in:
Jack Hsu 2023-05-12 16:09:20 -04:00 committed by GitHub
parent 0aa3e45e2e
commit 95421c6945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 248 additions and 144 deletions

View File

@ -113,7 +113,7 @@ Workspace name (e.g. org name)
Type: `boolean` Type: `boolean`
Add Experimental app/ layout for next.js Enable the App Router for Next.js
### nxCloud ### nxCloud

View File

@ -118,9 +118,9 @@
}, },
"appDir": { "appDir": {
"type": "boolean", "type": "boolean",
"default": false, "default": true,
"description": "Enable experimental app directory for the project", "description": "Enable the App Router for this project.",
"x-prompt": "Do you want to use experimental app/ in this project?" "x-prompt": "Would you like to use the App Router (recommended)?"
}, },
"rootProject": { "rootProject": {
"description": "Create an application at the root of the workspace.", "description": "Create an application at the root of the workspace.",

View File

@ -113,7 +113,7 @@ Workspace name (e.g. org name)
Type: `boolean` Type: `boolean`
Add Experimental app/ layout for next.js Enable the App Router for Next.js
### nxCloud ### nxCloud

View File

@ -70,9 +70,9 @@
"enum": ["express", "koa", "fastify", "nest", "none"] "enum": ["express", "koa", "fastify", "nest", "none"]
}, },
"nextAppDir": { "nextAppDir": {
"description": "Enable experimental app directory for the project", "description": "Enable the App Router for this project.",
"type": "boolean", "type": "boolean",
"default": false "default": true
}, },
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",

View File

@ -82,9 +82,9 @@
"default": false "default": false
}, },
"nextAppDir": { "nextAppDir": {
"description": "Enable experimental app/ for the project", "description": "Enable the App Router for this project.",
"type": "boolean", "type": "boolean",
"default": false "default": true
}, },
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",

View File

@ -0,0 +1,56 @@
import {
cleanupProject,
isNotWindows,
killPorts,
newProject,
runCLI,
runCommandUntil,
tmpProjPath,
uniq,
updateFile,
} from '@nx/e2e/utils';
import { getData } from 'ajv/dist/compile/validate';
import { detectPackageManager } from 'nx/src/utils/package-manager';
import { checkApp } from './utils';
import { p } from 'vitest/dist/types-b7007192';
describe('Next.js App Router', () => {
let proj: string;
beforeEach(() => {
proj = newProject();
});
afterEach(() => {
cleanupProject();
});
it('should be able to generate and build app with default App Router', async () => {
const appName = uniq('app');
const jsLib = uniq('tslib');
runCLI(`generate @nx/next:app ${appName}`);
runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`);
updateFile(
`apps/${appName}/app/page.tsx`,
`
import React from 'react';
import { ${jsLib} } from '@${proj}/${jsLib}';
export default async function Page() {
return (
<p>{${jsLib}()}</p>
);
};
`
);
await checkApp(appName, {
checkUnitTest: false,
checkLint: true,
checkE2E: false,
checkExport: false,
});
}, 300_000);
});

View File

@ -65,7 +65,7 @@ describe('NextJs Component Testing', () => {
}); });
function createAppWithCt(appName: string) { function createAppWithCt(appName: string) {
runCLI(`generate @nx/next:app ${appName} --no-interactive`); runCLI(`generate @nx/next:app ${appName} --no-interactive --appDir=false`);
runCLI( runCLI(
`generate @nx/next:component button --project=${appName} --directory=components --flat --no-interactive` `generate @nx/next:component button --project=${appName} --directory=components --flat --no-interactive`
); );

View File

@ -21,7 +21,9 @@ describe('Next.js apps', () => {
it('should support different --style options', async () => { it('should support different --style options', async () => {
const lessApp = uniq('app'); const lessApp = uniq('app');
runCLI(`generate @nx/next:app ${lessApp} --no-interactive --style=less`); runCLI(
`generate @nx/next:app ${lessApp} --no-interactive --style=less --appDir=false`
);
await checkApp(lessApp, { await checkApp(lessApp, {
checkUnitTest: false, checkUnitTest: false,
@ -32,7 +34,9 @@ describe('Next.js apps', () => {
const stylusApp = uniq('app'); const stylusApp = uniq('app');
runCLI(`generate @nx/next:app ${stylusApp} --no-interactive --style=styl`); runCLI(
`generate @nx/next:app ${stylusApp} --no-interactive --style=styl --appDir=false`
);
await checkApp(stylusApp, { await checkApp(stylusApp, {
checkUnitTest: false, checkUnitTest: false,
@ -44,7 +48,7 @@ describe('Next.js apps', () => {
const scApp = uniq('app'); const scApp = uniq('app');
runCLI( runCLI(
`generate @nx/next:app ${scApp} --no-interactive --style=styled-components` `generate @nx/next:app ${scApp} --no-interactive --style=styled-components --appDir=false`
); );
await checkApp(scApp, { await checkApp(scApp, {
@ -57,7 +61,7 @@ describe('Next.js apps', () => {
const emotionApp = uniq('app'); const emotionApp = uniq('app');
runCLI( runCLI(
`generate @nx/next:app ${emotionApp} --no-interactive --style=@emotion/styled` `generate @nx/next:app ${emotionApp} --no-interactive --style=@emotion/styled --appDir=false`
); );
await checkApp(emotionApp, { await checkApp(emotionApp, {

View File

@ -52,7 +52,9 @@ describe('Next.js Applications', () => {
const jsLib = uniq('tslib'); const jsLib = uniq('tslib');
const buildableLib = uniq('buildablelib'); const buildableLib = uniq('buildablelib');
runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); runCLI(
`generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false`
);
runCLI(`generate @nx/next:lib ${nextLib} --no-interactive`); runCLI(`generate @nx/next:lib ${nextLib} --no-interactive`);
runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`);
runCLI( runCLI(
@ -231,7 +233,7 @@ describe('Next.js Applications', () => {
const port = 4200; const port = 4200;
runCLI(`generate @nx/next:app ${appName}`); runCLI(`generate @nx/next:app ${appName} --appDir=false`);
runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`);
const proxyConf = { const proxyConf = {
@ -297,7 +299,9 @@ describe('Next.js Applications', () => {
it('should support custom next.config.js and output it in dist', async () => { it('should support custom next.config.js and output it in dist', async () => {
const appName = uniq('app'); const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); runCLI(
`generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false`
);
updateFile( updateFile(
`apps/${appName}/next.config.js`, `apps/${appName}/next.config.js`,
@ -354,7 +358,9 @@ describe('Next.js Applications', () => {
it('should support --js flag', async () => { it('should support --js flag', async () => {
const appName = uniq('app'); const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --js`); runCLI(
`generate @nx/next:app ${appName} --no-interactive --js --appDir=false`
);
checkFilesExist(`apps/${appName}/pages/index.js`); checkFilesExist(`apps/${appName}/pages/index.js`);

View File

@ -18,14 +18,6 @@ export async function checkApp(
} }
) { ) {
const appsDir = opts.appsDir ?? 'apps'; const appsDir = opts.appsDir ?? 'apps';
const buildResult = runCLI(`build ${appName}`);
expect(buildResult).toContain(`Compiled successfully`);
checkFilesExist(`dist/${appsDir}/${appName}/.next/build-manifest.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();
if (opts.checkLint) { if (opts.checkLint) {
const lintResults = runCLI(`lint ${appName}`); const lintResults = runCLI(`lint ${appName}`);
@ -39,8 +31,19 @@ export async function checkApp(
); );
} }
const buildResult = runCLI(`build ${appName}`);
expect(buildResult).toContain(`Successfully ran target build`);
checkFilesExist(`dist/${appsDir}/${appName}/.next/build-manifest.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();
if (opts.checkE2E && runCypressTests()) { if (opts.checkE2E && runCypressTests()) {
const e2eResults = runCLI(`e2e ${appName}-e2e --no-watch`); const e2eResults = runCLI(
`e2e ${appName}-e2e --no-watch --configuration=production`
);
expect(e2eResults).toContain('All specs passed!'); expect(e2eResults).toContain('All specs passed!');
expect(await killPorts()).toBeTruthy(); expect(await killPorts()).toBeTruthy();
} }

View File

@ -107,7 +107,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
type: 'boolean', type: 'boolean',
}) })
.option('nextAppDir', { .option('nextAppDir', {
describe: chalk.dim`Add Experimental app/ layout for next.js`, describe: chalk.dim`Enable the App Router for Next.js`,
type: 'boolean', type: 'boolean',
}), }),
withNxCloud, withNxCloud,
@ -638,8 +638,8 @@ async function isNextAppDir(parsedArgs: yargs.Arguments<Arguments>) {
.prompt<{ appDir: 'Yes' | 'No' }>([ .prompt<{ appDir: 'Yes' | 'No' }>([
{ {
name: 'appDir', name: 'appDir',
message: 'Do you want to use experimental app/ in this project?', message: 'Would you like to use the App Router (recommended)?',
type: 'autocomplete', type: 'autocomplete' as const,
choices: [ choices: [
{ {
name: 'No', name: 'No',
@ -648,7 +648,7 @@ async function isNextAppDir(parsedArgs: yargs.Arguments<Arguments>) {
name: 'Yes', name: 'Yes',
}, },
], ],
initial: 'No' as any, initial: 'Yes' as any,
}, },
]) ])
.then((choice) => choice.appDir === 'Yes'); .then((choice) => choice.appDir === 'Yes');

View File

@ -316,6 +316,22 @@
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
} }
} }
},
"16.2.0": {
"version": "16.2.0-beta.0",
"requires": {
"next": ">=13.0.0"
},
"packages": {
"next": {
"version": "13.4.1",
"alwaysAddToPackageJson": false
},
"eslint-config-next": {
"version": "13.4.1",
"alwaysAddToPackageJson": false
}
}
} }
} }
} }

View File

@ -6,14 +6,6 @@ import { WithNxOptions } from './with-nx';
const addLessToRegExp = (rx) => const addLessToRegExp = (rx) =>
new RegExp(rx.source.replace('|sass', '|sass|less'), rx.flags); new RegExp(rx.source.replace('|sass', '|sass|less'), rx.flags);
function patchNextCSSWithLess(
nextCSSModule: any = require('next/dist/build/webpack/config/blocks/css')
) {
nextCSSModule.regexLikeCss = addLessToRegExp(nextCSSModule.regexLikeCss);
}
patchNextCSSWithLess();
export function withLess( export function withLess(
configOrFn: NextConfigFn | WithNxOptions configOrFn: NextConfigFn | WithNxOptions
): NextConfigFn { ): NextConfigFn {
@ -108,4 +100,3 @@ export function withLess(
module.exports = withLess; module.exports = withLess;
module.exports.withLess = withLess; module.exports.withLess = withLess;
module.exports.patchNext = patchNextCSSWithLess;

View File

@ -6,14 +6,6 @@ import { WithNxOptions } from './with-nx';
const addStylusToRegExp = (rx) => const addStylusToRegExp = (rx) =>
new RegExp(rx.source.replace('|sass', '|sass|styl'), rx.flags); new RegExp(rx.source.replace('|sass', '|sass|styl'), rx.flags);
function patchNextCSSWithStylus(
nextCSSModule = require('next/dist/build/webpack/config/blocks/css') as any
) {
nextCSSModule.regexLikeCss = addStylusToRegExp(nextCSSModule.regexLikeCss);
}
patchNextCSSWithStylus();
export function withStylus( export function withStylus(
configOrFn: WithNxOptions | NextConfigFn configOrFn: WithNxOptions | NextConfigFn
): NextConfigFn { ): NextConfigFn {
@ -107,4 +99,3 @@ export function withStylus(
module.exports = withStylus; module.exports = withStylus;
module.exports.withStylus = withStylus; module.exports.withStylus = withStylus;
module.exports.patchNext = patchNextCSSWithStylus;

View File

@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app --style styled-jsx should use <style jsx> in index page 1`] = ` exports[`app --style styled-jsx should use <style jsx> in index page 1`] = `
"export function Index() { "'use client';
export default async function Index() {
/* /*
* Replace the elements below with your own. * Replace the elements below with your own.
* *
@ -420,7 +422,5 @@ exports[`app --style styled-jsx should use <style jsx> in index page 1`] = `
</div> </div>
); );
} }
export default Index;
" "
`; `;

View File

@ -50,12 +50,23 @@ describe('app', () => {
}); });
}); });
it('should generate files', async () => { it('should generate files for app router layout', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
name: 'myApp', name: 'myApp',
style: 'css', style: 'css',
}); });
expect(tree.exists('apps/my-app/tsconfig.json')).toBeTruthy(); expect(tree.exists('apps/my-app/tsconfig.json')).toBeTruthy();
expect(tree.exists('apps/my-app/app/page.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/app/page.module.css')).toBeTruthy();
});
it('should generate files for pages layout', async () => {
await applicationGenerator(tree, {
name: 'myApp',
style: 'css',
appDir: false,
});
expect(tree.exists('apps/my-app/tsconfig.json')).toBeTruthy();
expect(tree.exists('apps/my-app/pages/index.tsx')).toBeTruthy(); expect(tree.exists('apps/my-app/pages/index.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/specs/index.spec.tsx')).toBeTruthy(); expect(tree.exists('apps/my-app/specs/index.spec.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/pages/index.module.css')).toBeTruthy(); expect(tree.exists('apps/my-app/pages/index.module.css')).toBeTruthy();
@ -91,13 +102,11 @@ describe('app', () => {
style: 'scss', style: 'scss',
}); });
expect(tree.exists('apps/my-app/pages/index.module.scss')).toBeTruthy(); expect(tree.exists('apps/my-app/app/page.module.scss')).toBeTruthy();
expect(tree.exists('apps/my-app/pages/styles.css')).toBeTruthy(); expect(tree.exists('apps/my-app/app/global.css')).toBeTruthy();
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).toContain( expect(indexContent).toContain(`import styles from './page.module.scss'`);
`import styles from './index.module.scss'`
);
}); });
}); });
@ -108,13 +117,11 @@ describe('app', () => {
style: 'less', style: 'less',
}); });
expect(tree.exists('apps/my-app/pages/index.module.less')).toBeTruthy(); expect(tree.exists('apps/my-app/app/page.module.less')).toBeTruthy();
expect(tree.exists('apps/my-app/pages/styles.less')).toBeTruthy(); expect(tree.exists('apps/my-app/app/global.less')).toBeTruthy();
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).toContain( expect(indexContent).toContain(`import styles from './page.module.less'`);
`import styles from './index.module.less'`
);
}); });
}); });
@ -125,13 +132,11 @@ describe('app', () => {
style: 'styl', style: 'styl',
}); });
expect(tree.exists('apps/my-app/pages/index.module.styl')).toBeTruthy(); expect(tree.exists('apps/my-app/app/page.module.styl')).toBeTruthy();
expect(tree.exists('apps/my-app/pages/styles.styl')).toBeTruthy(); expect(tree.exists('apps/my-app/app/global.styl')).toBeTruthy();
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).toContain( expect(indexContent).toContain(`import styles from './page.module.styl'`);
`import styles from './index.module.styl'`
);
}); });
}); });
@ -143,12 +148,12 @@ describe('app', () => {
}); });
expect( expect(
tree.exists('apps/my-app/pages/index.module.styled-components') tree.exists('apps/my-app/app/page.module.styled-components')
).toBeFalsy(); ).toBeFalsy();
expect(tree.exists('apps/my-app/pages/styles.css')).toBeTruthy(); expect(tree.exists('apps/my-app/app/global.css')).toBeTruthy();
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).not.toContain(`import styles from './index.module`); expect(indexContent).not.toContain(`import styles from './page.module`);
expect(indexContent).toContain(`import styled from 'styled-components'`); expect(indexContent).toContain(`import styled from 'styled-components'`);
}); });
}); });
@ -161,12 +166,12 @@ describe('app', () => {
}); });
expect( expect(
tree.exists('apps/my-app/pages/index.module.styled-components') tree.exists('apps/my-app/app/page.module.styled-components')
).toBeFalsy(); ).toBeFalsy();
expect(tree.exists('apps/my-app/pages/styles.css')).toBeTruthy(); expect(tree.exists('apps/my-app/app/global.css')).toBeTruthy();
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).not.toContain(`import styles from './index.module`); expect(indexContent).not.toContain(`import styles from './page.module`);
expect(indexContent).toContain(`import styled from '@emotion/styled'`); expect(indexContent).toContain(`import styled from '@emotion/styled'`);
}); });
@ -191,15 +196,13 @@ describe('app', () => {
style: 'styled-jsx', style: 'styled-jsx',
}); });
const indexContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(indexContent).toMatchSnapshot(); expect(indexContent).toMatchSnapshot();
expect( expect(tree.exists('apps/my-app/app/page.module.styled-jsx')).toBeFalsy();
tree.exists('apps/my-app/pages/index.module.styled-jsx') expect(tree.exists('apps/my-app/app/global.css')).toBeTruthy();
).toBeFalsy();
expect(tree.exists('apps/my-app/pages/styles.css')).toBeTruthy();
expect(indexContent).not.toContain(`import styles from './index.module`); expect(indexContent).not.toContain(`import styles from './page.module`);
expect(indexContent).not.toContain( expect(indexContent).not.toContain(
`import styled from 'styled-components'` `import styled from 'styled-components'`
); );
@ -311,7 +314,7 @@ describe('app', () => {
style: 'css', style: 'css',
}); });
const appContent = tree.read('apps/my-app/pages/index.tsx', 'utf-8'); const appContent = tree.read('apps/my-app/app/page.tsx', 'utf-8');
expect(appContent).not.toMatch(/extends Component/); expect(appContent).not.toMatch(/extends Component/);
}); });
@ -419,7 +422,7 @@ describe('app', () => {
js: true, js: true,
}); });
expect(tree.exists('apps/my-app/pages/index.js')).toBeTruthy(); expect(tree.exists('apps/my-app/app/page.js')).toBeTruthy();
expect(tree.exists('apps/my-app/specs/index.spec.js')).toBeTruthy(); expect(tree.exists('apps/my-app/specs/index.spec.js')).toBeTruthy();
expect(tree.exists('apps/my-app/index.d.js')).toBeFalsy(); expect(tree.exists('apps/my-app/index.d.js')).toBeFalsy();
expect(tree.exists('apps/my-app/index.d.ts')).toBeFalsy(); expect(tree.exists('apps/my-app/index.d.ts')).toBeFalsy();
@ -450,8 +453,17 @@ describe('app', () => {
appDir: true, appDir: true,
}); });
expect(tree.exists('apps/testApp/pages/styles.css')).toBeFalsy(); const tsConfig = readJson(tree, 'apps/test-app/tsconfig.json');
expect(tsConfig.include).toEqual([
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx',
'../../apps/test-app/.next/types/**/*.ts',
'../../dist/apps/test-app/.next/types/**/*.ts',
'next-env.d.ts',
]);
expect(tree.exists('apps/test-app/pages/styles.css')).toBeFalsy();
expect(tree.exists('apps/test-app/app/global.css')).toBeTruthy(); expect(tree.exists('apps/test-app/app/global.css')).toBeTruthy();
expect(tree.exists('apps/test-app/app/page.tsx')).toBeTruthy(); expect(tree.exists('apps/test-app/app/page.tsx')).toBeTruthy();
expect(tree.exists('apps/test-app/app/layout.tsx')).toBeTruthy(); expect(tree.exists('apps/test-app/app/layout.tsx')).toBeTruthy();
@ -459,5 +471,25 @@ describe('app', () => {
expect(tree.exists('apps/test-app/app/page.module.css')).toBeTruthy(); expect(tree.exists('apps/test-app/app/page.module.css')).toBeTruthy();
expect(tree.exists('apps/test-app/public/favicon.ico')).toBeTruthy(); expect(tree.exists('apps/test-app/public/favicon.ico')).toBeTruthy();
}); });
it('should add layout types correctly for standalone apps', async () => {
await applicationGenerator(tree, {
name: 'testApp',
style: 'css',
appDir: true,
rootProject: true,
});
const tsConfig = readJson(tree, 'tsconfig.json');
expect(tsConfig.include).toEqual([
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx',
'.next/types/**/*.ts',
'dist/test-app/.next/types/**/*.ts',
'next-env.d.ts',
]);
});
}); });
}); });

View File

@ -1,8 +1,7 @@
import Head from 'next/head';
import './global.<%= stylesExt %>'; import './global.<%= stylesExt %>';
export const metadata = { export const metadata = {
title: 'Nx Next App', title: 'Welcome to <%= name %>',
description: 'Generated by create-nx-workspace', description: 'Generated by create-nx-workspace',
} }
@ -13,9 +12,6 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<Head>
<title>Welcome to <%= name %>!</title>
</Head>
<body>{children}</body> <body>{children}</body>
</html> </html>
) )

View File

@ -17,11 +17,6 @@ const nextConfig = {
// See: https://github.com/gregberge/svgr // See: https://github.com/gregberge/svgr
svgr: false, svgr: false,
}, },
<% if(appDir) { %>
experimental: {
appDir: true
},
<% } %>
}; };
const plugins = [ const plugins = [
@ -43,11 +38,6 @@ const nextConfig = {
// See: https://github.com/gregberge/svgr // See: https://github.com/gregberge/svgr
svgr: false, svgr: false,
}, },
<% if(appDir) { %>
experimental: {
appDir: true
},
<% } %>
}; };
const plugins = [ const plugins = [
@ -72,11 +62,6 @@ const nextConfig = {
// See: https://github.com/gregberge/svgr // See: https://github.com/gregberge/svgr
svgr: false, svgr: false,
}, },
<% if(appDir) { %>
experimental: {
appDir: true
},
<% } %>
}; };
const plugins = [ const plugins = [
@ -96,11 +81,6 @@ const nextConfig = {
// See: https://github.com/gregberge/svgr // See: https://github.com/gregberge/svgr
svgr: false, svgr: false,
}, },
<% if(appDir) { %>
experimental: {
appDir: true
},
<% } %>
}; };
const plugins = [ const plugins = [

View File

@ -14,6 +14,16 @@
"incremental": true, "incremental": true,
"plugins": [{ "name": "next" }] "plugins": [{ "name": "next" }]
}, },
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
<% if (appDir) { %>
"<%= layoutTypeSrcPath %>",
"<%= layoutTypeDistPath %>",
<% } %>
"next-env.d.ts"
],
"exclude": ["node_modules", "jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] "exclude": ["node_modules", "jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
} }

View File

@ -1,7 +1,6 @@
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { import {
addProjectConfiguration, addProjectConfiguration,
joinPathFragments,
ProjectConfiguration, ProjectConfiguration,
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
@ -9,18 +8,13 @@ import {
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const targets: Record<string, any> = {}; const targets: Record<string, any> = {};
const outputPath = joinPathFragments(
'dist',
options.appProjectRoot,
...(options.rootProject ? [options.name] : [])
);
targets.build = { targets.build = {
executor: '@nx/next:build', executor: '@nx/next:build',
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
defaultConfiguration: 'production', defaultConfiguration: 'production',
options: { options: {
root: options.appProjectRoot, root: options.appProjectRoot,
outputPath: outputPath, outputPath: options.outputPath,
}, },
configurations: { configurations: {
development: { development: {

View File

@ -3,6 +3,7 @@ import {
generateFiles, generateFiles,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot as _offsetFromRoot,
readJson, readJson,
toJS, toJS,
Tree, Tree,
@ -17,11 +18,25 @@ import {
} from './create-application-files.helpers'; } from './create-application-files.helpers';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) { export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
const offsetFromRoot = _offsetFromRoot(options.appProjectRoot);
const layoutTypeSrcPath = joinPathFragments(
offsetFromRoot,
options.appProjectRoot,
'.next/types/**/*.ts'
);
const layoutTypeDistPath = joinPathFragments(
offsetFromRoot,
options.outputPath,
'.next/types/**/*.ts'
);
const templateVariables = { const templateVariables = {
...names(options.name), ...names(options.name),
...options, ...options,
dot: '.', dot: '.',
tmpl: '', tmpl: '',
offsetFromRoot,
layoutTypeSrcPath,
layoutTypeDistPath,
rootTsConfigPath: getRelativePathToRootTsConfig( rootTsConfigPath: getRelativePathToRootTsConfig(
host, host,
options.appProjectRoot options.appProjectRoot
@ -29,6 +44,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
appContent: createAppJsx(options.name), appContent: createAppJsx(options.name),
styleContent: createStyleRules(), styleContent: createStyleRules(),
pageStyleContent: `.page {}`, pageStyleContent: `.page {}`,
stylesExt: stylesExt:
options.style === 'less' || options.style === 'styl' options.style === 'less' || options.style === 'styl'
? options.style ? options.style

View File

@ -13,6 +13,7 @@ import { Schema } from '../schema';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
projectName: string; projectName: string;
appProjectRoot: string; appProjectRoot: string;
outputPath: string;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
parsedTags: string[]; parsedTags: string[];
@ -28,6 +29,7 @@ export function normalizeOptions(
const { layoutDirectory, projectDirectory } = extractLayoutDirectory( const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
options.directory options.directory
); );
const name = names(options.name).fileName;
const appDirectory = projectDirectory const appDirectory = projectDirectory
? `${names(projectDirectory).fileName}/${names(options.name).fileName}` ? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
@ -46,13 +48,19 @@ export function normalizeOptions(
? '.' ? '.'
: joinPathFragments(appsDir, `${appDirectory}-e2e`); : joinPathFragments(appsDir, `${appDirectory}-e2e`);
const outputPath = joinPathFragments(
'dist',
appProjectRoot,
...(options.rootProject ? [name] : [])
);
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const fileName = 'index'; const fileName = 'index';
const appDir = options.appDir ?? false; const appDir = options.appDir ?? true;
const styledModule = /^(css|scss|less|styl)$/.test(options.style) const styledModule = /^(css|scss|less|styl)$/.test(options.style)
? null ? null
@ -63,17 +71,18 @@ export function normalizeOptions(
return { return {
...options, ...options,
appDir, appDir,
name: names(options.name).fileName,
projectName: appProjectName,
linter: options.linter || Linter.EsLint,
unitTestRunner: options.unitTestRunner || 'jest',
e2eTestRunner: options.e2eTestRunner || 'cypress',
style: options.style || 'css',
appProjectRoot, appProjectRoot,
e2eProjectRoot,
e2eProjectName, e2eProjectName,
parsedTags, e2eProjectRoot,
e2eTestRunner: options.e2eTestRunner || 'cypress',
fileName, fileName,
linter: options.linter || Linter.EsLint,
name,
outputPath,
parsedTags,
projectName: appProjectName,
style: options.style || 'css',
styledModule, styledModule,
unitTestRunner: options.unitTestRunner || 'jest',
}; };
} }

View File

@ -4,7 +4,7 @@ import { NormalizedSchema } from './normalize-options';
export function showPossibleWarnings(tree: Tree, options: NormalizedSchema) { export function showPossibleWarnings(tree: Tree, options: NormalizedSchema) {
if (options.style === '@emotion/styled' && options.appDir) { if (options.style === '@emotion/styled' && options.appDir) {
logger.warn( logger.warn(
`Emotion may not work with the experimental appDir layout. See: https://beta.nextjs.org/docs/styling/css-in-js` `Emotion may not work with the App Router. See: https://beta.nextjs.org/docs/styling/css-in-js`
); );
} }
} }

View File

@ -121,9 +121,9 @@
}, },
"appDir": { "appDir": {
"type": "boolean", "type": "boolean",
"default": false, "default": true,
"description": "Enable experimental app directory for the project", "description": "Enable the App Router for this project.",
"x-prompt": "Do you want to use experimental app/ in this project?" "x-prompt": "Would you like to use the App Router (recommended)?"
}, },
"rootProject": { "rootProject": {
"description": "Create an application at the root of the workspace.", "description": "Create an application at the root of the workspace.",

View File

@ -1,10 +1,10 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const nextVersion = '13.3.0'; export const nextVersion = '13.4.1';
export const eslintConfigNextVersion = '13.3.0'; export const eslintConfigNextVersion = '13.4.1';
export const sassVersion = '1.61.0'; export const sassVersion = '1.62.1';
export const lessLoader = '11.1.0'; export const lessLoader = '11.1.0';
export const stylusLoader = '7.1.0'; export const stylusLoader = '7.1.0';
export const emotionServerVersion = '11.10.0'; export const emotionServerVersion = '11.11.0';
export const babelPluginStyledComponentsVersion = '1.10.7'; export const babelPluginStyledComponentsVersion = '1.10.7';
export const tsLibVersion = '^2.3.0'; export const tsLibVersion = '^2.3.0';

View File

@ -73,9 +73,9 @@
"enum": ["express", "koa", "fastify", "nest", "none"] "enum": ["express", "koa", "fastify", "nest", "none"]
}, },
"nextAppDir": { "nextAppDir": {
"description": "Enable experimental app directory for the project", "description": "Enable the App Router for this project.",
"type": "boolean", "type": "boolean",
"default": false "default": true
}, },
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",

View File

@ -48,7 +48,7 @@ describe('preset', () => {
style: 'css', style: 'css',
linter: 'eslint', linter: 'eslint',
}); });
expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true); expect(tree.exists('/apps/proj/app/page.tsx')).toBe(true);
}); });
it(`should create files (preset = ${Preset.Express})`, async () => { it(`should create files (preset = ${Preset.Express})`, async () => {

View File

@ -85,9 +85,9 @@
"default": false "default": false
}, },
"nextAppDir": { "nextAppDir": {
"description": "Enable experimental app/ for the project", "description": "Enable the App Router for this project.",
"type": "boolean", "type": "boolean",
"default": false "default": true
}, },
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",