From c43b22dc887b10d527aa3a11e7f7af2a9961e7f8 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Wed, 17 Jan 2024 11:22:51 -0700 Subject: [PATCH] feat(nextjs): Standalone projects now default to src (#21010) --- docs/generated/cli/create-nx-workspace.md | 6 ++ .../packages/next/generators/application.json | 6 ++ .../nx/documents/create-nx-workspace.md | 6 ++ .../packages/workspace/generators/new.json | 5 ++ .../packages/workspace/generators/preset.json | 5 ++ e2e/next-core/src/next-structure.test.ts | 6 +- e2e/next-core/src/next.test.ts | 6 +- .../src/next-component-tests.test.ts | 4 +- e2e/next-extensions/src/next-styles.test.ts | 2 +- e2e/nx-misc/src/workspace.test.ts | 9 ++- e2e/utils/create-project-utils.ts | 6 ++ .../src/create-nx-workspace.test.ts | 9 ++- .../bin/create-nx-workspace.ts | 40 +++++++++++- .../application/application.spec.ts | 64 ++++++++++--------- .../specs/__fileName__.spec.tsx__tmpl__ | 6 +- .../lib/create-application-files.ts | 20 +++--- .../application/lib/normalize-options.ts | 2 + .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 6 ++ .../convert-to-monorepo.spec.ts | 3 +- .../src/generators/new/generate-preset.ts | 1 + packages/workspace/src/generators/new/new.ts | 1 + .../workspace/src/generators/new/schema.json | 5 ++ .../src/generators/preset/preset.spec.ts | 2 +- .../workspace/src/generators/preset/preset.ts | 2 + .../src/generators/preset/schema.d.ts | 1 + .../src/generators/preset/schema.json | 5 ++ 27 files changed, 172 insertions(+), 57 deletions(-) diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index 202c214eda..b27439a064 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -119,6 +119,12 @@ Type: `boolean` Enable the App Router for Next.js +### nextSrcDir + +Type: `boolean` + +Generate a 'src/' directory for Next.js + ### nxCloud Type: `boolean` diff --git a/docs/generated/packages/next/generators/application.json b/docs/generated/packages/next/generators/application.json index 652508f2eb..ec6cee0951 100644 --- a/docs/generated/packages/next/generators/application.json +++ b/docs/generated/packages/next/generators/application.json @@ -124,6 +124,12 @@ "description": "Enable the App Router for this project.", "x-prompt": "Would you like to use the App Router (recommended)?" }, + "src": { + "type": "boolean", + "default": true, + "description": "Generate a `src` directory for the project.", + "x-prompt": "Would you like to use `src/` directory?" + }, "rootProject": { "description": "Create an application at the root of the workspace.", "type": "boolean", diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index 202c214eda..b27439a064 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -119,6 +119,12 @@ Type: `boolean` Enable the App Router for Next.js +### nextSrcDir + +Type: `boolean` + +Generate a 'src/' directory for Next.js + ### nxCloud Type: `boolean` diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index ead20a6283..64b534f755 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -65,6 +65,11 @@ "type": "boolean", "default": true }, + "nextSrcDir": { + "description": "Generate a `src` directory for this project.", + "type": "boolean", + "default": true + }, "e2eTestRunner": { "description": "The tool to use for running e2e tests.", "type": "string", diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index 4c8e09ab65..35a7d51a28 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -82,6 +82,11 @@ "type": "boolean", "default": true }, + "nextSrcDir": { + "description": "Generate a `src` directory for this project.", + "type": "boolean", + "default": true + }, "e2eTestRunner": { "description": "The tool to use for running e2e tests.", "type": "string", diff --git a/e2e/next-core/src/next-structure.test.ts b/e2e/next-core/src/next-structure.test.ts index 81ff088151..95de4159c2 100644 --- a/e2e/next-core/src/next-structure.test.ts +++ b/e2e/next-core/src/next-structure.test.ts @@ -45,7 +45,7 @@ describe('Next.js Apps Libs', () => { const buildableLib = uniq('buildablelib'); runCLI( - `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false --src=false` + `generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false` ); runCLI(`generate @nx/next:lib ${nextLib} --no-interactive`); runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); @@ -100,11 +100,11 @@ describe('Next.js Apps Libs', () => { ` ); - const mainPath = `packages/${appName}/pages/index.tsx`; + const mainPath = `packages/${appName}/src/pages/index.tsx`; const content = readFile(mainPath); updateFile( - `packages/${appName}/pages/api/hello.ts`, + `packages/${appName}/src/pages/api/hello.ts`, ` import { jsLibAsync } from '@${proj}/${jsLib}'; diff --git a/e2e/next-core/src/next.test.ts b/e2e/next-core/src/next.test.ts index fe6353f5ff..0db2b35ede 100644 --- a/e2e/next-core/src/next.test.ts +++ b/e2e/next-core/src/next.test.ts @@ -42,7 +42,7 @@ describe('Next.js Applications', () => { // check files are generated without the layout directory ("apps/") and // using the project name as the directory when no directory is provided - checkFilesExist(`${appName}/app/page.tsx`); + checkFilesExist(`${appName}/src/app/page.tsx`); // check build works expect(runCLI(`build ${appName}`)).toContain( `Successfully ran target build for project ${appName}` @@ -179,7 +179,7 @@ describe('Next.js Applications', () => { `generate @nx/next:app ${appName} --no-interactive --js --appDir=false` ); - checkFilesExist(`apps/${appName}/pages/index.js`); + checkFilesExist(`apps/${appName}/src/pages/index.js`); await checkApp(appName, { checkUnitTest: true, @@ -195,7 +195,7 @@ describe('Next.js Applications', () => { `generate @nx/next:lib ${libName} --no-interactive --style=none --js` ); - const mainPath = `apps/${appName}/pages/index.js`; + const mainPath = `apps/${appName}/src/pages/index.js`; updateFile( mainPath, `import '@${proj}/${libName}';\n` + readFile(mainPath) diff --git a/e2e/next-extensions/src/next-component-tests.test.ts b/e2e/next-extensions/src/next-component-tests.test.ts index 3f3f10f6b0..72d29e6e54 100644 --- a/e2e/next-extensions/src/next-component-tests.test.ts +++ b/e2e/next-extensions/src/next-component-tests.test.ts @@ -108,7 +108,9 @@ function addBabelSupport(path: string) { } function createAppWithCt(appName: string) { - runCLI(`generate @nx/next:app ${appName} --no-interactive --appDir=false`); + runCLI( + `generate @nx/next:app ${appName} --no-interactive --appDir=false --src=false` + ); runCLI( `generate @nx/next:component button --project=${appName} --directory=components --flat --no-interactive` ); diff --git a/e2e/next-extensions/src/next-styles.test.ts b/e2e/next-extensions/src/next-styles.test.ts index 2f6e0e74e2..e9e4a88ab6 100644 --- a/e2e/next-extensions/src/next-styles.test.ts +++ b/e2e/next-extensions/src/next-styles.test.ts @@ -22,7 +22,7 @@ describe('Next.js Styles', () => { const lessApp = uniq('app'); runCLI( - `generate @nx/next:app ${lessApp} --no-interactive --style=less --appDir=false` + `generate @nx/next:app ${lessApp} --no-interactive --style=less --appDir=false --src=false` ); await checkApp(lessApp, { diff --git a/e2e/nx-misc/src/workspace.test.ts b/e2e/nx-misc/src/workspace.test.ts index aeab6f3c57..0fc6620ded 100644 --- a/e2e/nx-misc/src/workspace.test.ts +++ b/e2e/nx-misc/src/workspace.test.ts @@ -12,6 +12,7 @@ import { getPackageManagerCommand, getSelectedPackageManager, runCommand, + runE2ETests, } from '@nx/e2e/utils'; import { join } from 'path'; @@ -41,7 +42,9 @@ describe('@nx/workspace:convert-to-monorepo', () => { expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint e2e`)).not.toThrow(); - expect(() => runCLI(`e2e e2e`)).not.toThrow(); + if (runE2ETests()) { + expect(() => runCLI(`e2e e2e`)).not.toThrow(); + } }); it('should be convert a standalone vite and playwright react project to a monorepo', async () => { @@ -61,7 +64,9 @@ describe('@nx/workspace:convert-to-monorepo', () => { expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint e2e`)).not.toThrow(); - expect(() => runCLI(`e2e e2e`)).not.toThrow(); + if (runE2ETests()) { + expect(() => runCLI(`e2e e2e`)).not.toThrow(); + } }); }); diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 4722f14af9..0a557c678c 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -229,6 +229,7 @@ export function runCreateWorkspace( standaloneApi, docker, nextAppDir, + nextSrcDir, e2eTestRunner, ssr, framework, @@ -247,6 +248,7 @@ export function runCreateWorkspace( routing?: boolean; docker?: boolean; nextAppDir?: boolean; + nextSrcDir?: boolean; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; ssr?: boolean; framework?: string; @@ -275,6 +277,10 @@ export function runCreateWorkspace( command += ` --nextAppDir=${nextAppDir}`; } + if (nextSrcDir !== undefined) { + command += ` --nextSrcDir=${nextSrcDir}`; + } + if (docker !== undefined) { command += ` --docker=${docker}`; } diff --git a/e2e/workspace-create/src/create-nx-workspace.test.ts b/e2e/workspace-create/src/create-nx-workspace.test.ts index 07f564a0aa..a621c57b4e 100644 --- a/e2e/workspace-create/src/create-nx-workspace.test.ts +++ b/e2e/workspace-create/src/create-nx-workspace.test.ts @@ -223,11 +223,12 @@ describe('create-nx-workspace', () => { style: 'css', appName, nextAppDir: false, + nextSrcDir: true, packageManager, e2eTestRunner: 'none', }); - checkFilesExist(`apps/${appName}/pages/index.tsx`); + checkFilesExist(`apps/${appName}/src/pages/index.tsx`); expectNoAngularDevkit(); expectCodeIsFormatted(); @@ -240,12 +241,13 @@ describe('create-nx-workspace', () => { preset: 'nextjs-standalone', style: 'css', nextAppDir: true, + nextSrcDir: true, appName, packageManager, e2eTestRunner: 'none', }); - checkFilesExist('app/page.tsx'); + checkFilesExist('src/app/page.tsx'); expectNoAngularDevkit(); expectCodeIsFormatted(); @@ -258,12 +260,13 @@ describe('create-nx-workspace', () => { preset: 'nextjs-standalone', style: 'css', nextAppDir: false, + nextSrcDir: true, appName, packageManager, e2eTestRunner: 'none', }); - checkFilesExist('pages/index.tsx'); + checkFilesExist('src/pages/index.tsx'); expectNoAngularDevkit(); expectCodeIsFormatted(); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 3942ba7f22..c513910ba7 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -49,6 +49,7 @@ interface ReactArguments extends BaseArguments { style: string; bundler: 'webpack' | 'vite' | 'rspack'; nextAppDir: boolean; + nextSrcDir: boolean; e2eTestRunner: 'none' | 'cypress' | 'playwright'; } @@ -164,6 +165,10 @@ export const commandsObject: yargs.Argv = yargs describe: chalk.dim`Enable the App Router for Next.js`, type: 'boolean', }) + .option('nextSrcDir', { + describe: chalk.dim`Generate a 'src/' directory for Next.js`, + type: 'boolean', + }) .option('e2eTestRunner', { describe: chalk.dim`Test runner to use for end to end (E2E) tests.`, choices: ['cypress', 'playwright', 'none'], @@ -512,6 +517,7 @@ async function determineReactOptions( let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined; let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; let nextAppDir = false; + let nextSrcDir = false; if (parsedArgs.preset && parsedArgs.preset !== Preset.React) { preset = parsedArgs.preset; @@ -563,6 +569,7 @@ async function determineReactOptions( e2eTestRunner = await determineE2eTestRunner(parsedArgs); } else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) { nextAppDir = await determineNextAppDir(parsedArgs); + nextSrcDir = await determineNextSrcDir(parsedArgs); e2eTestRunner = await determineE2eTestRunner(parsedArgs); } @@ -614,7 +621,15 @@ async function determineReactOptions( style = reply.style; } - return { preset, style, appName, bundler, nextAppDir, e2eTestRunner }; + return { + preset, + style, + appName, + bundler, + nextAppDir, + nextSrcDir, + e2eTestRunner, + }; } async function determineVueOptions( @@ -1066,6 +1081,29 @@ async function determineNextAppDir( return reply.nextAppDir === 'Yes'; } +async function determineNextSrcDir( + parsedArgs: yargs.Arguments +): Promise { + if (parsedArgs.nextSrcDir !== undefined) return parsedArgs.nextSrcDir; + const reply = await enquirer.prompt<{ nextSrcDir: 'Yes' | 'No' }>([ + { + name: 'nextSrcDir', + message: 'Would you like to use the src/ directory?', + type: 'autocomplete', + choices: [ + { + name: 'Yes', + }, + { + name: 'No', + }, + ], + initial: 'Yes' as any, + }, + ]); + return reply.nextSrcDir === 'Yes'; +} + async function determineVueFramework( parsedArgs: yargs.Arguments ): Promise<'none' | 'nuxt'> { diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 0d8569d018..6d836461db 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -82,12 +82,12 @@ describe('app', () => { `../dist/${name}/.next/types/**/*.ts`, 'next-env.d.ts', ]); - expect(tree.exists(`${name}/pages/styles.css`)).toBeFalsy(); - expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); - expect(tree.exists(`${name}/app/page.tsx`)).toBeTruthy(); - expect(tree.exists(`${name}/app/layout.tsx`)).toBeTruthy(); - expect(tree.exists(`${name}/app/api/hello/route.ts`)).toBeTruthy(); - expect(tree.exists(`${name}/app/page.module.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/pages/styles.css`)).toBeFalsy(); + expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.tsx`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/layout.tsx`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/api/hello/route.ts`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.module.css`)).toBeTruthy(); expect(tree.exists(`${name}/public/favicon.ico`)).toBeTruthy(); }); @@ -123,7 +123,7 @@ describe('app', () => { projectNameAndRootFormat: 'as-provided', }); - const content = tree.read('app/page.tsx').toString(); + const content = tree.read('src/app/page.tsx').toString(); expect(content).not.toContain('import styles from'); expect(content).not.toContain('const StyledPage'); @@ -138,6 +138,7 @@ describe('app', () => { name, style: 'css', appDir: false, + src: false, projectNameAndRootFormat: 'as-provided', }); expect(tree.exists(`${name}/tsconfig.json`)).toBeTruthy(); @@ -166,6 +167,7 @@ describe('app', () => { name, style: 'none', appDir: false, + src: false, projectNameAndRootFormat: 'as-provided', }); @@ -186,12 +188,12 @@ describe('app', () => { projectNameAndRootFormat: 'as-provided', }); - expect(tree.exists(`${name}/app/page.module.scss`)).toBeTruthy(); - expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.module.scss`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy(); - const indexContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).toContain(`import styles from './page.module.scss'`); - expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "import './global.css'; @@ -225,12 +227,12 @@ describe('app', () => { projectNameAndRootFormat: 'as-provided', }); - expect(tree.exists(`${name}/app/page.module.less`)).toBeTruthy(); - expect(tree.exists(`${name}/app/global.less`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.module.less`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/global.less`)).toBeTruthy(); - const indexContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).toContain(`import styles from './page.module.less'`); - expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "import './global.less'; @@ -265,14 +267,14 @@ describe('app', () => { }); expect( - tree.exists(`${name}/app/page.module.styled-components`) + tree.exists(`${name}/src/app/page.module.styled-components`) ).toBeFalsy(); - expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy(); - const indexContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from 'styled-components'`); - expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "import './global.css'; import { StyledComponentsRegistry } from './registry'; @@ -297,7 +299,7 @@ describe('app', () => { } " `); - expect(tree.read(`${name}/app/registry.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/registry.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "'use client'; @@ -348,14 +350,14 @@ describe('app', () => { }); expect( - tree.exists(`${name}/app/page.module.styled-components`) + tree.exists(`${name}/src/app/page.module.styled-components`) ).toBeFalsy(); - expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy(); - const indexContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from '@emotion/styled'`); - expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "import './global.css'; @@ -406,17 +408,17 @@ describe('app', () => { projectNameAndRootFormat: 'as-provided', }); - const indexContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).toMatchSnapshot(); - expect(tree.exists(`${name}/app/page.module.styled-jsx`)).toBeFalsy(); - expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.module.styled-jsx`)).toBeFalsy(); + expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy(); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).not.toContain( `import styled from 'styled-components'` ); - expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "import './global.css'; import { StyledJsxRegistry } from './registry'; @@ -436,7 +438,7 @@ describe('app', () => { } " `); - expect(tree.read(`${name}/app/registry.tsx`, 'utf-8')) + expect(tree.read(`${name}/src/app/registry.tsx`, 'utf-8')) .toMatchInlineSnapshot(` "'use client'; @@ -588,7 +590,7 @@ describe('app', () => { projectNameAndRootFormat: 'as-provided', }); - const appContent = tree.read(`${name}/app/page.tsx`, 'utf-8'); + const appContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(appContent).not.toMatch(/extends Component/); }); @@ -709,7 +711,7 @@ describe('app', () => { js: true, }); - expect(tree.exists(`${name}/app/page.js`)).toBeTruthy(); + expect(tree.exists(`${name}/src/app/page.js`)).toBeTruthy(); expect(tree.exists(`${name}/specs/index.spec.js`)).toBeTruthy(); expect(tree.exists(`${name}/index.d.js`)).toBeFalsy(); expect(tree.exists(`${name}/index.d.ts`)).toBeFalsy(); diff --git a/packages/next/src/generators/application/files/common/specs/__fileName__.spec.tsx__tmpl__ b/packages/next/src/generators/application/files/common/specs/__fileName__.spec.tsx__tmpl__ index 42c94022af..f364a500a6 100644 --- a/packages/next/src/generators/application/files/common/specs/__fileName__.spec.tsx__tmpl__ +++ b/packages/next/src/generators/application/files/common/specs/__fileName__.spec.tsx__tmpl__ @@ -1,8 +1,10 @@ import React from 'react'; import { render } from '@testing-library/react'; - +<% if (src) { %> +import Index from '../src/pages/index'; +<% } else { %> import Index from '../pages/index'; - +<% } %> describe('Index', () => { it('should render successfully', () => { const { baseElement } = render(); diff --git a/packages/next/src/generators/application/lib/create-application-files.ts b/packages/next/src/generators/application/lib/create-application-files.ts index 0a0766eef3..47806a3fd2 100644 --- a/packages/next/src/generators/application/lib/create-application-files.ts +++ b/packages/next/src/generators/application/lib/create-application-files.ts @@ -48,6 +48,10 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { stylesExt: options.style === 'less' ? options.style : 'css', }; + const generatedAppFilePath = options.src + ? join(options.appProjectRoot, 'src') + : options.appProjectRoot; + generateFiles( host, join(__dirname, '../files/common'), @@ -59,7 +63,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { generateFiles( host, join(__dirname, '../files/app'), - join(options.appProjectRoot, 'app'), + join(generatedAppFilePath, 'app'), templateVariables ); @@ -76,21 +80,21 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { generateFiles( host, join(__dirname, '../files/app-styled-components'), - join(options.appProjectRoot, 'app'), + join(generatedAppFilePath, 'app'), templateVariables ); } else if (options.style === 'styled-jsx') { generateFiles( host, join(__dirname, '../files/app-styled-jsx'), - join(options.appProjectRoot, 'app'), + join(generatedAppFilePath, 'app'), templateVariables ); } else { generateFiles( host, join(__dirname, '../files/app-default-layout'), - join(options.appProjectRoot, 'app'), + join(generatedAppFilePath, 'app'), templateVariables ); } @@ -98,7 +102,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { generateFiles( host, join(__dirname, '../files/pages'), - join(options.appProjectRoot, 'pages'), + join(generatedAppFilePath, 'pages'), templateVariables ); } @@ -151,16 +155,16 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { if (options.styledModule) { if (options.appDir) { - host.delete(`${options.appProjectRoot}/app/page.module.${options.style}`); + host.delete(`${generatedAppFilePath}/app/page.module.${options.style}`); } else { host.delete( - `${options.appProjectRoot}/pages/${options.fileName}.module.${options.style}` + `${generatedAppFilePath}/pages/${options.fileName}.module.${options.style}` ); } } if (options.style !== 'styled-components') { - host.delete(`${options.appProjectRoot}/pages/_document.tsx`); + host.delete(`${generatedAppFilePath}/pages/_document.tsx`); } if (options.js) { diff --git a/packages/next/src/generators/application/lib/normalize-options.ts b/packages/next/src/generators/application/lib/normalize-options.ts index d4b1850293..b956a15f29 100644 --- a/packages/next/src/generators/application/lib/normalize-options.ts +++ b/packages/next/src/generators/application/lib/normalize-options.ts @@ -53,6 +53,7 @@ export async function normalizeOptions( const fileName = 'index'; const appDir = options.appDir ?? true; + const src = options.src ?? true; const styledModule = /^(css|scss|less)$/.test(options.style) ? null @@ -63,6 +64,7 @@ export async function normalizeOptions( return { ...options, appDir, + src, appProjectRoot, e2eProjectName, e2eProjectRoot, diff --git a/packages/next/src/generators/application/schema.d.ts b/packages/next/src/generators/application/schema.d.ts index a83160ae60..8174bc9f2c 100644 --- a/packages/next/src/generators/application/schema.d.ts +++ b/packages/next/src/generators/application/schema.d.ts @@ -18,5 +18,6 @@ export interface Schema { customServer?: boolean; skipPackageJson?: boolean; appDir?: boolean; + src?: boolean; rootProject?: boolean; } diff --git a/packages/next/src/generators/application/schema.json b/packages/next/src/generators/application/schema.json index 2c3a51b74d..242e9fed49 100644 --- a/packages/next/src/generators/application/schema.json +++ b/packages/next/src/generators/application/schema.json @@ -127,6 +127,12 @@ "description": "Enable the App Router for this project.", "x-prompt": "Would you like to use the App Router (recommended)?" }, + "src": { + "type": "boolean", + "default": true, + "description": "Generate a `src` directory for the project.", + "x-prompt": "Would you like to use `src/` directory?" + }, "rootProject": { "description": "Create an application at the root of the workspace.", "type": "boolean", diff --git a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts index 2420df4db2..0f1f5107a1 100644 --- a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts +++ b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.spec.ts @@ -111,6 +111,7 @@ describe('monorepo generator', () => { unitTestRunner: 'jest', e2eTestRunner: 'none', appDir: true, + src: true, linter: 'eslint', rootProject: true, }); @@ -121,7 +122,7 @@ describe('monorepo generator', () => { expect(readProjectConfiguration(tree, 'demo')).toMatchObject({ sourceRoot: 'apps/demo', }); - expect(tree.read('apps/demo/app/page.tsx', 'utf-8')).toContain('demo'); + expect(tree.read('apps/demo/src/app/page.tsx', 'utf-8')).toContain('demo'); expect(readProjectConfiguration(tree, 'util')).toMatchObject({ sourceRoot: 'libs/util/src', }); diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index 51681b0db4..5095515c18 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -70,6 +70,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { opts.docker ? `--docker=${opts.docker}` : null, opts.js ? `--js` : null, opts.nextAppDir ? '--nextAppDir=true' : '--nextAppDir=false', + opts.nextSrcDir ? '--nextSrcDir=true' : '--nextSrcDir=false', opts.packageManager ? `--packageManager=${opts.packageManager}` : null, opts.standaloneApi !== undefined ? `--standaloneApi=${opts.standaloneApi}` diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index fb45215e7a..77f9b425f1 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -26,6 +26,7 @@ interface Schema { docker?: boolean; js?: boolean; nextAppDir?: boolean; + nextSrcDir?: boolean; linter?: Linter; bundler?: 'vite' | 'webpack'; standaloneApi?: boolean; diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index 3794e31911..59ba64cf25 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -68,6 +68,11 @@ "type": "boolean", "default": true }, + "nextSrcDir": { + "description": "Generate a `src` directory for this project.", + "type": "boolean", + "default": true + }, "e2eTestRunner": { "description": "The tool to use for running e2e tests.", "type": "string", diff --git a/packages/workspace/src/generators/preset/preset.spec.ts b/packages/workspace/src/generators/preset/preset.spec.ts index 868c42211e..00d1d788b3 100644 --- a/packages/workspace/src/generators/preset/preset.spec.ts +++ b/packages/workspace/src/generators/preset/preset.spec.ts @@ -70,7 +70,7 @@ describe('preset', () => { style: 'css', linter: 'eslint', }); - expect(tree.exists('/apps/proj/app/page.tsx')).toBe(true); + expect(tree.exists('/apps/proj/src/app/page.tsx')).toBe(true); }); it(`should create files (preset = ${Preset.Express})`, async () => { diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index 196d6357db..835e579f78 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -143,6 +143,7 @@ async function createPreset(tree: Tree, options: Schema) { style: options.style, linter: options.linter, appDir: options.nextAppDir, + src: options.nextSrcDir, e2eTestRunner: options.e2eTestRunner ?? 'cypress', }); } else if (options.preset === Preset.NextJsStandalone) { @@ -155,6 +156,7 @@ async function createPreset(tree: Tree, options: Schema) { style: options.style, linter: options.linter, appDir: options.nextAppDir, + src: options.nextSrcDir, e2eTestRunner: options.e2eTestRunner ?? 'cypress', rootProject: true, }); diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index e609314a4d..13fbbfbc61 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -12,6 +12,7 @@ export interface Schema { bundler?: 'vite' | 'webpack' | 'rspack' | 'esbuild'; docker?: boolean; nextAppDir?: boolean; + nextSrcDir?: boolean; routing?: boolean; standaloneApi?: boolean; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index 2fa4d19605..8c556d5321 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -85,6 +85,11 @@ "type": "boolean", "default": true }, + "nextSrcDir": { + "description": "Generate a `src` directory for this project.", + "type": "boolean", + "default": true + }, "e2eTestRunner": { "description": "The tool to use for running e2e tests.", "type": "string",