From 363088a8ae2d60d383c5a98b34aaca4b245ff457 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Fri, 14 Mar 2025 13:06:54 -0600 Subject: [PATCH] feat(react): Add react-router to create-nx-workspace and react app generator (#30316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request introduces improvements to React Router integration and removes the Remix preset. ## Key Changes: - Updated `create-nx-workspace` to support React Router. - Removed the Remix option from `create-nx-workspace`, but the package remains to support existing users. ## SSR & React Router Support - New users who want SSR in their React apps can enable it via the React option and select React Router for SSR support. - The ecosystem has shifted to migrating from Remix to React Router for SSR needs. - This option is only available for plain React apps and uses Vite. Other types of React apps (Micro Frontends, Webpack, Rspack, etc.) remain unaffected. ## Default Routing Behavior `--routing` is now enabled by default when creating a React app using `create-nx-workspace`, aligning with Angular’s default behaviour. --- docs/generated/cli/create-nx-workspace.md | 67 +- .../nx/documents/create-nx-workspace.md | 67 +- .../react/generators/application.json | 7 +- .../packages/workspace/generators/new.json | 5 + .../packages/workspace/generators/preset.json | 5 + e2e/react/src/react-router.test.ts | 68 ++ e2e/utils/create-project-utils.ts | 6 + .../__snapshots__/application.spec.ts.snap | 2 +- .../bin/create-nx-workspace.ts | 84 +- .../src/create-workspace.ts | 1 - .../src/utils/preset/preset.ts | 2 - .../__snapshots__/application.spec.ts.snap | 4 +- .../__snapshots__/application.spec.ts.snap | 4 +- .../application/application.spec.ts | 98 ++ .../src/generators/application/application.ts | 61 +- .../common/app/app-nav.tsx__tmpl__ | 15 + .../common/app/entry.client.tsx__tmpl__ | 18 + .../common/app/entry.server.tsx__tmpl__ | 74 ++ .../common/app/root.tsx__tmpl__ | 51 ++ .../common/app/routes.tsx__tmpl__ | 6 + .../common/app/routes/about.tsx__tmpl__ | 7 + .../common/public/favicon.ico | Bin 0 -> 15086 bytes .../common/react-router.config.ts__tmpl__ | 5 + .../tests/routes/_index.spec.tsx__tmpl__ | 16 + .../common/tsconfig.app.json__tmpl__ | 23 + .../common/tsconfig.json__tmpl__ | 27 + .../non-root/.gitignore__tmpl__ | 5 + .../non-root/package.json__tmpl__ | 24 + .../claimed/app/nx-welcome.tsx__tmpl__ | 866 ++++++++++++++++++ .../not-configured/app/nx-welcome.tsx__tmpl__ | 866 ++++++++++++++++++ .../unclaimed/app/nx-welcome.tsx__tmpl__ | 864 +++++++++++++++++ .../ts-solution/package.json__tmpl__ | 24 + .../ts-solution/tsconfig.app.json__tmpl__ | 39 + .../src/generators/application/lib/add-e2e.ts | 36 +- .../generators/application/lib/add-linting.ts | 51 ++ .../generators/application/lib/add-project.ts | 25 +- .../generators/application/lib/add-routing.ts | 2 +- .../application/lib/bundlers/add-vite.ts | 23 +- .../lib/create-application-files.ts | 84 +- .../lib/install-common-dependencies.ts | 20 +- .../application/lib/normalize-options.ts | 4 + .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 7 +- .../__snapshots__/library.spec.ts.snap | 2 +- .../src/generators/library/library.spec.ts | 2 +- packages/react/src/utils/ast-utils.ts | 7 +- packages/react/src/utils/versions.ts | 6 +- .../__snapshots__/configuration.spec.ts.snap | 6 +- .../vitest/__snapshots__/vitest.spec.ts.snap | 8 +- .../src/utils/e2e-web-server-info-utils.ts | 29 + .../vite/src/utils/generator-utils.spec.ts | 2 +- packages/vite/src/utils/generator-utils.ts | 2 +- .../__snapshots__/application.spec.ts.snap | 4 +- .../application/application.spec.ts | 2 +- .../__snapshots__/library.spec.ts.snap | 4 +- .../generate-workspace-files.spec.ts.snap | 528 ----------- .../src/generators/new/generate-preset.ts | 7 +- .../new/generate-workspace-files.ts | 14 - packages/workspace/src/generators/new/new.ts | 1 + .../workspace/src/generators/new/schema.json | 5 + .../src/generators/preset/preset.spec.ts | 6 +- .../workspace/src/generators/preset/preset.ts | 35 +- .../src/generators/preset/schema.d.ts | 1 + .../src/generators/preset/schema.json | 5 + .../workspace/src/generators/utils/presets.ts | 2 - .../generators/generate-cnw-documentation.ts | 2 - 66 files changed, 3595 insertions(+), 749 deletions(-) create mode 100644 e2e/react/src/react-router.test.ts create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/app-nav.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/entry.client.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/entry.server.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/root.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/routes.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/app/routes/about.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/public/favicon.ico create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/react-router.config.ts__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/tests/routes/_index.spec.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.app.json__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/common/tsconfig.json__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/non-root/.gitignore__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/non-root/package.json__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/nx-welcome/claimed/app/nx-welcome.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/nx-welcome/not-configured/app/nx-welcome.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/nx-welcome/unclaimed/app/nx-welcome.tsx__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/ts-solution/package.json__tmpl__ create mode 100644 packages/react/src/generators/application/files/react-router-ssr/ts-solution/tsconfig.app.json__tmpl__ 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 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 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 { + 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 ( + <> +