diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index d88c4a39ad..008b241dd1 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -17,38 +17,39 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n ## Options -| Option | Type | Description | -| -------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--allPrompts`, `--a` | boolean | Show all prompts. (Default: `false`) | -| `--appName` | string | The name of the app when using a monorepo with certain stacks. | -| `--bundler` | string | Bundler to be used to build the app. | -| `--commit.email` | string | E-mail of the committer. | -| `--commit.message` | string | Commit message. (Default: `Initial commit`) | -| `--commit.name` | string | Name of the committer. | -| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) | -| `--docker` | boolean | Generate a Dockerfile for the Node API. | -| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. | -| `--formatter` | string | Code formatter to use. | -| `--framework` | string | Framework option to be used with certain stacks. | -| `--help` | boolean | Show help. | -| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) | -| `--name` | string | Workspace name (e.g. org name). | -| `--nextAppDir` | boolean | Enable the App Router for Next.js. | -| `--nextSrcDir` | boolean | Generate a 'src/' directory for Next.js. | -| `--nxCloud`, `--ci` | `github`, `gitlab`, `azure`, `bitbucket-pipelines`, `circleci`, `skip`, `yes` | Which CI provider would you like to use? | -| `--packageManager`, `--pm` | `bun`, `npm`, `pnpm`, `yarn` | Package manager to use. (Default: `npm`) | -| `--prefix` | string | Prefix to use for Angular component and directive selectors. | -| `--preset` | string | Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset. | -| `--routing` | boolean | Add a routing setup for an Angular app. (Default: `true`) | -| `--skipGit`, `--g` | boolean | Skip initializing a git repository. (Default: `false`) | -| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. | -| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) | -| `--style` | string | Stylesheet type to be used with certain stacks. | -| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. | -| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) | -| `--version` | boolean | Show version number. | -| `--workspaces` | boolean | Use package manager workspaces. (Default: `true`) | -| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. | +| Option | Type | Description | +| -------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--allPrompts`, `--a` | boolean | Show all prompts. (Default: `false`) | +| `--appName` | string | The name of the app when using a monorepo with certain stacks. | +| `--bundler` | string | Bundler to be used to build the app. | +| `--commit.email` | string | E-mail of the committer. | +| `--commit.message` | string | Commit message. (Default: `Initial commit`) | +| `--commit.name` | string | Name of the committer. | +| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) | +| `--docker` | boolean | Generate a Dockerfile for the Node API. | +| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. | +| `--formatter` | string | Code formatter to use. | +| `--framework` | string | Framework option to be used with certain stacks. | +| `--help` | boolean | Show help. | +| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) | +| `--name` | string | Workspace name (e.g. org name). | +| `--nextAppDir` | boolean | Enable the App Router for Next.js. | +| `--nextSrcDir` | boolean | Generate a 'src/' directory for Next.js. | +| `--nxCloud`, `--ci` | `github`, `gitlab`, `azure`, `bitbucket-pipelines`, `circleci`, `skip`, `yes` | Which CI provider would you like to use? | +| `--packageManager`, `--pm` | `bun`, `npm`, `pnpm`, `yarn` | Package manager to use. (Default: `npm`) | +| `--prefix` | string | Prefix to use for Angular component and directive selectors. | +| `--preset` | string | Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset. | +| `--routing` | boolean | Add a routing setup for an Angular or React app. (Default: `true`) | +| `--skipGit`, `--g` | boolean | Skip initializing a git repository. (Default: `false`) | +| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. | +| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) | +| `--style` | string | Stylesheet type to be used with certain stacks. | +| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. | +| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) | +| `--useReactRouter` | boolean | Generate a Server-Side Rendered (SSR) React app using React Router. | +| `--version` | boolean | Show version number. | +| `--workspaces` | boolean | Use package manager workspaces. (Default: `true`) | +| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. | ## Presets @@ -72,8 +73,6 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n | react-monorepo | A React monorepo | | react-native | A monorepo with a React Native application | | react-standalone | A single React application | -| remix-monorepo | A Remix monorepo | -| remix-standalone | A single Remix application | | ts | A basic integrated style repository starting with TypeScript configured but no projects | | ts-standalone | A single TypeScript application | | vue | Allows you to choose between the vue-standalone or vue-monorepo presets | diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index d88c4a39ad..008b241dd1 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -17,38 +17,39 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n ## Options -| Option | Type | Description | -| -------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--allPrompts`, `--a` | boolean | Show all prompts. (Default: `false`) | -| `--appName` | string | The name of the app when using a monorepo with certain stacks. | -| `--bundler` | string | Bundler to be used to build the app. | -| `--commit.email` | string | E-mail of the committer. | -| `--commit.message` | string | Commit message. (Default: `Initial commit`) | -| `--commit.name` | string | Name of the committer. | -| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) | -| `--docker` | boolean | Generate a Dockerfile for the Node API. | -| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. | -| `--formatter` | string | Code formatter to use. | -| `--framework` | string | Framework option to be used with certain stacks. | -| `--help` | boolean | Show help. | -| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) | -| `--name` | string | Workspace name (e.g. org name). | -| `--nextAppDir` | boolean | Enable the App Router for Next.js. | -| `--nextSrcDir` | boolean | Generate a 'src/' directory for Next.js. | -| `--nxCloud`, `--ci` | `github`, `gitlab`, `azure`, `bitbucket-pipelines`, `circleci`, `skip`, `yes` | Which CI provider would you like to use? | -| `--packageManager`, `--pm` | `bun`, `npm`, `pnpm`, `yarn` | Package manager to use. (Default: `npm`) | -| `--prefix` | string | Prefix to use for Angular component and directive selectors. | -| `--preset` | string | Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset. | -| `--routing` | boolean | Add a routing setup for an Angular app. (Default: `true`) | -| `--skipGit`, `--g` | boolean | Skip initializing a git repository. (Default: `false`) | -| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. | -| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) | -| `--style` | string | Stylesheet type to be used with certain stacks. | -| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. | -| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) | -| `--version` | boolean | Show version number. | -| `--workspaces` | boolean | Use package manager workspaces. (Default: `true`) | -| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. | +| Option | Type | Description | +| -------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--allPrompts`, `--a` | boolean | Show all prompts. (Default: `false`) | +| `--appName` | string | The name of the app when using a monorepo with certain stacks. | +| `--bundler` | string | Bundler to be used to build the app. | +| `--commit.email` | string | E-mail of the committer. | +| `--commit.message` | string | Commit message. (Default: `Initial commit`) | +| `--commit.name` | string | Name of the committer. | +| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) | +| `--docker` | boolean | Generate a Dockerfile for the Node API. | +| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. | +| `--formatter` | string | Code formatter to use. | +| `--framework` | string | Framework option to be used with certain stacks. | +| `--help` | boolean | Show help. | +| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) | +| `--name` | string | Workspace name (e.g. org name). | +| `--nextAppDir` | boolean | Enable the App Router for Next.js. | +| `--nextSrcDir` | boolean | Generate a 'src/' directory for Next.js. | +| `--nxCloud`, `--ci` | `github`, `gitlab`, `azure`, `bitbucket-pipelines`, `circleci`, `skip`, `yes` | Which CI provider would you like to use? | +| `--packageManager`, `--pm` | `bun`, `npm`, `pnpm`, `yarn` | Package manager to use. (Default: `npm`) | +| `--prefix` | string | Prefix to use for Angular component and directive selectors. | +| `--preset` | string | Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset. | +| `--routing` | boolean | Add a routing setup for an Angular or React app. (Default: `true`) | +| `--skipGit`, `--g` | boolean | Skip initializing a git repository. (Default: `false`) | +| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. | +| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) | +| `--style` | string | Stylesheet type to be used with certain stacks. | +| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. | +| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) | +| `--useReactRouter` | boolean | Generate a Server-Side Rendered (SSR) React app using React Router. | +| `--version` | boolean | Show version number. | +| `--workspaces` | boolean | Use package manager workspaces. (Default: `true`) | +| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. | ## Presets @@ -72,8 +73,6 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n | react-monorepo | A React monorepo | | react-native | A monorepo with a React Native application | | react-standalone | A single React application | -| remix-monorepo | A Remix monorepo | -| remix-standalone | A single Remix application | | ts | A basic integrated style repository starting with TypeScript configured but no projects | | ts-standalone | A single TypeScript application | | vue | Allows you to choose between the vue-standalone or vue-monorepo presets | diff --git a/docs/generated/packages/react/generators/application.json b/docs/generated/packages/react/generators/application.json index e4f2ecedfd..c96ff9c043 100644 --- a/docs/generated/packages/react/generators/application.json +++ b/docs/generated/packages/react/generators/application.json @@ -77,7 +77,12 @@ "routing": { "type": "boolean", "description": "Generate application with routes.", - "x-prompt": "Would you like to add React Router to this application?", + "x-prompt": "Would you like to add routing to this application?", + "default": false + }, + "useReactRouter": { + "description": "Use React Router for routing.", + "type": "boolean", "default": false }, "skipFormat": { diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index d1384abb3f..24848877a9 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -25,6 +25,11 @@ "type": "boolean", "default": true }, + "useReactRouter": { + "description": "Use React Router for routing.", + "type": "boolean", + "default": false + }, "standaloneApi": { "description": "Use Standalone Components if generating an Angular application.", "type": "boolean", diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index adf2e6c67d..5a41e61a4e 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -25,6 +25,11 @@ "type": "boolean", "default": true }, + "useReactRouter": { + "description": "Use React Router for routing.", + "type": "boolean", + "default": false + }, "style": { "description": "The file extension to be used for style files.", "type": "string", diff --git a/e2e/react/src/react-router.test.ts b/e2e/react/src/react-router.test.ts new file mode 100644 index 0000000000..67c2f5edf3 --- /dev/null +++ b/e2e/react/src/react-router.test.ts @@ -0,0 +1,68 @@ +import { + checkFilesExist, + cleanupProject, + ensureCypressInstallation, + newProject, + readFile, + runCLI, + uniq, +} from '@nx/e2e/utils'; + +describe('React Router Applications', () => { + beforeAll(() => { + newProject({ packages: ['@nx/react'] }); + ensureCypressInstallation(); + }); + + afterAll(() => cleanupProject()); + + it('should generate a react-router application', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --use-react-router --routing --no-interactive` + ); + + const packageJson = JSON.parse(readFile('package.json')); + expect(packageJson.dependencies['react-router']).toBeDefined(); + expect(packageJson.dependencies['@react-router/node']).toBeDefined(); + expect(packageJson.dependencies['@react-router/serve']).toBeDefined(); + expect(packageJson.dependencies['isbot']).toBeDefined(); + + checkFilesExist(`${appName}/app/app.tsx`); + checkFilesExist(`${appName}/app/entry.client.tsx`); + checkFilesExist(`${appName}/app/entry.server.tsx`); + checkFilesExist(`${appName}/app/routes.tsx`); + checkFilesExist(`${appName}/react-router.config.ts`); + checkFilesExist(`${appName}/vite.config.ts`); + }); + + it('should be able to build a react-router application', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --use-react-router --routing --no-interactive` + ); + + const buildResult = runCLI(`build ${appName}`); + expect(buildResult).toContain('Successfully ran target build'); + }); + + it('should be able to lint a react-router application', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --use-react-router --routing --linter=eslint --no-interactive` + ); + + const buildResult = runCLI(`lint ${appName}`); + expect(buildResult).toContain('Successfully ran target lint'); + }); + + it('should be able to test a react-router application', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --use-react-router --routing --unit-test-runner=vitest --no-interactive` + ); + + const buildResult = runCLI(`test ${appName}`); + expect(buildResult).toContain('Successfully ran target test'); + }); +}); diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 7fe587502a..0a090df851 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -222,6 +222,7 @@ export function runCreateWorkspace( cwd = e2eCwd, bundler, routing, + useReactRouter, standaloneApi, docker, nextAppDir, @@ -244,6 +245,7 @@ export function runCreateWorkspace( bundler?: 'webpack' | 'vite'; standaloneApi?: boolean; routing?: boolean; + useReactRouter?: boolean; docker?: boolean; nextAppDir?: boolean; nextSrcDir?: boolean; @@ -295,6 +297,10 @@ export function runCreateWorkspace( command += ` --routing=${routing}`; } + if (useReactRouter !== undefined) { + command += ` --useReactRouter=${useReactRouter}`; + } + if (base) { command += ` --defaultBase="${base}"`; } diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index 85d9bb9b62..4cff4fb21e 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -495,7 +495,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], setupFiles: ['src/test-setup.ts'], reporters: ['default'], coverage: { diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index d95617e935..f1dbacc2a3 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -48,11 +48,13 @@ interface ReactArguments extends BaseArguments { stack: 'react'; workspaceType: 'standalone' | 'integrated'; appName: string; - framework: 'none' | 'next' | 'remix'; + framework: 'none' | 'next'; style: string; bundler: 'webpack' | 'vite' | 'rspack'; nextAppDir: boolean; nextSrcDir: boolean; + useReactRouter: boolean; + routing: boolean; unitTestRunner: 'none' | 'jest' | 'vitest'; e2eTestRunner: 'none' | 'cypress' | 'playwright'; } @@ -156,10 +158,14 @@ export const commandsObject: yargs.Argv = yargs default: true, }) .option('routing', { - describe: chalk.dim`Add a routing setup for an Angular app.`, + describe: chalk.dim`Add a routing setup for an Angular or React app.`, type: 'boolean', default: true, }) + .option('useReactRouter', { + describe: chalk.dim`Generate a Server-Side Rendered (SSR) React app using React Router.`, + type: 'boolean', + }) .option('bundler', { describe: chalk.dim`Bundler to be used to build the app.`, type: 'string', @@ -378,8 +384,6 @@ async function determineStack( case Preset.ReactMonorepo: case Preset.NextJs: case Preset.NextJsStandalone: - case Preset.RemixStandalone: - case Preset.RemixMonorepo: case Preset.ReactNative: case Preset.Expo: return 'react'; @@ -591,6 +595,8 @@ async function determineReactOptions( let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined; let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined; let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; + let useReactRouter = false; + let routing = true; let nextAppDir = false; let nextSrcDir = false; let linter: undefined | 'none' | 'eslint'; @@ -602,8 +608,7 @@ async function determineReactOptions( preset = parsedArgs.preset; if ( preset === Preset.ReactStandalone || - preset === Preset.NextJsStandalone || - preset === Preset.RemixStandalone + preset === Preset.NextJsStandalone ) { appName = parsedArgs.appName ?? parsedArgs.name; } else { @@ -629,17 +634,12 @@ async function determineReactOptions( } else { preset = Preset.NextJs; } - } else if (framework === 'remix') { - if (isStandalone) { - preset = Preset.RemixStandalone; - } else { - preset = Preset.RemixMonorepo; - } } else if (framework === 'react-native') { preset = Preset.ReactNative; } else if (framework === 'expo') { preset = Preset.Expo; } else { + useReactRouter = await determineReactRouter(parsedArgs); if (isStandalone) { preset = Preset.ReactStandalone; } else { @@ -649,7 +649,7 @@ async function determineReactOptions( } if (preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo) { - bundler = await determineReactBundler(parsedArgs); + bundler = useReactRouter ? 'vite' : await determineReactBundler(parsedArgs); unitTestRunner = await determineUnitTestRunner(parsedArgs, { preferVitest: bundler === 'vite', }); @@ -661,14 +661,6 @@ async function determineReactOptions( exclude: 'vitest', }); e2eTestRunner = await determineE2eTestRunner(parsedArgs); - } else if ( - preset === Preset.RemixMonorepo || - preset === Preset.RemixStandalone - ) { - unitTestRunner = await determineUnitTestRunner(parsedArgs, { - preferVitest: true, - }); - e2eTestRunner = await determineE2eTestRunner(parsedArgs); } else if (preset === Preset.ReactNative || preset === Preset.Expo) { unitTestRunner = await determineUnitTestRunner(parsedArgs, { exclude: 'vitest', @@ -748,6 +740,8 @@ async function determineReactOptions( nextSrcDir, unitTestRunner, e2eTestRunner, + useReactRouter, + routing, linter, formatter, workspaces, @@ -1221,9 +1215,9 @@ async function determineAppName( async function determineReactFramework( parsedArgs: yargs.Arguments -): Promise<'none' | 'nextjs' | 'remix' | 'expo' | 'react-native'> { +): Promise<'none' | 'nextjs' | 'expo' | 'react-native'> { const reply = await enquirer.prompt<{ - framework: 'none' | 'nextjs' | 'remix' | 'expo' | 'react-native'; + framework: 'none' | 'nextjs' | 'expo' | 'react-native'; }>([ { name: 'framework', @@ -1233,23 +1227,19 @@ async function determineReactFramework( { name: 'none', message: 'None', - hint: ' I only want react and react-dom', + hint: ' I only want react, react-dom or react-router', }, { name: 'nextjs', - message: 'Next.js [ https://nextjs.org/ ]', - }, - { - name: 'remix', - message: 'Remix [ https://remix.run/ ]', + message: 'Next.js [ https://nextjs.org/ ]', }, { name: 'expo', - message: 'Expo [ https://expo.io/ ]', + message: 'Expo [ https://expo.io/ ]', }, { name: 'react-native', - message: 'React Native [ https://reactnative.dev/ ]', + message: 'React Native [ https://reactnative.dev/ ]', }, ], initial: 0, @@ -1494,3 +1484,35 @@ async function determineE2eTestRunner( ]); return reply.e2eTestRunner; } + +async function determineReactRouter( + parsedArgs: yargs.Arguments<{ + useReactRouter?: boolean; + }> +): Promise { + if (parsedArgs.routing !== undefined && parsedArgs.routing === false) + return false; + if (parsedArgs.useReactRouter !== undefined) return parsedArgs.useReactRouter; + const reply = await enquirer.prompt<{ + response: 'Yes' | 'No'; + }>([ + { + message: + 'Would you like to use React Router for server-side rendering [https://reactrouter.com/]?', + type: 'autocomplete', + name: 'response', + skip: !parsedArgs.interactive || isCI(), + choices: [ + { + name: 'Yes', + hint: 'I want to use React Router', + }, + { + name: 'No', + }, + ], + initial: 0, + }, + ]); + return reply.response === 'Yes'; +} diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index f007c9f12c..77072c0bda 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -112,7 +112,6 @@ function getWorkspaceGlobsFromPreset(preset: string): string[] { case Preset.Nuxt: case Preset.ReactNative: case Preset.ReactMonorepo: - case Preset.RemixMonorepo: case Preset.VueMonorepo: case Preset.WebComponents: return ['apps/*']; diff --git a/packages/create-nx-workspace/src/utils/preset/preset.ts b/packages/create-nx-workspace/src/utils/preset/preset.ts index c539e06400..a5e5211308 100644 --- a/packages/create-nx-workspace/src/utils/preset/preset.ts +++ b/packages/create-nx-workspace/src/utils/preset/preset.ts @@ -13,8 +13,6 @@ export enum Preset { NuxtStandalone = 'nuxt-standalone', NextJs = 'next', NextJsStandalone = 'nextjs-standalone', - RemixMonorepo = 'remix-monorepo', - RemixStandalone = 'remix-standalone', ReactNative = 'react-native', Expo = 'expo', Nest = 'nest', diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index 8997954f20..09c48d1778 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -184,7 +184,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: '../coverage/my-app', @@ -585,7 +585,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: '../coverage/myApp', diff --git a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap index 1d4153440e..163ebcd023 100644 --- a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap @@ -448,7 +448,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: '../coverage/my-app', @@ -511,7 +511,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: '../coverage/my-app', diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 57bbb4c338..06327f198a 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1065,6 +1065,104 @@ describe('app', () => { }); }); + describe('--use-react-router', () => { + it('should add react-router to vite.config', async () => { + await applicationGenerator(appTree, { + ...schema, + skipFormat: false, + useReactRouter: true, + routing: true, + bundler: 'vite', + unitTestRunner: 'vitest', + }); + + expect(appTree.read('my-app/vite.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import { reactRouter } from '@react-router/dev/vite'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../node_modules/.vite/my-app', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [ + !process.env.VITEST && reactRouter(), + nxViteTsPaths(), + nxCopyAssetsPlugin(['*.md']), + ], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../dist/my-app', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../coverage/my-app', + provider: 'v8' as const, + }, + }, + })); + " + `); + }); + + it('should add types to tsconfig', async () => { + await applicationGenerator(appTree, { + ...schema, + skipFormat: false, + useReactRouter: true, + routing: true, + bundler: 'vite', + unitTestRunner: 'vitest', + }); + const tsconfigSpec = readJson(appTree, 'my-app/tsconfig.json'); + expect(tsconfigSpec.compilerOptions.types).toEqual([ + 'vite/client', + 'vitest', + '@react-router/node', + ]); + }); + + it('should have a project package.json', async () => { + await applicationGenerator(appTree, { + ...schema, + skipFormat: false, + useReactRouter: true, + routing: true, + bundler: 'vite', + unitTestRunner: 'vitest', + }); + + const packageJson = readJson(appTree, 'my-app/package.json'); + expect(packageJson.dependencies['@react-router/node']).toBeDefined(); + expect(packageJson.dependencies['@react-router/serve']).toBeDefined(); + expect(packageJson.dependencies['react-router']).toBeDefined(); + expect(packageJson.devDependencies['@react-router/dev']).toBeDefined(); + }); + }); + describe('--directory="." (--root-project)', () => { it('should create files at the root', async () => { await applicationGenerator(appTree, { diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index ab08f5aea8..b9a4b84483 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -5,6 +5,7 @@ import { readNxJson, runTasksInSerial, Tree, + updateJson, updateNxJson, } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; @@ -43,6 +44,7 @@ import { } from './lib/bundlers/add-vite'; import { Schema } from './schema'; import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; +import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; export async function applicationGenerator( tree: Tree, @@ -73,6 +75,34 @@ export async function applicationGeneratorInternal( const options = await normalizeOptions(tree, schema); + options.useReactRouter = options.routing + ? options.useReactRouter ?? + (await promptWhenInteractive<{ + response: 'Yes' | 'No'; + }>( + { + name: 'response', + message: + 'Would you like to use react-router for server-side rendering?', + type: 'autocomplete', + choices: [ + { + name: 'Yes', + message: + 'I want to use react-router [ https://reactrouter.com/start/framework/routing ]', + }, + { + name: 'No', + message: + 'I do not want to use react-router for server-side rendering', + }, + ], + initial: 0, + }, + { response: 'No' } + ).then((r) => r.response === 'Yes')) + : false; + showPossibleWarnings(tree, options); const initTask = await reactInitGenerator(tree, { @@ -158,18 +188,41 @@ export async function applicationGeneratorInternal( // Handle tsconfig.spec.json for jest or vitest updateSpecConfig(tree, options); - const stylePreprocessorTask = await installCommonDependencies(tree, options); - tasks.push(stylePreprocessorTask); + const commonDependencyTask = await installCommonDependencies(tree, options); + tasks.push(commonDependencyTask); const styledTask = addStyledModuleDependencies(tree, options); tasks.push(styledTask); - const routingTask = addRouting(tree, options); - tasks.push(routingTask); + if (!options.useReactRouter) { + const routingTask = addRouting(tree, options); + tasks.push(routingTask); + } setDefaults(tree, options); if (options.bundler === 'rspack' && options.style === 'styled-jsx') { handleStyledJsxForRspack(tasks, tree, options); } + if (options.useReactRouter) { + updateJson( + tree, + joinPathFragments(options.appProjectRoot, 'tsconfig.json'), + (json) => { + const types = new Set(json.compilerOptions?.types || []); + types.add('@react-router/node'); + return { + ...json, + compilerOptions: { + ...json.compilerOptions, + jsx: 'react-jsx', + moduleResolution: 'bundler', + types: Array.from(types), + }, + }; + } + ); + } + + // Only for the new TS solution updateTsconfigFiles( tree, options.appProjectRoot, diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/app-nav.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/app-nav.tsx__tmpl__ new file mode 100644 index 0000000000..79a0ca213a --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/app-nav.tsx__tmpl__ @@ -0,0 +1,15 @@ +import * as React from "react"; +import { NavLink } from "react-router"; + +export function AppNav() { + return ( + + ); +} \ No newline at end of file diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.client.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.client.tsx__tmpl__ new file mode 100644 index 0000000000..ce6ae20bdb --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.client.tsx__tmpl__ @@ -0,0 +1,18 @@ +/** + * By default, React Router will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx react-router reveal` ✨ + * For more information, see https://reactrouter.com/explanation/special-files#entryclienttsx + */ + +import { HydratedRouter } from 'react-router/dom'; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.server.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.server.tsx__tmpl__ new file mode 100644 index 0000000000..e66b7a7ea5 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/entry.server.tsx__tmpl__ @@ -0,0 +1,74 @@ +/** + * By default, React Router will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://reactrouter.com/explanation/special-files#entryservertsx + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "react-router"; +import { createReadableStreamFromReadable } from "@react-router/node"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import type { RenderToPipeableStreamOptions } from "react-dom/server"; +import { renderToPipeableStream } from "react-dom/server"; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + const readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? "onAllReady" + : "onShellReady"; + + const { pipe, abort } = renderToPipeableStream( + , + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); + }); +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/root.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/root.tsx__tmpl__ new file mode 100644 index 0000000000..8caaf72989 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/root.tsx__tmpl__ @@ -0,0 +1,51 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + type MetaFunction, + type LinksFunction +} from "react-router"; + +import { AppNav } from './app-nav' + +export const meta: MetaFunction = () => ([{ + title: "New Nx React Router App", +}]); + +export const links: LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/routes.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/routes.tsx__tmpl__ new file mode 100644 index 0000000000..50f3775dc7 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/routes.tsx__tmpl__ @@ -0,0 +1,6 @@ +import { type RouteConfig, index, route } from "@react-router/dev/routes"; + +export default [ + index('<%- js ? `./${fileName}.jsx` : `./${fileName}.tsx` %>'), + route('about', './routes/about.tsx') + ] satisfies RouteConfig; \ No newline at end of file diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/app/routes/about.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/app/routes/about.tsx__tmpl__ new file mode 100644 index 0000000000..267a9bc8d7 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/app/routes/about.tsx__tmpl__ @@ -0,0 +1,7 @@ +export default function AboutComponent() { + return ( +
+

About!!!

+
+ ); +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/public/favicon.ico b/packages/react/src/generators/application/files/react-router-ssr/common/public/favicon.ico new file mode 100644 index 0000000000..317ebcb233 Binary files /dev/null and b/packages/react/src/generators/application/files/react-router-ssr/common/public/favicon.ico differ diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/react-router.config.ts__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/react-router.config.ts__tmpl__ new file mode 100644 index 0000000000..31decfde12 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/react-router.config.ts__tmpl__ @@ -0,0 +1,5 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + ssr: true, +} satisfies Config; \ No newline at end of file diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/tests/routes/_index.spec.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/tests/routes/_index.spec.tsx__tmpl__ new file mode 100644 index 0000000000..193e642ee9 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/tests/routes/_index.spec.tsx__tmpl__ @@ -0,0 +1,16 @@ +import { createRoutesStub } from 'react-router'; +import { render, screen, waitFor } from '@testing-library/react'; +import App from '../../app/app'; + +test('renders loader data', async () => { + const ReactRouterStub = createRoutesStub([ + { + path: '/', + Component: App, + }, + ]); + + render(); + + await waitFor(() => screen.findByText('Hello there,')); +}); diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.app.json__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.app.json__tmpl__ new file mode 100644 index 0000000000..72fbe340e2 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.app.json__tmpl__ @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "exclude": [ + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx" + ] +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.json__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.json__tmpl__ new file mode 100644 index 0000000000..8d2c233692 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.json__tmpl__ @@ -0,0 +1,27 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "types": ["@react-router/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + // Vite takes care of building everything. + "noEmit": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/non-root/.gitignore__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/non-root/.gitignore__tmpl__ new file mode 100644 index 0000000000..a70276cde7 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/non-root/.gitignore__tmpl__ @@ -0,0 +1,5 @@ +.cache +build +public/build +.env +.react-router diff --git a/packages/react/src/generators/application/files/react-router-ssr/non-root/package.json__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/non-root/package.json__tmpl__ new file mode 100644 index 0000000000..30e36f47b7 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/non-root/package.json__tmpl__ @@ -0,0 +1,24 @@ +{ + "name": "<%= projectName %>", + "private": true, + "type": "module", + "scripts": {}, + "dependencies": { + "@react-router/node": "<%= reactRouterVersion %>", + "@react-router/serve": "<%= reactRouterVersion %>", + "isbot": "<%= reactRouterIsBotVersion %>", + "react": "<%= reactVersion %>", + "react-dom": "<%= reactVersion %>", + "react-router": "<%= reactRouterVersion %>" + }, + "devDependencies": { + "@react-router/dev": "<%= reactRouterVersion %>", + "@types/node": "<%= typesNodeVersion %>", + "@types/react": "<%= reactVersion %>", + "@types/react-dom": "<%= reactVersion %>" + }, + "engines": { + "node": ">=20" + }, + "sideEffects": false +} diff --git a/packages/react/src/generators/application/files/react-router-ssr/nx-welcome/claimed/app/nx-welcome.tsx__tmpl__ b/packages/react/src/generators/application/files/react-router-ssr/nx-welcome/claimed/app/nx-welcome.tsx__tmpl__ new file mode 100644 index 0000000000..36a76bf424 --- /dev/null +++ b/packages/react/src/generators/application/files/react-router-ssr/nx-welcome/claimed/app/nx-welcome.tsx__tmpl__ @@ -0,0 +1,866 @@ +/* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + This is a starter component and can be deleted. + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + Delete this file and get started with your project! + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ +export function NxWelcome({ title }: { title: string }) { + return ( + <> +