Skip to content

Commit

Permalink
feat: automtic flowLayout if match exists
Browse files Browse the repository at this point in the history
Automatically enable flowLayout
for views if a server layout
exists that matches, the flag
is not explicitly defined.

Closes #2709
  • Loading branch information
caalador committed Sep 11, 2024
1 parent e74fe7e commit 4c5ca39
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class RouterConfigurationBuilder {
withFileRoutes(routes: readonly AgnosticRoute[]): this {
return this.update(routes, (original, added, children) => {
if (added) {
const { module, path } = added;
const { module, path, flowLayout } = added;
if (!isReactRouteModule(module)) {
throw new Error(`The module for the "${path}" section doesn't have the React component exported by default`);
}
Expand All @@ -75,6 +75,7 @@ export class RouterConfigurationBuilder {
const handle = {
...module?.config,
title: module?.config?.title ?? convertComponentNameToTitle(module?.default),
flowLayout: module?.config?.flowLayout ?? flowLayout,
};

if (path === '' && !children) {
Expand Down Expand Up @@ -118,7 +119,8 @@ export class RouterConfigurationBuilder {
];

this.update(fallbackRoutes, (original, added, children) => {
if (original) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (original && !original.handle?.ignoreFallback) {
if (!children) {
return original;
}
Expand Down Expand Up @@ -157,13 +159,17 @@ export class RouterConfigurationBuilder {
{
element: createElement(layoutComponent),
children: nestedRoutes,
handle: {
ignoreFallback: true,
},
},
];
}

function checkFlowLayout(route: RouteObject): boolean {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
let flowLayout = typeof route.handle === 'object' && 'flowLayout' in route.handle && route.handle.flowLayout;
// Check children if they have layout. If yes then parent should have layout also.
if (!flowLayout && route.children) {
flowLayout = route.children.filter((child) => checkFlowLayout(child)).length > 0;
}
Expand Down
17 changes: 15 additions & 2 deletions packages/ts/file-router/src/runtime/createRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@ import type { AgnosticRoute, Module } from '../types.js';
*
* @returns A framework-agnostic route object.
*/
export function createRoute(path: string, children?: readonly AgnosticRoute[]): AgnosticRoute;
export function createRoute(path: string, module: Module, children?: readonly AgnosticRoute[]): AgnosticRoute;
export function createRoute(path: string, flowLayout: boolean, children?: readonly AgnosticRoute[]): AgnosticRoute;
export function createRoute(
path: string,
flowLayout: boolean,
module: Module,
children?: readonly AgnosticRoute[],
): AgnosticRoute;
export function createRoute(
path: string,
flowLayout: boolean,
module: Module,
children?: readonly AgnosticRoute[],
): AgnosticRoute;
export function createRoute(
path: string,
flowLayout: boolean,
moduleOrChildren?: Module | readonly AgnosticRoute[],
children?: readonly AgnosticRoute[],
): AgnosticRoute {
Expand All @@ -28,5 +40,6 @@ export function createRoute(
path,
module,
children,
flowLayout,
};
}
1 change: 1 addition & 0 deletions packages/ts/file-router/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type AgnosticRoute = Readonly<{
path: string;
module?: Module;
children?: readonly AgnosticRoute[];
flowLayout?: boolean;
}>;

/**
Expand Down
1 change: 1 addition & 0 deletions packages/ts/file-router/src/vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function vitePluginFileSystemRouter({
runtimeUrls = {
json: new URL('file-routes.json', isDevMode ? _generatedDir : _outDir),
code: new URL('file-routes.ts', _generatedDir),
layouts: new URL('layouts.json', _generatedDir),
};
},
async buildStart() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type RouteMeta = Readonly<{
path: string;
file?: URL;
layout?: URL;
flowLayout?: boolean;
children?: readonly RouteMeta[];
}>;

Expand Down
14 changes: 10 additions & 4 deletions packages/ts/file-router/src/vite-plugin/createRoutesFromMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ function createImport(mod: string, file: string): ImportDeclaration {
* @param mod - The name of the route module imported as a namespace.
* @param children - The list of child route call expressions.
*/
function createRouteData(path: string, mod: string | undefined, children?: readonly CallExpression[]): CallExpression {
function createRouteData(
path: string,
flowLayout: boolean | undefined,
mod: string | undefined,
children?: readonly CallExpression[],
): CallExpression {
const serverLayout = flowLayout ?? false;
return template(
`const route = createRoute("${path}"${mod ? `, ${mod}` : ''}${children ? `, CHILDREN` : ''})`,
`const route = createRoute("${path}",${serverLayout}${mod ? `, ${mod}` : ''}${children ? `, CHILDREN` : ''})`,
([statement]) => (statement as VariableStatement).declarationList.declarations[0].initializer as CallExpression,
[
transformer((node) =>
Expand Down Expand Up @@ -84,7 +90,7 @@ export default function createRoutesFromMeta(views: readonly RouteMeta[], { code
.map((dup) => `console.error("Two views share the same path: ${dup}");`),
);

return metas.map(({ file, layout, path, children }) => {
return metas.map(({ file, layout, path, children, flowLayout }) => {
let _children: readonly CallExpression[] | undefined;

if (children) {
Expand All @@ -103,7 +109,7 @@ export default function createRoutesFromMeta(views: readonly RouteMeta[], { code
imports.push(createImport(mod, relativize(layout, codeDir)));
}

return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), mod, _children);
return createRouteData(convertFSRouteSegmentToURLPatternFormat(path), flowLayout, mod, _children);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default async function createViewConfigJson(views: readonly RouteMeta[]):
views,
async (routes, next) =>
await Promise.all(
routes.map(async ({ path, file, layout, children }) => {
routes.map(async ({ path, file, layout, children, flowLayout }) => {
const newChildren = children ? await next(...children) : undefined;

if (!file && !layout) {
Expand All @@ -59,6 +59,12 @@ export default async function createViewConfigJson(views: readonly RouteMeta[]):
const code = node.initializer.getText(sourceFile);
const script = new Script(`(${code})`);
config = script.runInThisContext() as ViewConfig;
if (config.flowLayout === undefined) {
const copy = JSON.parse(JSON.stringify(config));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
copy.flowLayout = flowLayout ?? false;
config = copy;
}
}
} else if (node.getText(sourceFile).startsWith('export default')) {
waitingForIdentifier = true;
Expand Down
47 changes: 45 additions & 2 deletions packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import type { Logger } from 'vite';
import collectRoutesFromFS from './collectRoutesFromFS.js';
import collectRoutesFromFS, { type RouteMeta } from './collectRoutesFromFS.js';
import createRoutesFromMeta from './createRoutesFromMeta.js';
import createViewConfigJson from './createViewConfigJson.js';

Expand All @@ -18,6 +18,10 @@ export type RuntimeFileUrls = Readonly<{
* The URL of the module with the routes tree in a framework-agnostic format.
*/
code: URL;
/**
* The URL of the JSON file containing server layout path information.
*/
layouts: URL;
}>;

/**
Expand All @@ -43,6 +47,44 @@ async function generateRuntimeFile(url: URL, data: string): Promise<void> {
}
}

async function applyLayouts(routeMeta: readonly RouteMeta[], layouts: URL): Promise<readonly RouteMeta[]> {
if (!existsSync(layouts)) {
return routeMeta;
}
const layoutContents = await readFile(layouts, 'utf-8');
const availableLayouts: any[] = JSON.parse(layoutContents);
function layoutExists(routePath: string) {

Check warning on line 56 in packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts#L56

Added line #L56 was not covered by tests
return (
availableLayouts.filter((layout: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
const normalizedLayout = layout.path[0] === '/' ? layout.path.substring(1) : layout.path;
const normalizedRoute = routePath.startsWith('/') ? routePath.substring(1) : routePath;
return normalizedRoute.startsWith(normalizedLayout);
}).length > 0
);
}
function enableFlowLayout(route: RouteMeta) {

Check warning on line 66 in packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts#L66

Added line #L66 was not covered by tests
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
route.flowLayout = true;
if (route.children) {
// eslint-disable-next-line @typescript-eslint/no-for-in-array,no-restricted-syntax
for (const position in route.children) {
enableFlowLayout(route.children[position]);
}
}
}

routeMeta
.filter((route) => route.layout === undefined && layoutExists(route.path))
.map((route) => {

Check warning on line 80 in packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/file-router/src/vite-plugin/generateRuntimeFiles.ts#L79-L80

Added lines #L79 - L80 were not covered by tests
enableFlowLayout(route);
return route;
});

return routeMeta;
}

/**
* Collects all file-based routes from the given directory, and based on them generates two files
* described by {@link RuntimeFileUrls} type.
Expand All @@ -59,10 +101,11 @@ export async function generateRuntimeFiles(
logger: Logger,
debug: boolean,
): Promise<void> {
const routeMeta = existsSync(viewsDir) ? await collectRoutesFromFS(viewsDir, { extensions, logger }) : [];
let routeMeta = existsSync(viewsDir) ? await collectRoutesFromFS(viewsDir, { extensions, logger }) : [];
if (debug) {
logger.info('Collected file-based routes');
}
routeMeta = await applyLayouts(routeMeta, urls.layouts);
const runtimeRoutesCode = createRoutesFromMeta(routeMeta, urls);
const viewConfigJson = await createViewConfigJson(routeMeta);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,30 @@ import * as Page12 from "../views/test/issue-002378/{requiredParam}/edit.js";
import * as Layout15 from "../views/test/issue-002571-empty-layout/@layout.js";
import * as Page16 from "../views/test/issue-002879-config-below.js";
const routes: readonly AgnosticRoute[] = [
createRoute("nameToReplace", Page0),
createRoute("profile", [
createRoute("", Page1),
createRoute("account", Layout5, [
createRoute("security", [
createRoute("password", Page2),
createRoute("two-factor-auth", Page3)
createRoute("nameToReplace", false, Page0),
createRoute("profile", false, [
createRoute("", false, Page1),
createRoute("account", false, Layout5, [
createRoute("security", false, [
createRoute("password", false, Page2),
createRoute("two-factor-auth", false, Page3)
])
]),
createRoute("friends", Layout8, [
createRoute("list", Page6),
createRoute(":user", Page7)
createRoute("friends", false, Layout8, [
createRoute("list", false, Page6),
createRoute(":user", false, Page7)
])
]),
createRoute("test", [
createRoute(":optional?", Page10),
createRoute("*", Page11),
createRoute("issue-002378", [
createRoute(":requiredParam", [
createRoute("edit", Page12)
createRoute("test", false, [
createRoute(":optional?", false, Page10),
createRoute("*", false, Page11),
createRoute("issue-002378", false, [
createRoute(":requiredParam", false, [
createRoute("edit", false, Page12)
])
]),
createRoute("issue-002571-empty-layout", Layout15, []),
createRoute("issue-002879-config-below", Page16)
createRoute("issue-002571-empty-layout", false, Layout15, []),
createRoute("issue-002879-config-below", false, Page16)
])
];
export default routes;
Expand Down

0 comments on commit 4c5ca39

Please sign in to comment.