diff --git a/.changeset/v2-route-convention.md b/.changeset/v2-route-convention.md new file mode 100644 index 00000000000..9ddfb8450b0 --- /dev/null +++ b/.changeset/v2-route-convention.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": major +"@remix-run/react": major +"@remix-run/server-runtime": major +"@remix-run/testing": major +--- + +Remove `v2_routeConvention` flag. The flat route file convention is now standard. diff --git a/docs/file-conventions/route-files-v2.md b/docs/file-conventions/route-files-v2.md index 3c4a380f324..18222849cfc 100644 --- a/docs/file-conventions/route-files-v2.md +++ b/docs/file-conventions/route-files-v2.md @@ -1,403 +1,11 @@ --- title: Route File Naming (v2) -new: true +toc: false +hidden: true --- # Route File Naming (v2) -You can opt in to the new route file naming convention with a future flag in Remix config. +[The v2 route file naming convention has now been stabilized.][moved] -```js filename=remix.config.js -/** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - future: { - v2_routeConvention: true, - }, -}; -``` - -While you can configure routes in [remix.config.js][remix-config], most routes are created with this file system convention. Add a file, get a route. - -Please note that you can use either `.js`, `.jsx`, `.ts` or `.tsx` file extensions. We'll stick with `.tsx` in the examples to avoid duplication. - -## Root Route - - -```markdown lines=[3] -app/ -├── routes/ -└── root.tsx -``` - -The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes, so you can export a [`loader`][loader], [`action`][action], etc. - -The root route typically looks something like this. It serves as the root layout of the entire app, all other routes will render inside the ``. - -```tsx -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; - -export default function Root() { - return ( - - - - - - - - - - - - ); -} -``` - -## Basic Routes - -Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route]. - - -```markdown lines=[3-4] -app/ -├── routes/ -│ ├── _index.tsx -│ └── about.tsx -└── root.tsx -``` - -| URL | Matched Routes | -| -------- | -------------- | -| `/` | `_index.tsx` | -| `/about` | `about.tsx` | - -Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested-routing]. - -## Dot Delimiters - -Adding a `.` to a route filename will create a `/` in the URL. - - -```markdown lines=[5-7] -app/ -├── routes/ -│ ├── _index.tsx -│ ├── about.tsx -│ ├── concerts.trending.tsx -│ ├── concerts.salt-lake-city.tsx -│ └── concerts.san-diego.tsx -└── root.tsx -``` - -| URL | Matched Route | -| -------------------------- | ----------------------------- | -| `/concerts/trending` | `concerts.trending.tsx` | -| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` | -| `/concerts/san-diego` | `concerts.san-diego.tsx` | - -The dot delimiter also creates nesting, see the [nesting section][nested-routes] for more information. - -## Dynamic Segments - -Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix. - - -```markdown lines=[5] -app/ -├── routes/ -│ ├── _index.tsx -│ ├── about.tsx -│ ├── concerts.$city.tsx -│ └── concerts.trending.tsx -└── root.tsx -``` - -| URL | Matched Route | -| -------------------------- | ----------------------- | -| `/concerts/trending` | `concerts.trending.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | -| `/concerts/san-diego` | `concerts.$city.tsx` | - -Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders][loader] and [actions][action]. - -```tsx -export function loader({ params }: LoaderArgs) { - return fakeDb.getAllConcertsForCity(params.city); -} -``` - -You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`. - -Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name: - -```tsx -export function loader({ params }: LoaderArgs) { - return fake.db.getConcerts({ - date: params.date, - city: params.city, - }); -} -``` - -See the [routing guide][routing-guide] for more information. - -## Nested Routes - -Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested-routing]. - -You create nested routes with [dot delimiters][dot-delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes: - - -```markdown lines=[5-8] -app/ -├── routes/ -│ ├── _index.tsx -│ ├── about.tsx -│ ├── concerts._index.tsx -│ ├── concerts.$city.tsx -│ ├── concerts.trending.tsx -│ └── concerts.tsx -└── root.tsx -``` - -All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet]. - -| URL | Matched Route | Layout | -| -------------------------- | ----------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/about` | `about.tsx` | `root.tsx` | -| `/concerts` | `concerts._index.tsx` | `concerts.tsx` | -| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | - -Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly. - -## Nested URLs without Layout Nesting - -Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment: - - -```markdown lines=[8] -app/ -├── routes/ -│ ├── _index.tsx -│ ├── about.tsx -│ ├── concerts.$city.tsx -│ ├── concerts.trending.tsx -│ ├── concerts.tsx -│ └── concerts_.mine.tsx -└── root.tsx -``` - -| URL | Matched Route | Layout | -| -------------------------- | ----------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/concerts/mine` | `concerts_.mine.tsx` | `root.tsx` | -| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | - -Note that `/concerts/mine` does not nest with `concerts.tsx` anymore, but `root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting. - -Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting. - -## Nested Layouts without Nested URLs - -We call these Pathless Routes - -Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore. - - -```markdown lines=[3-5] -app/ -├── routes/ -│ ├── _auth.login.tsx -│ ├── _auth.register.tsx -│ ├── _auth.tsx -│ ├── _index.tsx -│ ├── concerts.$city.tsx -│ └── concerts.tsx -└── root.tsx -``` - -| URL | Matched Route | Layout | -| -------------------------- | -------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/login` | `_auth.login.tsx` | `_auth.tsx` | -| `/register` | `_auth.register.tsx` | `_auth.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | - -Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL. - -## Optional Segments - -Wrapping a route segment in parentheses will make the segment optional. - - -```markdown lines=[3-5] -app/ -├── routes/ -│ ├── ($lang)._index.tsx -│ ├── ($lang).$productId.tsx -│ └── ($lang).categories.tsx -└── root.tsx -``` - -| URL | Matched Route | -| -------------------------- | ------------------------ | -| `/` | `($lang)._index.tsx` | -| `/categories` | `($lang).categories.tsx` | -| `/en/categories` | `($lang).categories.tsx` | -| `/fr/categories` | `($lang).categories.tsx` | -| `/american-flag-speedo` | `($lang)._index.tsx` | -| `/en/american-flag-speedo` | `($lang).$productId.tsx` | -| `/fr/american-flag-speedo` | `($lang).$productId.tsx` | - -You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code. - -## Splat Routes - -While [dynamic segments][dynamic-segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes. - - -```markdown lines=[4,6] -app/ -├── routes/ -│ ├── _index.tsx -│ ├── $.tsx -│ ├── about.tsx -│ └── files.$.tsx -└── root.tsx -``` - -| URL | Matched Route | -| -------------------------------------------- | ------------- | -| `/` | `_index.tsx` | -| `/beef/and/cheese` | `$.tsx` | -| `/files` | `files.$.tsx` | -| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` | -| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` | -| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` | - -Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. - -```tsx filename=app/routes/files.$.tsx -export function loader({ params }) { - const filePath = params["*"]; - return fake.getFileInfo(filePath); -} -``` - -## Escaping Special Characters - -If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. - -| Filename | URL | -| ------------------------------- | ------------------- | -| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` | -| `routes/[sitemap.xml].tsx` | `/sitemap.xml` | -| `routes/weird-url.[_index].tsx` | `/weird-url/_index` | -| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` | -| `routes/[[so-weird]].tsx` | `/[so-weird]` | - -## Folders for Organization - -Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders. - -The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name - -Consider these routes: - -``` -routes/ - _landing._index.tsx - _landing.about.tsx - _landing.tsx - app._index.tsx - app.projects.tsx - app.tsx - app_.projects.$id.roadmap.tsx -``` - -Some, or all of them can be folders holding their own `route` module inside. - -``` -routes/ - _landing._index/ - route.tsx - scroll-experience.tsx - _landing.about/ - employee-profile-card.tsx - get-employee-data.server.tsx - route.tsx - team-photo.jpg - _landing/ - header.tsx - footer.tsx - route.tsx - app._index/ - route.tsx - stats.tsx - app.projects/ - get-projects.server.tsx - project-card.tsx - project-buttons.tsx - route.tsx - app/ - primary-nav.tsx - route.tsx - footer.tsx - app_.projects.$id.roadmap/ - route.tsx - chart.tsx - update-timeline.server.tsx - contact-us.tsx -``` - -Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example: - -``` -# these are the same route: -routes/app.tsx -routes/app/route.tsx - -# as are these -routes/app._index.tsx -routes/app._index/route.tsx -``` - -## Scaling - -Our general recommendation for scale is to make every route a folder and put the modules used exclusively by that route in the folder, then put the shared modules outside of routes folder elsewhere. This has a couple benefits: - -- Easy to identify shared modules, so tread lightly when changing them -- Easy to organize and refactor the modules for a specific route without creating "file organization fatigue" and cluttering up other parts of the app - -## More Flexibility - -While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in the [remix config][remix-config]. - -There's also the [Flat Routes][flat-routes] third-party package with configurable options beyond the defaults in Remix. - -[loader]: ../route/loader -[action]: ../route/action -[outlet]: ../components/outlet -[routing-guide]: ../guides/routing -[root-route]: #root-route -[resource-route]: ../guides/resource-routes -[routeconvention-v2]: ./route-files-v2 -[flatroutes-rfc]: https://github.com/remix-run/remix/discussions/4482 -[root-route]: #root-route -[index-route]: ../guides/routing#index-routes -[nested-routing]: ../guides/routing#what-is-nested-routing -[nested-routes]: #nested-routes -[remix-config]: ./remix-config#routes -[dot-delimiters]: #dot-delimiters -[dynamic-segments]: #dynamic-segments -[remix-config]: ./remix-config#routes -[flat-routes]: https://github.com/kiliman/remix-flat-routes +[moved]: ./route-files diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md index 37616858b9e..3e929dde060 100644 --- a/docs/file-conventions/routes-files.md +++ b/docs/file-conventions/routes-files.md @@ -1,12 +1,11 @@ --- title: Route File Naming +new: true --- # Route File Naming -The route file convention is changing in v2. You can prepare for this change at your convenience with the `v2_routeConvention` future flag. For instructions on making this change see the [v2 guide][v2guide]. - -Setting up routes in Remix is as simple as creating files in your `app` directory. These are the conventions you should know to understand how routing in Remix works. +While you can configure routes in [remix.config.js][remix-config], most routes are created with this file system convention. Add a file, get a route. Please note that you can use either `.js`, `.jsx`, `.ts` or `.tsx` file extensions. We'll stick with `.tsx` in the examples to avoid duplication. @@ -19,298 +18,375 @@ app/ └── root.tsx ``` -The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes: - -- You can export a [`loader`][loader], [`action`][action], [`meta`][meta], [`headers`][headers], or [`links`][links] function -- You can export an [`ErrorBoundary`][error-boundary] -- Your default export is the layout component that renders the rest of your app in an [``][outlet] +The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes, so you can export a [`loader`][loader], [`action`][action], etc. + +The root route typically looks something like this. It serves as the root layout of the entire app, all other routes will render inside the ``. + +```tsx +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export default function Root() { + return ( + + + + + + + + + + + + ); +} +``` ## Basic Routes -Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `index.tsx` which maps to the root pathname. +Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route]. ```markdown lines=[3-4] app/ ├── routes/ -│ ├── about.tsx -│ └── index.tsx +│ ├── _index.tsx +│ └── about.tsx └── root.tsx ``` -| URL | Matched Route | -| -------- | ---------------------- | -| `/` | `app/routes/index.tsx` | -| `/about` | `app/routes/about.tsx` | +| URL | Matched Routes | +| -------- | -------------- | +| `/` | `_index.tsx` | +| `/about` | `about.tsx` | + +Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested-routing]. -The default export in this file is the component that is rendered at that route and will render within the `` rendered by the root route. +## Dot Delimiters -## Dynamic Route Parameters +Adding a `.` to a route filename will create a `/` in the URL. -```markdown lines=[4] +```markdown lines=[5-7] app/ ├── routes/ -│ ├── blog/ -│ │ ├── $postId.tsx -│ │ ├── categories.tsx -│ │ └── index.tsx +│ ├── _index.tsx │ ├── about.tsx -│ └── index.tsx +│ ├── concerts.trending.tsx +│ ├── concerts.salt-lake-city.tsx +│ └── concerts.san-diego.tsx └── root.tsx ``` -
+| URL | Matched Route | +| -------------------------- | ----------------------------- | +| `/concerts/trending` | `concerts.trending.tsx` | +| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` | +| `/concerts/san-diego` | `concerts.san-diego.tsx` | -URL Route Matches +The dot delimiter also creates nesting, see the [nesting section][nested-routes] for more information. -| URL | Matched Route | -| ------------------ | -------------------------------- | -| `/blog` | `app/routes/blog/index.tsx` | -| `/blog/categories` | `app/routes/blog/categories.tsx` | -| `/blog/my-post` | `app/routes/blog/$postId.tsx` | +## Dynamic Segments -
+Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix. -Routes that begin with a `$` character indicate the name of a dynamic segment of the URL. It will be parsed and passed to your loader and action data as a value on the `param` object. - -For example: `app/routes/blog/$postId.tsx` will match the following URLs: + +```markdown lines=[5] +app/ +├── routes/ +│ ├── _index.tsx +│ ├── about.tsx +│ ├── concerts.$city.tsx +│ └── concerts.trending.tsx +└── root.tsx +``` -- `/blog/my-story` -- `/blog/once-upon-a-time` -- `/blog/how-to-ride-a-bike` +| URL | Matched Route | +| -------------------------- | ----------------------- | +| `/concerts/trending` | `concerts.trending.tsx` | +| `/concerts/salt-lake-city` | `concerts.$city.tsx` | +| `/concerts/san-diego` | `concerts.$city.tsx` | -On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in `/dashboard/:client/invoices/:invoiceId` [view example app][view-example-app]) and all parameters can be accessed within components via [`useParams`][use-params] and within loaders/actions via the argument's [`params`][params] property: +Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders][loader] and [actions][action]. -```tsx filename=app/routes/blog/$postId.tsx -import type { - ActionArgs, - LoaderArgs, -} from "@remix-run/node"; // or cloudflare/deno -import { useParams } from "@remix-run/react"; +```tsx +export function loader({ params }: LoaderArgs) { + return fakeDb.getAllConcertsForCity(params.city); +} +``` -export const loader = async ({ params }: LoaderArgs) => { - console.log(params.postId); -}; +You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`. -export const action = async ({ params }: ActionArgs) => { - console.log(params.postId); -}; +Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name: -export default function PostRoute() { - const params = useParams(); - console.log(params.postId); +```tsx +export function loader({ params }: LoaderArgs) { + return fake.db.getConcerts({ + date: params.date, + city: params.city, + }); } ``` -Nested routes can also contain dynamic segments by using the `$` character in the parent's directory name. For example, `app/routes/blog/$postId/edit.tsx` might represent the editor page for blog entries. - See the [routing guide][routing-guide] for more information. -## Optional Segments +## Nested Routes -Wrapping a route segment in parens will make the segment optional. +Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested-routing]. + +You create nested routes with [dot delimiters][dot-delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes: -```markdown lines=[3] +```markdown lines=[5-8] app/ ├── routes/ -│ ├── ($lang)/ -│ │ ├── $pid.tsx -│ │ ├── categories.tsx -│ └── index.tsx +│ ├── _index.tsx +│ ├── about.tsx +│ ├── concerts._index.tsx +│ ├── concerts.$city.tsx +│ ├── concerts.trending.tsx +│ └── concerts.tsx └── root.tsx ``` -
+All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet]. -URL Route Matches +| URL | Matched Route | Layout | +| -------------------------- | ----------------------- | -------------- | +| `/` | `_index.tsx` | `root.tsx` | +| `/about` | `about.tsx` | `root.tsx` | +| `/concerts` | `concerts._index.tsx` | `concerts.tsx` | +| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | +| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | -| URL | Matched Route | -| -------------------------- | ----------------------------------- | -| `/categories` | `app/routes/($lang)/categories.tsx` | -| `/en/categories` | `app/routes/($lang)/categories.tsx` | -| `/fr/categories` | `app/routes/($lang)/categories.tsx` | -| `/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` | -| `/en/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` | -| `/fr/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` | +Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly. -
+## Nested URLs without Layout Nesting -## Layout Routes +Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment: -```markdown lines=[3,8] +```markdown lines=[8] app/ ├── routes/ -│ ├── blog/ -│ │ ├── $postId.tsx -│ │ ├── categories.tsx -│ │ └── index.tsx +│ ├── _index.tsx │ ├── about.tsx -│ ├── blog.tsx -│ └── index.tsx +│ ├── concerts.$city.tsx +│ ├── concerts.trending.tsx +│ ├── concerts.tsx +│ └── concerts_.mine.tsx └── root.tsx ``` -
+| URL | Matched Route | Layout | +| -------------------------- | ----------------------- | -------------- | +| `/` | `_index.tsx` | `root.tsx` | +| `/concerts/mine` | `concerts_.mine.tsx` | `root.tsx` | +| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | +| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | -URL Route Matches +Note that `/concerts/mine` does not nest with `concerts.tsx` anymore, but `root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting. -| URL | Matched Route | Layout | -| ------------------ | -------------------------------- | --------------------- | -| `/` | `app/routes/index.tsx` | `app/root.tsx` | -| `/about` | `app/routes/about.tsx` | `app/root.tsx` | -| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` | -| `/blog/categories` | `app/routes/blog/categories.tsx` | `app/routes/blog.tsx` | -| `/blog/my-post` | `app/routes/blog/$postId.tsx` | `app/routes/blog.tsx` | +Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting. -
+## Nested Layouts without Nested URLs -In the example above, the `blog.tsx` is a "layout route" for everything within the `blog` directory (`blog/index.tsx` and `blog/categories.tsx`). When a route has the same name as its directory (`routes/blog.tsx` and `routes/blog/`), it becomes a layout route for all the routes inside that directory ("child routes"). Similar to your [root route][root-route], the parent route should render an `` where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs. +We call these Pathless Routes -## Pathless Layout Routes +Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore. -```markdown lines=[3,7,10-11] +```markdown lines=[3-5] app/ ├── routes/ -│ ├── __app/ -│ │ ├── dashboard.tsx -│ │ └── $userId/ -│ │ └── profile.tsx -│ └── __marketing -│ │ ├── index.tsx -│ │ └── product.tsx -│ ├── __app.tsx -│ └── __marketing.tsx +│ ├── _auth.login.tsx +│ ├── _auth.register.tsx +│ ├── _auth.tsx +│ ├── _index.tsx +│ ├── concerts.$city.tsx +│ └── concerts.tsx └── root.tsx ``` -
- -URL Route Matches - -| URL | Matched Route | Layout | -| ----------------- | -------------------------------------- | ---------------------------- | -| `/` | `app/routes/__marketing/index.tsx` | `app/routes/__marketing.tsx` | -| `/product` | `app/routes/__marketing/product.tsx` | `app/routes/__marketing.tsx` | -| `/dashboard` | `app/routes/__app/dashboard.tsx` | `app/routes/__app.tsx` | -| `/chance/profile` | `app/routes/__app/$userId/profile.tsx` | `app/routes/__app.tsx` | +| URL | Matched Route | Layout | +| -------------------------- | -------------------- | -------------- | +| `/` | `_index.tsx` | `root.tsx` | +| `/login` | `_auth.login.tsx` | `_auth.tsx` | +| `/register` | `_auth.register.tsx` | `_auth.tsx` | +| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | -
+Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL. -You can also create layout routes _without adding segments to the URL_ by prepending the directory and associated parent route file with double underscores: `__`. - -For example, all of your marketing pages could be in `app/routes/__marketing/*` and then share a layout by creating `app/routes/__marketing.tsx`. A route `app/routes/__marketing/product.tsx` would be accessible at the `/product` URL because `__marketing` won't add segments to the URL, just UI hierarchy. - -Be careful, pathless layout routes introduce the possibility of URL conflicts +## Optional Segments -## Dot Delimiters +Wrapping a route segment in parentheses will make the segment optional. -```markdown lines=[8] +```markdown lines=[3-5] app/ ├── routes/ -│ ├── blog/ -│ │ ├── $postId.tsx -│ │ ├── categories.tsx -│ │ └── index.tsx -│ ├── about.tsx -│ ├── blog.authors.tsx -│ ├── blog.tsx -│ └── index.tsx +│ ├── ($lang)._index.tsx +│ ├── ($lang).$productId.tsx +│ └── ($lang).categories.tsx └── root.tsx ``` -
- -URL Route Matches +| URL | Matched Route | +| -------------------------- | ------------------------ | +| `/` | `($lang)._index.tsx` | +| `/categories` | `($lang).categories.tsx` | +| `/en/categories` | `($lang).categories.tsx` | +| `/fr/categories` | `($lang).categories.tsx` | +| `/american-flag-speedo` | `($lang)._index.tsx` | +| `/en/american-flag-speedo` | `($lang).$productId.tsx` | +| `/fr/american-flag-speedo` | `($lang).$productId.tsx` | -| URL | Matched Route | Layout | -| ------------------ | -------------------------------- | --------------------- | -| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` | -| `/blog/categories` | `app/routes/blog/categories.tsx` | `app/routes/blog.tsx` | -| `/blog/authors` | `app/routes/blog.authors.tsx` | `app/root.tsx` | - -
- -By creating a file with `.` characters between segments, you can create a nested URL without nested layouts. For example, a file `app/routes/blog.authors.tsx` will route to the pathname `/blog/authors`, but it will not share a layout with routes in the `app/routes/blog/` directory. +You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code. ## Splat Routes +While [dynamic segments][dynamic-segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes. + -```markdown lines=[7] +```markdown lines=[4,6] app/ ├── routes/ -│ ├── blog/ -│ │ ├── $postId.tsx -│ │ ├── categories.tsx -│ │ └── index.tsx +│ ├── _index.tsx │ ├── $.tsx │ ├── about.tsx -│ ├── blog.authors.tsx -│ ├── blog.tsx -│ └── index.tsx +│ └── files.$.tsx └── root.tsx ``` -
+| URL | Matched Route | +| -------------------------------------------- | ------------- | +| `/` | `_index.tsx` | +| `/beef/and/cheese` | `$.tsx` | +| `/files` | `files.$.tsx` | +| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` | +| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` | +| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` | -URL Route Matches +Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. -| URL | Matched Route | Layout | -| ----------------- | --------------------------- | --------------------- | -| `/` | `app/routes/index.tsx` | `app/root.tsx` | -| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` | -| `/somewhere-else` | `app/routes/$.tsx` | `app/root.tsx` | +```tsx filename=app/routes/files.$.tsx +export function loader({ params }) { + const filePath = params["*"]; + return fake.getFileInfo(filePath); +} +``` -
+## Escaping Special Characters -Files that are named `$.tsx` are called "splat" (or "catch-all") routes. These routes will map to any URL not matched by other route files in the same directory. +If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. -Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. +| Filename | URL | +| ------------------------------- | ------------------- | +| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` | +| `routes/[sitemap.xml].tsx` | `/sitemap.xml` | +| `routes/weird-url.[_index].tsx` | `/weird-url/_index` | +| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` | +| `routes/[[so-weird]].tsx` | `/[so-weird]` | -```tsx filename=app/routes/$.tsx -import type { - ActionArgs, - LoaderArgs, -} from "@remix-run/node"; // or cloudflare/deno -import { useParams } from "@remix-run/react"; +## Folders for Organization -export const loader = async ({ params }: LoaderArgs) => { - console.log(params["*"]); -}; +Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders. -export const action = async ({ params }: ActionArgs) => { - console.log(params["*"]); -}; +The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name -export default function PostRoute() { - const params = useParams(); - console.log(params["*"]); -} +Consider these routes: + +``` +routes/ + _landing._index.tsx + _landing.about.tsx + _landing.tsx + app._index.tsx + app.projects.tsx + app.tsx + app_.projects.$id.roadmap.tsx ``` -## Escaping special characters +Some, or all of them can be folders holding their own `route` module inside. -Because some characters have special meaning, you must use our escaping syntax if you want those characters to actually appear in the route. For example, if I wanted to make a [Resource Route][resource-route] for a `/sitemap.xml`, I could name the file `app/routes/[sitemap.xml].tsx`. So you simply wrap any part of the filename with brackets and that will escape any special characters. +``` +routes/ + _landing._index/ + route.tsx + scroll-experience.tsx + _landing.about/ + employee-profile-card.tsx + get-employee-data.server.tsx + route.tsx + team-photo.jpg + _landing/ + header.tsx + footer.tsx + route.tsx + app._index/ + route.tsx + stats.tsx + app.projects/ + get-projects.server.tsx + project-card.tsx + project-buttons.tsx + route.tsx + app/ + primary-nav.tsx + route.tsx + footer.tsx + app_.projects.$id.roadmap/ + route.tsx + chart.tsx + update-timeline.server.tsx + contact-us.tsx +``` + +Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example: + +``` +# these are the same route: +routes/app.tsx +routes/app/route.tsx + +# as are these +routes/app._index.tsx +routes/app._index/route.tsx +``` - - Note, you could even do `app/routes/sitemap[.]xml.tsx` if you wanted to only wrap the part that needs to be escaped. It makes no difference. Choose the one you like best. - +## Scaling + +Our general recommendation for scale is to make every route a folder and put the modules used exclusively by that route in the folder, then put the shared modules outside of routes folder elsewhere. This has a couple benefits: + +- Easy to identify shared modules, so tread lightly when changing them +- Easy to organize and refactor the modules for a specific route without creating "file organization fatigue" and cluttering up other parts of the app + +## More Flexibility + +While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in the [remix config][remix-config]. + +There's also the [Flat Routes][flat-routes] third-party package with configurable options beyond the defaults in Remix. [loader]: ../route/loader [action]: ../route/action -[meta]: ../route/meta -[headers]: ../route/headers -[links]: ../route/links -[error-boundary]: ../route/error-boundary [outlet]: ../components/outlet -[view-example-app]: https://github.com/remix-run/examples/tree/main/multiple-params -[use-params]: https://reactrouter.com/hooks/use-params -[params]: ../route/loader#params [routing-guide]: ../guides/routing [root-route]: #root-route [resource-route]: ../guides/resource-routes -[v2guide]: ../pages/v2#file-system-route-convention +[routeconvention-v2]: ./route-files-v2 +[flatroutes-rfc]: https://github.com/remix-run/remix/discussions/4482 +[root-route]: #root-route +[index-route]: ../guides/routing#index-routes +[nested-routing]: ../guides/routing#what-is-nested-routing +[nested-routes]: #nested-routes +[remix-config]: ./remix-config#routes +[dot-delimiters]: #dot-delimiters +[dynamic-segments]: #dynamic-segments +[remix-config]: ./remix-config#routes +[flat-routes]: https://github.com/kiliman/remix-flat-routes diff --git a/docs/guides/data-loading.md b/docs/guides/data-loading.md index dadd8d5a349..3e70da6df72 100644 --- a/docs/guides/data-loading.md +++ b/docs/guides/data-loading.md @@ -146,7 +146,7 @@ export { db }; And then your routes can import it and make queries against it: -```tsx filename=app/routes/products/$categoryId.tsx +```tsx filename=app/routes/products.$categoryId.tsx import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; @@ -176,7 +176,7 @@ export default function ProductCategory() { If you are using TypeScript, you can use type inference to use Prisma Client generated types when calling `useLoaderData`. This allows better type safety and intellisense when writing code that uses the loaded data. -```tsx filename=app/routes/products/$productId.tsx +```tsx filename=app/routes/products.$productId.tsx import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; @@ -324,7 +324,7 @@ Sometimes you need to read and change the search params from your component inst Perhaps the most common way to set search params is letting the user control them with a form: -```tsx filename=app/routes/products/shoes.tsx lines=[8,9,16,17] +```tsx filename=app/routes/products.shoes.tsx lines=[8,9,16,17] export default function ProductFilters() { return (
diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index 1187ab9e1a6..70e9fc6c4e3 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -143,7 +143,7 @@ Whether you use `` or `` though, you write the very same code. You c Let's start with our project form from earlier but make it usable: -Let's say you've got the route `app/routes/projects/new.js` with this form in it: +Let's say you've got the route `app/routes/projects.new.js` with this form in it: ```tsx export default function NewProject() { diff --git a/docs/guides/mdx.md b/docs/guides/mdx.md index 0e7621c89f4..e66c22a75f3 100644 --- a/docs/guides/mdx.md +++ b/docs/guides/mdx.md @@ -51,7 +51,7 @@ import SomeComponent from "~/components/some-component"; ### Example -By creating a `app/routes/posts/first-post.mdx` we can start writing a blog post: +By creating a `app/routes/posts.first-post.mdx` we can start writing a blog post: ```mdx --- @@ -126,7 +126,7 @@ import Component, { The following example demonstrates how you might build a simple blog with MDX, including individual pages for the posts themselves and an index page that shows all posts. -```tsx filename=app/routes/index.tsx +```tsx filename=app/routes/_index.tsx import { json } from "@remix-run/node"; // or cloudflare/deno import { Link, useLoaderData } from "@remix-run/react"; diff --git a/docs/guides/not-found.md b/docs/guides/not-found.md index 34106cf48d3..fbb85869b92 100644 --- a/docs/guides/not-found.md +++ b/docs/guides/not-found.md @@ -17,7 +17,7 @@ The first case is already handled by Remix, you don't have to throw a response y As soon as you know you don't have what the user is looking for you should _throw a response_. -```tsx filename=routes/page/$slug.js +```tsx filename=app/routes/page.$slug.js export async function loader({ params }: LoaderArgs) { const page = await db.page.findOne({ where: { slug: params.slug }, diff --git a/docs/guides/optimistic-ui.md b/docs/guides/optimistic-ui.md index 14dc3d705fd..e24f81048e4 100644 --- a/docs/guides/optimistic-ui.md +++ b/docs/guides/optimistic-ui.md @@ -22,7 +22,7 @@ Remix can help you build optimistic UI with [`useNavigation`][use-navigation] an Consider the workflow for viewing and creating a new project. The project route loads the project and renders it. -```tsx filename=app/routes/project/$id.tsx +```tsx filename=app/routes/project.$id.tsx import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; @@ -59,7 +59,7 @@ export function ProjectView({ project }) { Now we can get to the fun part. Here's what a "new project" route might look like: -```tsx filename=app/routes/projects/new.tsx +```tsx filename=app/routes/projects.new.tsx import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form } from "@remix-run/react"; @@ -92,7 +92,7 @@ export default function NewProject() { At this point, typically you'd render a busy spinner on the page while the user waits for the project to be sent to the server, added to the database, and sent back to the browser and then redirected to the project. Remix makes that pretty easy: -```tsx filename=app/routes/projects/new.tsx lines=[3,15,27,29-31] +```tsx filename=app/routes/projects.new.tsx lines=[3,15,27,29-31] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form, useNavigation } from "@remix-run/react"; @@ -133,7 +133,7 @@ export default function NewProject() { Since we know that almost every time this form is submitted it's going to succeed, we can just skip the busy spinners and show the UI as we know it's going to be: the ``. -```tsx filename=app/routes/projects/new.tsx lines=[5,17-23,31-32] +```tsx filename=app/routes/projects.new.tsx lines=[5,17-23,31-32] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { redirect } from "@remix-run/node"; // or cloudflare/deno import { Form, useNavigation } from "@remix-run/react"; @@ -178,7 +178,7 @@ One of the hardest parts about implementing optimistic UI is how to handle failu If you want to have more control over the UI when an error occurs and put the user right back where they were without losing any state, you can catch your own error and send it down through action data. -```tsx filename=app/routes/projects/new.tsx lines=[5,15-23,27,47] +```tsx filename=app/routes/projects.new.tsx lines=[5,15-23,27,47] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { @@ -235,7 +235,7 @@ Now in the rare case of an error on the server, the UI reverts back to the form, For this to work best, you'll want a bit of client-side validation so that form-validation issues on the server don't cause the app to flash between optimistic UI and validation messages. Fortunately, [HTML usually has everything you need][html-input] built-in. The browser will validate the fields before the form is even submitted to the server to avoid sending bad data and getting flashes of optimistic UI. -```tsx filename=app/routes/projects/new.tsx lines=[43,45] +```tsx filename=app/routes/projects.new.tsx lines=[43,45] import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno import { diff --git a/docs/guides/resource-routes.md b/docs/guides/resource-routes.md index 012e7a474fb..07ddca5a938 100644 --- a/docs/guides/resource-routes.md +++ b/docs/guides/resource-routes.md @@ -20,7 +20,7 @@ If a route doesn't export a default component, it can be used as a Resource Rout For example, consider a UI Route that renders a report, note the link: -```tsx filename=app/routes/reports/$id.js lines=[10-12] +```tsx filename=app/routes/reports.$id.js lines=[10-12] export async function loader({ params }: LoaderArgs) { return json(await getReport(params.id)); } @@ -41,7 +41,7 @@ export default function Report() { It's linking to a PDF version of the page. To make this work we can create a Resource Route below it. Notice that it has no component: that makes it a Resource Route. -```tsx filename=app/routes/reports/$id/pdf.tsx +```tsx filename=app/routes/reports.$id.pdf.tsx export async function loader({ params }: LoaderArgs) { const report = await getReport(params.id); const pdf = await generateReportPDF(report); @@ -71,14 +71,14 @@ To add a `.` to a route's path, use the `[]` escape characters. Our PDF route fi ```sh # original # /reports/123/pdf -app/routes/reports/$id/pdf.ts +app/routes/reports.$id.pdf.ts # with a file extension # /reports/123.pdf -app/routes/reports/$id[.pdf].ts +app/routes/reports.$id[.pdf].ts # or like this, the resulting URL is the same -app/routes/reports/$id[.]pdf.ts +app/routes/reports.$id[.]pdf.ts ``` ## Handling different request methods diff --git a/docs/guides/routing.md b/docs/guides/routing.md index 471446aee74..81237216721 100644 --- a/docs/guides/routing.md +++ b/docs/guides/routing.md @@ -74,32 +74,6 @@ app └── sales.tsx ``` -
- -Or if using the v1 routing convention: - -``` -app -├── root.tsx -└── routes - ├── accounts.tsx - ├── dashboard.tsx - ├── expenses.tsx - ├── index.tsx - ├── reports.tsx - ├── sales - │ ├── customers.tsx - │ ├── deposits.tsx - │ ├── index.tsx - │ ├── invoices - │ │ ├── $invoiceId.tsx - │ │ └── index.tsx - │ ├── invoices.tsx - │ └── subscriptions.tsx - └── sales.tsx -``` - -
- `root.tsx` is the "root route" that serves as the layout for the entire application. Every route will render inside its ``. - Note that there are files with `.` delimiters. The `.` creates a `/` in the URL for that route, as well as layout nesting with another route that matches the segments before the `.`. For example, `sales.tsx` is the **parent route** for all the **child routes** that look like `sales.[the nested path].tsx`. The `` in `sales.tsx` will render the matching child route. diff --git a/docs/guides/styling.md b/docs/guides/styling.md index cf1dd70ddfa..3cefa8b30e5 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -97,7 +97,7 @@ export function links() { } ``` -```tsx filename=app/routes/dashboard/accounts.tsx +```tsx filename=app/routes/dashboard.accounts.tsx import styles from "~/styles/accounts.css"; export function links() { @@ -105,7 +105,7 @@ export function links() { } ``` -```tsx filename=app/routes/dashboard/sales.tsx +```tsx filename=app/routes/dashboard.sales.tsx import styles from "~/styles/sales.css"; export function links() { @@ -230,9 +230,9 @@ Note that the primary button's `links` include the base button's links. This way Because these buttons are not routes, and therefore not associated with a URL segment, Remix doesn't know when to prefetch, load, or unload the styles. We need to "surface" the links up to the routes that use the components. -Consider that `routes/index.js` uses the primary button component: +Consider that `routes/_index.js` uses the primary button component: -```tsx filename=app/routes/index.js lines=[1-4,9] +```tsx filename=app/routes/_index.js lines=[1-4,9] import { PrimaryButton, links as primaryButtonLinks, diff --git a/docs/hooks/use-matches.md b/docs/hooks/use-matches.md index 4b96e7c006c..dff434d4258 100644 --- a/docs/hooks/use-matches.md +++ b/docs/hooks/use-matches.md @@ -48,7 +48,7 @@ You can put whatever you want on a route `handle`. Here we'll use `breadcrumb`. 2. We can do the same for a child route - ```tsx filename=app/routes/parent/child.tsx + ```tsx filename=app/routes/parent.child.tsx export const handle = { breadcrumb: () => ( Child Route diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md index 60ed13b57ff..09463fb138c 100644 --- a/docs/pages/api-development-strategy.md +++ b/docs/pages/api-development-strategy.md @@ -55,7 +55,6 @@ The lifecycle is thus either: | `v2_dev` | Enable the new development server (including HMR/HDR support) | | `v2_headers` | Leverage ancestor `headers` if children do not export `headers` | | `v2_meta` | Enable the new API for your `meta` functions | -| `v2_routeConvention` | Enable the flat routes style of file-based routing | [future-flags-blog-post]: https://remix.run/blog/future-flags [feature-flowchart]: /docs-images/feature-flowchart.png diff --git a/docs/pages/faq.md b/docs/pages/faq.md index cabe0a51928..c6e3bbd0cf8 100644 --- a/docs/pages/faq.md +++ b/docs/pages/faq.md @@ -81,7 +81,7 @@ We find option (1) to be the simplest because you don't have to mess around with HTML buttons can send a value, so it's the easiest way to implement this: -```tsx filename=app/routes/projects/$id.tsx lines=[3-4,33,39] +```tsx filename=app/routes/projects.$id.tsx lines=[3-4,33,39] export async function action({ request }: ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); diff --git a/docs/pages/gotchas.md b/docs/pages/gotchas.md index 4b10260deb9..afc7a10699f 100644 --- a/docs/pages/gotchas.md +++ b/docs/pages/gotchas.md @@ -18,7 +18,7 @@ TypeError: Cannot read properties of undefined (reading 'root') For example, you can't import "fs-extra" directly into a route module: -```tsx bad filename=app/routes/index.tsx lines=[2] nocopy +```tsx bad filename=app/routes/_index.tsx lines=[2] nocopy import { json } from "@remix-run/node"; // or cloudflare/deno import fs from "fs-extra"; @@ -39,7 +39,7 @@ export { default } from "fs-extra"; And then change our import in the route to the new "wrapper" module: -```tsx filename=app/routes/index.tsx lines=[3] +```tsx filename=app/routes/_index.tsx lines=[3] import { json } from "@remix-run/node"; // or cloudflare/deno import fs from "~/utils/fs-extra.server"; diff --git a/docs/route/loader.md b/docs/route/loader.md index 639968614ec..77fc56719da 100644 --- a/docs/route/loader.md +++ b/docs/route/loader.md @@ -74,7 +74,7 @@ export default function SomeRoute() { Route params are defined by route file names. If a segment begins with `$` like `$invoiceId`, the value from the URL for that segment will be passed to your loader. -```tsx filename=app/routes/invoices/$invoiceId.tsx nocopy +```tsx filename=app/routes/invoices.$invoiceId.tsx nocopy // if the user visits /invoices/123 export async function loader({ params }: LoaderArgs) { params.invoiceId; // "123" @@ -83,7 +83,7 @@ export async function loader({ params }: LoaderArgs) { Params are mostly useful for looking up records by ID: -```tsx filename=app/routes/invoices/$invoiceId.tsx +```tsx filename=app/routes/invoices.$invoiceId.tsx // if the user visits /invoices/123 export async function loader({ params }: LoaderArgs) { const invoice = await fakeDb.getInvoice(params.invoiceId); @@ -233,7 +233,7 @@ export async function requireUserSession(request) { } ``` -```tsx filename=app/routes/invoice/$invoiceId.tsx +```tsx filename=app/routes/invoice.$invoiceId.tsx import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { diff --git a/docs/route/meta-v2.md b/docs/route/meta-v2.md index 1fd24470f2a..96fcb024959 100644 --- a/docs/route/meta-v2.md +++ b/docs/route/meta-v2.md @@ -172,7 +172,7 @@ export const meta: V2_MetaFunction = () => { }; ``` -```tsx bad filename=app/routes/projects/$id.tsx +```tsx bad filename=app/routes/projects.$id.tsx export const meta: V2_MetaFunction = ({ data, }) => { diff --git a/docs/route/should-revalidate.md b/docs/route/should-revalidate.md index d66e2bcc047..4bef41285f3 100644 --- a/docs/route/should-revalidate.md +++ b/docs/route/should-revalidate.md @@ -92,7 +92,7 @@ For instance, consider an event slug with the id and an human-friendly title: - `/events/blink-182-united-center-saint-paul--ae3f9` - `/events/blink-182-little-caesars-arena-detroit--e87ad` -```tsx filename=app/routes/events/$slug.tsx +```tsx filename=app/routes/events.$slug.tsx export async function loader({ params }: LoaderArgs) { const id = params.slug.split("--")[1]; return loadEvent(id); diff --git a/docs/utils/cookies.md b/docs/utils/cookies.md index 5a087e80d28..3f2b95bd22b 100644 --- a/docs/utils/cookies.md +++ b/docs/utils/cookies.md @@ -30,7 +30,7 @@ Then, you can `import` the cookie and use it in your `loader` and/or `action`. T **Note:** We recommend (for now) that you create all the cookies your app needs in a `*.server.ts` file and `import` them into your route modules. This allows the Remix compiler to correctly prune these imports out of the browser build where they are not needed. We hope to eventually remove this caveat. -```tsx filename=app/routes/index.tsx lines=[8,12-13,19-20,24] +```tsx filename=app/routes/_index.tsx lines=[8,12-13,19-20,24] import type { ActionArgs, LoaderArgs, diff --git a/integration/abort-signal-test.ts b/integration/abort-signal-test.ts index df4b5c38b1b..0888e3e61ca 100644 --- a/integration/abort-signal-test.ts +++ b/integration/abort-signal-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/action-test.ts b/integration/action-test.ts index 84f4edee4e6..bcaf27263ba 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -17,11 +17,6 @@ test.describe("actions", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/urlencoded.jsx": js` import { Form, useActionData } from "@remix-run/react"; diff --git a/integration/browser-entry-test.ts b/integration/browser-entry-test.ts index 7a85b0ee219..3644a958ab8 100644 --- a/integration/browser-entry-test.ts +++ b/integration/browser-entry-test.ts @@ -10,7 +10,7 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ files: { - "app/routes/index.jsx": js` + "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; export default function Index() { diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts index 0d6fe2c890a..3c596d60aee 100644 --- a/integration/bug-report-test.ts +++ b/integration/bug-report-test.ts @@ -49,9 +49,6 @@ test.beforeEach(async ({ context }) => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, //////////////////////////////////////////////////////////////////////////// // 💿 Next, add files to this object, just like files in a real app, // `createFixture` will make an app and run your tests against it. diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts index 9665527553e..ba94ec1cd48 100644 --- a/integration/catch-boundary-data-test.ts +++ b/integration/catch-boundary-data-test.ts @@ -34,11 +34,6 @@ test.beforeEach(async ({ context }) => { test.describe("ErrorBoundary (thrown responses)", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 00527d2c481..9bc4b6da261 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -24,11 +24,6 @@ test.describe("ErrorBoundary (thrown responses)", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/cf-compiler-test.ts b/integration/cf-compiler-test.ts index 1077ed434cc..e10172dfe7b 100644 --- a/integration/cf-compiler-test.ts +++ b/integration/cf-compiler-test.ts @@ -28,9 +28,6 @@ test.describe("cloudflare compiler", () => { test.beforeAll(async () => { projectDir = await createFixtureProject({ - config: { - future: { v2_routeConvention: true }, - }, setup: "cloudflare", template: "cf-template", files: { diff --git a/integration/compiler-mjs-output-test.ts b/integration/compiler-mjs-output-test.ts index 4f675344427..3df4aba4078 100644 --- a/integration/compiler-mjs-output-test.ts +++ b/integration/compiler-mjs-output-test.ts @@ -14,7 +14,6 @@ test.beforeAll(async () => { export default { serverModuleFormat: "esm", serverBuildPath: "build/index.mjs", - future: { v2_routeConvention: true }, }; `, "package.json": js` diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index 4cc7e861765..92e0962f9db 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -27,9 +27,6 @@ test.describe("compiler", () => { "remix.config.js": js` let { getDependenciesToBundle } = require("@remix-run/dev"); module.exports = { - future: { - v2_routeConvention: true, - }, serverDependenciesToBundle: [ "esm-only-pkg", "esm-only-single-export", @@ -331,9 +328,6 @@ test.describe("compiler", () => { await expect(() => createFixtureProject({ - config: { - future: { v2_routeConvention: true }, - }, buildStdio, files: { "app/routes/_index.jsx": js` diff --git a/integration/conventional-routes-test.ts b/integration/conventional-routes-test.ts deleted file mode 100644 index 665ad3516c1..00000000000 --- a/integration/conventional-routes-test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { test, expect } from "@playwright/test"; - -import { PlaywrightFixture } from "./helpers/playwright-fixture"; -import type { Fixture, AppFixture } from "./helpers/create-fixture"; -import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; - -let fixture: Fixture; -let appFixture: AppFixture; - -test.beforeAll(async () => { - fixture = await createFixture({ - files: { - "app/root.tsx": js` - import { Link, Outlet, Scripts, useMatches } from "@remix-run/react"; - - export default function App() { - let matches = 'Number of matches: ' + useMatches().length; - return ( - - - -

{matches}

- - - - - ); - } - `, - "app/routes/nested/index.jsx": js` - export default function Index() { - return

Index

; - } - `, - "app/routes/nested/__pathless.jsx": js` - import { Outlet } from "@remix-run/react"; - - export default function Layout() { - return ( - <> -
Pathless Layout
- - - ); - } - `, - "app/routes/nested/__pathless/foo.jsx": js` - export default function Foo() { - return

Foo

; - } - `, - "app/routes/nested/__pathless2.jsx": js` - import { Outlet } from "@remix-run/react"; - - export default function Layout() { - return ( - <> -
Pathless 2 Layout
- - - ); - } - `, - "app/routes/nested/__pathless2/bar.jsx": js` - export default function Bar() { - return

Bar

; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); -}); - -test.afterAll(async () => appFixture.close()); - -test.describe("with JavaScript", () => { - runTests(); -}); - -test.describe("without JavaScript", () => { - test.use({ javaScriptEnabled: false }); - runTests(); -}); - -/** - * Routes for this test look like this, for reference for the matches assertions: - * - * - * - * - * - * - * - * - * - * - */ - -function runTests() { - test("displays index page and not pathless layout page", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/nested"); - expect(await app.getHtml()).toMatch("Index"); - expect(await app.getHtml()).not.toMatch("Pathless Layout"); - expect(await app.getHtml()).toMatch("Number of matches: 2"); - }); - - test("displays page inside of pathless layout", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/nested/foo"); - expect(await app.getHtml()).not.toMatch("Index"); - expect(await app.getHtml()).toMatch("Pathless Layout"); - expect(await app.getHtml()).toMatch("Foo"); - expect(await app.getHtml()).toMatch("Number of matches: 3"); - }); - - // This also asserts that we support multiple sibling pathless route layouts - test("displays page inside of second pathless layout", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/nested/bar"); - expect(await app.getHtml()).not.toMatch("Index"); - expect(await app.getHtml()).toMatch("Pathless 2 Layout"); - expect(await app.getHtml()).toMatch("Bar"); - expect(await app.getHtml()).toMatch("Number of matches: 3"); - }); -} diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts index d2869cb35c7..1ec95916b7d 100644 --- a/integration/css-modules-test.ts +++ b/integration/css-modules-test.ts @@ -19,11 +19,6 @@ test.describe("CSS Modules", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Outlet } from "@remix-run/react"; diff --git a/integration/css-side-effect-imports-test.ts b/integration/css-side-effect-imports-test.ts index 2dc88893ede..814d491f036 100644 --- a/integration/css-side-effect-imports-test.ts +++ b/integration/css-side-effect-imports-test.ts @@ -20,9 +20,6 @@ test.describe("CSS side-effect imports", () => { fixture = await createFixture({ config: { serverDependenciesToBundle: [/@test-package/], - future: { - v2_routeConvention: true, - }, }, files: { "app/root.jsx": js` diff --git a/integration/custom-entry-server-test.ts b/integration/custom-entry-server-test.ts index 86115324ca7..c7f4f64b17b 100644 --- a/integration/custom-entry-server-test.ts +++ b/integration/custom-entry-server-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/entry.server.jsx": js` import * as React from "react"; diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts index 543bcc79b00..d61bbe646f7 100644 --- a/integration/defer-loader-test.ts +++ b/integration/defer-loader-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { useLoaderData, Link } from "@remix-run/react"; diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 91b42b07ff9..bdd76a9902a 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -42,11 +42,6 @@ test.describe("non-aborted", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/components/counter.tsx": js` import { useState } from "react"; @@ -980,9 +975,6 @@ test.describe("aborted", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, //////////////////////////////////////////////////////////////////////////// // 💿 Next, add files to this object, just like files in a real app, // `createFixture` will make an app and run your tests against it. diff --git a/integration/deno-compiler-test.ts b/integration/deno-compiler-test.ts index 3c74e03f8b5..e9013a453b0 100644 --- a/integration/deno-compiler-test.ts +++ b/integration/deno-compiler-test.ts @@ -34,9 +34,6 @@ const searchFiles = async (pattern: string | RegExp, files: string[]) => { test.beforeAll(async () => { projectDir = await createFixtureProject({ - config: { - future: { v2_routeConvention: true }, - }, template: "deno-template", files: { "package.json": json({ diff --git a/integration/deterministic-build-output-test.ts b/integration/deterministic-build-output-test.ts index c8c107d06ed..3d81d20d28a 100644 --- a/integration/deterministic-build-output-test.ts +++ b/integration/deterministic-build-output-test.ts @@ -26,11 +26,6 @@ test("builds deterministically under different paths", async () => { // * serverRouteModulesPlugin (implicitly tested by build) // * vanillaExtractPlugin (via app/routes/foo.tsx' .css.ts file import) let init: FixtureInit = { - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "postcss.config.js": js` module.exports = { diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts index 1680e24c8b6..cd29cbc6d89 100644 --- a/integration/error-boundary-test.ts +++ b/integration/error-boundary-test.ts @@ -42,11 +42,6 @@ test.describe("ErrorBoundary", () => { console.error = () => {}; fixture = await createFixture( { - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -499,11 +494,6 @@ test.describe("ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -669,11 +659,6 @@ test.describe("loaderData in ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -1022,11 +1007,6 @@ test.describe("Default ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture( { - config: { - future: { - v2_routeConvention: true, - }, - }, files: getFiles({ includeRootErrorBoundary: false }), }, ServerMode.Development @@ -1098,11 +1078,6 @@ test.describe("Default ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture( { - config: { - future: { - v2_routeConvention: true, - }, - }, files: getFiles({ includeRootErrorBoundary: true }), }, ServerMode.Development @@ -1168,11 +1143,6 @@ test.describe("Default ErrorBoundary", () => { test.describe("When the root route has a boundary but it also throws 😦", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: getFiles({ includeRootErrorBoundary: true, rootErrorBoundaryThrows: true, @@ -1257,11 +1227,6 @@ test("Allows back-button out of an error boundary after a hard reload", async ({ console.error = () => {}; let fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react"; diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts index 8d76adc5a4f..b43b55e3b1c 100644 --- a/integration/error-boundary-v2-test.ts +++ b/integration/error-boundary-v2-test.ts @@ -13,11 +13,6 @@ test.describe("ErrorBoundary", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/error-data-request-test.ts b/integration/error-data-request-test.ts index f852d4786c1..d62ccb169f7 100644 --- a/integration/error-data-request-test.ts +++ b/integration/error-data-request-test.ts @@ -16,9 +16,6 @@ test.describe("ErrorBoundary", () => { }; fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts index 8f356e59e03..85cc942eec7 100644 --- a/integration/error-sanitization-test.ts +++ b/integration/error-sanitization-test.ts @@ -27,7 +27,7 @@ const routeFiles = { } `, - "app/routes/index.jsx": js` + "app/routes/_index.jsx": js` import { useLoaderData, useLocation, useRouteError } from "@remix-run/react"; export function loader({ request }) { @@ -177,7 +177,7 @@ test.describe("Error Sanitization", () => { expect(html).not.toMatch("LOADER"); expect(html).toMatch("MESSAGE:Unexpected Server Error"); expect(html).toMatch( - '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}' + '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}' ); expect(html).not.toMatch(/stack/i); expect(errorLogs.length).toBe(1); @@ -191,7 +191,7 @@ test.describe("Error Sanitization", () => { expect(html).toMatch("Index Error"); expect(html).toMatch("MESSAGE:Unexpected Server Error"); expect(html).toMatch( - '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}' + '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}' ); expect(html).not.toMatch(/stack/i); expect(errorLogs.length).toBe(1); @@ -225,7 +225,7 @@ test.describe("Error Sanitization", () => { }); test("returns data without errors", async () => { - let response = await fixture.requestData("/", "routes/index"); + let response = await fixture.requestData("/", "routes/_index"); let text = await response.text(); expect(text).toMatch("LOADER"); expect(text).not.toMatch("MESSAGE:"); @@ -233,7 +233,7 @@ test.describe("Error Sanitization", () => { }); test("sanitizes loader errors in data requests", async () => { - let response = await fixture.requestData("/?loader", "routes/index"); + let response = await fixture.requestData("/?loader", "routes/_index"); let text = await response.text(); expect(text).toBe('{"message":"Unexpected Server Error"}'); expect(errorLogs.length).toBe(1); @@ -338,7 +338,7 @@ test.describe("Error Sanitization", () => { expect(html).toMatch("

MESSAGE:Loader Error"); expect(html).toMatch("

STACK:Error: Loader Error"); expect(html).toMatch( - 'errors":{"routes/index":{"message":"Loader Error","stack":"Error: Loader Error\\n' + 'errors":{"routes/_index":{"message":"Loader Error","stack":"Error: Loader Error\\n' ); expect(errorLogs.length).toBe(1); expect(errorLogs[0][0].message).toMatch("Loader Error"); @@ -352,7 +352,7 @@ test.describe("Error Sanitization", () => { expect(html).toMatch("

MESSAGE:Render Error"); expect(html).toMatch("

STACK:Error: Render Error"); expect(html).toMatch( - 'errors":{"routes/index":{"message":"Render Error","stack":"Error: Render Error\\n' + 'errors":{"routes/_index":{"message":"Render Error","stack":"Error: Render Error\\n' ); expect(errorLogs.length).toBe(1); expect(errorLogs[0][0].message).toMatch("Render Error"); @@ -382,7 +382,7 @@ test.describe("Error Sanitization", () => { }); test("returns data without errors", async () => { - let response = await fixture.requestData("/", "routes/index"); + let response = await fixture.requestData("/", "routes/_index"); let text = await response.text(); expect(text).toMatch("LOADER"); expect(text).not.toMatch("MESSAGE:"); @@ -390,7 +390,7 @@ test.describe("Error Sanitization", () => { }); test("does not sanitize loader errors in data requests", async () => { - let response = await fixture.requestData("/?loader", "routes/index"); + let response = await fixture.requestData("/?loader", "routes/_index"); let text = await response.text(); expect(text).toMatch( '{"message":"Loader Error","stack":"Error: Loader Error' @@ -536,7 +536,7 @@ test.describe("Error Sanitization", () => { expect(html).not.toMatch("LOADER"); expect(html).toMatch("MESSAGE:Unexpected Server Error"); expect(html).toMatch( - '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}' + '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}' ); expect(html).not.toMatch(/stack/i); expect(errorLogs[0][0]).toEqual("App Specific Error Logging:"); @@ -552,7 +552,7 @@ test.describe("Error Sanitization", () => { expect(html).toMatch("Index Error"); expect(html).toMatch("MESSAGE:Unexpected Server Error"); expect(html).toMatch( - '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}' + '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}' ); expect(html).not.toMatch(/stack/i); expect(errorLogs[0][0]).toEqual("App Specific Error Logging:"); @@ -588,7 +588,7 @@ test.describe("Error Sanitization", () => { }); test("returns data without errors", async () => { - let response = await fixture.requestData("/", "routes/index"); + let response = await fixture.requestData("/", "routes/_index"); let text = await response.text(); expect(text).toMatch("LOADER"); expect(text).not.toMatch("MESSAGE:"); @@ -596,12 +596,12 @@ test.describe("Error Sanitization", () => { }); test("sanitizes loader errors in data requests", async () => { - let response = await fixture.requestData("/?loader", "routes/index"); + let response = await fixture.requestData("/?loader", "routes/_index"); let text = await response.text(); expect(text).toBe('{"message":"Unexpected Server Error"}'); expect(errorLogs[0][0]).toEqual("App Specific Error Logging:"); expect(errorLogs[1][0]).toEqual( - " Request: GET test://test/?loader=&_data=routes%2Findex" + " Request: GET test://test/?loader=&_data=routes%2F_index" ); expect(errorLogs[2][0]).toEqual(" Error: Loader Error"); expect(errorLogs[3][0]).toMatch(" at "); diff --git a/integration/fetcher-layout-test.ts b/integration/fetcher-layout-test.ts index 1a8d8c44ba8..c7929ce4311 100644 --- a/integration/fetcher-layout-test.ts +++ b/integration/fetcher-layout-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/layout-action.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts index 2bca2fcbf15..2f8489784a3 100644 --- a/integration/fetcher-test.ts +++ b/integration/fetcher-test.ts @@ -17,9 +17,6 @@ test.describe("useFetcher", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/resource-route-action-only.ts": js` import { json } from "@remix-run/node"; @@ -425,11 +422,6 @@ test.describe("fetcher aborts and adjacent forms", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/_index.jsx": js` import * as React from "react"; diff --git a/integration/flat-routes-test.ts b/integration/flat-routes-test.ts index 8af9c20038e..646083c3c19 100644 --- a/integration/flat-routes-test.ts +++ b/integration/flat-routes-test.ts @@ -15,9 +15,6 @@ test.describe("flat routes", () => { fixture = await createFixture({ config: { ignoredRouteFiles: [IGNORED_ROUTE], - future: { - v2_routeConvention: true, - }, }, files: { "app/root.jsx": js` @@ -165,56 +162,6 @@ test.describe("flat routes", () => { }); }); -test.describe("warns when v1 routesConvention is used", () => { - let buildStdio = new PassThrough(); - let buildOutput: string; - - let originalConsoleLog = console.log; - let originalConsoleWarn = console.warn; - let originalConsoleError = console.error; - - test.beforeAll(async () => { - console.log = () => {}; - console.warn = () => {}; - console.error = () => {}; - await createFixtureProject({ - buildStdio, - config: { - future: { v2_routeConvention: false }, - }, - files: { - "routes/index.tsx": js` - export default function () { - return

routes/index

; - } - `, - }, - }); - - let chunks: Buffer[] = []; - buildOutput = await new Promise((resolve, reject) => { - buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk))); - buildStdio.on("error", (err) => reject(err)); - buildStdio.on("end", () => - resolve(Buffer.concat(chunks).toString("utf8")) - ); - }); - }); - - test.afterAll(() => { - console.log = originalConsoleLog; - console.warn = originalConsoleWarn; - console.error = originalConsoleError; - }); - - test("v2_routeConvention is not enabled", () => { - console.log(buildOutput); - expect(buildOutput).toContain( - "The route file convention is changing in v2" - ); - }); -}); - test.describe("emits warnings for route conflicts", async () => { let buildStdio = new PassThrough(); let buildOutput: string; @@ -229,9 +176,6 @@ test.describe("emits warnings for route conflicts", async () => { console.error = () => {}; await createFixtureProject({ buildStdio, - config: { - future: { v2_routeConvention: true }, - }, files: { "routes/_dashboard._index.tsx": js` export default function () { @@ -287,9 +231,6 @@ test.describe("", () => { console.error = () => {}; await createFixtureProject({ buildStdio, - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index/route.jsx": js``, "app/routes/_index/utils.js": js``, @@ -320,9 +261,6 @@ test.describe("", () => { test.describe("pathless routes and route collisions", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.tsx": js` import { Link, Outlet, Scripts, useMatches } from "@remix-run/react"; @@ -407,11 +345,11 @@ test.describe("pathless routes and route collisions", () => { * * * - * - * + * + * * - * - * + * + * * * */ diff --git a/integration/form-data-test.ts b/integration/form-data-test.ts index 2697c512e61..431beb3cf2c 100644 --- a/integration/form-data-test.ts +++ b/integration/form-data-test.ts @@ -7,9 +7,6 @@ let fixture: Fixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/form-test.ts b/integration/form-test.ts index 658a3c8d9ea..e3f1d2ef744 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -56,9 +56,6 @@ test.describe("Forms", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/get-submission.jsx": js` import { useLoaderData, Form } from "@remix-run/react"; diff --git a/integration/headers-test.ts b/integration/headers-test.ts index 17955377815..5413ba19174 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -17,8 +17,6 @@ test.describe("headers export", () => { { config: { future: { - v2_routeConvention: true, - v2_headers: true, }, }, @@ -226,9 +224,6 @@ test.describe("headers export", () => { let fixture = await createFixture( { - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; @@ -438,8 +433,6 @@ test.describe("v1 behavior (future.v2_headers=false)", () => { { config: { future: { - v2_routeConvention: true, - v2_headers: false, }, }, diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts index 1691476cf5d..283895e34d8 100644 --- a/integration/hmr-log-test.ts +++ b/integration/hmr-log-test.ts @@ -17,8 +17,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ v2_dev: { port: options.devPort, }, - v2_routeConvention: true, - v2_meta: true, v2_headers: true, }, diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index 8df8a747317..e8c2d3d54f7 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -17,8 +17,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ v2_dev: { port: options.devPort, }, - v2_routeConvention: true, - v2_meta: true, v2_headers: true, }, diff --git a/integration/hook-useSubmit-test.ts b/integration/hook-useSubmit-test.ts index 083fb07345f..8a19b04a816 100644 --- a/integration/hook-useSubmit-test.ts +++ b/integration/hook-useSubmit-test.ts @@ -10,9 +10,6 @@ test.describe("`useSubmit()` returned function", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { useLoaderData, useSubmit } from "@remix-run/react"; diff --git a/integration/js-routes-test.ts b/integration/js-routes-test.ts index fa0d08c0590..47ef5a33b51 100644 --- a/integration/js-routes-test.ts +++ b/integration/js-routes-test.ts @@ -10,11 +10,6 @@ test.describe(".js route files", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/js.js": js` export default () =>
Rendered with .js ext
; diff --git a/integration/layout-route-test.ts b/integration/layout-route-test.ts index 749c759188b..9f3ac291c2d 100644 --- a/integration/layout-route-test.ts +++ b/integration/layout-route-test.ts @@ -10,11 +10,6 @@ test.describe("pathless layout routes", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/_layout.jsx": js` import { Outlet } from "@remix-run/react"; diff --git a/integration/link-test.ts b/integration/link-test.ts index b301a0fb4e7..e6168e539c5 100644 --- a/integration/link-test.ts +++ b/integration/link-test.ts @@ -32,11 +32,6 @@ test.describe("route module link export", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/favicon.ico": js``, diff --git a/integration/loader-test.ts b/integration/loader-test.ts index 297db10b3d1..89656e431de 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -12,9 +12,6 @@ test.describe("loader", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { json } from "@remix-run/node"; @@ -78,11 +75,6 @@ test.describe("loader in an app", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Outlet } from '@remix-run/react' diff --git a/integration/matches-test.ts b/integration/matches-test.ts index c1b20037fe8..3516075152f 100644 --- a/integration/matches-test.ts +++ b/integration/matches-test.ts @@ -10,9 +10,6 @@ test.describe("useMatches", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import * as React from 'react'; diff --git a/integration/mdx-test.ts b/integration/mdx-test.ts index 28e628a7736..27a55b9cc5b 100644 --- a/integration/mdx-test.ts +++ b/integration/mdx-test.ts @@ -16,9 +16,6 @@ test.describe("mdx", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/meta-test.ts b/integration/meta-test.ts index a04a61e91bc..bc2260409b8 100644 --- a/integration/meta-test.ts +++ b/integration/meta-test.ts @@ -15,9 +15,6 @@ test.describe("meta", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { json } from "@remix-run/node"; @@ -403,7 +400,6 @@ test.describe("v2_meta", () => { ignoredRouteFiles: ["**/.*"], future: { v2_meta: true, - v2_routeConvention: true, }, }, files: { diff --git a/integration/multiple-cookies-test.ts b/integration/multiple-cookies-test.ts index 7b261cb55de..19ff6348d47 100644 --- a/integration/multiple-cookies-test.ts +++ b/integration/multiple-cookies-test.ts @@ -10,11 +10,6 @@ test.describe("pathless layout routes", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/_index.jsx": js` import { redirect, json } from "@remix-run/node"; diff --git a/integration/navigation-state-test.ts b/integration/navigation-state-test.ts index 319e9b58fea..05858633873 100644 --- a/integration/navigation-state-test.ts +++ b/integration/navigation-state-test.ts @@ -63,7 +63,7 @@ test.describe("navigation states", () => { ); } `, - "app/routes/index.jsx": js` + "app/routes/_index.jsx": js` import { Form, Link, useFetcher } from "@remix-run/react"; export function loader() { return null; } export default function() { diff --git a/integration/path-mapping-test.ts b/integration/path-mapping-test.ts index 6e8ea1ffb21..43cfcf0afa8 100644 --- a/integration/path-mapping-test.ts +++ b/integration/path-mapping-test.ts @@ -7,9 +7,6 @@ let fixture: Fixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/components/my-lib/index.ts": js` export const pizza = "this is a pizza"; diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts index 2d689632e08..c5e05226e29 100644 --- a/integration/postcss-test.ts +++ b/integration/postcss-test.ts @@ -33,11 +33,6 @@ test.describe("PostCSS enabled", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { // We provide a test plugin that replaces the strings // "TEST_PADDING_VALUE" and "TEST_POSTCSS_CONTEXT". diff --git a/integration/prefetch-test.ts b/integration/prefetch-test.ts index a70c714e10c..0272552f7bd 100644 --- a/integration/prefetch-test.ts +++ b/integration/prefetch-test.ts @@ -12,9 +12,6 @@ import { PlaywrightFixture } from "./helpers/playwright-fixture"; // Generate the test app using the given prefetch mode function fixtureFactory(mode: RemixLinkProps["prefetch"]): FixtureInit { return { - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { @@ -278,9 +275,6 @@ test.describe("prefetch=viewport", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; @@ -355,9 +349,6 @@ test.describe("other scenarios", () => { page, }) => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Scripts, useFetcher } from "@remix-run/react"; diff --git a/integration/redirects-test.ts b/integration/redirects-test.ts index 661f118f9fe..114708f183f 100644 --- a/integration/redirects-test.ts +++ b/integration/redirects-test.ts @@ -10,9 +10,6 @@ test.describe("redirects", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/action.jsx": js` import { Outlet, useLoaderData } from "@remix-run/react"; diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts index 44a357a84f3..d7abc2dc5bf 100644 --- a/integration/rendering-test.ts +++ b/integration/rendering-test.ts @@ -10,9 +10,6 @@ test.describe("rendering", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/request-test.ts b/integration/request-test.ts index 9458f456263..37bfc4f2721 100644 --- a/integration/request-test.ts +++ b/integration/request-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { json } from "@remix-run/node"; diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts index 1282200a86a..6b724074863 100644 --- a/integration/resource-routes-test.ts +++ b/integration/resource-routes-test.ts @@ -16,9 +16,6 @@ test.describe("loader in an app", async () => { _consoleError = console.error; console.error = () => {}; fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { Form, Link } from "@remix-run/react"; @@ -240,11 +237,6 @@ test.describe("Development server", async () => { fixture = await createFixture( { - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; diff --git a/integration/revalidate-test.ts b/integration/revalidate-test.ts index 492245de0f8..58690e753f4 100644 --- a/integration/revalidate-test.ts +++ b/integration/revalidate-test.ts @@ -10,11 +10,6 @@ test.describe("Revalidation", () => { test.beforeAll(async () => { appFixture = await createAppFixture( await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Link, Outlet, Scripts, useNavigation } from "@remix-run/react"; diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts index f136d223cb0..38d16e9484e 100644 --- a/integration/route-collisions-test.ts +++ b/integration/route-collisions-test.ts @@ -32,157 +32,7 @@ let LEAF_FILE_CONTENTS = js` } `; -test.describe("build failures (v1 routes)", () => { - let errorLogs: string[]; - let oldConsoleError: typeof console.error; - - test.beforeEach(() => { - errorLogs = []; - oldConsoleError = console.error; - console.error = (str) => errorLogs.push(str); - }); - - test.afterEach(() => { - console.error = oldConsoleError; - }); - - test("detects path collisions inside pathless layout routes", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/foo"' - ); - expect(errorLogs.length).toBe(1); - } - }); - - test("detects path collisions across pathless layout routes", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/__pathless2.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless2/foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/__pathless/foo" conflicts with route "routes/__pathless2/foo"' - ); - expect(errorLogs.length).toBe(1); - } - }); - - test("detects path collisions inside multiple pathless layout routes", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/__again.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/__again/foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/__again/foo"' - ); - expect(errorLogs.length).toBe(1); - } - }); - - test("detects path collisions of index files inside pathless layouts", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/index.jsx": LEAF_FILE_CONTENTS, - "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/index.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "/" defined by route "routes/index" conflicts with route "routes/__pathless/index"' - ); - expect(errorLogs.length).toBe(1); - } - }); - - test("detects path collisions of index files across multiple pathless layouts", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/nested/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/nested/__pathless/index.jsx": LEAF_FILE_CONTENTS, - "app/routes/nested/__oops.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/nested/__oops/index.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "nested" defined by route "routes/nested/__oops/index" conflicts with route "routes/nested/__pathless/index"' - ); - expect(errorLogs.length).toBe(1); - } - }); - - test("detects path collisions of param routes inside pathless layouts", async () => { - try { - await createFixture({ - config: { - future: { v2_routeConvention: false }, - }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/$param.jsx": LEAF_FILE_CONTENTS, - "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/__pathless/$param.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path ":param" defined by route "routes/$param" conflicts with route "routes/__pathless/$param"' - ); - expect(errorLogs.length).toBe(1); - } - }); -}); - -test.describe("build failures (v2 routes)", () => { +test.describe("build failures", () => { let originalConsoleLog = console.log; let originalConsoleWarn = console.warn; let originalConsoleError = console.error; @@ -204,9 +54,6 @@ test.describe("build failures (v2 routes)", () => { let buildOutput: string; await createFixture({ buildStdio, - config: { - future: { v2_routeConvention: true }, - }, files, }); let chunks: Buffer[] = []; diff --git a/integration/scroll-test.ts b/integration/scroll-test.ts index 6f1428a1f1b..196100dbb8b 100644 --- a/integration/scroll-test.ts +++ b/integration/scroll-test.ts @@ -9,9 +9,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/_index.jsx": js` import { redirect } from "@remix-run/node"; diff --git a/integration/server-code-in-browser-message-test.ts b/integration/server-code-in-browser-message-test.ts index f1221620e02..fafc6bfcb4e 100644 --- a/integration/server-code-in-browser-message-test.ts +++ b/integration/server-code-in-browser-message-test.ts @@ -14,9 +14,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "node_modules/has-side-effects/package.json": json({ name: "has-side-effects", diff --git a/integration/server-entry-test.ts b/integration/server-entry-test.ts index d5eee980441..b252c57fb3a 100644 --- a/integration/server-entry-test.ts +++ b/integration/server-entry-test.ts @@ -12,9 +12,6 @@ test.describe("Custom Server Entry", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/entry.server.jsx": js` export default function handleRequest() { @@ -51,7 +48,7 @@ test.describe("Default Server Entry", () => { test.beforeAll(async () => { fixture = await createFixture({ files: { - "app/routes/index.jsx": js` + "app/routes/_index.jsx": js` export default function () { return

Hello World

} @@ -72,7 +69,7 @@ test.describe("Default Server Entry (React 17)", () => { test.beforeAll(async () => { fixture = await createFixture({ files: { - "app/routes/index.jsx": js` + "app/routes/_index.jsx": js` export default function () { return

Hello World

} diff --git a/integration/server-source-maps-test.ts b/integration/server-source-maps-test.ts index f0833d7c18d..d4a5f3d72e5 100644 --- a/integration/server-source-maps-test.ts +++ b/integration/server-source-maps-test.ts @@ -9,9 +9,6 @@ let fixture: Fixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, sourcemap: true, files: { "app/routes/_index.jsx": js` diff --git a/integration/set-cookie-revalidation-test.ts b/integration/set-cookie-revalidation-test.ts index 8a87d0edee7..bda7f490a3e 100644 --- a/integration/set-cookie-revalidation-test.ts +++ b/integration/set-cookie-revalidation-test.ts @@ -11,9 +11,6 @@ let BANNER_MESSAGE = "you do not have permission to view /protected"; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/session.server.js": js` import { createCookieSessionStorage } from "@remix-run/node"; diff --git a/integration/shared-route-imports-test.ts b/integration/shared-route-imports-test.ts index 8cd54fdc57f..47f78d44666 100644 --- a/integration/shared-route-imports-test.ts +++ b/integration/shared-route-imports-test.ts @@ -10,9 +10,6 @@ let appFixture: AppFixture; test.describe("v1 compiler", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/parent.jsx": js` import { createContext, useContext } from "react"; @@ -101,7 +98,9 @@ test.describe("v2 compiler", () => { test.beforeAll(async () => { fixture = await createFixture({ config: { - future: { v2_routeConvention: true, v2_dev: true }, + future: { + v2_dev: true, + }, }, files: { "app/routes/parent.jsx": js` diff --git a/integration/splat-routes-test.ts b/integration/splat-routes-test.ts index 17e37e5afa0..ae54f277443 100644 --- a/integration/splat-routes-test.ts +++ b/integration/splat-routes-test.ts @@ -16,9 +16,6 @@ test.describe("rendering", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts index d1673972089..59394c73b0e 100644 --- a/integration/tailwind-test.ts +++ b/integration/tailwind-test.ts @@ -43,11 +43,6 @@ function runTests(ext: typeof extensions[number]) { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { [tailwindConfigName]: tailwindConfig, diff --git a/integration/transition-test.ts b/integration/transition-test.ts index a1a99f4f33c..bc3844a93cc 100644 --- a/integration/transition-test.ts +++ b/integration/transition-test.ts @@ -19,9 +19,6 @@ test.describe("rendering", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; diff --git a/integration/upload-test.ts b/integration/upload-test.ts index ba08e5f00f4..491f49b8f02 100644 --- a/integration/upload-test.ts +++ b/integration/upload-test.ts @@ -10,9 +10,6 @@ let appFixture: AppFixture; test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, files: { "app/routes/file-upload-handler.jsx": js` import { diff --git a/integration/vanilla-extract-test.ts b/integration/vanilla-extract-test.ts index 47cc7e335c8..2b51370432e 100644 --- a/integration/vanilla-extract-test.ts +++ b/integration/vanilla-extract-test.ts @@ -12,11 +12,6 @@ test.describe("Vanilla Extract", () => { test.beforeAll(async () => { fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, files: { "app/root.jsx": js` import { Links, Outlet } from "@remix-run/react"; diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index da6bbd772be..90e807ad631 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -27,7 +27,6 @@ describe("readConfig", () => { future: { v2_headers: expect.any(Boolean), v2_meta: expect.any(Boolean), - v2_routeConvention: expect.any(Boolean), }, }, ` @@ -45,7 +44,6 @@ describe("readConfig", () => { "v2_dev": false, "v2_headers": Any, "v2_meta": Any, - "v2_routeConvention": Any, }, "mdx": undefined, "postcss": true, diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts deleted file mode 100644 index 93e8f635a8a..00000000000 --- a/packages/remix-dev/__tests__/routesConvention-test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as path from "path"; - -import { - createRoutePath, - defineConventionalRoutes, -} from "../config/routesConvention"; - -describe("createRoutePath", () => { - describe("creates proper route paths", () => { - let tests: [string, string | undefined][] = [ - ["routes/$", "routes/*"], - ["routes/sub/$", "routes/sub/*"], - ["routes.sub/$", "routes/sub/*"], - ["routes/$slug", "routes/:slug"], - ["routes/sub/$slug", "routes/sub/:slug"], - ["routes.sub/$slug", "routes/sub/:slug"], - ["$", "*"], - ["nested/$", "nested/*"], - ["flat.$", "flat/*"], - ["$slug", ":slug"], - ["nested/$slug", "nested/:slug"], - ["flat.$slug", "flat/:slug"], - ["flat.sub", "flat/sub"], - ["nested/index", "nested"], - ["flat.index", "flat"], - ["index", undefined], - ["__layout/index", undefined], - ["__layout/test", "test"], - ["__layout.test", "test"], - ["__layout/$slug", ":slug"], - ["nested/__layout/$slug", "nested/:slug"], - ["$slug[.]json", ":slug.json"], - ["sub/[sitemap.xml]", "sub/sitemap.xml"], - ["posts/$slug/[image.jpg]", "posts/:slug/image.jpg"], - ["$[$dollabills].[.]lol[/]what/[$].$", ":$dollabills/.lol/what/$/*"], - ["sub.[[]", "sub/["], - ["sub.]", "sub/]"], - ["sub.[[]]", "sub/[]"], - ["sub.[[]", "sub/["], - ["beef]", "beef]"], - ["[index]", "index"], - ["test/inde[x]", "test/index"], - ["[i]ndex/[[].[[]]", "index/[/[]"], - - // Optional segment routes - ["(routes)/$", "routes?/*"], - ["(routes)/(sub)/$", "routes?/sub?/*"], - ["(routes).(sub)/$", "routes?/sub?/*"], - ["(routes)/($slug)", "routes?/:slug?"], - ["(routes)/sub/($slug)", "routes?/sub/:slug?"], - ["(routes).sub/($slug)", "routes?/sub/:slug?"], - ["(nested)/$", "nested?/*"], - ["(flat).$", "flat?/*"], - ["($slug)", ":slug?"], - ["(nested)/($slug)", "nested?/:slug?"], - ["(flat).($slug)", "flat?/:slug?"], - ["flat.(sub)", "flat/sub?"], - ["__layout/(test)", "test?"], - ["__layout.(test)", "test?"], - ["__layout/($slug)", ":slug?"], - ["(nested)/__layout/($slug)", "nested?/:slug?"], - ["($slug[.]json)", ":slug.json?"], - ["(sub)/([sitemap.xml])", "sub?/sitemap.xml?"], - ["(sub)/[(sitemap.xml)]", "sub?/(sitemap.xml)"], - ["(posts)/($slug)/([image.jpg])", "posts?/:slug?/image.jpg?"], - [ - "($[$dollabills]).([.]lol)[/](what)/([$]).$", - ":$dollabills?/.lol)/(what?/$?/*", - ], - [ - "($[$dollabills]).([.]lol)/(what)/([$]).($up)", - ":$dollabills?/.lol?/what?/$?/:up?", - ], - ["(sub).([[])", "sub?/[?"], - ["(sub).(])", "sub?/]?"], - ["(sub).([[]])", "sub?/[]?"], - ["(sub).([[])", "sub?/[?"], - ["(beef])", "beef]?"], - ["([index])", "index?"], - ["(test)/(inde[x])", "test?/index?"], - ["([i]ndex)/([[]).([[]])", "index?/[?/[]?"], - ]; - - for (let [input, expected] of tests) { - it(`"${input}" -> "${expected}"`, () => { - expect(createRoutePath(input)).toBe(expected); - }); - } - - describe("optional segments", () => { - it("will only work when starting and ending a segment with parenthesis", () => { - let [input, expected] = ["(routes.sub)/$", "(routes/sub)/*"]; - expect(createRoutePath(input)).toBe(expected); - }); - - it("throws error on optional to splat routes", () => { - expect(() => createRoutePath("(routes)/($)")).toThrow("Splat"); - expect(() => createRoutePath("($)")).toThrow("Splat"); - }); - - it("throws errors on optional index without brackets routes", () => { - expect(() => createRoutePath("(nested)/(index)")).toThrow("index"); - expect(() => createRoutePath("(flat).(index)")).toThrow("index"); - expect(() => createRoutePath("(index)")).toThrow("index"); - }); - }); - }); -}); - -describe("defineConventionalRoutes", () => { - it("creates a route manifest from the routes directory", () => { - let routes = defineConventionalRoutes( - path.join(__dirname, "fixtures/indie-stack/app") - ); - let keys = Object.keys(routes); - expect(keys).toHaveLength(1); - expect(keys.filter((key) => routes[key].parentId).length).toBe(1); - expect(keys.filter((key) => routes[key].index).length).toBe(1); - }); -}); diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 609ce31691f..9507412e3be 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -9,7 +9,6 @@ import type { NodePolyfillsOptions as EsbuildPluginsNodeModulesPolyfillOptions } import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; -import { defineConventionalRoutes } from "./config/routesConvention"; import { ServerMode, isValidServerMode } from "./config/serverModes"; import { serverBuildVirtualModule } from "./compiler/server/virtualModules"; import { flatRoutes } from "./config/flat-routes"; @@ -40,7 +39,6 @@ interface FutureConfig { v2_dev: boolean | Dev; v2_headers: boolean; v2_meta: boolean; - v2_routeConvention: boolean; } type ServerNodeBuiltinsPolyfillOptions = Pick< @@ -600,21 +598,9 @@ export async function readConfig( root: { path: "", id: "root", file: rootRouteFile }, }; - let routesConvention: typeof flatRoutes; - - if (appConfig.future?.v2_routeConvention) { - routesConvention = flatRoutes; - } else { - flatRoutesWarning(); - routesConvention = defineConventionalRoutes; - } - if (fse.existsSync(path.resolve(appDirectory, "routes"))) { - let conventionalRoutes = routesConvention( - appDirectory, - appConfig.ignoredRouteFiles - ); - for (let route of Object.values(conventionalRoutes)) { + let fileRoutes = flatRoutes(appDirectory, appConfig.ignoredRouteFiles); + for (let route of Object.values(fileRoutes)) { routes[route.id] = { ...route, parentId: route.parentId || "root" }; } } @@ -655,7 +641,6 @@ export async function readConfig( v2_dev: appConfig.future?.v2_dev ?? false, v2_headers: appConfig.future?.v2_headers === true, v2_meta: appConfig.future?.v2_meta === true, - v2_routeConvention: appConfig.future?.v2_routeConvention === true, }; return { @@ -782,12 +767,6 @@ let futureFlagWarning = }); }; -let flatRoutesWarning = futureFlagWarning({ - message: "The route file convention is changing in v2", - flag: "v2_routeConvention", - link: "https://remix.run/docs/en/v1.15.0/pages/v2#file-system-route-convention", -}); - let metaWarning = futureFlagWarning({ message: "The route `meta` API is changing in v2", flag: "v2_meta", diff --git a/packages/remix-dev/config/flat-routes.ts b/packages/remix-dev/config/flat-routes.ts index 6f1d8ad289e..c46d4a8dfed 100644 --- a/packages/remix-dev/config/flat-routes.ts +++ b/packages/remix-dev/config/flat-routes.ts @@ -5,15 +5,15 @@ import { makeRe } from "minimatch"; import type { ConfigRoute, RouteManifest } from "./routes"; import { normalizeSlashes } from "./routes"; import { findConfig } from "../config"; -import { - escapeEnd, - escapeStart, - isSegmentSeparator, - optionalEnd, - optionalStart, - paramPrefixChar, - routeModuleExts, -} from "./routesConvention"; + +export const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]; + +export let paramPrefixChar = "$" as const; +export let escapeStart = "[" as const; +export let escapeEnd = "]" as const; + +export let optionalStart = "(" as const; +export let optionalEnd = ")" as const; const PrefixLookupTrieEndSymbol = Symbol("PrefixLookupTrieEndSymbol"); type PrefixLookupNode = { @@ -543,3 +543,8 @@ export function getRouteIdConflictErrorMessage( "\n" ); } + +export function isSegmentSeparator(checkChar: string | undefined) { + if (!checkChar) return false; + return ["/", ".", path.win32.sep].includes(checkChar); +} diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts deleted file mode 100644 index 35713e756bc..00000000000 --- a/packages/remix-dev/config/routesConvention.ts +++ /dev/null @@ -1,343 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { Minimatch } from "minimatch"; - -import type { RouteManifest, DefineRouteFunction } from "./routes"; -import { defineRoutes, createRouteId } from "./routes"; - -export const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]; - -export function isRouteModuleFile(filename: string): boolean { - return routeModuleExts.includes(path.extname(filename)); -} - -/** - * Defines routes using the filesystem convention in `app/routes`. The rules are: - * - * - Route paths are derived from the file path. A `.` in the filename indicates - * a `/` in the URL (a "nested" URL, but no route nesting). A `$` in the - * filename indicates a dynamic URL segment. - * - Subdirectories are used for nested routes. - * - * For example, a file named `app/routes/gists/$username.tsx` creates a route - * with a path of `gists/:username`. - */ -export function defineConventionalRoutes( - appDir: string, - ignoredFilePatterns?: string[] -): RouteManifest { - let files: { [routeId: string]: string } = {}; - - // First, find all route modules in app/routes - visitFiles(path.join(appDir, "routes"), (file) => { - if ( - ignoredFilePatterns && - ignoredFilePatterns.some((pattern) => { - let minimatch = new Minimatch(pattern); - return minimatch.match(file); - }) - ) { - return; - } - - if (isRouteModuleFile(file)) { - let routeId = createRouteId(path.join("routes", file)); - files[routeId] = path.join("routes", file); - return; - } - - throw new Error( - `Invalid route module file: ${path.join(appDir, "routes", file)}` - ); - }); - - let routeIds = Object.keys(files).sort(byLongestFirst); - let parentRouteIds = getParentRouteIds(routeIds); - - let uniqueRoutes = new Map(); - - // Then, recurse through all routes using the public defineRoutes() API - function defineNestedRoutes( - defineRoute: DefineRouteFunction, - parentId?: string - ): void { - let childRouteIds = routeIds.filter( - (id) => parentRouteIds[id] === parentId - ); - - for (let routeId of childRouteIds) { - let routePath: string | undefined = createRoutePath( - routeId.slice((parentId || "routes").length + 1) - ); - - let isIndexRoute = routeId.endsWith("/index"); - let fullPath = createRoutePath(routeId.slice("routes".length + 1)); - let uniqueRouteId = (fullPath || "") + (isIndexRoute ? "?index" : ""); - let isPathlessLayoutRoute = - routeId.split("/").pop()?.startsWith("__") === true; - - /** - * We do not try to detect path collisions for pathless layout route - * files because, by definition, they create the potential for route - * collisions _at that level in the tree_. - * - * Consider example where a user may want multiple pathless layout routes - * for different subfolders - * - * routes/ - * account.tsx - * account/ - * __public/ - * login.tsx - * perks.tsx - * __private/ - * orders.tsx - * profile.tsx - * __public.tsx - * __private.tsx - * - * In order to support both a public and private layout for `/account/*` - * URLs, we are creating a mutually exclusive set of URLs beneath 2 - * separate pathless layout routes. In this case, the route paths for - * both account/__public.tsx and account/__private.tsx is the same - * (/account), but we're again not expecting to match at that level. - * - * By only ignoring this check when the final portion of the filename is - * pathless, we will still detect path collisions such as: - * - * routes/parent/__pathless/foo.tsx - * routes/parent/__pathless2/foo.tsx - * - * and - * - * routes/parent/__pathless/index.tsx - * routes/parent/__pathless2/index.tsx - */ - if (uniqueRouteId && !isPathlessLayoutRoute) { - if (uniqueRoutes.has(uniqueRouteId)) { - throw new Error( - `Path ${JSON.stringify( - fullPath || "/" - )} defined by route ${JSON.stringify( - routeId - )} conflicts with route ${JSON.stringify( - uniqueRoutes.get(uniqueRouteId) - )}` - ); - } else { - uniqueRoutes.set(uniqueRouteId, routeId); - } - } - - if (isIndexRoute) { - let invalidChildRoutes = routeIds.filter( - (id) => parentRouteIds[id] === routeId - ); - - if (invalidChildRoutes.length > 0) { - throw new Error( - `Child routes are not allowed in index routes. Please remove child routes of ${routeId}` - ); - } - - defineRoute(routePath, files[routeId], { - index: true, - }); - } else { - defineRoute(routePath, files[routeId], () => { - defineNestedRoutes(defineRoute, routeId); - }); - } - } - } - - return defineRoutes(defineNestedRoutes); -} - -export let paramPrefixChar = "$" as const; -export let escapeStart = "[" as const; -export let escapeEnd = "]" as const; - -export let optionalStart = "(" as const; -export let optionalEnd = ")" as const; - -// TODO: Cleanup and write some tests for this function -export function createRoutePath(partialRouteId: string): string | undefined { - let result = ""; - let rawSegmentBuffer = ""; - - let inEscapeSequence = 0; - let inOptionalSegment = 0; - let optionalSegmentIndex = null; - let skipSegment = false; - for (let i = 0; i < partialRouteId.length; i++) { - let char = partialRouteId.charAt(i); - let prevChar = i > 0 ? partialRouteId.charAt(i - 1) : undefined; - let nextChar = - i < partialRouteId.length - 1 ? partialRouteId.charAt(i + 1) : undefined; - - function isNewEscapeSequence() { - return ( - !inEscapeSequence && char === escapeStart && prevChar !== escapeStart - ); - } - - function isCloseEscapeSequence() { - return inEscapeSequence && char === escapeEnd && nextChar !== escapeEnd; - } - - function isStartOfLayoutSegment() { - return char === "_" && nextChar === "_" && !rawSegmentBuffer; - } - - function isNewOptionalSegment() { - return ( - char === optionalStart && - prevChar !== optionalStart && - (isSegmentSeparator(prevChar) || prevChar === undefined) && - !inOptionalSegment && - !inEscapeSequence - ); - } - - function isCloseOptionalSegment() { - return ( - char === optionalEnd && - nextChar !== optionalEnd && - (isSegmentSeparator(nextChar) || nextChar === undefined) && - inOptionalSegment && - !inEscapeSequence - ); - } - - if (skipSegment) { - if (isSegmentSeparator(char)) { - skipSegment = false; - } - continue; - } - - if (isNewEscapeSequence()) { - inEscapeSequence++; - continue; - } - - if (isCloseEscapeSequence()) { - inEscapeSequence--; - continue; - } - - if (isNewOptionalSegment()) { - inOptionalSegment++; - optionalSegmentIndex = result.length; - result += optionalStart; - continue; - } - - if (isCloseOptionalSegment()) { - if (optionalSegmentIndex !== null) { - result = - result.slice(0, optionalSegmentIndex) + - result.slice(optionalSegmentIndex + 1); - } - optionalSegmentIndex = null; - inOptionalSegment--; - result += "?"; - continue; - } - - if (inEscapeSequence) { - result += char; - continue; - } - - if (isSegmentSeparator(char)) { - if (rawSegmentBuffer === "index" && result.endsWith("index")) { - result = result.replace(/\/?index$/, ""); - } else { - result += "/"; - } - - rawSegmentBuffer = ""; - inOptionalSegment = 0; - optionalSegmentIndex = null; - continue; - } - - if (isStartOfLayoutSegment()) { - skipSegment = true; - continue; - } - - rawSegmentBuffer += char; - - if (char === paramPrefixChar) { - if (nextChar === optionalEnd) { - throw new Error( - `Invalid route path: ${partialRouteId}. Splat route $ is already optional` - ); - } - result += typeof nextChar === "undefined" ? "*" : ":"; - continue; - } - - result += char; - } - - if (rawSegmentBuffer === "index" && result.endsWith("index")) { - result = result.replace(/\/?index$/, ""); - } else { - result = result.replace(/\/$/, ""); - } - - if (rawSegmentBuffer === "index" && result.endsWith("index?")) { - throw new Error( - `Invalid route path: ${partialRouteId}. Make index route optional by using (index)` - ); - } - - return result || undefined; -} - -export function isSegmentSeparator(checkChar: string | undefined) { - if (!checkChar) return false; - return ["/", ".", path.win32.sep].includes(checkChar); -} - -function getParentRouteIds( - routeIds: string[] -): Record { - return routeIds.reduce>( - (parentRouteIds, childRouteId) => ({ - ...parentRouteIds, - [childRouteId]: routeIds.find((id) => childRouteId.startsWith(`${id}/`)), - }), - {} - ); -} - -function byLongestFirst(a: string, b: string): number { - return b.length - a.length; -} - -function visitFiles( - dir: string, - visitor: (file: string) => void, - baseDir = dir -): void { - for (let filename of fs.readdirSync(dir)) { - let file = path.resolve(dir, filename); - let stat = fs.lstatSync(file); - - if (stat.isDirectory()) { - visitFiles(file, visitor, baseDir); - } else if (stat.isFile()) { - visitor(path.relative(baseDir, file)); - } - } -} - -/* -eslint - no-loop-func: "off", -*/ diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index d1513a7f6b3..db1e7fbb0a5 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -36,7 +36,6 @@ export interface FutureConfig { v2_dev: boolean | Dev; v2_headers: boolean; v2_meta: boolean; - v2_routeConvention: boolean; } export interface AssetsManifest { diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index 39b06976e4b..11ec90c3b97 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -417,14 +417,14 @@ describe("shared server runtime", () => { root: { default: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, }, }); let handler = createRequestHandler(build, ServerMode.Test); - let request = new Request(`${baseUrl}/?_data=routes/index`, { + let request = new Request(`${baseUrl}/?_data=routes/_index`, { method: "get", }); @@ -439,7 +439,7 @@ describe("shared server runtime", () => { root: { default: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, loader: () => null, @@ -469,7 +469,7 @@ describe("shared server runtime", () => { root: { default: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, loader: () => null, @@ -499,7 +499,7 @@ describe("shared server runtime", () => { default: {}, loader: rootLoader, }, - "routes/index": { + "routes/_index": { parentId: "root", loader: indexLoader, index: true, @@ -507,7 +507,7 @@ describe("shared server runtime", () => { }); let handler = createRequestHandler(build, ServerMode.Test); - let request = new Request(`${baseUrl}/?_data=routes/index`, { + let request = new Request(`${baseUrl}/?_data=routes/_index`, { method: "get", }); @@ -760,7 +760,7 @@ describe("shared server runtime", () => { loader: rootLoader, action: rootAction, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, }, @@ -790,7 +790,7 @@ describe("shared server runtime", () => { default: {}, loader: rootLoader, }, - "routes/index": { + "routes/_index": { parentId: "root", action: indexAction, index: true, @@ -798,7 +798,7 @@ describe("shared server runtime", () => { }); let handler = createRequestHandler(build, ServerMode.Test); - let request = new Request(`${baseUrl}/?index&_data=routes/index`, { + let request = new Request(`${baseUrl}/?index&_data=routes/_index`, { method: "post", }); @@ -880,7 +880,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -920,7 +920,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -942,7 +942,7 @@ describe("shared server runtime", () => { expect(calls.length).toBe(1); let context = calls[0][3].staticHandlerContext as StaticHandlerContext; expect(context.errors).toBeTruthy(); - expect(context.errors!["routes/index"].status).toBe(400); + expect(context.errors!["routes/_index"].status).toBe(400); expect(context.loaderData).toEqual({ root: "root", }); @@ -1011,7 +1011,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1038,7 +1038,7 @@ describe("shared server runtime", () => { expect(context.errors!.root.status).toBe(400); expect(context.loaderData).toEqual({ root: null, - "routes/index": null, + "routes/_index": null, }); }); @@ -1105,7 +1105,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1129,10 +1129,10 @@ describe("shared server runtime", () => { expect(calls.length).toBe(1); let context = calls[0][3].staticHandlerContext as StaticHandlerContext; expect(context.errors).toBeTruthy(); - expect(context.errors!["routes/index"].status).toBe(400); + expect(context.errors!["routes/_index"].status).toBe(400); expect(context.loaderData).toEqual({ root: "root", - "routes/index": null, + "routes/_index": null, }); }); @@ -1261,7 +1261,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1303,7 +1303,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1325,11 +1325,11 @@ describe("shared server runtime", () => { expect(calls.length).toBe(1); let context = calls[0][3].staticHandlerContext as StaticHandlerContext; expect(context.errors).toBeTruthy(); - expect(context.errors!["routes/index"]).toBeInstanceOf(Error); - expect(context.errors!["routes/index"].message).toBe( + expect(context.errors!["routes/_index"]).toBeInstanceOf(Error); + expect(context.errors!["routes/_index"].message).toBe( "Unexpected Server Error" ); - expect(context.errors!["routes/index"].stack).toBeUndefined(); + expect(context.errors!["routes/_index"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ root: "root", }); @@ -1400,7 +1400,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1429,7 +1429,7 @@ describe("shared server runtime", () => { expect(context.errors!.root.stack).toBeUndefined(); expect(context.loaderData).toEqual({ root: null, - "routes/index": null, + "routes/_index": null, }); }); @@ -1500,7 +1500,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", index: true, default: {}, @@ -1524,14 +1524,14 @@ describe("shared server runtime", () => { expect(calls.length).toBe(1); let context = calls[0][3].staticHandlerContext as StaticHandlerContext; expect(context.errors).toBeTruthy(); - expect(context.errors!["routes/index"]).toBeInstanceOf(Error); - expect(context.errors!["routes/index"].message).toBe( + expect(context.errors!["routes/_index"]).toBeInstanceOf(Error); + expect(context.errors!["routes/_index"].message).toBe( "Unexpected Server Error" ); - expect(context.errors!["routes/index"].stack).toBeUndefined(); + expect(context.errors!["routes/_index"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ root: "root", - "routes/index": null, + "routes/_index": null, }); }); @@ -1668,7 +1668,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", default: {}, loader: indexLoader, @@ -1713,7 +1713,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", default: {}, loader: indexLoader, @@ -1753,7 +1753,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", default: {}, loader: indexLoader, @@ -1805,7 +1805,7 @@ describe("shared server runtime", () => { loader: rootLoader, ErrorBoundary: {}, }, - "routes/index": { + "routes/_index": { parentId: "root", default: {}, loader: indexLoader, diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index eba9031e093..394ef84079d 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -24,7 +24,6 @@ export interface FutureConfig { v2_dev: boolean | Dev; v2_headers: boolean; v2_meta: boolean; - v2_routeConvention: boolean; } export interface AssetsManifest { diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx index 27cbd7fa6a8..9573ddd902a 100644 --- a/packages/remix-testing/create-remix-stub.tsx +++ b/packages/remix-testing/create-remix-stub.tsx @@ -127,7 +127,6 @@ export function createRemixStub( v2_dev: false, v2_headers: false, v2_meta: false, - v2_routeConvention: false, ...remixConfigFuture, }, manifest: createManifest(routerRef.current.routes), diff --git a/templates/arc/remix.config.js b/templates/arc/remix.config.js index 310cfa560fd..62d5ba14e15 100644 --- a/templates/arc/remix.config.js +++ b/templates/arc/remix.config.js @@ -12,6 +12,5 @@ export default { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/cloudflare-pages/remix.config.js b/templates/cloudflare-pages/remix.config.js index c5005a25e29..ac60a1e2751 100644 --- a/templates/cloudflare-pages/remix.config.js +++ b/templates/cloudflare-pages/remix.config.js @@ -18,6 +18,5 @@ export default { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/cloudflare-workers/remix.config.js b/templates/cloudflare-workers/remix.config.js index b5866e0a1b4..216ed73d0a9 100644 --- a/templates/cloudflare-workers/remix.config.js +++ b/templates/cloudflare-workers/remix.config.js @@ -20,6 +20,5 @@ export default { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/deno/remix.config.js b/templates/deno/remix.config.js index deee56e1864..5730c0ad8a8 100644 --- a/templates/deno/remix.config.js +++ b/templates/deno/remix.config.js @@ -21,6 +21,5 @@ module.exports = { future: { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/express/remix.config.js b/templates/express/remix.config.js index ccaabb773a4..3238eac42d5 100644 --- a/templates/express/remix.config.js +++ b/templates/express/remix.config.js @@ -11,6 +11,5 @@ export default { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/fly/remix.config.js b/templates/fly/remix.config.js index 0bd3fd95c8a..d32a1292132 100644 --- a/templates/fly/remix.config.js +++ b/templates/fly/remix.config.js @@ -11,6 +11,5 @@ module.exports = { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/netlify/remix.config.js b/templates/netlify/remix.config.js index abf7871a9fc..5cb9a5227fb 100644 --- a/templates/netlify/remix.config.js +++ b/templates/netlify/remix.config.js @@ -15,6 +15,5 @@ module.exports = { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/remix-javascript/remix.config.js b/templates/remix-javascript/remix.config.js index 7a9bff97ff4..e2f9e20acb8 100644 --- a/templates/remix-javascript/remix.config.js +++ b/templates/remix-javascript/remix.config.js @@ -8,6 +8,5 @@ module.exports = { serverModuleFormat: "cjs", future: { v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/remix/remix.config.js b/templates/remix/remix.config.js index 0bd3fd95c8a..d32a1292132 100644 --- a/templates/remix/remix.config.js +++ b/templates/remix/remix.config.js @@ -11,6 +11,5 @@ module.exports = { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, }; diff --git a/templates/vercel/remix.config.js b/templates/vercel/remix.config.js index 564f8cee51d..b6aa5ad2398 100644 --- a/templates/vercel/remix.config.js +++ b/templates/vercel/remix.config.js @@ -15,6 +15,5 @@ module.exports = { v2_headers: true, v2_meta: true, - v2_routeConvention: true, }, };