feat(remix): add remix (#20641)

This commit is contained in:
Colum Ferry 2023-12-13 16:07:08 +00:00 committed by GitHub
parent 79cab697bf
commit 3b5bf6d1d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
181 changed files with 11540 additions and 30 deletions

View File

@ -56,6 +56,11 @@ rust-toolchain @nrwl/nx-native-reviewers
/packages/react-native/** @nrwl/nx-react-reviewers
/e2e/react-native/** @nrwl/nx-react-reviewers
## remix
/docs/generated/packages/remix/** @nrwl/nx-react-reviewers @nrwl/nx-docs-reviewers @Coly010
/packages/remix/** @nrwl/nx-react-reviewers @Coly010
/e2e/remix/** @nrwl/nx-react-reviewers @Coly010
# Vue
/packages/vue/** @nrwl/nx-vue-reviewers
/e2e/vue/** @nrwl/nx-vue-reviewers

View File

@ -8941,6 +8941,186 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "remix",
"path": "/nx-api/remix",
"name": "remix",
"children": [
{
"id": "documents",
"path": "/nx-api/remix/documents",
"name": "documents",
"children": [
{
"name": "Overview",
"path": "/nx-api/remix/documents/overview",
"id": "overview",
"isExternal": false,
"children": [],
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "executors",
"path": "/nx-api/remix/executors",
"name": "executors",
"children": [
{
"id": "serve",
"path": "/nx-api/remix/executors/serve",
"name": "serve",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "build",
"path": "/nx-api/remix/executors/build",
"name": "build",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "generators",
"path": "/nx-api/remix/generators",
"name": "generators",
"children": [
{
"id": "preset",
"path": "/nx-api/remix/generators/preset",
"name": "preset",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "setup",
"path": "/nx-api/remix/generators/setup",
"name": "setup",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "application",
"path": "/nx-api/remix/generators/application",
"name": "application",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "cypress-component-configuration",
"path": "/nx-api/remix/generators/cypress-component-configuration",
"name": "cypress-component-configuration",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "library",
"path": "/nx-api/remix/generators/library",
"name": "library",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "route",
"path": "/nx-api/remix/generators/route",
"name": "route",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "resource-route",
"path": "/nx-api/remix/generators/resource-route",
"name": "resource-route",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "action",
"path": "/nx-api/remix/generators/action",
"name": "action",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "loader",
"path": "/nx-api/remix/generators/loader",
"name": "loader",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "style",
"path": "/nx-api/remix/generators/style",
"name": "style",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "setup-tailwind",
"path": "/nx-api/remix/generators/setup-tailwind",
"name": "setup-tailwind",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "storybook-configuration",
"path": "/nx-api/remix/generators/storybook-configuration",
"name": "storybook-configuration",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "meta",
"path": "/nx-api/remix/generators/meta",
"name": "meta",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "error-boundary",
"path": "/nx-api/remix/generators/error-boundary",
"name": "error-boundary",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "cypress",
"path": "/nx-api/remix/generators/cypress",
"name": "cypress",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "rollup",
"path": "/nx-api/rollup",

View File

@ -2414,6 +2414,185 @@
},
"path": "/nx-api/react-native"
},
"remix": {
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "remix",
"packageName": "@nx/remix",
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"documents": {
"/nx-api/remix/documents/overview": {
"id": "overview",
"name": "Overview",
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"file": "generated/packages/remix/documents/overview",
"itemList": [],
"isExternal": false,
"path": "/nx-api/remix/documents/overview",
"tags": [],
"originalFilePath": "shared/packages/remix/remix-plugin"
}
},
"root": "/packages/remix",
"source": "/packages/remix/src",
"executors": {
"/nx-api/remix/executors/serve": {
"description": "Serve a Remix application.",
"file": "generated/packages/remix/executors/serve.json",
"hidden": false,
"name": "serve",
"originalFilePath": "/packages/remix/src/executors/serve/schema.json",
"path": "/nx-api/remix/executors/serve",
"type": "executor"
},
"/nx-api/remix/executors/build": {
"description": "Build a Remix application.",
"file": "generated/packages/remix/executors/build.json",
"hidden": false,
"name": "build",
"originalFilePath": "/packages/remix/src/executors/build/schema.json",
"path": "/nx-api/remix/executors/build",
"type": "executor"
}
},
"generators": {
"/nx-api/remix/generators/preset": {
"description": "Generate a new Remix workspace",
"file": "generated/packages/remix/generators/preset.json",
"hidden": true,
"name": "preset",
"originalFilePath": "/packages/remix/src/generators/preset/schema.json",
"path": "/nx-api/remix/generators/preset",
"type": "generator"
},
"/nx-api/remix/generators/setup": {
"description": "Setup a Remix in an existing workspace",
"file": "generated/packages/remix/generators/setup.json",
"hidden": true,
"name": "setup",
"originalFilePath": "/packages/remix/src/generators/setup/schema.json",
"path": "/nx-api/remix/generators/setup",
"type": "generator"
},
"/nx-api/remix/generators/application": {
"description": "Generate a new Remix application",
"file": "generated/packages/remix/generators/application.json",
"hidden": false,
"name": "application",
"originalFilePath": "/packages/remix/src/generators/application/schema.json",
"path": "/nx-api/remix/generators/application",
"type": "generator"
},
"/nx-api/remix/generators/cypress-component-configuration": {
"description": "Generate a Cypress Component Testing configuration for a Remix project",
"file": "generated/packages/remix/generators/cypress-component-configuration.json",
"hidden": false,
"name": "cypress-component-configuration",
"originalFilePath": "/packages/remix/src/generators/cypress-component-configuration/schema.json",
"path": "/nx-api/remix/generators/cypress-component-configuration",
"type": "generator"
},
"/nx-api/remix/generators/library": {
"description": "Generate a new library",
"file": "generated/packages/remix/generators/library.json",
"hidden": false,
"name": "library",
"originalFilePath": "/packages/remix/src/generators/library/schema.json",
"path": "/nx-api/remix/generators/library",
"type": "generator"
},
"/nx-api/remix/generators/route": {
"description": "Generate a new route",
"file": "generated/packages/remix/generators/route.json",
"hidden": false,
"name": "route",
"originalFilePath": "/packages/remix/src/generators/route/schema.json",
"path": "/nx-api/remix/generators/route",
"type": "generator"
},
"/nx-api/remix/generators/resource-route": {
"description": "Generate a new resource route",
"file": "generated/packages/remix/generators/resource-route.json",
"hidden": false,
"name": "resource-route",
"originalFilePath": "/packages/remix/src/generators/resource-route/schema.json",
"path": "/nx-api/remix/generators/resource-route",
"type": "generator"
},
"/nx-api/remix/generators/action": {
"description": "Add an action function to an existing route",
"file": "generated/packages/remix/generators/action.json",
"hidden": false,
"name": "action",
"originalFilePath": "/packages/remix/src/generators/action/schema.json",
"path": "/nx-api/remix/generators/action",
"type": "generator"
},
"/nx-api/remix/generators/loader": {
"description": "Add a loader function to an existing route",
"file": "generated/packages/remix/generators/loader.json",
"hidden": false,
"name": "loader",
"originalFilePath": "/packages/remix/src/generators/loader/schema.json",
"path": "/nx-api/remix/generators/loader",
"type": "generator"
},
"/nx-api/remix/generators/style": {
"description": "Generates a new stylesheet and adds it to an existing route",
"file": "generated/packages/remix/generators/style.json",
"hidden": false,
"name": "style",
"originalFilePath": "/packages/remix/src/generators/style/schema.json",
"path": "/nx-api/remix/generators/style",
"type": "generator"
},
"/nx-api/remix/generators/setup-tailwind": {
"description": "Generates a TailwindCSS configuration for the Remix application",
"file": "generated/packages/remix/generators/setup-tailwind.json",
"hidden": false,
"name": "setup-tailwind",
"originalFilePath": "/packages/remix/src/generators/setup-tailwind/schema.json",
"path": "/nx-api/remix/generators/setup-tailwind",
"type": "generator"
},
"/nx-api/remix/generators/storybook-configuration": {
"description": "Generates a Storybook configuration for a Remix application",
"file": "generated/packages/remix/generators/storybook-configuration.json",
"hidden": false,
"name": "storybook-configuration",
"originalFilePath": "/packages/remix/src/generators/storybook-configuration/schema.json",
"path": "/nx-api/remix/generators/storybook-configuration",
"type": "generator"
},
"/nx-api/remix/generators/meta": {
"description": "Add a meta function to an existing route",
"file": "generated/packages/remix/generators/meta.json",
"hidden": false,
"name": "meta",
"originalFilePath": "/packages/remix/src/generators/meta/schema.json",
"path": "/nx-api/remix/generators/meta",
"type": "generator"
},
"/nx-api/remix/generators/error-boundary": {
"description": "Add an ErrorBoundary to an existing route",
"file": "generated/packages/remix/generators/error-boundary.json",
"hidden": false,
"name": "error-boundary",
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "/nx-api/remix/generators/error-boundary",
"type": "generator"
},
"/nx-api/remix/generators/cypress": {
"description": "Generate a project for testing Remix apps using Cypress",
"file": "generated/packages/remix/generators/cypress.json",
"hidden": false,
"name": "cypress",
"originalFilePath": "/packages/remix/src/generators/cypress/schema.json",
"path": "/nx-api/remix/generators/cypress",
"type": "generator"
}
},
"path": "/nx-api/remix"
},
"rollup": {
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "rollup",

View File

@ -2392,6 +2392,184 @@
"root": "/packages/react-native",
"source": "/packages/react-native/src"
},
{
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"documents": [
{
"id": "overview",
"name": "Overview",
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"file": "generated/packages/remix/documents/overview",
"itemList": [],
"isExternal": false,
"path": "remix/documents/overview",
"tags": [],
"originalFilePath": "shared/packages/remix/remix-plugin"
}
],
"executors": [
{
"description": "Serve a Remix application.",
"file": "generated/packages/remix/executors/serve.json",
"hidden": false,
"name": "serve",
"originalFilePath": "/packages/remix/src/executors/serve/schema.json",
"path": "remix/executors/serve",
"type": "executor"
},
{
"description": "Build a Remix application.",
"file": "generated/packages/remix/executors/build.json",
"hidden": false,
"name": "build",
"originalFilePath": "/packages/remix/src/executors/build/schema.json",
"path": "remix/executors/build",
"type": "executor"
}
],
"generators": [
{
"description": "Generate a new Remix workspace",
"file": "generated/packages/remix/generators/preset.json",
"hidden": true,
"name": "preset",
"originalFilePath": "/packages/remix/src/generators/preset/schema.json",
"path": "remix/generators/preset",
"type": "generator"
},
{
"description": "Setup a Remix in an existing workspace",
"file": "generated/packages/remix/generators/setup.json",
"hidden": true,
"name": "setup",
"originalFilePath": "/packages/remix/src/generators/setup/schema.json",
"path": "remix/generators/setup",
"type": "generator"
},
{
"description": "Generate a new Remix application",
"file": "generated/packages/remix/generators/application.json",
"hidden": false,
"name": "application",
"originalFilePath": "/packages/remix/src/generators/application/schema.json",
"path": "remix/generators/application",
"type": "generator"
},
{
"description": "Generate a Cypress Component Testing configuration for a Remix project",
"file": "generated/packages/remix/generators/cypress-component-configuration.json",
"hidden": false,
"name": "cypress-component-configuration",
"originalFilePath": "/packages/remix/src/generators/cypress-component-configuration/schema.json",
"path": "remix/generators/cypress-component-configuration",
"type": "generator"
},
{
"description": "Generate a new library",
"file": "generated/packages/remix/generators/library.json",
"hidden": false,
"name": "library",
"originalFilePath": "/packages/remix/src/generators/library/schema.json",
"path": "remix/generators/library",
"type": "generator"
},
{
"description": "Generate a new route",
"file": "generated/packages/remix/generators/route.json",
"hidden": false,
"name": "route",
"originalFilePath": "/packages/remix/src/generators/route/schema.json",
"path": "remix/generators/route",
"type": "generator"
},
{
"description": "Generate a new resource route",
"file": "generated/packages/remix/generators/resource-route.json",
"hidden": false,
"name": "resource-route",
"originalFilePath": "/packages/remix/src/generators/resource-route/schema.json",
"path": "remix/generators/resource-route",
"type": "generator"
},
{
"description": "Add an action function to an existing route",
"file": "generated/packages/remix/generators/action.json",
"hidden": false,
"name": "action",
"originalFilePath": "/packages/remix/src/generators/action/schema.json",
"path": "remix/generators/action",
"type": "generator"
},
{
"description": "Add a loader function to an existing route",
"file": "generated/packages/remix/generators/loader.json",
"hidden": false,
"name": "loader",
"originalFilePath": "/packages/remix/src/generators/loader/schema.json",
"path": "remix/generators/loader",
"type": "generator"
},
{
"description": "Generates a new stylesheet and adds it to an existing route",
"file": "generated/packages/remix/generators/style.json",
"hidden": false,
"name": "style",
"originalFilePath": "/packages/remix/src/generators/style/schema.json",
"path": "remix/generators/style",
"type": "generator"
},
{
"description": "Generates a TailwindCSS configuration for the Remix application",
"file": "generated/packages/remix/generators/setup-tailwind.json",
"hidden": false,
"name": "setup-tailwind",
"originalFilePath": "/packages/remix/src/generators/setup-tailwind/schema.json",
"path": "remix/generators/setup-tailwind",
"type": "generator"
},
{
"description": "Generates a Storybook configuration for a Remix application",
"file": "generated/packages/remix/generators/storybook-configuration.json",
"hidden": false,
"name": "storybook-configuration",
"originalFilePath": "/packages/remix/src/generators/storybook-configuration/schema.json",
"path": "remix/generators/storybook-configuration",
"type": "generator"
},
{
"description": "Add a meta function to an existing route",
"file": "generated/packages/remix/generators/meta.json",
"hidden": false,
"name": "meta",
"originalFilePath": "/packages/remix/src/generators/meta/schema.json",
"path": "remix/generators/meta",
"type": "generator"
},
{
"description": "Add an ErrorBoundary to an existing route",
"file": "generated/packages/remix/generators/error-boundary.json",
"hidden": false,
"name": "error-boundary",
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "remix/generators/error-boundary",
"type": "generator"
},
{
"description": "Generate a project for testing Remix apps using Cypress",
"file": "generated/packages/remix/generators/cypress.json",
"hidden": false,
"name": "cypress",
"originalFilePath": "/packages/remix/src/generators/cypress/schema.json",
"path": "remix/generators/cypress",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "remix",
"packageName": "@nx/remix",
"root": "/packages/remix",
"source": "/packages/remix/src"
},
{
"description": "The Nx Plugin for Rollup contains executors and generators that support building applications using Rollup.",
"documents": [],

View File

@ -0,0 +1,236 @@
The Nx Plugin for Remix contains executors, generators, and utilities for managing Remix applications and libraries
within an Nx workspace. It provides:
- Integration with libraries such as Storybook, Jest, Vitest and Cypress.
- Generators to help scaffold code quickly, including:
- Libraries, both internal to your codebase and publishable to npm
- Routes
- Loaders
- Actions
- Meta
- Utilities for automatic workspace refactoring.
## Setting up the Remix plugin
{% callout type="note" title="Keep Nx Package Versions In Sync" %}
Make sure to install the `@nx/remix` version that matches the version of `nx` in your repository. If the version
numbers get out of sync, you can encounter some difficult to debug errors. You
can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync).
{% /callout %}
Adding the Remix plugin to an existing Nx workspace can be done with the following:
```shell
yarn add -D @nx/remix
```
```shell
npm install -D @nx/remix
```
## Using the Remix Plugin
## Generate a Remix Application
{% callout type="note" title="Directory Flag Behavior Changes" %}
The command below uses the `as-provided` directory flag behavior, which is the default in Nx 16.8.0. If you're on an earlier version of Nx or using the `derived` option, omit the `--directory` flag. See the [as-provided vs. derived documentation](/deprecated/as-provided-vs-derived) for more details.
{% /callout %}
```{% command="nx g @nx/remix:app myapp --directory=apps/myapp" path="~/acme" %}
> NX Generating @nx/remix:application
✔ What unit test runner should be used? · vitest
CREATE apps/myapp/project.json
UPDATE package.json
CREATE apps/myapp/README.md
CREATE apps/myapp/app/root.tsx
CREATE apps/myapp/app/routes/_index.tsx
CREATE apps/myapp/public/favicon.ico
CREATE apps/myapp/remix.config.js
CREATE apps/myapp/remix.env.d.ts
CREATE apps/myapp/tsconfig.json
CREATE apps/myapp/.gitignore
CREATE apps/myapp/package.json
UPDATE nx.json
CREATE tsconfig.base.json
CREATE .prettierrc
CREATE .prettierignore
UPDATE .vscode/extensions.json
CREATE apps/myapp/vite.config.ts
CREATE apps/myapp/tsconfig.spec.json
CREATE apps/myapp/test-setup.ts
CREATE apps/myapp-e2e/cypress.config.ts
CREATE apps/myapp-e2e/src/e2e/app.cy.ts
CREATE apps/myapp-e2e/src/fixtures/example.json
CREATE apps/myapp-e2e/src/support/commands.ts
CREATE apps/myapp-e2e/src/support/e2e.ts
CREATE apps/myapp-e2e/tsconfig.json
CREATE apps/myapp-e2e/project.json
CREATE .eslintrc.json
CREATE .eslintignore
CREATE apps/myapp-e2e/.eslintrc.json
```
## Build, Serve and Test your Application
1. To build your application run:
```{% command="nx build myapp" path="~/acme" %}
> nx run myapp:build
Building Remix app in production mode...
Built in 857ms
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> NX Successfully ran target build for project myapp (3s)
```
2. To serve your application for use during development run:
```{% command="nx serve myapp" path="~/acme" %}
> nx run myapp:serve
💿 Building...
💿 Rebuilt in 377ms
Remix App Server started at http://localhost:3000 (http://192.168.0.14:3000)
```
3. To test the application using vitest run:
```{% command="nx test myapp" path="~/acme" %}
> nx run myapp:test
RUN v0.31.4 /Users/columferry/dev/nrwl/issues/remixguide/acme/apps/myapp
stderr | app/routes/index.spec.ts > test > should render
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
✓ app/routes/index.spec.ts (1 test) 10ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 16:15:45
Duration 1.20s (transform 51ms, setup 139ms, collect 180ms, tests 10ms, environment 379ms, prepare 103ms)
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> NX Successfully ran target test for project myapp (2s)
```
## Generating an Nx Library
When developing your application, it often makes sense to split your codebase into smaller more focused libraries.
To generate a library to use in your Remix application run:
```{% command="nx g @nx/remix:lib login --directory=libs/login" path="~/acme" %}
> NX Generating @nx/remix:library
✔ What test runner should be used? · vitest
UPDATE nx.json
UPDATE package.json
CREATE babel.config.json
CREATE libs/login/project.json
CREATE libs/login/.eslintrc.json
CREATE libs/login/README.md
CREATE libs/login/src/index.ts
CREATE libs/login/tsconfig.lib.json
CREATE libs/login/tsconfig.json
CREATE libs/login/vite.config.ts
CREATE libs/login/tsconfig.spec.json
CREATE libs/login/src/lib/login.module.css
CREATE libs/login/src/lib/login.spec.tsx
CREATE libs/login/src/lib/login.tsx
UPDATE tsconfig.base.json
CREATE libs/login/src/test-setup.ts
CREATE libs/login/src/server.ts
```
You can then use the library by importing one of the exports into your application:
`apps/myapp/app/routes/index.tsx`
```tsx
import { Login } from '@acme/login';
export default function Index() {
return (
<div>
<Login />
</div>
);
}
```
You can also run test on your library:
`nx test login`
## Generating a Route
To generate a route for your application:
```{% command="nx g @nx/remix:route admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:route
CREATE apps/myapp/app/routes/admin.tsx
CREATE apps/myapp/app/styles/admin.css
```
## Using a loader from your Library
To use a Route Loader where the logic lives in your library, follow the steps below.
1. Generate a loader for your route:
```{% command="nx g @nx/remix:loader admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:loader
UPDATE apps/myapp/app/routes/admin.tsx
```
2. Add a new file in your `login` lib
`libs/login/src/lib/admin/admin.loader.ts`
```ts
import { json, LoaderFunctionArgs } from '@remix-run/node';
export const adminLoader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});
};
```
Export the function from the `libs/login/src/server.ts` file:
```ts
export * from './lib/admin/admin.loader';
```
3. Use the loader in your `apps/myapp/app/routes/admin.tsx`
Replace the default loader code:
```tsx
export const loader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});
};
```
with
```tsx
import { adminLoader } from '@acme/login/server';
export const loader = adminLoader;
```
## GitHub Repository with Example
You can see an example of an Nx Workspace using Remix by clicking below.
{% github-repository url="https://github.com/nrwl/nx-recipes/tree/main/remix" /%}

View File

@ -0,0 +1,48 @@
{
"name": "build",
"implementation": "/packages/remix/src/executors/build/build.impl.ts",
"schema": {
"version": 2,
"outputCapture": "pipe",
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "Remix Build",
"description": "Build a Remix app.",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory",
"x-priority": "important"
},
"includeDevDependenciesInPackageJson": {
"type": "boolean",
"description": "Include `devDependencies` in the generated package.json file. By default only production `dependencies` are included.",
"default": false
},
"generatePackageJson": {
"type": "boolean",
"description": "Generate package.json file in the output folder.",
"default": false
},
"generateLockfile": {
"type": "boolean",
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false
},
"sourcemap": {
"type": "boolean",
"description": "Generate source maps for production.",
"default": false
}
},
"required": ["outputPath"],
"presets": []
},
"description": "Build a Remix application.",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/executors/build/schema.json",
"type": "executor"
}

View File

@ -0,0 +1,51 @@
{
"name": "serve",
"implementation": "/packages/remix/src/executors/serve/serve.impl.ts",
"schema": {
"version": 2,
"outputCapture": "pipe",
"cli": "nx",
"title": "Remix Serve",
"description": "Serve a Remix app.",
"type": "object",
"properties": {
"port": {
"type": "number",
"description": "Set PORT environment variable that can be used to serve the Remix application.",
"default": 4200
},
"devServerPort": {
"type": "number",
"description": "Port to start the dev server on."
},
"debug": {
"type": "boolean",
"description": "Attach a Node.js inspector.",
"default": false
},
"command": {
"type": "string",
"description": "Command used to run your app server."
},
"manual": {
"type": "boolean",
"description": "Enable manual mode",
"default": false
},
"tlsKey": {
"type": "string",
"description": "Path to TLS key (key.pem)."
},
"tlsCert": {
"type": "string",
"description": "Path to TLS certificate (cert.pem)."
}
},
"presets": []
},
"description": "Serve a Remix application.",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/executors/serve/schema.json",
"type": "executor"
}

View File

@ -0,0 +1,38 @@
{
"name": "action",
"implementation": "/packages/remix/src/generators/action/action.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "action",
"type": "object",
"description": "Generate an action for a given route.",
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the action in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
}
},
"required": ["path"],
"presets": []
},
"description": "Add an action function to an existing route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/action/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,76 @@
{
"name": "application",
"implementation": "/packages/remix/src/generators/application/application.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixApplication",
"title": "Create an Application",
"description": "Generate a new Remix application.",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the name of the application?"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"directory": {
"type": "string",
"description": "A directory where the app is placed.",
"alias": "dir",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
"default": "cypress",
"description": "Test runner to use for e2e tests"
},
"tags": {
"type": "string",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false
},
"rootProject": {
"type": "boolean",
"x-priority": "internal",
"default": false
}
},
"presets": []
},
"description": "Generate a new Remix application",
"aliases": ["app"],
"x-type": "application",
"hidden": false,
"path": "/packages/remix/src/generators/application/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,51 @@
{
"name": "cypress-component-configuration",
"implementation": "/packages/remix/src/generators/cypress-component-configuration/cypress-component-configuration.impl.ts",
"schema": {
"$schema": "https://json-schema.org/schema",
"cli": "nx",
"$id": "NxRemixCypressComponentTestConfiguration",
"title": "Add Cypress component testing",
"description": "Add a Cypress component testing configuration to an existing project.",
"type": "object",
"examples": [
{
"command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project",
"description": "Add component testing to your Remix project"
},
{
"command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project --generate-tests",
"description": "Add component testing to your Remix project and generate component tests for your existing components"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing configuration to",
"x-dropdown": "projects",
"x-prompt": "What project should we add Cypress component testing to?",
"x-priority": "important"
},
"generateTests": {
"type": "boolean",
"description": "Generate default component tests for existing components in the project",
"x-prompt": "Automatically generate tests for components declared in this project?",
"default": false,
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false,
"x-priority": "internal"
}
},
"required": ["project"],
"presets": []
},
"description": "Generate a Cypress Component Testing configuration for a Remix project",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/cypress-component-configuration/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,66 @@
{
"name": "cypress",
"implementation": "/packages/remix/src/generators/cypress/cypress.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixCypress",
"title": "",
"type": "object",
"description": "Generate a Cypress e2e project for a given application.",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": { "$source": "projectName" }
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"baseUrl": {
"type": "string",
"description": "URL to access the application on",
"default": "http://localhost:3000"
},
"name": {
"type": "string",
"description": "Name of the E2E Project",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
}
},
"required": ["name"],
"presets": []
},
"description": "Generate a project for testing Remix apps using Cypress",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/cypress/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,49 @@
{
"name": "error-boundary",
"implementation": "/packages/remix/src/generators/error-boundary/error-boundary.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixErrorBoundary",
"title": "Create an ErrorBoundary for a Route",
"description": "Generate an ErrorBoundary for a given route.",
"type": "object",
"examples": [
{
"command": "g error-boundary --routePath=apps/demo/app/routes/my-route.tsx",
"description": "Generate an ErrorBoundary for my-route.tsx"
}
],
"properties": {
"path": {
"type": "string",
"description": "The path to route file relative to the project root."
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the error boundary in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project contains the route file that this ErrorBoundary is for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files after generation.",
"default": false,
"x-priority": "internal"
}
},
"required": ["path"],
"presets": []
},
"description": "Add an ErrorBoundary to an existing route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/error-boundary/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,82 @@
{
"name": "library",
"implementation": "/packages/remix/src/generators/library/library.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixLibrary",
"title": "Create a Library",
"description": "Generate a Remix library to help structure workspace and application.",
"type": "object",
"examples": [
{
"command": "g lib mylib --directory=myapp",
"description": "Generate libs/myapp/mylib"
}
],
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the library?",
"pattern": "^[a-zA-Z].*$"
},
"directory": {
"type": "string",
"description": "A directory where the lib is placed.",
"alias": "dir",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting)"
},
"style": {
"type": "string",
"description": "Generate a stylesheet",
"enum": ["none", "css"],
"default": "css"
},
"buildable": {
"type": "boolean",
"description": "Should the library be buildable?",
"default": false
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "vitest", "none"],
"description": "Test Runner to use for Unit Tests",
"x-prompt": "What test runner should be used?",
"default": "vitest"
},
"importPath": {
"type": "string",
"description": "The library name used to import it, like @myorg/my-awesome-lib"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files after generator runs",
"default": false,
"x-priority": "internal"
}
},
"required": ["name"],
"presets": []
},
"description": "Generate a new library",
"aliases": ["lib"],
"x-type": "library",
"hidden": false,
"path": "/packages/remix/src/generators/library/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,38 @@
{
"name": "loader",
"implementation": "/packages/remix/src/generators/loader/loader.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "data-loader",
"type": "object",
"description": "Generate an loader for a given route.",
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the loader in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
}
},
"required": ["path"],
"presets": []
},
"description": "Add a loader function to an existing route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/loader/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,38 @@
{
"name": "meta",
"implementation": "/packages/remix/src/generators/meta/meta.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "meta",
"type": "object",
"description": "Generate a meta function for a given route.",
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the meta function in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
}
},
"required": ["path"],
"presets": []
},
"description": "Add a meta function to an existing route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/meta/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,24 @@
{
"name": "preset",
"implementation": "/packages/remix/src/generators/preset/preset.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "Remix",
"title": "",
"type": "object",
"description": "Generate a Remix application in a standalone workspace. Can be used with `create-nx-workspace --preset=@nx/remix`.",
"properties": {
"tags": {
"type": "string",
"description": "Add tags to the app (used for linting).",
"alias": "t"
}
},
"presets": []
},
"description": "Generate a new Remix workspace",
"hidden": true,
"aliases": [],
"path": "/packages/remix/src/generators/preset/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,60 @@
{
"name": "resource-route",
"implementation": "/packages/remix/src/generators/resource-route/resource-route.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixResourceRoute",
"title": "Create a Resource Route",
"type": "object",
"description": "Generate a resource route.",
"examples": [
{
"command": "g resource-route 'path/to/page'",
"description": "Generate resource route at /path/to/page"
}
],
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
},
"action": {
"type": "boolean",
"description": "Generate an action function",
"default": false
},
"loader": {
"type": "boolean",
"description": "Generate a loader function",
"default": true
},
"skipChecks": {
"type": "boolean",
"description": "Skip route error detection",
"default": false
}
},
"required": ["path"],
"presets": []
},
"description": "Generate a new resource route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/resource-route/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,71 @@
{
"name": "route",
"implementation": "/packages/remix/src/generators/route/route.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixRoute",
"title": "Create a Route",
"description": "Generate a route.",
"type": "object",
"examples": [
{
"command": "g route 'path/to/page'",
"description": "Generate route at /path/to/page"
}
],
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the workspace root.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the route in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and path relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
},
"style": {
"type": "string",
"description": "Generate a stylesheet",
"enum": ["none", "css"],
"default": "css"
},
"meta": {
"type": "boolean",
"description": "Generate a meta function",
"default": false
},
"action": {
"type": "boolean",
"description": "Generate an action function",
"default": false
},
"loader": {
"type": "boolean",
"description": "Generate a loader function",
"default": false
},
"skipChecks": {
"type": "boolean",
"description": "Skip route error detection",
"default": false
}
},
"required": ["path"],
"presets": []
},
"description": "Generate a new route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/route/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,44 @@
{
"name": "setup-tailwind",
"implementation": "/packages/remix/src/generators/setup-tailwind/setup-tailwind.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixTailwind",
"title": "Add TailwindCSS to a Remix App",
"description": "Setup tailwindcss for a given project.",
"type": "object",
"examples": [
{
"command": "g setup-tailwind --project=myapp",
"description": "Generate a TailwindCSS config for your Remix app"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add tailwind to",
"$default": { "$source": "projectName" },
"x-prompt": "What project would you like to add Tailwind to?",
"pattern": "^[a-zA-Z].*$"
},
"js": {
"type": "boolean",
"description": "Generate a JavaScript config file instead of a TypeScript config file",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files after generator runs",
"default": false,
"x-priority": "internal"
}
},
"required": ["project"],
"presets": []
},
"description": "Generates a TailwindCSS configuration for the Remix application",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/setup-tailwind/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,24 @@
{
"name": "setup",
"implementation": "/packages/remix/src/generators/setup/setup.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixSetup",
"title": "",
"type": "object",
"description": "Generate initial files required for Remix to work within the workspace.",
"properties": {
"packageManager": {
"type": "string",
"description": "The package manager to setup for",
"enum": ["yarn", "npm", "pnpm"]
}
},
"presets": []
},
"description": "Setup a Remix in an existing workspace",
"hidden": true,
"aliases": [],
"path": "/packages/remix/src/generators/setup/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,93 @@
{
"name": "storybook-configuration",
"implementation": "/packages/remix/src/generators/storybook-configuration/storybook-configuration.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxRemixStorybookConfigure",
"title": "Remix Storybook Configuration",
"description": "Set up Storybook for a Remix library.",
"type": "object",
"properties": {
"project": {
"type": "string",
"aliases": ["name", "projectName"],
"description": "Project for which to generate Storybook configuration.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "For which project do you want to generate Storybook configuration?",
"x-dropdown": "projects",
"x-priority": "important"
},
"configureCypress": {
"type": "boolean",
"description": "Run the cypress-configure generator.",
"x-prompt": "Configure a cypress e2e app to run against the storybook instance?",
"default": true,
"x-priority": "important"
},
"generateStories": {
"type": "boolean",
"description": "Automatically generate `*.stories.ts` files for components declared in this project?",
"x-prompt": "Automatically generate *.stories.ts files for components declared in this project?",
"default": true,
"x-priority": "important"
},
"generateCypressSpecs": {
"type": "boolean",
"description": "Automatically generate test files in the Cypress E2E app generated by the `cypress-configure` generator.",
"x-prompt": "Automatically generate test files in the Cypress E2E app generated by the cypress-configure generator?",
"default": true,
"x-priority": "important"
},
"configureStaticServe": {
"type": "boolean",
"description": "Specifies whether to configure a static file server target for serving storybook. Helpful for speeding up CI build/test times.",
"x-prompt": "Configure a static file server for the storybook instance?",
"default": true,
"x-priority": "important"
},
"cypressDirectory": {
"type": "string",
"description": "A directory where the Cypress project will be placed. Placed at the root by default."
},
"js": {
"type": "boolean",
"description": "Generate JavaScript story files rather than TypeScript story files.",
"default": false
},
"tsConfiguration": {
"type": "boolean",
"description": "Configure your project with TypeScript. Generate main.ts and preview.ts files, instead of main.js and preview.js.",
"default": false
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
},
"ignorePaths": {
"type": "array",
"description": "Paths to ignore when looking for components.",
"items": { "type": "string", "description": "Path to ignore." },
"examples": [
"**/**/src/**/not-stories/**",
"libs/my-lib/**/*.something.ts",
"**/**/src/**/*.other.*",
"libs/my-lib/src/not-stories/**,**/**/src/**/*.other.*,apps/my-app/**/*.something.ts"
]
},
"configureTestRunner": {
"type": "boolean",
"description": "Add a Storybook Test-Runner target."
}
},
"required": ["name"],
"presets": []
},
"description": "Generates a Storybook configuration for a Remix application",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/storybook-configuration/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,45 @@
{
"name": "style",
"implementation": "/packages/remix/src/generators/style/style.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixRouteStyle",
"title": "Add style import to a route",
"description": "Generate a style import and file for a given route.",
"type": "object",
"examples": [
{
"command": "g style --path='apps/demo/app/routes/path/to/page.tsx'",
"description": "Generate route at apps/demo/app/routes/path/to/page.tsx"
}
],
"properties": {
"path": {
"type": "string",
"description": "Route path",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the styles in the path as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" },
"x-prompt": "What project is this route in?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
}
},
"required": ["path"],
"presets": []
},
"description": "Generates a new stylesheet and adds it to an existing route",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/style/schema.json",
"type": "generator"
}

View File

@ -2390,6 +2390,19 @@
}
]
},
{
"name": "remix",
"id": "remix",
"description": "Remix package.",
"itemList": [
{
"id": "overview",
"name": "Overview",
"path": "/nx-api/remix",
"file": "shared/packages/remix/remix-plugin"
}
]
},
{
"name": "detox",
"id": "detox",

View File

@ -62,7 +62,7 @@ npx create-nx-workspace@latest
{% link-card title="Next" appearance="small" url="/nx-api/next" icon="nextjs" /%}
{% link-card title="Nuxt" appearance="small" url="/showcase/example-repos/add-nuxt" icon="nuxt" /%}
{% link-card title="Nest" appearance="small" url="/nx-api/nest" icon="nestjs" /%}
{% link-card title="Remix" appearance="small" url="/recipes/react/remix" icon="remix" /%}
{% link-card title="Remix" appearance="small" url="/nx-api/remix" icon="remix" /%}
{% link-card title="Expo" appearance="small" url="/nx-api/expo" icon="expo" /%}
{% link-card title="React Native" appearance="small" url="/nx-api/react-native" icon="react" /%}
{% link-card title="Fastify" appearance="small" url="/showcase/example-repos/mongo-fastify" icon="fastify" /%}

View File

@ -28,7 +28,7 @@ In this recipe, we'll show you how to create a [Remix](https://remix.run) applic
## Install Nx Remix Plugin
{% callout type="note" title="Keep Nx Package Versions In Sync" %}
Make sure to install the `@nx/remix` version that is on the same minor version as the `nx` version in your repository. If the version numbers get out of sync, you can encounter some difficult to debug errors. You can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync). The `@nx/remix` package is still being developed under [nx-labs](https://github.com/nrwl/nx-labs), so the publishing cadence is not perfectly coordinated with the other Nx packages.
Make sure to install the `@nx/remix` version that is on the same minor version as the `nx` version in your repository. If the version numbers get out of sync, you can encounter some difficult to debug errors. You can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync).
{% /callout %}
```shell
@ -50,7 +50,7 @@ CREATE apps/myapp/project.json
UPDATE package.json
CREATE apps/myapp/README.md
CREATE apps/myapp/app/root.tsx
CREATE apps/myapp/app/routes/index.tsx
CREATE apps/myapp/app/routes/_index.tsx
CREATE apps/myapp/public/favicon.ico
CREATE apps/myapp/remix.config.js
CREATE apps/myapp/remix.env.d.ts
@ -175,7 +175,7 @@ You can also run test on your library:
To generate a route for your application:
```{% command="nx g @nx/remix:route admin --project=myapp" path="~/acme" %}
```{% command="nx g @nx/remix:route admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:route
CREATE apps/myapp/app/routes/admin.tsx
@ -188,7 +188,7 @@ To use a Route Loader where the logic lives in your library, follow the steps be
1. Generate a loader for your route:
```{% command="nx g @nx/remix:loader admin --project=myapp" path="~/acme" %}
```{% command="nx g @nx/remix:loader admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:loader
UPDATE apps/myapp/app/routes/admin.tsx
@ -199,9 +199,9 @@ UPDATE apps/myapp/app/routes/admin.tsx
`libs/login/src/lib/admin/admin.loader.ts`
```ts
import { json, LoaderArgs } from '@remix-run/node';
import { json, LoaderFunctionArgs } from '@remix-run/node';
export const adminLoader = async ({ request }: LoaderArgs) => {
export const adminLoader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});
@ -219,7 +219,7 @@ export * from './lib/admin/admin.loader';
Replace the default loader code:
```tsx
export const loader = async ({ request }: LoaderArgs) => {
export const loader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});

View File

@ -0,0 +1,236 @@
The Nx Plugin for Remix contains executors, generators, and utilities for managing Remix applications and libraries
within an Nx workspace. It provides:
- Integration with libraries such as Storybook, Jest, Vitest and Cypress.
- Generators to help scaffold code quickly, including:
- Libraries, both internal to your codebase and publishable to npm
- Routes
- Loaders
- Actions
- Meta
- Utilities for automatic workspace refactoring.
## Setting up the Remix plugin
{% callout type="note" title="Keep Nx Package Versions In Sync" %}
Make sure to install the `@nx/remix` version that matches the version of `nx` in your repository. If the version
numbers get out of sync, you can encounter some difficult to debug errors. You
can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync).
{% /callout %}
Adding the Remix plugin to an existing Nx workspace can be done with the following:
```shell
yarn add -D @nx/remix
```
```shell
npm install -D @nx/remix
```
## Using the Remix Plugin
## Generate a Remix Application
{% callout type="note" title="Directory Flag Behavior Changes" %}
The command below uses the `as-provided` directory flag behavior, which is the default in Nx 16.8.0. If you're on an earlier version of Nx or using the `derived` option, omit the `--directory` flag. See the [as-provided vs. derived documentation](/deprecated/as-provided-vs-derived) for more details.
{% /callout %}
```{% command="nx g @nx/remix:app myapp --directory=apps/myapp" path="~/acme" %}
> NX Generating @nx/remix:application
✔ What unit test runner should be used? · vitest
CREATE apps/myapp/project.json
UPDATE package.json
CREATE apps/myapp/README.md
CREATE apps/myapp/app/root.tsx
CREATE apps/myapp/app/routes/_index.tsx
CREATE apps/myapp/public/favicon.ico
CREATE apps/myapp/remix.config.js
CREATE apps/myapp/remix.env.d.ts
CREATE apps/myapp/tsconfig.json
CREATE apps/myapp/.gitignore
CREATE apps/myapp/package.json
UPDATE nx.json
CREATE tsconfig.base.json
CREATE .prettierrc
CREATE .prettierignore
UPDATE .vscode/extensions.json
CREATE apps/myapp/vite.config.ts
CREATE apps/myapp/tsconfig.spec.json
CREATE apps/myapp/test-setup.ts
CREATE apps/myapp-e2e/cypress.config.ts
CREATE apps/myapp-e2e/src/e2e/app.cy.ts
CREATE apps/myapp-e2e/src/fixtures/example.json
CREATE apps/myapp-e2e/src/support/commands.ts
CREATE apps/myapp-e2e/src/support/e2e.ts
CREATE apps/myapp-e2e/tsconfig.json
CREATE apps/myapp-e2e/project.json
CREATE .eslintrc.json
CREATE .eslintignore
CREATE apps/myapp-e2e/.eslintrc.json
```
## Build, Serve and Test your Application
1. To build your application run:
```{% command="nx build myapp" path="~/acme" %}
> nx run myapp:build
Building Remix app in production mode...
Built in 857ms
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> NX Successfully ran target build for project myapp (3s)
```
2. To serve your application for use during development run:
```{% command="nx serve myapp" path="~/acme" %}
> nx run myapp:serve
💿 Building...
💿 Rebuilt in 377ms
Remix App Server started at http://localhost:3000 (http://192.168.0.14:3000)
```
3. To test the application using vitest run:
```{% command="nx test myapp" path="~/acme" %}
> nx run myapp:test
RUN v0.31.4 /Users/columferry/dev/nrwl/issues/remixguide/acme/apps/myapp
stderr | app/routes/index.spec.ts > test > should render
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
✓ app/routes/index.spec.ts (1 test) 10ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 16:15:45
Duration 1.20s (transform 51ms, setup 139ms, collect 180ms, tests 10ms, environment 379ms, prepare 103ms)
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> NX Successfully ran target test for project myapp (2s)
```
## Generating an Nx Library
When developing your application, it often makes sense to split your codebase into smaller more focused libraries.
To generate a library to use in your Remix application run:
```{% command="nx g @nx/remix:lib login --directory=libs/login" path="~/acme" %}
> NX Generating @nx/remix:library
✔ What test runner should be used? · vitest
UPDATE nx.json
UPDATE package.json
CREATE babel.config.json
CREATE libs/login/project.json
CREATE libs/login/.eslintrc.json
CREATE libs/login/README.md
CREATE libs/login/src/index.ts
CREATE libs/login/tsconfig.lib.json
CREATE libs/login/tsconfig.json
CREATE libs/login/vite.config.ts
CREATE libs/login/tsconfig.spec.json
CREATE libs/login/src/lib/login.module.css
CREATE libs/login/src/lib/login.spec.tsx
CREATE libs/login/src/lib/login.tsx
UPDATE tsconfig.base.json
CREATE libs/login/src/test-setup.ts
CREATE libs/login/src/server.ts
```
You can then use the library by importing one of the exports into your application:
`apps/myapp/app/routes/index.tsx`
```tsx
import { Login } from '@acme/login';
export default function Index() {
return (
<div>
<Login />
</div>
);
}
```
You can also run test on your library:
`nx test login`
## Generating a Route
To generate a route for your application:
```{% command="nx g @nx/remix:route admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:route
CREATE apps/myapp/app/routes/admin.tsx
CREATE apps/myapp/app/styles/admin.css
```
## Using a loader from your Library
To use a Route Loader where the logic lives in your library, follow the steps below.
1. Generate a loader for your route:
```{% command="nx g @nx/remix:loader admin --path=apps/myapp/app/routes" path="~/acme" %}
> NX Generating @nx/remix:loader
UPDATE apps/myapp/app/routes/admin.tsx
```
2. Add a new file in your `login` lib
`libs/login/src/lib/admin/admin.loader.ts`
```ts
import { json, LoaderFunctionArgs } from '@remix-run/node';
export const adminLoader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});
};
```
Export the function from the `libs/login/src/server.ts` file:
```ts
export * from './lib/admin/admin.loader';
```
3. Use the loader in your `apps/myapp/app/routes/admin.tsx`
Replace the default loader code:
```tsx
export const loader = async ({ request }: LoaderFunctionArgs) => {
return json({
message: 'Hello, world!',
});
};
```
with
```tsx
import { adminLoader } from '@acme/login/server';
export const loader = adminLoader;
```
## GitHub Repository with Example
You can see an example of an Nx Workspace using Remix by clicking below.
{% github-repository url="https://github.com/nrwl/nx-recipes/tree/main/remix" /%}

View File

@ -612,6 +612,28 @@
- [component-story](/nx-api/react-native/generators/component-story)
- [stories](/nx-api/react-native/generators/stories)
- [upgrade-native](/nx-api/react-native/generators/upgrade-native)
- [remix](/nx-api/remix)
- [documents](/nx-api/remix/documents)
- [Overview](/nx-api/remix/documents/overview)
- [executors](/nx-api/remix/executors)
- [serve](/nx-api/remix/executors/serve)
- [build](/nx-api/remix/executors/build)
- [generators](/nx-api/remix/generators)
- [preset](/nx-api/remix/generators/preset)
- [setup](/nx-api/remix/generators/setup)
- [application](/nx-api/remix/generators/application)
- [cypress-component-configuration](/nx-api/remix/generators/cypress-component-configuration)
- [library](/nx-api/remix/generators/library)
- [route](/nx-api/remix/generators/route)
- [resource-route](/nx-api/remix/generators/resource-route)
- [action](/nx-api/remix/generators/action)
- [loader](/nx-api/remix/generators/loader)
- [style](/nx-api/remix/generators/style)
- [setup-tailwind](/nx-api/remix/generators/setup-tailwind)
- [storybook-configuration](/nx-api/remix/generators/storybook-configuration)
- [meta](/nx-api/remix/generators/meta)
- [error-boundary](/nx-api/remix/generators/error-boundary)
- [cypress](/nx-api/remix/generators/cypress)
- [rollup](/nx-api/rollup)
- [executors](/nx-api/rollup/executors)
- [rollup](/nx-api/rollup/executors/rollup)

13
e2e/remix/jest.config.ts Normal file
View File

@ -0,0 +1,13 @@
/* eslint-disable */
export default {
displayName: 'e2e-remix',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: {},
globalSetup: '../utils/global-setup.ts',
globalTeardown: '../utils/global-teardown.ts',
};

10
e2e/remix/project.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "e2e-remix",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/remix",
"projectType": "application",
"targets": {
"e2e": {}
},
"implicitDependencies": ["remix"]
}

View File

@ -0,0 +1,175 @@
import {
cleanupProject,
killPorts,
newProject,
runCLI,
checkFilesExist,
readJson,
uniq,
updateFile,
runCommandAsync,
} from '@nx/e2e/utils';
describe('remix e2e', () => {
let proj: string;
beforeAll(() => {
proj = newProject({ packages: ['@nx/remix'] });
});
afterAll(() => {
killPorts();
cleanupProject();
});
it('should create a standalone remix app', async () => {
const appName = uniq('remix');
runCLI(`generate @nx/remix:preset --name ${appName} --verbose`);
// Can import using ~ alias like a normal Remix setup.
updateFile(`app/foo.ts`, `export const foo = 'foo';`);
updateFile(
`app/routes/index.tsx`,
`
import { foo } from '~/foo';
export default function Index() {
return (
<h1>{foo}</h1>
);
}
`
);
const result = runCLI(`build ${appName}`);
expect(result).toContain('Successfully ran target build');
}, 120_000);
it('should create app', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin}`);
const buildResult = runCLI(`build ${plugin}`);
expect(buildResult).toContain('Successfully ran target build');
const testResult = runCLI(`test ${plugin}`);
expect(testResult).toContain('Successfully ran target test');
}, 120000);
describe('--directory', () => {
it('should create src in the specified directory --projectNameAndRootFormat=derived', async () => {
const plugin = uniq('remix');
const appName = `sub-${plugin}`;
runCLI(
`generate @nx/remix:app ${plugin} --directory=sub --projectNameAndRootFormat=derived --rootProject=false`
);
const project = readJson(`sub/${plugin}/project.json`);
expect(project.targets.build.options.outputPath).toEqual(
`dist/sub/${plugin}`
);
const result = runCLI(`build ${appName}`);
expect(result).toContain('Successfully ran target build');
}, 120000);
it('should create src in the specified directory --projectNameAndRootFormat=as-provided', async () => {
const plugin = uniq('remix');
runCLI(
`generate @nx/remix:app ${plugin} --directory=subdir --projectNameAndRootFormat=as-provided --rootProject=false`
);
const project = readJson(`subdir/project.json`);
expect(project.targets.build.options.outputPath).toEqual(`dist/subdir`);
const result = runCLI(`build ${plugin}`);
expect(result).toContain('Successfully ran target build');
}, 120000);
});
describe('--tags', () => {
it('should add tags to the project', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin} --tags e2etag,e2ePackage`);
const project = readJson(`${plugin}/project.json`);
expect(project.tags).toEqual(['e2etag', 'e2ePackage']);
}, 120000);
});
describe('--js', () => {
it('should create js app and build correctly', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin} --js=true`);
const result = runCLI(`build ${plugin}`);
expect(result).toContain('Successfully ran target build');
}, 120000);
});
describe('--unitTestRunner', () => {
it('should generate a library with vitest and test correctly', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:library ${plugin} --unitTestRunner=vitest`);
const result = runCLI(`test ${plugin}`);
expect(result).toContain(`Successfully ran target test`);
}, 120_000);
});
describe('error checking', () => {
const plugin = uniq('remix');
beforeAll(async () => {
runCLI(`generate @nx/remix:app ${plugin} --tags e2etag,e2ePackage`);
}, 120000);
it('should check for un-escaped dollar signs in routes', async () => {
await expect(async () =>
runCLI(
`generate @nx/remix:route --project ${plugin} --path my.route.$withParams.tsx`
)
).rejects.toThrow();
runCLI(
`generate @nx/remix:route --project ${plugin} --path my.route.\\$withParams.tsx`
);
expect(() =>
checkFilesExist(`${plugin}/app/routes/my.route.$withParams.tsx`)
).not.toThrow();
}, 120000);
it('should pass un-escaped dollar signs in routes with skipChecks flag', async () => {
await runCommandAsync(
`someWeirdUseCase=route-segment && yarn nx generate @nx/remix:route --project ${plugin} --path my.route.$someWeirdUseCase.tsx --force`
);
expect(() =>
checkFilesExist(`${plugin}/app/routes/my.route.route-segment.tsx`)
).not.toThrow();
}, 120000);
it('should check for un-escaped dollar signs in resource routes', async () => {
await expect(async () =>
runCLI(
`generate @nx/remix:resource-route --project ${plugin} --path my.route.$withParams.ts`
)
).rejects.toThrow();
runCLI(
`generate @nx/remix:resource-route --project ${plugin} --path my.route.\\$withParams.ts`
);
expect(() =>
checkFilesExist(`${plugin}/app/routes/my.route.$withParams.ts`)
).not.toThrow();
}, 120000);
it('should pass un-escaped dollar signs in resource routes with skipChecks flag', async () => {
await runCommandAsync(
`someWeirdUseCase=route-segment && yarn nx generate @nx/remix:resource-route --project ${plugin} --path my.route.$someWeirdUseCase.ts --force`
);
expect(() =>
checkFilesExist(`${plugin}/app/routes/my.route.route-segment.ts`)
).not.toThrow();
}, 120000);
});
});

13
e2e/remix/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}

View File

@ -53,6 +53,7 @@ const nxPackages = [
`@nx/playwright`,
`@nx/rollup`,
`@nx/react`,
`@nx/remix`,
`@nx/storybook`,
`@nx/vue`,
`@nx/vite`,

View File

@ -0,0 +1,10 @@
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(0.5,0,0,0.5,0,0)">
<g transform="matrix(1.33333,0,0,1.33333,-133,-133.667)">
<path d="M587.947,527.768C592.201,582.418 592.201,608.036 592.201,636L465.756,636C465.756,629.909 465.865,624.337 465.975,618.687C466.317,601.123 466.674,582.807 463.828,545.819C460.067,491.667 436.748,479.634 393.871,479.634L195,479.634L195,381.109L399.889,381.109C454.049,381.109 481.13,364.633 481.13,321.011C481.13,282.654 454.049,259.41 399.889,259.41L195,259.41L195,163L422.456,163C545.069,163 606,220.912 606,313.42C606,382.613 563.123,427.739 505.201,435.26C554.096,445.037 582.681,472.865 587.947,527.768Z" style="fill:rgb(18,18,18);"/>
</g>
<g transform="matrix(1.33333,0,0,1.33333,-133,-133.667)">
<path d="M195,636L195,562.553L328.697,562.553C351.029,562.553 355.878,579.116 355.878,588.994L355.878,636L195,636Z" style="fill:rgb(18,18,18);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1015 B

View File

@ -22,6 +22,7 @@ export const iconsMap: Record<string, string> = {
plugin: '/images/icons/nx.svg',
react: '/images/icons/react.svg',
'react-native': '/images/icons/react.svg',
remix: '/images/icons/remix.svg',
rollup: '/images/icons/rollup.svg',
rspack: '/images/icons/rspack.svg',
storybook: '/images/icons/storybook.svg',

View File

@ -84,6 +84,8 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@pnpm/lockfile-types": "^5.0.0",
"@reduxjs/toolkit": "1.9.0",
"@remix-run/dev": "^2.3.0",
"@remix-run/node": "^2.3.0",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^20.0.0",
"@rollup/plugin-image": "^2.1.0",

View File

@ -0,0 +1,11 @@
## @nrwl/remix has been deprecated!
@nrwl/remix has been deprecated in favor of [@nx/remix](https://www.npmjs.com/package/@nx/remix). Please use that instead.
@nrwl/remix will no longer be published in Nx v17.
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
# Nx: Smart, Fast and Extensible Build System
Nx is a next generation build system with first class monorepo support and powerful integrations.

View File

@ -0,0 +1,4 @@
{
"extends": ["@nx/remix"],
"schematics": {}
}

View File

@ -0,0 +1 @@
export * from '@nx/remix';

View File

@ -0,0 +1,35 @@
{
"name": "@nrwl/remix",
"version": "0.0.1",
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
"directory": "packages-legacy/remix"
},
"keywords": [
"Monorepo",
"Remix",
"React",
"Web",
"CLI"
],
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"homepage": "https://nx.dev",
"main": "index.js",
"typings": "./index.d.ts",
"generators": "./generators.json",
"dependencies": {
"@nx/remix": "file:../../packages/remix"
},
"nx-migrations": {
"migrations": "@nx/remix/migrations.json"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,38 @@
{
"name": "remix-legacy",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages-legacy/remix",
"projectType": "library",
"targets": {
"build": {
"outputs": ["{workspaceRoot}/build/packages/{projectName}/README.md"],
"command": "node ./scripts/copy-readme.js react-legacy"
},
"build-base": {
"executor": "@nrwl/js:tsc",
"dependsOn": ["^build"],
"options": {
"main": "packages-legacy/remix/index.ts",
"tsConfig": "packages-legacy/remix/tsconfig.json",
"outputPath": "build/packages/remix-legacy",
"updateBuildableProjectDepsInPackageJson": false,
"assets": [
"packages-legacy/remix/*.md",
{
"input": "packages-legacy/remix",
"glob": "**/*.json",
"ignore": ["**/tsconfig*.json", "project.json"],
"output": "/"
},
{
"input": "packages-legacy/remix",
"glob": "**/*.d.ts",
"output": "/"
},
"LICENSE"
]
}
}
},
"tags": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"declaration": true
},
"include": ["**/*.ts"],
"files": ["index.ts"]
}

View File

@ -134,6 +134,8 @@
"@nrwl/react-native",
"@nx/rollup",
"@nrwl/rollup",
"@nx/remix",
"@nrwl/remix",
"@nx/storybook",
"@nrwl/storybook",
"@nrwl/tao",

View File

@ -68,6 +68,10 @@ export function fetchCorePlugins(): CorePlugin[] {
name: '@nx/react-native',
capabilities: 'executors,generators',
},
{
name: '@nx/remix',
capabilities: 'executors,generators',
},
{
name: '@nx/rollup',
capabilities: 'executors,generators',

View File

@ -0,0 +1,92 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": [
"./package.json",
"./generators.json",
"./executors.json",
"./migrations.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error"
}
},
{
"files": ["./package.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"buildTargets": ["build-base"],
"ignoredDependencies": [
"nx",
"eslint",
"prettier",
"typescript",
"react",
"@nx/workspace",
"fs-extra",
"@remix-run/dev",
"@nx/web",
"@nx/eslint",
// These are installed by ensurePackage so missing in package.json
"@nx/cypress",
"@nx/playwright",
"@nx/jest",
"@nx/rollup",
"@nx/storybook",
"@nx/vite",
"@nx/webpack",
// These are brought in by the webpack, rollup, or vite packages via init generators.
"@babel/preset-react",
"@phenomnomnominal/tsquery",
"@pmmmwh/react-refresh-webpack-plugin",
"@svgr/rollup",
"@rollup/plugin-url",
"@svgr/webpack",
"@swc/jest",
"babel-jest",
"babel-loader",
"babel-plugin-emotion",
"babel-plugin-styled-components",
"css-loader",
"file-loader",
"less-loader",
"react-refresh",
"rollup",
"sass",
"sass-loader",
"style-loader",
"stylus-loader",
"swc-loader",
"tsconfig-paths-webpack-plugin",
"url-loader",
"webpack",
"webpack-merge",
// used via the CT react plugin installed via vite plugin
"vite"
]
}
]
}
}
]
}

13
packages/remix/README.md Normal file
View File

@ -0,0 +1,13 @@
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart Monorepos · Fast CI"></p>
{{links}}
<hr>
# Nx: Smart Monorepos · Fast CI
Nx is a build system with built-in tooling and advanced CI capabilities. It helps you maintain and scale monorepos, both locally and on CI.
This package is a [Remix plugin for Nx](https://nx.dev/packages/remix).
{{content}}

View File

@ -0,0 +1,14 @@
{
"executors": {
"serve": {
"implementation": "./src/executors/serve/serve.impl",
"schema": "./src/executors/serve/schema.json",
"description": "Serve a Remix application."
},
"build": {
"implementation": "./src/executors/build/build.impl",
"schema": "./src/executors/build/schema.json",
"description": "Build a Remix application."
}
}
}

View File

@ -0,0 +1,89 @@
{
"$schema": "http://json-schema.org/schema",
"name": "NxRemix",
"version": "0.0.1",
"extends": ["@nx/react"],
"generators": {
"preset": {
"implementation": "./src/generators/preset/preset.impl",
"schema": "./src/generators/preset/schema.json",
"description": "Generate a new Remix workspace",
"hidden": true
},
"setup": {
"implementation": "./src/generators/setup/setup.impl",
"schema": "./src/generators/setup/schema.json",
"description": "Setup a Remix in an existing workspace",
"hidden": true
},
"application": {
"implementation": "./src/generators/application/application.impl",
"schema": "./src/generators/application/schema.json",
"description": "Generate a new Remix application",
"aliases": ["app"],
"x-type": "application"
},
"cypress-component-configuration": {
"implementation": "./src/generators/cypress-component-configuration/cypress-component-configuration.impl",
"schema": "./src/generators/cypress-component-configuration/schema.json",
"description": "Generate a Cypress Component Testing configuration for a Remix project"
},
"library": {
"implementation": "./src/generators/library/library.impl",
"schema": "./src/generators/library/schema.json",
"description": "Generate a new library",
"aliases": ["lib"],
"x-type": "library"
},
"route": {
"implementation": "./src/generators/route/route.impl",
"schema": "./src/generators/route/schema.json",
"description": "Generate a new route"
},
"resource-route": {
"implementation": "./src/generators/resource-route/resource-route.impl",
"schema": "./src/generators/resource-route/schema.json",
"description": "Generate a new resource route"
},
"action": {
"implementation": "./src/generators/action/action.impl",
"schema": "./src/generators/action/schema.json",
"description": "Add an action function to an existing route"
},
"loader": {
"implementation": "./src/generators/loader/loader.impl",
"schema": "./src/generators/loader/schema.json",
"description": "Add a loader function to an existing route"
},
"style": {
"implementation": "./src/generators/style/style.impl",
"schema": "./src/generators/style/schema.json",
"description": "Generates a new stylesheet and adds it to an existing route"
},
"setup-tailwind": {
"implementation": "./src/generators/setup-tailwind/setup-tailwind.impl",
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Generates a TailwindCSS configuration for the Remix application"
},
"storybook-configuration": {
"implementation": "./src/generators/storybook-configuration/storybook-configuration.impl",
"schema": "./src/generators/storybook-configuration/schema.json",
"description": "Generates a Storybook configuration for a Remix application"
},
"meta": {
"implementation": "./src/generators/meta/meta.impl",
"schema": "./src/generators/meta/schema.json",
"description": "Add a meta function to an existing route"
},
"error-boundary": {
"implementation": "./src/generators/error-boundary/error-boundary.impl",
"schema": "./src/generators/error-boundary/schema.json",
"description": "Add an ErrorBoundary to an existing route"
},
"cypress": {
"implementation": "./src/generators/cypress/cypress.impl",
"schema": "./src/generators/cypress/schema.json",
"description": "Generate a project for testing Remix apps using Cypress"
}
}
}

View File

@ -0,0 +1,14 @@
export * from './src/generators/action/action.impl';
export * from './src/generators/application/application.impl';
export * from './src/generators/cypress-component-configuration/cypress-component-configuration.impl';
export * from './src/generators/cypress/cypress.impl';
export * from './src/generators/error-boundary/error-boundary.impl';
export * from './src/generators/library/library.impl';
export * from './src/generators/loader/loader.impl';
export * from './src/generators/meta/meta.impl';
export * from './src/generators/preset/preset.impl';
export * from './src/generators/resource-route/resource-route.impl';
export * from './src/generators/route/route.impl';
export * from './src/generators/setup-tailwind/setup-tailwind.impl';
export * from './src/generators/storybook-configuration/storybook-configuration.impl';
export * from './src/generators/style/style.impl';

1
packages/remix/index.ts Normal file
View File

@ -0,0 +1 @@
export { createWatchPaths } from './src/utils/create-watch-paths';

View File

@ -0,0 +1,15 @@
/* eslint-disable */
export default {
displayName: 'remix',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/packages/remix',
};

View File

@ -0,0 +1,54 @@
{
"generators": {},
"packageJsonUpdates": {
"17.2.1": {
"version": "17.2.1-beta.0",
"packages": {
"@remix-run/node": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"@remix-run/react": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"@remix-run/serve": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"@remix-run/dev": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"@remix-run/css-bundle": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"@remix-run/eslint-config": {
"version": "^2.3.0",
"alwaysAddToPackageJson": true
},
"isbot": {
"version": "^3.6.8",
"alwaysAddToPackageJson": true
},
"eslint": {
"version": "^8.38.0",
"alwaysAddToPackageJson": true
},
"@testing-library/react": {
"version": "^14.1.2",
"alwaysAddToPackageJson": false
},
"@testing-library/jest-dom": {
"version": "^6.1.4",
"alwaysAddToPackageJson": false
},
"@testing-library/user-event": {
"version": "^14.5.1",
"alwaysAddToPackageJson": false
}
}
}
}
}

BIN
packages/remix/nx-remix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@ -0,0 +1,41 @@
{
"name": "@nx/remix",
"version": "0.0.1",
"description": "The Remix plugin for Nx contains executors and generators for managing Remix applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Vitest, Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, routes, loaders, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
"directory": "packages/remix"
},
"keywords": [
"Monorepo",
"Remix",
"React",
"Web",
"CLI"
],
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"homepage": "https://nx.dev",
"main": "./index.js",
"typings": "./index.d.ts",
"generators": "./generators.json",
"executors": "./executors.json",
"nx-migrations": {
"migrations": "./migrations.json"
},
"dependencies": {
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"@nx/react": "file:../react",
"tslib": "^2.3.1",
"@phenomnomnominal/tsquery": "~5.0.1"
},
"peerDependencies": {},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,85 @@
import { nxBaseCypressPreset } from '@nx/cypress/plugins/cypress-preset';
import { joinPathFragments, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
type ViteDevServer = {
framework: 'react';
bundler: 'vite';
viteConfig?: any;
};
/**
* Remix nx preset for Cypress Component Testing
*
* This preset contains the base configuration
* for your component tests that nx recommends.
* including a devServer that supports nx workspaces.
* you can easily extend this within your cypress config via spreading the preset
* @example
* export default defineConfig({
* component: {
* ...nxComponentTestingPreset(__dirname)
* // add your own config here
* }
* })
*
* @param pathToConfig will be used for loading project options and to construct the output paths for videos and screenshots
*/
export function nxComponentTestingPreset(pathToConfig: string): {
specPattern: string;
devServer: ViteDevServer;
videosFolder: string;
screenshotsFolder: string;
chromeWebSecurity: boolean;
} {
const normalizedProjectRootPath = ['.ts', '.js'].some((ext) =>
pathToConfig.endsWith(ext)
)
? pathToConfig
: dirname(pathToConfig);
return {
...nxBaseCypressPreset(pathToConfig),
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
devServer: {
...({ framework: 'react', bundler: 'vite' } as const),
viteConfig: async () => {
const viteConfigPath = findViteConfig(normalizedProjectRootPath);
const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } =
await import('vite');
const resolved = await loadConfigFromFile(
{
mode: 'watch',
command: 'serve',
},
viteConfigPath
);
return mergeConfig(resolved.config, {
server: {
fs: {
allow: [
searchForWorkspaceRoot(normalizedProjectRootPath),
workspaceRoot,
joinPathFragments(workspaceRoot, 'node_modules/vite'),
],
},
},
});
},
},
};
}
function findViteConfig(projectRootFullPath: string): string {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if (existsSync(join(projectRootFullPath, `vite.config.${ext}`))) {
return join(projectRootFullPath, `vite.config.${ext}`);
}
}
}

View File

@ -0,0 +1,53 @@
{
"name": "remix",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/remix/src",
"projectType": "library",
"targets": {
"build": {
"command": "node ./scripts/copy-readme.js remix",
"outputs": ["{workspaceRoot}/build/packages/remix"]
},
"lint": {},
"test": {},
"build-base": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "build/packages/remix",
"tsConfig": "packages/remix/tsconfig.lib.json",
"packageJson": "packages/remix/package.json",
"main": "packages/remix/index.ts",
"generateExportsField": true,
"additionalEntryPoints": [
"{projectRoot}/{executors,generators,migrations}.json",
"{projectRoot}/generators.ts"
],
"assets": [
"packages/remix/*.md",
{
"input": "./packages/remix/src",
"glob": "**/!(*.ts)",
"output": "./src"
},
{
"input": "./packages/remix/src",
"glob": "**/*.d.ts",
"output": "./src"
},
{
"input": "./packages/remix",
"glob": "**.json",
"output": ".",
"ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"]
},
"LICENSE"
]
}
},
"add-extra-dependencies": {
"command": "node ./scripts/add-dependency-to-build.js remix @nrwl/remix"
}
},
"tags": []
}

View File

@ -0,0 +1,141 @@
import {
detectPackageManager,
logger,
readJsonFile,
writeJsonFile,
type ExecutorContext,
} from '@nx/devkit';
import { createLockFile, createPackageJson, getLockFileName } from '@nx/js';
import { directoryExists } from '@nx/workspace/src/utilities/fileutils';
import { fork } from 'child_process';
import { copySync, mkdir, writeFileSync } from 'fs-extra';
import { type PackageJson } from 'nx/src/utils/package-json';
import { join } from 'path';
import { type RemixBuildSchema } from './schema';
function buildRemixBuildArgs(options: RemixBuildSchema) {
const args = ['build'];
if (options.sourcemap) {
args.push(`--sourcemap`);
}
return args;
}
async function runBuild(
options: RemixBuildSchema,
context: ExecutorContext
): Promise<void> {
const projectRoot = context.projectGraph.nodes[context.projectName].data.root;
return new Promise<void>((resolve, reject) => {
const remixBin = require.resolve('@remix-run/dev/dist/cli');
const args = buildRemixBuildArgs(options);
const p = fork(remixBin, args, {
cwd: join(context.root, projectRoot),
stdio: 'inherit',
});
p.on('exit', (code) => {
if (code === 0) resolve();
else reject();
});
});
}
export default async function buildExecutor(
options: RemixBuildSchema,
context: ExecutorContext
) {
const projectRoot = context.projectGraph.nodes[context.projectName].data.root;
try {
await runBuild(options, context);
} catch (error) {
logger.error(
`Error occurred while trying to build application. See above for more details.`
);
return { success: false };
}
if (!directoryExists(options.outputPath)) {
mkdir(options.outputPath);
}
let packageJson: PackageJson;
if (options.generatePackageJson) {
packageJson = createPackageJson(context.projectName, context.projectGraph, {
target: context.targetName,
root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
});
// Update `package.json` to reflect how users should run the build artifacts
packageJson.scripts ??= {};
// Don't override existing custom script since project may have its own server.
if (!packageJson.scripts.start) {
packageJson.scripts['start'] = 'remix-serve ./build';
}
updatePackageJson(packageJson, context);
writeJsonFile(`${options.outputPath}/package.json`, packageJson);
} else {
packageJson = readJsonFile(join(projectRoot, 'package.json'));
}
if (options.generateLockfile) {
const packageManager = detectPackageManager(context.root);
const lockFile = createLockFile(
packageJson,
context.projectGraph,
packageManager
);
writeFileSync(
`${options.outputPath}/${getLockFileName(packageManager)}`,
lockFile,
{
encoding: 'utf-8',
}
);
}
// If output path is different from source path, then copy over the config and public files.
// This is the default behavior when running `nx build <app>`.
if (options.outputPath.replace(/\/$/, '') !== projectRoot) {
copySync(join(projectRoot, 'public'), join(options.outputPath, 'public'), {
dereference: true,
});
copySync(join(projectRoot, 'build'), join(options.outputPath, 'build'), {
dereference: true,
});
}
return { success: true };
}
function updatePackageJson(packageJson: PackageJson, context: ExecutorContext) {
if (!packageJson.scripts) {
packageJson.scripts = {};
}
if (!packageJson.scripts.start) {
packageJson.scripts.start = 'remix-serve build';
}
packageJson.dependencies ??= {};
// These are always required for a production Remix app to run.
const requiredPackages = [
'react',
'react-dom',
'isbot',
'@remix-run/css-bundle',
'@remix-run/node',
'@remix-run/react',
'@remix-run/serve',
'@remix-run/dev',
];
for (const pkg of requiredPackages) {
const externalNode = context.projectGraph.externalNodes[`npm:${pkg}`];
if (externalNode) {
packageJson.dependencies[pkg] ??= externalNode.data.version;
}
}
}

View File

@ -0,0 +1,7 @@
export interface RemixBuildSchema {
outputPath: string;
includeDevDependenciesInPackageJson?: boolean;
generatePackageJson?: boolean;
generateLockfile?: boolean;
sourcemap?: boolean;
}

View File

@ -0,0 +1,38 @@
{
"version": 2,
"outputCapture": "pipe",
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "Remix Build",
"description": "Build a Remix app.",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory",
"x-priority": "important"
},
"includeDevDependenciesInPackageJson": {
"type": "boolean",
"description": "Include `devDependencies` in the generated package.json file. By default only production `dependencies` are included.",
"default": false
},
"generatePackageJson": {
"type": "boolean",
"description": "Generate package.json file in the output folder.",
"default": false
},
"generateLockfile": {
"type": "boolean",
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false
},
"sourcemap": {
"type": "boolean",
"description": "Generate source maps for production.",
"default": false
}
},
"required": ["outputPath"]
}

View File

@ -0,0 +1,9 @@
export interface RemixServeSchema {
port: number;
devServerPort?: number;
debug?: boolean;
command?: string;
manual?: boolean;
tlsKey?: string;
tlsCert?: string;
}

View File

@ -0,0 +1,41 @@
{
"version": 2,
"outputCapture": "pipe",
"cli": "nx",
"title": "Remix Serve",
"description": "Serve a Remix app.",
"type": "object",
"properties": {
"port": {
"type": "number",
"description": "Set PORT environment variable that can be used to serve the Remix application.",
"default": 4200
},
"devServerPort": {
"type": "number",
"description": "Port to start the dev server on."
},
"debug": {
"type": "boolean",
"description": "Attach a Node.js inspector.",
"default": false
},
"command": {
"type": "string",
"description": "Command used to run your app server."
},
"manual": {
"type": "boolean",
"description": "Enable manual mode",
"default": false
},
"tlsKey": {
"type": "string",
"description": "Path to TLS key (key.pem)."
},
"tlsCert": {
"type": "string",
"description": "Path to TLS certificate (cert.pem)."
}
}
}

View File

@ -0,0 +1,95 @@
import { workspaceRoot, type ExecutorContext } from '@nx/devkit';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { fork } from 'node:child_process';
import { join } from 'node:path';
import { type RemixServeSchema } from './schema';
function normalizeOptions(schema: RemixServeSchema) {
return {
...schema,
port: schema.port ?? 4200,
debug: schema.debug ?? false,
manual: schema.manual ?? false,
} as RemixServeSchema;
}
function buildRemixDevArgs(options: RemixServeSchema) {
const args = [];
if (options.command) {
args.push(`--command=${options.command}`);
}
if (options.devServerPort) {
args.push(`--port=${options.devServerPort}`);
}
if (options.debug) {
args.push(`--debug`);
}
if (options.manual) {
args.push(`--manual`);
}
if (options.tlsKey) {
args.push(`--tls-key=${options.tlsKey}`);
}
if (options.tlsCert) {
args.push(`--tls-cert=${options.tlsCert}`);
}
return args;
}
export default async function* serveExecutor(
schema: RemixServeSchema,
context: ExecutorContext
) {
const options = normalizeOptions(schema);
const projectRoot = context.workspace.projects[context.projectName].root;
const remixBin = require.resolve('@remix-run/dev/dist/cli');
const args = buildRemixDevArgs(options);
// Cast to any to overwrite NODE_ENV
(process.env as any).NODE_ENV = process.env.NODE_ENV
? process.env.NODE_ENV
: 'development';
process.env.PORT = `${options.port}`;
yield* createAsyncIterable<{ success: boolean; baseUrl: string }>(
async ({ done, next, error }) => {
const server = fork(remixBin, ['dev', ...args], {
cwd: join(workspaceRoot, projectRoot),
stdio: 'inherit',
});
server.once('exit', (code) => {
if (code === 0) {
done();
} else {
error(new Error(`Remix app exited with code ${code}`));
}
});
const killServer = () => {
if (server.connected) {
server.kill('SIGTERM');
}
};
process.on('exit', () => killServer());
process.on('SIGINT', () => killServer());
process.on('SIGTERM', () => killServer());
process.on('SIGHUP', () => killServer());
await waitForPortOpen(options.port);
next({
success: true,
baseUrl: `http://localhost:${options.port}`,
});
}
);
}

View File

@ -0,0 +1,90 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import applicationGenerator from '../application/application.impl';
import routeGenerator from '../route/route.impl';
import actionGenerator from './action.impl';
describe('action', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write('.gitignore', `/node_modules/dist`);
await applicationGenerator(tree, { name: 'demo' });
await routeGenerator(tree, {
path: 'example',
project: 'demo',
style: 'none',
loader: false,
action: false,
meta: false,
skipChecks: false,
});
});
[
{
path: 'apps/demo/app/routes/example.tsx',
},
{
path: 'example',
},
{
path: 'example.tsx',
},
].forEach((config) => {
describe(`Generating action using path ${config.path}`, () => {
beforeEach(async () => {
await actionGenerator(tree, {
path: config.path,
// path: 'apps/demo/app/routes/example.tsx',
project: 'demo',
});
});
it('should add imports', async () => {
const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8');
expect(content).toMatch(`import { json } from '@remix-run/node';`);
expect(content).toMatch(
`import type { ActionFunctionArgs } from '@remix-run/node';`
);
expect(content).toMatch(
`import { useActionData } from '@remix-run/react';`
);
});
it('should add action function', () => {
const actionFunction = `export const action = async ({ request }: ActionFunctionArgs)`;
const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8');
expect(content).toMatch(actionFunction);
});
it('should add useActionData to component', () => {
const useActionData = `const actionMessage = useActionData<typeof action>();`;
const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8');
expect(content).toMatch(useActionData);
});
});
});
it('--nameAndDirectoryFormat=as-provided', async () => {
// ACT
await actionGenerator(tree, {
path: 'apps/demo/app/routes/example.tsx',
});
// ASSERT
const content = tree.read('apps/demo/app/routes/example.tsx', 'utf-8');
const useActionData = `const actionMessage = useActionData<typeof action>();`;
const actionFunction = `export const action = async ({ request }: ActionFunctionArgs)`;
expect(content).toMatch(`import { json } from '@remix-run/node';`);
expect(content).toMatch(
`import type { ActionFunctionArgs } from '@remix-run/node';`
);
expect(content).toMatch(
`import { useActionData } from '@remix-run/react';`
);
expect(content).toMatch(useActionData);
expect(content).toMatch(actionFunction);
});
});

View File

@ -0,0 +1,48 @@
import { formatFiles, Tree } from '@nx/devkit';
import { insertImport } from '../../utils/insert-import';
import { insertStatementAfterImports } from '../../utils/insert-statement-after-imports';
import { insertStatementInDefaultFunction } from '../../utils/insert-statement-in-default-function';
import { resolveRemixRouteFile } from '../../utils/remix-route-utils';
import { LoaderSchema } from './schema';
export default async function (tree: Tree, schema: LoaderSchema) {
const routeFilePath =
schema.nameAndDirectoryFormat === 'as-provided'
? schema.path
: await resolveRemixRouteFile(tree, schema.path, schema.project);
if (!tree.exists(routeFilePath)) {
throw new Error(
`Route path does not exist: ${routeFilePath}. Please generate a Remix route first.`
);
}
insertImport(tree, routeFilePath, 'ActionFunctionArgs', '@remix-run/node', {
typeOnly: true,
});
insertImport(tree, routeFilePath, 'json', '@remix-run/node');
insertImport(tree, routeFilePath, 'useActionData', '@remix-run/react');
insertStatementAfterImports(
tree,
routeFilePath,
`
export const action = async ({ request }: ActionFunctionArgs) => {
let formData = await request.formData();
return json({message: formData.toString()}, { status: 200 });
};
`
);
const statement = `\nconst actionMessage = useActionData<typeof action>();`;
try {
insertStatementInDefaultFunction(tree, routeFilePath, statement);
} catch (err) {
// eslint-disable-next-line no-empty
} finally {
await formatFiles(tree);
}
}

View File

@ -0,0 +1,10 @@
import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
export interface LoaderSchema {
path: string;
nameAndDirectoryFormat?: NameAndDirectoryFormat;
/**
* @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18.
*/
project?: string;
}

View File

@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "action",
"type": "object",
"description": "Generate an action for a given route.",
"properties": {
"path": {
"type": "string",
"description": "The route path or path to the filename of the route.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')"
},
"nameAndDirectoryFormat": {
"description": "Whether to generate the action in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
},
"x-prompt": "What project is this route for?",
"pattern": "^[a-zA-Z].*$",
"x-deprecated": "Provide the `path` option instead and use the `as-provided` format. The project will be determined from the path provided. It will be removed in Nx v18."
}
},
"required": ["path"]
}

View File

@ -0,0 +1,328 @@
import type { Tree } from '@nx/devkit';
import { joinPathFragments, readJson } from '@nx/devkit';
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import applicationGenerator from './application.impl';
describe('Remix Application', () => {
describe('Standalone Project Repo', () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
rootProject: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/root.tsx', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/routes/_index.tsx', 'utf-8')).toMatchSnapshot();
expect(
tree.read('tests/routes/_index.spec.tsx', 'utf-8')
).toMatchSnapshot();
expect(tree.read('vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
});
describe(`--js`, () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
js: true,
rootProject: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/root.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/routes/_index.js', 'utf-8')).toMatchSnapshot();
});
});
describe('--unitTestRunner', () => {
it('should generate the correct files for testing using vitest', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
unitTestRunner: 'vitest',
rootProject: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot();
expect(tree.read('vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('tests/routes/_index.spec.tsx', 'utf-8')
).toMatchSnapshot();
expect(tree.read('test-setup.ts', 'utf-8')).toMatchSnapshot();
});
it('should generate the correct files for testing using jest', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
unitTestRunner: 'jest',
rootProject: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.cjs', 'utf-8')).toMatchSnapshot();
expect(tree.read('jest.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test-setup.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('tests/routes/_index.spec.tsx', 'utf-8')
).toMatchSnapshot();
expect(tree.exists('jest.preset.cjs')).toBeTruthy();
});
});
describe('--e2eTestRunner', () => {
it('should generate an e2e application for the app', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
e2eTestRunner: 'cypress',
rootProject: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot();
});
});
});
describe.each([
['derived', 'apps/test', 'apps/test-e2e'],
['as-provided', 'test', 'test-e2e'],
])(
'Integrated Repo --projectNameAndRootFormat=%s',
(projectNameAndRootFormat: ProjectNameAndRootFormat, appDir, e2eDir) => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
name: 'test',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`${appDir}/app/root.tsx`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`${appDir}/app/routes/_index.tsx`, 'utf-8')
).toMatchSnapshot();
});
describe('--js', () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
name: 'test',
js: true,
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`${appDir}/app/root.js`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`${appDir}/app/routes/_index.js`, 'utf-8')
).toMatchSnapshot();
});
});
describe('--directory', () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const newAppDir =
projectNameAndRootFormat === 'as-provided'
? 'demo'
: 'apps/demo/test';
// ACT
await applicationGenerator(tree, {
name: 'test',
directory: 'demo',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, newAppDir);
expect(
tree.read(`${newAppDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${newAppDir}/app/root.tsx`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${newAppDir}/app/routes/_index.tsx`, 'utf-8')
).toMatchSnapshot();
});
it('should extract the layout directory from the directory options if it exists', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const newAppDir =
projectNameAndRootFormat === 'as-provided'
? 'apps/demo'
: 'apps/demo/test';
// ACT
await applicationGenerator(tree, {
name: 'test',
directory: 'apps/demo',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, newAppDir);
expect(
tree.read(`${newAppDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${newAppDir}/app/root.tsx`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${newAppDir}/app/routes/_index.tsx`, 'utf-8')
).toMatchSnapshot();
});
});
describe('--unitTestRunner', () => {
it('should generate the correct files for testing using vitest', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
name: 'test',
unitTestRunner: 'vitest',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${appDir}/vite.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${appDir}/test-setup.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the correct files for testing using jest', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
name: 'test',
unitTestRunner: 'jest',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.cjs`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${appDir}/jest.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`${appDir}/test-setup.ts`, 'utf-8')
).toMatchSnapshot();
});
});
describe('--e2eTestRunner', () => {
it('should generate an e2e application for the app', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
name: 'test',
e2eTestRunner: 'cypress',
projectNameAndRootFormat,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}-e2e/cypress.config.ts`, 'utf-8')
).toMatchSnapshot();
});
});
}
);
});
function expectTargetsToBeCorrect(tree: Tree, projectRoot: string) {
const { targets } = readJson(
tree,
joinPathFragments(projectRoot === '.' ? '/' : projectRoot, 'project.json')
);
expect(targets.lint).toBeTruthy();
expect(targets.build).toBeTruthy();
expect(targets.build.executor).toEqual('@nx/remix:build');
expect(targets.build.options.outputPath).toEqual(
joinPathFragments('dist', projectRoot)
);
expect(targets.serve).toBeTruthy();
expect(targets.serve.executor).toEqual('@nx/remix:serve');
expect(targets.serve.options.port).toEqual(4200);
expect(targets.start).toBeTruthy();
expect(targets.start.command).toEqual('remix-serve build/index.js');
expect(targets.start.options.cwd).toEqual(projectRoot);
expect(targets.typecheck).toBeTruthy();
expect(targets.typecheck.command).toEqual('tsc');
expect(targets.typecheck.options.cwd).toEqual(projectRoot);
}

View File

@ -0,0 +1,289 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
ensurePackage,
formatFiles,
generateFiles,
GeneratorCallback,
getPackageManagerCommand,
joinPathFragments,
offsetFromRoot,
readJson,
readProjectConfiguration,
runTasksInSerial,
toJS,
Tree,
updateJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config';
import {
eslintVersion,
getPackageVersion,
isbotVersion,
reactDomVersion,
reactVersion,
remixVersion,
typescriptVersion,
typesReactDomVersion,
typesReactVersion,
} from '../../utils/versions';
import {
NormalizedSchema,
normalizeOptions,
updateUnitTestConfig,
} from './lib';
import { NxRemixGeneratorSchema } from './schema';
export default async function (tree: Tree, _options: NxRemixGeneratorSchema) {
const options = await normalizeOptions(tree, _options);
const tasks: GeneratorCallback[] = [];
addProjectConfiguration(tree, options.projectName, {
root: options.projectRoot,
sourceRoot: `${options.projectRoot}`,
projectType: 'application',
tags: options.parsedTags,
targets: {
build: {
executor: '@nx/remix:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.projectRoot),
},
},
serve: {
executor: `@nx/remix:serve`,
options: {
command: `${
getPackageManagerCommand().exec
} remix-serve build/index.js`,
manual: true,
port: 4200,
},
},
start: {
dependsOn: ['build'],
command: `remix-serve build/index.js`,
options: {
cwd: options.projectRoot,
},
},
typecheck: {
command: `tsc`,
options: {
cwd: options.projectRoot,
},
},
},
});
const installTask = addDependenciesToPackageJson(
tree,
{
'@remix-run/node': remixVersion,
'@remix-run/react': remixVersion,
'@remix-run/serve': remixVersion,
isbot: isbotVersion,
react: reactVersion,
'react-dom': reactDomVersion,
},
{
'@remix-run/dev': remixVersion,
'@remix-run/eslint-config': remixVersion,
'@types/react': typesReactVersion,
'@types/react-dom': typesReactDomVersion,
eslint: eslintVersion,
typescript: typescriptVersion,
}
);
tasks.push(installTask);
const vars = {
...options,
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
remixVersion,
isbotVersion,
reactVersion,
reactDomVersion,
typesReactVersion,
typesReactDomVersion,
eslintVersion,
typescriptVersion,
};
generateFiles(
tree,
joinPathFragments(__dirname, 'files/common'),
options.projectRoot,
vars
);
if (options.rootProject) {
const gitignore = tree.read('.gitignore', 'utf-8');
tree.write(
'.gitignore',
`${gitignore}\n.cache\nbuild\npublic/build\n.env\n`
);
} else {
generateFiles(
tree,
joinPathFragments(__dirname, 'files/integrated'),
options.projectRoot,
vars
);
}
if (options.unitTestRunner !== 'none') {
if (options.unitTestRunner === 'vitest') {
const { vitestGenerator } = ensurePackage<typeof import('@nx/vite')>(
'@nx/vite',
getPackageVersion(tree, 'nx')
);
const vitestTask = await vitestGenerator(tree, {
uiFramework: 'react',
project: options.projectName,
coverageProvider: 'v8',
inSourceTests: false,
skipFormat: true,
testEnvironment: 'jsdom',
});
tasks.push(vitestTask);
} else {
const { configurationGenerator: jestConfigurationGenerator } =
ensurePackage<typeof import('@nx/jest')>(
'@nx/jest',
getPackageVersion(tree, 'nx')
);
const jestTask = await jestConfigurationGenerator(tree, {
project: options.projectName,
setupFile: 'none',
supportTsx: true,
skipSerializers: false,
skipPackageJson: false,
skipFormat: true,
});
const projectConfig = readProjectConfiguration(tree, options.projectName);
projectConfig.targets['test'].options.passWithNoTests = true;
updateProjectConfiguration(tree, options.projectName, projectConfig);
tasks.push(jestTask);
}
const pkgInstallTask = updateUnitTestConfig(
tree,
options.projectRoot,
options.unitTestRunner
);
tasks.push(pkgInstallTask);
} else {
tree.delete(
joinPathFragments(options.projectRoot, `tests/routes/_index.spec.tsx`)
);
}
if (options.linter !== 'none') {
const { lintProjectGenerator } = ensurePackage<typeof import('@nx/eslint')>(
'@nx/eslint',
getPackageVersion(tree, 'nx')
);
const eslintTask = await lintProjectGenerator(tree, {
linter: options.linter,
project: options.projectName,
tsConfigPaths: [
joinPathFragments(options.projectRoot, 'tsconfig.app.json'),
],
unitTestRunner: options.unitTestRunner,
skipFormat: true,
rootProject: options.rootProject,
});
tasks.push(eslintTask);
}
if (options.js) {
toJS(tree);
}
if (options.rootProject && tree.exists('tsconfig.base.json')) {
// If this is a standalone project, merge tsconfig.json and tsconfig.base.json.
const tsConfigBaseJson = readJson(tree, 'tsconfig.base.json');
updateJson(tree, 'tsconfig.json', (json) => {
delete json.extends;
json.compilerOptions = {
...tsConfigBaseJson.compilerOptions,
...json.compilerOptions,
// Taken from remix default setup
// https://github.com/remix-run/remix/blob/68c8982/templates/remix/tsconfig.json#L15-L17
paths: {
'~/*': ['./app/*'],
},
};
json.include = [
...(tsConfigBaseJson.include ?? []),
...(json.include ?? []),
];
json.exclude = [
...(tsConfigBaseJson.exclude ?? []),
...(json.exclude ?? []),
];
return json;
});
tree.delete('tsconfig.base.json');
} else {
// Otherwise, extract the tsconfig.base.json from tsconfig.json so we can share settings.
extractTsConfigBase(tree);
}
if (options.e2eTestRunner === 'cypress') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', getPackageVersion(tree, 'nx'));
addFileServerTarget(tree, options, 'serve-static');
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
tags: [],
implicitDependencies: [options.projectName],
});
tasks.push(
await configurationGenerator(tree, {
project: options.e2eProjectName,
directory: 'src',
skipFormat: true,
devServerTarget: `${options.projectName}:serve:development`,
baseUrl: 'http://localhost:4200',
})
);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
}
function addFileServerTarget(
tree: Tree,
options: NormalizedSchema,
targetName: string
) {
addDependenciesToPackageJson(
tree,
{},
{ '@nx/web': getPackageVersion(tree, 'nx') }
);
const projectConfig = readProjectConfiguration(tree, options.projectName);
projectConfig.targets[targetName] = {
executor: '@nx/web:file-server',
options: {
buildTarget: `${options.projectName}:build`,
port: 4200,
},
};
updateProjectConfiguration(tree, options.projectName, projectConfig);
}

View File

@ -0,0 +1,54 @@
# Welcome to Nx + Remix!
- [Remix Docs](https://remix.run/docs)
- [Nx Docs](https://nx.dev)
## Development
From your terminal:
```sh
npx nx dev <%= projectName %>
```
This starts your app in development mode, rebuilding assets on file changes.
## Deployment
First, build your app for production:
```sh
npx nx build <%= projectName %>
```
Then run the app in production mode:
```sh
npx nx start <%= projectName %>
```
Now you'll need to pick a host to deploy it to.
### DIY
If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
Make sure to deploy the output of `remix build`
- `packages/<%= projectName %>/build/`
- `packages/<%= projectName %>/public/build/`
### Using a Template
When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
```sh
cd ..
# create a new project, and pick a pre-configured host
npx create-remix@latest
cd my-new-remix-app
# remove the new project's app (not the old one!)
rm -rf app
# copy your app over
cp -R ../my-old-remix-app/app app
```

View File

@ -0,0 +1,32 @@
import type { MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export const meta: MetaFunction = () => ([{
charset: "utf-8",
title: "New Remix App",
viewport: "width=device-width,initial-scale=1",
}]);
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

View File

@ -0,0 +1,32 @@
export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/blog"
rel="noreferrer"
>
15m Quickstart Blog Tutorial
</a>
</li>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/jokes"
rel="noreferrer"
>
Deep Dive Jokes App Tutorial
</a>
</li>
<li>
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
Remix Docs
</a>
</li>
</ul>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,11 @@
/**
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => require("@nx/remix").createWatchPaths(__dirname),
};

View File

@ -0,0 +1,2 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />

View File

@ -0,0 +1,16 @@
import { createRemixStub } from '@remix-run/testing';
import { render, screen, waitFor } from '@testing-library/react';
import Index from '../../app/routes/_index';
test('renders loader data', async () => {
const RemixStub = createRemixStub([
{
path: '/',
Component: Index,
},
]);
render(<RemixStub />);
await waitFor(() => screen.findByText('Welcome to Remix'));
});

View File

@ -0,0 +1,18 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}

View File

@ -0,0 +1,4 @@
.cache
build
public/build
.env

View File

@ -0,0 +1,28 @@
{
"private": true,
"name": "<%= projectName %>",
"description": "",
"license": "",
"scripts": {},
"type": "module",
"dependencies": {
"@remix-run/node": "<%= remixVersion %>",
"@remix-run/react": "<%= remixVersion %>",
"@remix-run/serve": "<%= remixVersion %>",
"isbot": "<%= isbotVersion %>",
"react": "<%= reactVersion %>",
"react-dom": "<%= reactDomVersion %>"
},
"devDependencies": {
"@remix-run/dev": "<%= remixVersion %>",
"@remix-run/eslint-config": "<%= remixVersion %>",
"@types/react": "<%= typesReactVersion %>",
"@types/react-dom": "<%= typesReactDomVersion %>",
"eslint": "<%= eslintVersion %>",
"typescript": "<%= typescriptVersion %>"
},
"engines": {
"node": ">=14"
},
"sideEffects": false
}

View File

@ -0,0 +1,2 @@
export * from './normalize-options';
export * from './update-unit-test-config';

View File

@ -0,0 +1,45 @@
import { type Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { type NxRemixGeneratorSchema } from '../schema';
import { Linter } from '@nx/eslint';
export interface NormalizedSchema extends NxRemixGeneratorSchema {
projectName: string;
projectRoot: string;
e2eProjectName: string;
e2eProjectRoot: string;
parsedTags: string[];
}
export async function normalizeOptions(
tree: Tree,
options: NxRemixGeneratorSchema
): Promise<NormalizedSchema> {
const { projectName, projectRoot, projectNameAndRootFormat } =
await determineProjectNameAndRootOptions(tree, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator: '@nx/remix:application',
});
options.rootProject = projectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
const e2eProjectName = options.rootProject ? 'e2e' : `${projectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${projectRoot}-e2e`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
return {
...options,
linter: options.linter ?? Linter.EsLint,
projectName,
projectRoot,
e2eProjectName,
e2eProjectRoot,
parsedTags,
};
}

View File

@ -0,0 +1,70 @@
import {
addDependenciesToPackageJson,
joinPathFragments,
stripIndents,
type Tree,
workspaceRoot,
} from '@nx/devkit';
import {
updateJestTestSetup,
updateViteTestIncludes,
updateViteTestSetup,
} from '../../../utils/testing-config-utils';
import {
getRemixVersion,
testingLibraryJestDomVersion,
testingLibraryReactVersion,
testingLibraryUserEventsVersion,
} from '../../../utils/versions';
export function updateUnitTestConfig(
tree: Tree,
pathToRoot: string,
unitTestRunner: 'vitest' | 'jest'
) {
const pathToTestSetup = joinPathFragments(pathToRoot, `test-setup.ts`);
tree.write(
pathToTestSetup,
stripIndents`
import { installGlobals } from '@remix-run/node';
import '@testing-library/jest-dom/matchers';
installGlobals();`
);
if (unitTestRunner === 'vitest') {
const pathToViteConfig = joinPathFragments(pathToRoot, 'vite.config.ts');
updateViteTestIncludes(
tree,
pathToViteConfig,
'./app/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
);
updateViteTestIncludes(
tree,
pathToViteConfig,
'./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
);
updateViteTestSetup(tree, pathToViteConfig, './test-setup.ts');
} else if (unitTestRunner === 'jest') {
const pathToJestConfig = joinPathFragments(pathToRoot, 'jest.config.ts');
tree.rename('jest.preset.js', 'jest.preset.cjs');
updateJestTestSetup(tree, pathToJestConfig, `<rootDir>/test-setup.ts`);
tree.write(
pathToJestConfig,
tree
.read(pathToJestConfig, 'utf-8')
.replace('jest.preset.js', 'jest.preset.cjs')
);
}
return addDependenciesToPackageJson(
tree,
{},
{
'@testing-library/jest-dom': testingLibraryJestDomVersion,
'@testing-library/react': testingLibraryReactVersion,
'@testing-library/user-event': testingLibraryUserEventsVersion,
'@remix-run/node': getRemixVersion(tree),
'@remix-run/testing': getRemixVersion(tree),
}
);
}

View File

@ -0,0 +1,15 @@
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/eslint';
export interface NxRemixGeneratorSchema {
name: string;
tags?: string;
js?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
linter?: Linter;
unitTestRunner?: 'vitest' | 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean;
rootProject?: boolean;
}

View File

@ -0,0 +1,68 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixApplication",
"title": "Create an Application",
"description": "Generate a new Remix application.",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What is the name of the application?"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"directory": {
"type": "string",
"description": "A directory where the app is placed.",
"alias": "dir",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
"default": "cypress",
"description": "Test runner to use for e2e tests"
},
"tags": {
"type": "string",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false
},
"rootProject": {
"type": "boolean",
"x-priority": "internal",
"default": false
}
}
}

View File

@ -0,0 +1,51 @@
import { joinPathFragments, readProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import libraryGenerator from '../library/library.impl';
import cypressComponentConfigurationGenerator from './cypress-component-configuration.impl';
describe('CypressComponentConfiguration', () => {
it('should create the cypress configuration correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
await libraryGenerator(tree, {
name: 'cypress-test',
unitTestRunner: 'vitest',
style: 'css',
});
// ACT
await cypressComponentConfigurationGenerator(tree, {
project: 'cypress-test',
generateTests: true,
});
// ASSERT
const project = readProjectConfiguration(tree, 'cypress-test');
expect(
tree.read(joinPathFragments(project.root, 'cypress.config.ts'), 'utf-8')
).toMatchInlineSnapshot(`
"import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nx/remix/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
});
"
`);
expect(project.targets['component-test']).toMatchInlineSnapshot(`
{
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "cypress-test/cypress.config.ts",
"devServerTarget": "",
"skipServe": true,
"testingType": "component",
},
}
`);
expect(
tree.exists(joinPathFragments(project.root, 'cypress'))
).toBeTruthy();
});
});

View File

@ -0,0 +1,30 @@
import {
formatFiles,
generateFiles,
readProjectConfiguration,
type Tree,
} from '@nx/devkit';
import { join } from 'path';
import { type CypressComponentConfigurationSchema } from './schema';
import { cypressComponentConfigGenerator } from '@nx/react';
export default async function cypressComponentConfigurationGenerator(
tree: Tree,
options: CypressComponentConfigurationSchema
) {
await cypressComponentConfigGenerator(tree, {
project: options.project,
generateTests: options.generateTests,
skipFormat: true,
bundler: 'vite',
buildTarget: '',
});
const project = readProjectConfiguration(tree, options.project);
generateFiles(tree, join(__dirname, './files'), project.root, { tmpl: '' });
if (!options.skipFormat) {
await formatFiles(tree);
}
}

View File

@ -0,0 +1,6 @@
import {defineConfig} from 'cypress';
import {nxComponentTestingPreset} from '@nx/remix/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
});

View File

@ -0,0 +1,5 @@
export interface CypressComponentConfigurationSchema {
project: string;
generateTests?: boolean;
skipFormat?: boolean;
}

View File

@ -0,0 +1,41 @@
{
"$schema": "https://json-schema.org/schema",
"cli": "nx",
"$id": "NxRemixCypressComponentTestConfiguration",
"title": "Add Cypress component testing",
"description": "Add a Cypress component testing configuration to an existing project.",
"type": "object",
"examples": [
{
"command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project",
"description": "Add component testing to your Remix project"
},
{
"command": "nx g @nx/remix:cypress-component-configuration --project=my-remix-project --generate-tests",
"description": "Add component testing to your Remix project and generate component tests for your existing components"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add cypress component testing configuration to",
"x-dropdown": "projects",
"x-prompt": "What project should we add Cypress component testing to?",
"x-priority": "important"
},
"generateTests": {
"type": "boolean",
"description": "Generate default component tests for existing components in the project",
"x-prompt": "Automatically generate tests for components declared in this project?",
"default": false,
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false,
"x-priority": "internal"
}
},
"required": ["project"]
}

View File

@ -0,0 +1,35 @@
import { readProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import generator from './cypress.impl';
import applicationGenerator from '../application/application.impl';
describe('Cypress generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should generate cypress project', async () => {
await applicationGenerator(tree, { name: 'demo', e2eTestRunner: 'none' });
await generator(tree, { project: 'demo', name: 'demo-e2e' });
const config = readProjectConfiguration(tree, 'demo-e2e');
expect(config.targets).toEqual({
e2e: {
executor: '@nx/cypress:cypress',
options: {
cypressConfig: 'demo-e2e/cypress.config.ts',
testingType: 'e2e',
devServerTarget: 'demo:serve:development',
},
configurations: {
ci: {
devServerTarget: 'demo:serve-static',
},
},
},
lint: { executor: '@nx/eslint:lint', outputs: ['{options.outputFile}'] },
});
});
});

View File

@ -0,0 +1,113 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
GeneratorCallback,
joinPathFragments,
readProjectConfiguration,
runTasksInSerial,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { configurationGenerator } from '@nx/cypress';
import { CypressGeneratorSchema } from './schema';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { nxVersion } from '../../utils/versions';
export default async function (
tree: Tree,
options: CypressGeneratorSchema
): Promise<GeneratorCallback> {
const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } =
await determineProjectNameAndRootOptions(tree, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
callingGenerator: '@nx/remix:cypress',
});
const rootProject = e2eProjectRoot === '.';
let projectConfig = readProjectConfiguration(tree, options.project);
options.baseUrl ??= `http://localhost:${projectConfig.targets['serve'].options.port}`;
addFileServerTarget(tree, options, 'serve-static');
addProjectConfiguration(tree, e2eProjectName, {
projectType: 'application',
root: e2eProjectRoot,
sourceRoot: joinPathFragments(e2eProjectRoot, 'src'),
targets: {},
tags: [],
implicitDependencies: [options.name],
});
const installTask = await configurationGenerator(tree, {
project: e2eProjectName,
directory: 'src',
linter: options.linter,
skipPackageJson: false,
skipFormat: true,
devServerTarget: `${options.project}:serve:development`,
baseUrl: options.baseUrl,
rootProject,
});
projectConfig = readProjectConfiguration(tree, e2eProjectName);
tree.delete(
joinPathFragments(projectConfig.sourceRoot, 'support', 'app.po.ts')
);
tree.write(
joinPathFragments(projectConfig.sourceRoot, 'e2e', 'app.cy.ts'),
`describe('webapp', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
cy.get('h1').contains('Welcome to Remix');
});
});`
);
const supportFilePath = joinPathFragments(
projectConfig.sourceRoot,
'support',
'e2e.ts'
);
const supportContent = tree.read(supportFilePath, 'utf-8');
tree.write(
supportFilePath,
`${supportContent}
// from https://github.com/remix-run/indie-stack
Cypress.on("uncaught:exception", (err) => {
// Cypress and React Hydrating the document don't get along
// for some unknown reason. Hopefully we figure out why eventually
// so we can remove this.
if (
/hydrat/i.test(err.message) ||
/Minified React error #418/.test(err.message) ||
/Minified React error #423/.test(err.message)
) {
return false;
}
});`
);
return runTasksInSerial(installTask);
}
function addFileServerTarget(
tree: Tree,
options: CypressGeneratorSchema,
targetName: string
) {
addDependenciesToPackageJson(tree, {}, { '@nx/web': nxVersion });
const projectConfig = readProjectConfiguration(tree, options.project);
projectConfig.targets[targetName] = {
executor: '@nx/web:file-server',
options: {
buildTarget: `${options.project}:build`,
port: projectConfig.targets['serve'].options.port,
},
};
updateProjectConfiguration(tree, options.project, projectConfig);
}

View File

@ -0,0 +1,14 @@
import { type ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint';
export interface CypressGeneratorSchema {
project: string;
name: string;
baseUrl?: string;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
linter?: Linter;
js?: boolean;
skipFormat?: boolean;
setParserOptionsProject?: boolean;
}

View File

@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxRemixCypress",
"title": "",
"type": "object",
"description": "Generate a Cypress e2e project for a given application.",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": {
"$source": "projectName"
}
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"baseUrl": {
"type": "string",
"description": "URL to access the application on",
"default": "http://localhost:3000"
},
"name": {
"type": "string",
"description": "Name of the E2E Project",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
}
},
"required": ["name"]
}

View File

@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorBoundary --nameAndDirectoryFormat=as-provided --apiVersion=2 should correctly add the ErrorBoundary to the route file 1`] = `
"import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export function ErrorBoundary() {
const error = useRouteError();
// when true, this is what used to go to 'CatchBoundary'
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>Oops</h1>
<p>Status: {error.status}</p>
<p>{error.data.message}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
"
`;
exports[`ErrorBoundary --nameAndDirectoryFormat=as-provided --apiVersion=2 should correctly add the ErrorBoundary to the route file 2`] = `
"import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
export function ErrorBoundary() {
const error = useRouteError();
// when true, this is what used to go to 'CatchBoundary'
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>Oops</h1>
<p>Status: {error.status}</p>
<p>{error.data.message}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
"
`;

View File

@ -0,0 +1,67 @@
import { addProjectConfiguration } from '@nx/devkit';
import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import errorBoundaryGenerator from './error-boundary.impl';
describe('ErrorBoundary', () => {
describe.each([
['derived', 'app/routes/test.tsx', 'demo'],
['as-provided', 'app/routes/test.tsx', ''],
])(
`--nameAndDirectoryFormat=as-provided`,
(
nameAndDirectoryFormat: NameAndDirectoryFormat,
routeFilePath: string,
project: string
) => {
describe('--apiVersion=2', () => {
it('should correctly add the ErrorBoundary to the route file', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'demo', {
name: 'demo',
root: '.',
sourceRoot: '.',
projectType: 'application',
});
const routeFilePath = `app/routes/test.tsx`;
tree.write(routeFilePath, ``);
tree.write('remix.config.cjs', `module.exports = {}`);
// ACT
await errorBoundaryGenerator(tree, {
project,
nameAndDirectoryFormat,
path: routeFilePath,
});
// ASSERT
expect(tree.read(routeFilePath, 'utf-8')).toMatchSnapshot();
});
it('should error when the route file cannot be found', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'demo', {
name: 'demo',
root: '.',
sourceRoot: '.',
projectType: 'application',
});
const routeFilePath = `app/routes/test.tsx`;
tree.write(routeFilePath, ``);
tree.write('remix.config.cjs', `module.exports = {}`);
// ACT & ASSERT
await expect(
errorBoundaryGenerator(tree, {
project,
nameAndDirectoryFormat,
path: `my-route.tsx`,
})
).rejects.toThrow();
});
});
}
);
});

View File

@ -0,0 +1,16 @@
import { formatFiles, type Tree } from '@nx/devkit';
import { addV2ErrorBoundary, normalizeOptions } from './lib';
import type { ErrorBoundarySchema } from './schema';
export default async function errorBoundaryGenerator(
tree: Tree,
schema: ErrorBoundarySchema
) {
const options = await normalizeOptions(tree, schema);
addV2ErrorBoundary(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);
}
}

View File

@ -0,0 +1,41 @@
import { stripIndents, type Tree } from '@nx/devkit';
import { insertImport } from '../../../utils/insert-import';
import { insertStatementAfterImports } from '../../../utils/insert-statement-after-imports';
import type { ErrorBoundarySchema } from '../schema';
export function addV2ErrorBoundary(tree: Tree, options: ErrorBoundarySchema) {
insertImport(tree, options.path, `useRouteError`, '@remix-run/react');
insertImport(tree, options.path, `isRouteErrorResponse`, '@remix-run/react');
insertStatementAfterImports(
tree,
options.path,
stripIndents`
export function ErrorBoundary() {
const error = useRouteError();
// when true, this is what used to go to 'CatchBoundary'
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>Oops</h1>
<p>Status: {error.status}</p>
<p>{error.data.message}</p>
</div>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
`
);
}

View File

@ -0,0 +1,2 @@
export * from './add-v2-error-boundary';
export * from './normalize-options';

View File

@ -0,0 +1,24 @@
import { type Tree } from '@nx/devkit';
import { resolveRemixRouteFile } from '../../../utils/remix-route-utils';
import type { ErrorBoundarySchema } from '../schema';
export async function normalizeOptions(
tree: Tree,
schema: ErrorBoundarySchema
): Promise<ErrorBoundarySchema> {
const pathToRouteFile =
schema.nameAndDirectoryFormat === 'as-provided'
? schema.path
: await resolveRemixRouteFile(tree, schema.path, schema.project);
if (!tree.exists(pathToRouteFile)) {
throw new Error(
`Route file specified does not exist "${pathToRouteFile}". Please ensure you pass a correct path to the file.`
);
}
return {
...schema,
path: pathToRouteFile,
};
}

View File

@ -0,0 +1,11 @@
import { NameAndDirectoryFormat } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
export interface ErrorBoundarySchema {
path: string;
skipFormat?: false;
nameAndDirectoryFormat?: NameAndDirectoryFormat;
/**
* @deprecated Provide the `path` option instead. The project will be determined from the path provided. It will be removed in Nx v18.
*/
project?: string;
}

Some files were not shown because too many files have changed in this diff Show More