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

Skip typegen for routes outside the app directory #12996

Open
wants to merge 5 commits into
base: dev
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
5 changes: 5 additions & 0 deletions .changeset/funny-paws-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Prevent typegen with route files are outside the app directory
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@
- vonagam
- WalkAlone0325
- whxhlgy
- wilcoxmd
- willemarcel
- williamsdyyz
- willsawyerrrr
Expand Down
45 changes: 45 additions & 0 deletions integration/helpers/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fse from "fs-extra";
import stripIndent from "strip-indent";
import path from "node:path";
import url from "node:url";

const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const root = path.resolve(__dirname, "../..");
const TMP_DIR = path.join(root, ".tmp/integration");

export async function writeTestFiles(
Copy link
Author

Choose a reason for hiding this comment

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

This function, and the logic in createTestDirectory below were pulled from other files and just refactored into this file for reuse.

files: Record<string, string> | undefined,
dir: string
) {
await Promise.all(
Object.keys(files ?? {}).map(async (filename) => {
let filePath = path.join(dir, filename);
await fse.ensureDir(path.dirname(filePath));
let file = files![filename];

await fse.writeFile(filePath, stripIndent(file));
})
);
}

export async function getAllFilesInDir(dirPath: string): Promise<string[]> {
const entries = await fse.promises.readdir(dirPath, { withFileTypes: true });
const files = await Promise.all(
entries.map((entry) => {
const resolvedPath = path.resolve(dirPath, entry.name);
if (entry.isDirectory()) {
return getAllFilesInDir(resolvedPath);
} else {
return [resolvedPath];
}
})
);
return files.flat();
}

export async function createTestDirectory() {
let folderName = `rr-${Math.random().toString(32).slice(2)}`;
let testDir = path.join(TMP_DIR, folderName);
await fse.ensureDir(testDir);
return testDir;
}
48 changes: 35 additions & 13 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import url from "node:url";
import { createRequire } from "node:module";
import { platform } from "node:os";
import fse from "fs-extra";
import stripIndent from "strip-indent";
import waitOn from "wait-on";
import getPort from "get-port";
import shell from "shelljs";
Expand All @@ -15,13 +14,12 @@ import dedent from "dedent";
import type { Page } from "@playwright/test";
import { test as base, expect } from "@playwright/test";
import type { Config } from "@react-router/dev/config";
import { createTestDirectory, writeTestFiles } from "./filesystem";

const require = createRequire(import.meta.url);

const reactRouterBin = "node_modules/@react-router/dev/bin.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const root = path.resolve(__dirname, "../..");
const TMP_DIR = path.join(root, ".tmp/integration");

export const reactRouterConfig = ({
ssr,
Expand Down Expand Up @@ -157,26 +155,50 @@ export async function createProject(
files: Record<string, string> = {},
templateName: TemplateName = "vite-5-template"
) {
let projectName = `rr-${Math.random().toString(32).slice(2)}`;
let projectDir = path.join(TMP_DIR, projectName);
await fse.ensureDir(projectDir);
let projectDir = await createTestDirectory();

// base template
let templateDir = path.resolve(__dirname, templateName);
await fse.copy(templateDir, projectDir, { errorOnExist: true });

// user-defined files
await Promise.all(
Object.entries(files).map(async ([filename, contents]) => {
let filepath = path.join(projectDir, filename);
await fse.ensureDir(path.dirname(filepath));
await fse.writeFile(filepath, stripIndent(contents));
})
);
await writeTestFiles(files, projectDir);

return projectDir;
}

type WorkspaceFileCreatorFn = (
libDir: string,
appDir: string
) => { appFiles?: Record<string, string>; libFiles?: Record<string, string> };

export async function createWorkspaceProject(files: WorkspaceFileCreatorFn) {
const workspaceDir = await createTestDirectory();

const workspaceTemplateDir = path.resolve(
__dirname,
"workspace-routes-template"
);
await fse.copy(workspaceTemplateDir, workspaceDir, { errorOnExist: true });

const appProjectDir = path.resolve(workspaceDir, "apps", "test-app");
const libProjectDir = path.resolve(workspaceDir, "libs", "test-route-library");

const userFiles = files(libProjectDir, appProjectDir);
if (userFiles.libFiles) {
await writeTestFiles(userFiles.libFiles, libProjectDir);
}
if (userFiles.appFiles) {
await writeTestFiles(userFiles.appFiles, appProjectDir);
}

return {
appProjectDir,
libProjectDir,
workspaceDir,
};
}

// Avoid "Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env
// being set" in vite-ecosystem-ci which breaks empty stderr assertions. To fix
// this we always ensure that only NO_COLOR is set after spreading process.env.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
.env
.react-router
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes() satisfies RouteConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { MetaFunction } from "react-router";

export const meta: MetaFunction = () => {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
};

export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>Welcome to React Router</h1>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="@react-router/node" />
/// <reference types="vite/client" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "integration-workspace-test-app-template",
"version": "0.0.0",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "react-router dev",
"build": "react-router build",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@react-router/express": "workspace:*",
"@react-router/node": "workspace:*",
"@react-router/serve": "workspace:*",
"@vanilla-extract/css": "^1.10.0",
"@vanilla-extract/vite-plugin": "^3.9.2",
"express": "^4.19.2",
"isbot": "^5.1.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "workspace:*",
"test-route-library": "workspace:*",
"serialize-javascript": "^6.0.1"
},
"devDependencies": {
"@react-router/dev": "workspace:*",
"@react-router/fs-routes": "workspace:*",
"@react-router/remix-routes-option-adapter": "workspace:*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"eslint": "^8.38.0",
"typescript": "^5.1.6",
"vite": "^6.0.0",
"vite-env-only": "^3.0.1",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=20.0.0"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"include": [
"env.d.ts",
"**/*.ts",
"**/*.tsx",
".react-router/types/**/*.d.ts"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"noEmit": true,
"rootDirs": [".", ".react-router/types/"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
plugins: [reactRouter(), tsconfigPaths()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function sum(a: number, b: number): number;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function sum(a, b) {
return a + b;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "test-route-library",
"type": "module",
"private": true,
"exports": {
".": "./index.js"
},
"peerDependencies": {
"react": "^18.2.0",
"@react-router/dev": "workspace:*"
}
}
41 changes: 40 additions & 1 deletion integration/typegen-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { expect, test } from "@playwright/test";
import dedent from "dedent";
import fse from "fs-extra";

import { createProject } from "./helpers/vite";
import {
createProject,
createWorkspaceProject,
} from "./helpers/vite";
import { getAllFilesInDir } from "./helpers/filesystem";

const tsx = dedent;

Expand Down Expand Up @@ -444,4 +448,39 @@ test.describe("typegen", () => {
expect(proc.stderr.toString()).toBe("");
expect(proc.status).toBe(0);
});

test("route files from workspace packages", async () => {
const { appProjectDir } = await createWorkspaceProject((libDir) => ({
appFiles: {
"app/routes.ts": tsx`
import type { RouteConfig } from "@react-router/dev/routes";
export default [
{ path: 'lib-route', file: "${libDir}/my-route.jsx" }
] satisfies RouteConfig;
`,
},
libFiles: {
"my-route.jsx": tsx`
export default function MyRoute() {
return <h1>My Route</h1>;
}
`,
},
}));

const proc = typecheck(appProjectDir);

expect(proc.stdout.toString()).toBe("");
expect(proc.stderr.toString()).toBe("");
expect(proc.status).toBe(0);

// Expect that the route library's typing is not generated into the app project
const files = await getAllFilesInDir(appProjectDir);
const appProjectFiles = files.filter(
(name) => !name.includes("node_modules")
);
expect(
appProjectFiles.some((name) => name.includes("my-route"))
).toBeFalsy();
});
});
11 changes: 11 additions & 0 deletions packages/react-router-dev/typegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import pc from "picocolors";
import type vite from "vite";

import { createConfigLoader } from "../config/config";
import type { RouteManifestEntry } from "../config/routes";
import * as Babel from "../vite/babel";

import { generate } from "./generate";
Expand Down Expand Up @@ -75,11 +76,21 @@ async function createContext({
};
}

function isRouteInAppDirectory(ctx: Context, route: RouteManifestEntry) {
const absoluteRoutePath = Path.resolve(ctx.config.appDirectory, route.file);
Copy link
Author

Choose a reason for hiding this comment

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

return absoluteRoutePath.startsWith(ctx.config.appDirectory);
}

async function writeAll(ctx: Context): Promise<void> {
const typegenDir = getTypesDir(ctx);

fs.rmSync(typegenDir, { recursive: true, force: true });
Object.values(ctx.config.routes).forEach((route) => {
// We only generate types for routes in the app directory
if (!isRouteInAppDirectory(ctx, route)) {
return;
}

const typesPath = getTypesPath(ctx, route);
const content = generate(ctx, route);
fs.mkdirSync(Path.dirname(typesPath), { recursive: true });
Expand Down
Loading