diff --git a/packages/knip/fixtures/plugins/react-router/app/entry.client.tsx b/packages/knip/fixtures/plugins/react-router/app/entry.client.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/app/entry.server.tsx b/packages/knip/fixtures/plugins/react-router/app/entry.server.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/app/root.tsx b/packages/knip/fixtures/plugins/react-router/app/root.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/app/routes.ts b/packages/knip/fixtures/plugins/react-router/app/routes.ts new file mode 100644 index 000000000..37fc8cfdb --- /dev/null +++ b/packages/knip/fixtures/plugins/react-router/app/routes.ts @@ -0,0 +1,7 @@ +export default [ + { file: "routes/home.tsx", index: true }, + { + file: "routes/layout.tsx", + children: [{ file: "./routes/another-route.tsx" }], + }, +]; diff --git a/packages/knip/fixtures/plugins/react-router/app/routes/another-route.tsx b/packages/knip/fixtures/plugins/react-router/app/routes/another-route.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/app/routes/home.tsx b/packages/knip/fixtures/plugins/react-router/app/routes/home.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/app/routes/layout.tsx b/packages/knip/fixtures/plugins/react-router/app/routes/layout.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/react-router/package.json b/packages/knip/fixtures/plugins/react-router/package.json new file mode 100644 index 000000000..e841463d6 --- /dev/null +++ b/packages/knip/fixtures/plugins/react-router/package.json @@ -0,0 +1,7 @@ +{ + "name": "@fixtures/react-router", + "version": "*", + "devDependencies": { + "@react-router/dev": "*" + } +} \ No newline at end of file diff --git a/packages/knip/fixtures/plugins/react-router/react-router.config.ts b/packages/knip/fixtures/plugins/react-router/react-router.config.ts new file mode 100644 index 000000000..6ff16f917 --- /dev/null +++ b/packages/knip/fixtures/plugins/react-router/react-router.config.ts @@ -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; diff --git a/packages/knip/schema.json b/packages/knip/schema.json index 7803e67ef..a4944a210 100644 --- a/packages/knip/schema.json +++ b/packages/knip/schema.json @@ -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" diff --git a/packages/knip/src/plugins/index.ts b/packages/knip/src/plugins/index.ts index 566702c9e..f55f79e1b 100644 --- a/packages/knip/src/plugins/index.ts +++ b/packages/knip/src/plugins/index.ts @@ -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'; @@ -147,6 +148,7 @@ export const Plugins = { preconstruct, prettier, 'react-cosmos': reactCosmos, + 'react-router': reactRouter, 'release-it': releaseIt, remark, remix, diff --git a/packages/knip/src/plugins/react-router/index.ts b/packages/knip/src/plugins/react-router/index.ts new file mode 100644 index 000000000..61dda5526 --- /dev/null +++ b/packages/knip/src/plugins/react-router/index.ts @@ -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]; + +const entry: string[] = []; + +const production: string[] = []; + +const resolveEntryPaths: ResolveEntryPaths = 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); + } + + 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; diff --git a/packages/knip/src/plugins/react-router/types.ts b/packages/knip/src/plugins/react-router/types.ts new file mode 100644 index 000000000..40866b20e --- /dev/null +++ b/packages/knip/src/plugins/react-router/types.ts @@ -0,0 +1,8 @@ +export type PluginConfig = { + appDirectory?: string; +}; + +export interface RouteConfigEntry { + file: string; + children?: RouteConfigEntry[]; +} diff --git a/packages/knip/src/schema/plugins.ts b/packages/knip/src/schema/plugins.ts index 813b71ed8..b77ff9042 100644 --- a/packages/knip/src/schema/plugins.ts +++ b/packages/knip/src/schema/plugins.ts @@ -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, diff --git a/packages/knip/src/types/PluginNames.ts b/packages/knip/src/types/PluginNames.ts index c45cc6c72..cb22d414b 100644 --- a/packages/knip/src/types/PluginNames.ts +++ b/packages/knip/src/types/PluginNames.ts @@ -55,6 +55,7 @@ export type PluginName = | 'preconstruct' | 'prettier' | 'react-cosmos' + | 'react-router' | 'release-it' | 'remark' | 'remix' @@ -148,6 +149,7 @@ export const pluginNames = [ 'preconstruct', 'prettier', 'react-cosmos', + 'react-router', 'release-it', 'remark', 'remix', diff --git a/packages/knip/test/plugins/react-router.test.ts b/packages/knip/test/plugins/react-router.test.ts new file mode 100644 index 000000000..9e7c1ae53 --- /dev/null +++ b/packages/knip/test/plugins/react-router.test.ts @@ -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, + }); +});