Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add plugin for React router 7 framework mode #931

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions packages/knip/fixtures/plugins/react-router/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default [
{ file: "routes/home.tsx", index: true },
{
file: "routes/layout.tsx",
children: [{ file: "./routes/another-route.tsx" }],
},
];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seeing things in the docs like route("/", "./home.tsx") - do we cater for that as well? or how's that returned when loading app/routes.ts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route("/", "./home.tsx") returns a simple object containing { file: "./home.tsx", path: "/" }. Since we are joining the path like this: join(appDir, route.file) and the routes.ts file is defined in appDir this is catered for.

Empty file.
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions packages/knip/fixtures/plugins/react-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixtures/react-router",
"version": "*",
"devDependencies": {
"@react-router/dev": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Config } from "@react-router/dev/config";

export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;
4 changes: 4 additions & 0 deletions packages/knip/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,10 @@
"title": "react-cosmos plugin configuration (https://knip.dev/reference/plugins/react-cosmos)",
"$ref": "#/definitions/plugin"
},
"react-router": {
"title": "react-router plugin configuration (https://knip.dev/reference/plugins/react-router)",
"$ref": "#/definitions/plugin"
},
"release-it": {
"title": "Release It plugin configuration (https://knip.dev/reference/plugins/release-it)",
"$ref": "#/definitions/plugin"
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { default as postcss } from './postcss/index.js';
import { default as preconstruct } from './preconstruct/index.js';
import { default as prettier } from './prettier/index.js';
import { default as reactCosmos } from './react-cosmos/index.js';
import { default as reactRouter } from './react-router/index.js';
import { default as releaseIt } from './release-it/index.js';
import { default as remark } from './remark/index.js';
import { default as remix } from './remix/index.js';
Expand Down Expand Up @@ -147,6 +148,7 @@ export const Plugins = {
preconstruct,
prettier,
'react-cosmos': reactCosmos,
'react-router': reactRouter,
'release-it': releaseIt,
remark,
remix,
Expand Down
65 changes: 65 additions & 0 deletions packages/knip/src/plugins/react-router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { existsSync } from 'node:fs';
import type { IsPluginEnabled, Plugin, ResolveEntryPaths } from '../../types/config.js';
import { toEntry } from '../../util/input.js';
import { join } from '../../util/path.js';
import { hasDependency, load } from '../../util/plugin.js';
import vite from '../vite/index.js';
import type { PluginConfig, RouteConfigEntry } from './types.js';

// https://reactrouter.com/start/framework/routing

const title = 'react-router';

const enablers = ['@react-router/dev'];

const isEnabled: IsPluginEnabled = ({ dependencies }) => hasDependency(dependencies, enablers);

const config: string[] = ['react-router.config.{js,ts}', ...vite.config];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added vite.config aswell because the react-router.config is optional.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All config paths are loaded and if there's resolveConfig its executed with the default exported value of those config files. Also they're automatically added as entry files (so their imports are registered as dependencies like any other source file).

Since this plugin doesn't have resolveConfig I think 'react-router.config.{js,ts}' should be moved to the entry array (guess I've missed this before), and looks like there's no value in adding vite.config.

Just saying, the config array items do not influence whether the plugin is enabled or not.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance you could move config to entry and remove vite.config - I'd guess that gives the same results, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving react-router.config.ts to entry and removing vite doesn't seem to enable the plugin. When console logging inside resolveEntryPaths nothing happends. Not sure whats happening.


const entry: string[] = [];

const production: string[] = [];

const resolveEntryPaths: ResolveEntryPaths<PluginConfig> = async (localConfig, options) => {
const { configFileDir } = options;
const appDirectory = localConfig.appDirectory ?? 'app';
const appDir = join(configFileDir, appDirectory);

// If using flatRoutes from @react-router/fs-routes it will throw an error if this variable is not defined
// @ts-ignore
globalThis.__reactRouterAppDirectory = appDir;

let routeConfig: RouteConfigEntry[] = [];

const routesPathTs = join(appDir, 'routes.ts');
const routesPathJs = join(appDir, 'routes.js');

if (existsSync(routesPathTs)) {
routeConfig = await load(routesPathTs);
} else if (existsSync(routesPathJs)) {
routeConfig = await load(routesPathJs);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe good idea to check if routeConfig is actually set later on?

Copy link
Author

@lasseklovstad lasseklovstad Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initialize routeConfig as an empty array above. So if no routes file is defined it is ignored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha. Good!

}

const mapRoute = (route: RouteConfigEntry): string[] => {
return [join(appDir, route.file), ...(route.children ? route.children.flatMap(mapRoute) : [])];
};

const routes = routeConfig.flatMap(mapRoute);

return [
join(appDir, 'routes.{js,ts}'),
join(appDir, 'root.{jsx,tsx}'),
join(appDir, 'entry.{client,server}.{js,jsx,ts,tsx}'),
...routes,
].map(toEntry);
};

export default {
title,
enablers,
isEnabled,
config,
entry,
production,
resolveEntryPaths,
} satisfies Plugin;
8 changes: 8 additions & 0 deletions packages/knip/src/plugins/react-router/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type PluginConfig = {
appDirectory?: string;
};

export interface RouteConfigEntry {
file: string;
children?: RouteConfigEntry[];
}
1 change: 1 addition & 0 deletions packages/knip/src/schema/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const pluginsSchema = z.object({
preconstruct: pluginSchema,
prettier: pluginSchema,
'react-cosmos': pluginSchema,
'react-router': pluginSchema,
'release-it': pluginSchema,
remark: pluginSchema,
remix: pluginSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/types/PluginNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type PluginName =
| 'preconstruct'
| 'prettier'
| 'react-cosmos'
| 'react-router'
| 'release-it'
| 'remark'
| 'remix'
Expand Down Expand Up @@ -148,6 +149,7 @@ export const pluginNames = [
'preconstruct',
'prettier',
'react-cosmos',
'react-router',
'release-it',
'remark',
'remix',
Expand Down
21 changes: 21 additions & 0 deletions packages/knip/test/plugins/react-router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/react-router');

test('Find dependencies with the react-router plugin', async () => {
const { counters } = await main({
...baseArguments,
cwd,
});

assert.deepEqual(counters, {
...baseCounters,
processed: 8,
total: 8,
});
});