feat(nextjs): Standalone projects now default to src (#21010)

This commit is contained in:
Nicholas Cunningham 2024-01-17 11:22:51 -07:00 committed by GitHub
parent 49cff89908
commit c43b22dc88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 172 additions and 57 deletions

View File

@ -119,6 +119,12 @@ Type: `boolean`
Enable the App Router for Next.js Enable the App Router for Next.js
### nextSrcDir
Type: `boolean`
Generate a 'src/' directory for Next.js
### nxCloud ### nxCloud
Type: `boolean` Type: `boolean`

View File

@ -124,6 +124,12 @@
"description": "Enable the App Router for this project.", "description": "Enable the App Router for this project.",
"x-prompt": "Would you like to use the App Router (recommended)?" "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": { "rootProject": {
"description": "Create an application at the root of the workspace.", "description": "Create an application at the root of the workspace.",
"type": "boolean", "type": "boolean",

View File

@ -119,6 +119,12 @@ Type: `boolean`
Enable the App Router for Next.js Enable the App Router for Next.js
### nextSrcDir
Type: `boolean`
Generate a 'src/' directory for Next.js
### nxCloud ### nxCloud
Type: `boolean` Type: `boolean`

View File

@ -65,6 +65,11 @@
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"nextSrcDir": {
"description": "Generate a `src` directory for this project.",
"type": "boolean",
"default": true
},
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",
"type": "string", "type": "string",

View File

@ -82,6 +82,11 @@
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"nextSrcDir": {
"description": "Generate a `src` directory for this project.",
"type": "boolean",
"default": true
},
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",
"type": "string", "type": "string",

View File

@ -45,7 +45,7 @@ describe('Next.js Apps Libs', () => {
const buildableLib = uniq('buildablelib'); const buildableLib = uniq('buildablelib');
runCLI( 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/next:lib ${nextLib} --no-interactive`);
runCLI(`generate @nx/js:lib ${jsLib} --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); const content = readFile(mainPath);
updateFile( updateFile(
`packages/${appName}/pages/api/hello.ts`, `packages/${appName}/src/pages/api/hello.ts`,
` `
import { jsLibAsync } from '@${proj}/${jsLib}'; import { jsLibAsync } from '@${proj}/${jsLib}';

View File

@ -42,7 +42,7 @@ describe('Next.js Applications', () => {
// check files are generated without the layout directory ("apps/") and // check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided // 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 // check build works
expect(runCLI(`build ${appName}`)).toContain( expect(runCLI(`build ${appName}`)).toContain(
`Successfully ran target build for project ${appName}` `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` `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, { await checkApp(appName, {
checkUnitTest: true, checkUnitTest: true,
@ -195,7 +195,7 @@ describe('Next.js Applications', () => {
`generate @nx/next:lib ${libName} --no-interactive --style=none --js` `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( updateFile(
mainPath, mainPath,
`import '@${proj}/${libName}';\n` + readFile(mainPath) `import '@${proj}/${libName}';\n` + readFile(mainPath)

View File

@ -108,7 +108,9 @@ function addBabelSupport(path: string) {
} }
function createAppWithCt(appName: 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( 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

@ -22,7 +22,7 @@ describe('Next.js Styles', () => {
const lessApp = uniq('app'); const lessApp = uniq('app');
runCLI( 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, { await checkApp(lessApp, {

View File

@ -12,6 +12,7 @@ import {
getPackageManagerCommand, getPackageManagerCommand,
getSelectedPackageManager, getSelectedPackageManager,
runCommand, runCommand,
runE2ETests,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import { join } from 'path'; import { join } from 'path';
@ -41,7 +42,9 @@ describe('@nx/workspace:convert-to-monorepo', () => {
expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); expect(() => runCLI(`test ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint e2e`)).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 () => { 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(`test ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); expect(() => runCLI(`lint ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint e2e`)).not.toThrow(); expect(() => runCLI(`lint e2e`)).not.toThrow();
expect(() => runCLI(`e2e e2e`)).not.toThrow(); if (runE2ETests()) {
expect(() => runCLI(`e2e e2e`)).not.toThrow();
}
}); });
}); });

View File

@ -229,6 +229,7 @@ export function runCreateWorkspace(
standaloneApi, standaloneApi,
docker, docker,
nextAppDir, nextAppDir,
nextSrcDir,
e2eTestRunner, e2eTestRunner,
ssr, ssr,
framework, framework,
@ -247,6 +248,7 @@ export function runCreateWorkspace(
routing?: boolean; routing?: boolean;
docker?: boolean; docker?: boolean;
nextAppDir?: boolean; nextAppDir?: boolean;
nextSrcDir?: boolean;
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
ssr?: boolean; ssr?: boolean;
framework?: string; framework?: string;
@ -275,6 +277,10 @@ export function runCreateWorkspace(
command += ` --nextAppDir=${nextAppDir}`; command += ` --nextAppDir=${nextAppDir}`;
} }
if (nextSrcDir !== undefined) {
command += ` --nextSrcDir=${nextSrcDir}`;
}
if (docker !== undefined) { if (docker !== undefined) {
command += ` --docker=${docker}`; command += ` --docker=${docker}`;
} }

View File

@ -223,11 +223,12 @@ describe('create-nx-workspace', () => {
style: 'css', style: 'css',
appName, appName,
nextAppDir: false, nextAppDir: false,
nextSrcDir: true,
packageManager, packageManager,
e2eTestRunner: 'none', e2eTestRunner: 'none',
}); });
checkFilesExist(`apps/${appName}/pages/index.tsx`); checkFilesExist(`apps/${appName}/src/pages/index.tsx`);
expectNoAngularDevkit(); expectNoAngularDevkit();
expectCodeIsFormatted(); expectCodeIsFormatted();
@ -240,12 +241,13 @@ describe('create-nx-workspace', () => {
preset: 'nextjs-standalone', preset: 'nextjs-standalone',
style: 'css', style: 'css',
nextAppDir: true, nextAppDir: true,
nextSrcDir: true,
appName, appName,
packageManager, packageManager,
e2eTestRunner: 'none', e2eTestRunner: 'none',
}); });
checkFilesExist('app/page.tsx'); checkFilesExist('src/app/page.tsx');
expectNoAngularDevkit(); expectNoAngularDevkit();
expectCodeIsFormatted(); expectCodeIsFormatted();
@ -258,12 +260,13 @@ describe('create-nx-workspace', () => {
preset: 'nextjs-standalone', preset: 'nextjs-standalone',
style: 'css', style: 'css',
nextAppDir: false, nextAppDir: false,
nextSrcDir: true,
appName, appName,
packageManager, packageManager,
e2eTestRunner: 'none', e2eTestRunner: 'none',
}); });
checkFilesExist('pages/index.tsx'); checkFilesExist('src/pages/index.tsx');
expectNoAngularDevkit(); expectNoAngularDevkit();
expectCodeIsFormatted(); expectCodeIsFormatted();

View File

@ -49,6 +49,7 @@ interface ReactArguments extends BaseArguments {
style: string; style: string;
bundler: 'webpack' | 'vite' | 'rspack'; bundler: 'webpack' | 'vite' | 'rspack';
nextAppDir: boolean; nextAppDir: boolean;
nextSrcDir: boolean;
e2eTestRunner: 'none' | 'cypress' | 'playwright'; e2eTestRunner: 'none' | 'cypress' | 'playwright';
} }
@ -164,6 +165,10 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
describe: chalk.dim`Enable the App Router for Next.js`, describe: chalk.dim`Enable the App Router for Next.js`,
type: 'boolean', type: 'boolean',
}) })
.option('nextSrcDir', {
describe: chalk.dim`Generate a 'src/' directory for Next.js`,
type: 'boolean',
})
.option('e2eTestRunner', { .option('e2eTestRunner', {
describe: chalk.dim`Test runner to use for end to end (E2E) tests.`, describe: chalk.dim`Test runner to use for end to end (E2E) tests.`,
choices: ['cypress', 'playwright', 'none'], choices: ['cypress', 'playwright', 'none'],
@ -512,6 +517,7 @@ async function determineReactOptions(
let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined; let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined;
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
let nextAppDir = false; let nextAppDir = false;
let nextSrcDir = false;
if (parsedArgs.preset && parsedArgs.preset !== Preset.React) { if (parsedArgs.preset && parsedArgs.preset !== Preset.React) {
preset = parsedArgs.preset; preset = parsedArgs.preset;
@ -563,6 +569,7 @@ async function determineReactOptions(
e2eTestRunner = await determineE2eTestRunner(parsedArgs); e2eTestRunner = await determineE2eTestRunner(parsedArgs);
} else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) { } else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) {
nextAppDir = await determineNextAppDir(parsedArgs); nextAppDir = await determineNextAppDir(parsedArgs);
nextSrcDir = await determineNextSrcDir(parsedArgs);
e2eTestRunner = await determineE2eTestRunner(parsedArgs); e2eTestRunner = await determineE2eTestRunner(parsedArgs);
} }
@ -614,7 +621,15 @@ async function determineReactOptions(
style = reply.style; style = reply.style;
} }
return { preset, style, appName, bundler, nextAppDir, e2eTestRunner }; return {
preset,
style,
appName,
bundler,
nextAppDir,
nextSrcDir,
e2eTestRunner,
};
} }
async function determineVueOptions( async function determineVueOptions(
@ -1066,6 +1081,29 @@ async function determineNextAppDir(
return reply.nextAppDir === 'Yes'; return reply.nextAppDir === 'Yes';
} }
async function determineNextSrcDir(
parsedArgs: yargs.Arguments<ReactArguments>
): Promise<boolean> {
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( async function determineVueFramework(
parsedArgs: yargs.Arguments<VueArguments> parsedArgs: yargs.Arguments<VueArguments>
): Promise<'none' | 'nuxt'> { ): Promise<'none' | 'nuxt'> {

View File

@ -82,12 +82,12 @@ describe('app', () => {
`../dist/${name}/.next/types/**/*.ts`, `../dist/${name}/.next/types/**/*.ts`,
'next-env.d.ts', 'next-env.d.ts',
]); ]);
expect(tree.exists(`${name}/pages/styles.css`)).toBeFalsy(); expect(tree.exists(`${name}/src/pages/styles.css`)).toBeFalsy();
expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy();
expect(tree.exists(`${name}/app/page.tsx`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/page.tsx`)).toBeTruthy();
expect(tree.exists(`${name}/app/layout.tsx`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/layout.tsx`)).toBeTruthy();
expect(tree.exists(`${name}/app/api/hello/route.ts`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/api/hello/route.ts`)).toBeTruthy();
expect(tree.exists(`${name}/app/page.module.css`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/page.module.css`)).toBeTruthy();
expect(tree.exists(`${name}/public/favicon.ico`)).toBeTruthy(); expect(tree.exists(`${name}/public/favicon.ico`)).toBeTruthy();
}); });
@ -123,7 +123,7 @@ describe('app', () => {
projectNameAndRootFormat: 'as-provided', 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('import styles from');
expect(content).not.toContain('const StyledPage'); expect(content).not.toContain('const StyledPage');
@ -138,6 +138,7 @@ describe('app', () => {
name, name,
style: 'css', style: 'css',
appDir: false, appDir: false,
src: false,
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
}); });
expect(tree.exists(`${name}/tsconfig.json`)).toBeTruthy(); expect(tree.exists(`${name}/tsconfig.json`)).toBeTruthy();
@ -166,6 +167,7 @@ describe('app', () => {
name, name,
style: 'none', style: 'none',
appDir: false, appDir: false,
src: false,
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
}); });
@ -186,12 +188,12 @@ describe('app', () => {
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
}); });
expect(tree.exists(`${name}/app/page.module.scss`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/page.module.scss`)).toBeTruthy();
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).toContain(`import styles from './page.module.scss'`); 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(` .toMatchInlineSnapshot(`
"import './global.css'; "import './global.css';
@ -225,12 +227,12 @@ describe('app', () => {
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
}); });
expect(tree.exists(`${name}/app/page.module.less`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/page.module.less`)).toBeTruthy();
expect(tree.exists(`${name}/app/global.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(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(` .toMatchInlineSnapshot(`
"import './global.less'; "import './global.less';
@ -265,14 +267,14 @@ describe('app', () => {
}); });
expect( expect(
tree.exists(`${name}/app/page.module.styled-components`) tree.exists(`${name}/src/app/page.module.styled-components`)
).toBeFalsy(); ).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).not.toContain(`import styles from './page.module`);
expect(indexContent).toContain(`import styled from 'styled-components'`); 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(` .toMatchInlineSnapshot(`
"import './global.css'; "import './global.css';
import { StyledComponentsRegistry } from './registry'; 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(` .toMatchInlineSnapshot(`
"'use client'; "'use client';
@ -348,14 +350,14 @@ describe('app', () => {
}); });
expect( expect(
tree.exists(`${name}/app/page.module.styled-components`) tree.exists(`${name}/src/app/page.module.styled-components`)
).toBeFalsy(); ).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).not.toContain(`import styles from './page.module`);
expect(indexContent).toContain(`import styled from '@emotion/styled'`); 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(` .toMatchInlineSnapshot(`
"import './global.css'; "import './global.css';
@ -406,17 +408,17 @@ describe('app', () => {
projectNameAndRootFormat: 'as-provided', 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(indexContent).toMatchSnapshot();
expect(tree.exists(`${name}/app/page.module.styled-jsx`)).toBeFalsy(); expect(tree.exists(`${name}/src/app/page.module.styled-jsx`)).toBeFalsy();
expect(tree.exists(`${name}/app/global.css`)).toBeTruthy(); expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy();
expect(indexContent).not.toContain(`import styles from './page.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'`
); );
expect(tree.read(`${name}/app/layout.tsx`, 'utf-8')) expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"import './global.css'; "import './global.css';
import { StyledJsxRegistry } from './registry'; 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(` .toMatchInlineSnapshot(`
"'use client'; "'use client';
@ -588,7 +590,7 @@ describe('app', () => {
projectNameAndRootFormat: 'as-provided', 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/); expect(appContent).not.toMatch(/extends Component/);
}); });
@ -709,7 +711,7 @@ describe('app', () => {
js: true, 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}/specs/index.spec.js`)).toBeTruthy();
expect(tree.exists(`${name}/index.d.js`)).toBeFalsy(); expect(tree.exists(`${name}/index.d.js`)).toBeFalsy();
expect(tree.exists(`${name}/index.d.ts`)).toBeFalsy(); expect(tree.exists(`${name}/index.d.ts`)).toBeFalsy();

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
<% if (src) { %>
import Index from '../src/pages/index';
<% } else { %>
import Index from '../pages/index'; import Index from '../pages/index';
<% } %>
describe('Index', () => { describe('Index', () => {
it('should render successfully', () => { it('should render successfully', () => {
const { baseElement } = render(<Index />); const { baseElement } = render(<Index />);

View File

@ -48,6 +48,10 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
stylesExt: options.style === 'less' ? options.style : 'css', stylesExt: options.style === 'less' ? options.style : 'css',
}; };
const generatedAppFilePath = options.src
? join(options.appProjectRoot, 'src')
: options.appProjectRoot;
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/common'), join(__dirname, '../files/common'),
@ -59,7 +63,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/app'), join(__dirname, '../files/app'),
join(options.appProjectRoot, 'app'), join(generatedAppFilePath, 'app'),
templateVariables templateVariables
); );
@ -76,21 +80,21 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/app-styled-components'), join(__dirname, '../files/app-styled-components'),
join(options.appProjectRoot, 'app'), join(generatedAppFilePath, 'app'),
templateVariables templateVariables
); );
} else if (options.style === 'styled-jsx') { } else if (options.style === 'styled-jsx') {
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/app-styled-jsx'), join(__dirname, '../files/app-styled-jsx'),
join(options.appProjectRoot, 'app'), join(generatedAppFilePath, 'app'),
templateVariables templateVariables
); );
} else { } else {
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/app-default-layout'), join(__dirname, '../files/app-default-layout'),
join(options.appProjectRoot, 'app'), join(generatedAppFilePath, 'app'),
templateVariables templateVariables
); );
} }
@ -98,7 +102,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
generateFiles( generateFiles(
host, host,
join(__dirname, '../files/pages'), join(__dirname, '../files/pages'),
join(options.appProjectRoot, 'pages'), join(generatedAppFilePath, 'pages'),
templateVariables templateVariables
); );
} }
@ -151,16 +155,16 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
if (options.styledModule) { if (options.styledModule) {
if (options.appDir) { if (options.appDir) {
host.delete(`${options.appProjectRoot}/app/page.module.${options.style}`); host.delete(`${generatedAppFilePath}/app/page.module.${options.style}`);
} else { } else {
host.delete( host.delete(
`${options.appProjectRoot}/pages/${options.fileName}.module.${options.style}` `${generatedAppFilePath}/pages/${options.fileName}.module.${options.style}`
); );
} }
} }
if (options.style !== 'styled-components') { if (options.style !== 'styled-components') {
host.delete(`${options.appProjectRoot}/pages/_document.tsx`); host.delete(`${generatedAppFilePath}/pages/_document.tsx`);
} }
if (options.js) { if (options.js) {

View File

@ -53,6 +53,7 @@ export async function normalizeOptions(
const fileName = 'index'; const fileName = 'index';
const appDir = options.appDir ?? true; const appDir = options.appDir ?? true;
const src = options.src ?? true;
const styledModule = /^(css|scss|less)$/.test(options.style) const styledModule = /^(css|scss|less)$/.test(options.style)
? null ? null
@ -63,6 +64,7 @@ export async function normalizeOptions(
return { return {
...options, ...options,
appDir, appDir,
src,
appProjectRoot, appProjectRoot,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,

View File

@ -18,5 +18,6 @@ export interface Schema {
customServer?: boolean; customServer?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;
appDir?: boolean; appDir?: boolean;
src?: boolean;
rootProject?: boolean; rootProject?: boolean;
} }

View File

@ -127,6 +127,12 @@
"description": "Enable the App Router for this project.", "description": "Enable the App Router for this project.",
"x-prompt": "Would you like to use the App Router (recommended)?" "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": { "rootProject": {
"description": "Create an application at the root of the workspace.", "description": "Create an application at the root of the workspace.",
"type": "boolean", "type": "boolean",

View File

@ -111,6 +111,7 @@ describe('monorepo generator', () => {
unitTestRunner: 'jest', unitTestRunner: 'jest',
e2eTestRunner: 'none', e2eTestRunner: 'none',
appDir: true, appDir: true,
src: true,
linter: 'eslint', linter: 'eslint',
rootProject: true, rootProject: true,
}); });
@ -121,7 +122,7 @@ describe('monorepo generator', () => {
expect(readProjectConfiguration(tree, 'demo')).toMatchObject({ expect(readProjectConfiguration(tree, 'demo')).toMatchObject({
sourceRoot: 'apps/demo', 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({ expect(readProjectConfiguration(tree, 'util')).toMatchObject({
sourceRoot: 'libs/util/src', sourceRoot: 'libs/util/src',
}); });

View File

@ -70,6 +70,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) {
opts.docker ? `--docker=${opts.docker}` : null, opts.docker ? `--docker=${opts.docker}` : null,
opts.js ? `--js` : null, opts.js ? `--js` : null,
opts.nextAppDir ? '--nextAppDir=true' : '--nextAppDir=false', opts.nextAppDir ? '--nextAppDir=true' : '--nextAppDir=false',
opts.nextSrcDir ? '--nextSrcDir=true' : '--nextSrcDir=false',
opts.packageManager ? `--packageManager=${opts.packageManager}` : null, opts.packageManager ? `--packageManager=${opts.packageManager}` : null,
opts.standaloneApi !== undefined opts.standaloneApi !== undefined
? `--standaloneApi=${opts.standaloneApi}` ? `--standaloneApi=${opts.standaloneApi}`

View File

@ -26,6 +26,7 @@ interface Schema {
docker?: boolean; docker?: boolean;
js?: boolean; js?: boolean;
nextAppDir?: boolean; nextAppDir?: boolean;
nextSrcDir?: boolean;
linter?: Linter; linter?: Linter;
bundler?: 'vite' | 'webpack'; bundler?: 'vite' | 'webpack';
standaloneApi?: boolean; standaloneApi?: boolean;

View File

@ -68,6 +68,11 @@
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"nextSrcDir": {
"description": "Generate a `src` directory for this project.",
"type": "boolean",
"default": true
},
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",
"type": "string", "type": "string",

View File

@ -70,7 +70,7 @@ describe('preset', () => {
style: 'css', style: 'css',
linter: 'eslint', 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 () => { it(`should create files (preset = ${Preset.Express})`, async () => {

View File

@ -143,6 +143,7 @@ async function createPreset(tree: Tree, options: Schema) {
style: options.style, style: options.style,
linter: options.linter, linter: options.linter,
appDir: options.nextAppDir, appDir: options.nextAppDir,
src: options.nextSrcDir,
e2eTestRunner: options.e2eTestRunner ?? 'cypress', e2eTestRunner: options.e2eTestRunner ?? 'cypress',
}); });
} else if (options.preset === Preset.NextJsStandalone) { } else if (options.preset === Preset.NextJsStandalone) {
@ -155,6 +156,7 @@ async function createPreset(tree: Tree, options: Schema) {
style: options.style, style: options.style,
linter: options.linter, linter: options.linter,
appDir: options.nextAppDir, appDir: options.nextAppDir,
src: options.nextSrcDir,
e2eTestRunner: options.e2eTestRunner ?? 'cypress', e2eTestRunner: options.e2eTestRunner ?? 'cypress',
rootProject: true, rootProject: true,
}); });

View File

@ -12,6 +12,7 @@ export interface Schema {
bundler?: 'vite' | 'webpack' | 'rspack' | 'esbuild'; bundler?: 'vite' | 'webpack' | 'rspack' | 'esbuild';
docker?: boolean; docker?: boolean;
nextAppDir?: boolean; nextAppDir?: boolean;
nextSrcDir?: boolean;
routing?: boolean; routing?: boolean;
standaloneApi?: boolean; standaloneApi?: boolean;
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';

View File

@ -85,6 +85,11 @@
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"nextSrcDir": {
"description": "Generate a `src` directory for this project.",
"type": "boolean",
"default": true
},
"e2eTestRunner": { "e2eTestRunner": {
"description": "The tool to use for running e2e tests.", "description": "The tool to use for running e2e tests.",
"type": "string", "type": "string",