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 692d225 commit f3a11c7
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 30 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) {
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) {
// 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) => {
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 @@ -19,6 +19,7 @@ describe('@vaadin/hilla-file-router', () => {
runtimeUrls = {
json: new URL('server/file-routes.json', dir),
code: new URL('generated/file-routes.ts', dir),
layouts: new URL('generated/layouts.json', dir),
};
});

Expand All @@ -41,30 +42,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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, watch } from 'node:fs';
import { rm } from 'node:fs/promises';
import { rm, mkdir, writeFile } from 'node:fs/promises';
import { expect, use } from '@esm-bundle/chai';
import chaiAsPromised from 'chai-as-promised';
import type { Logger } from 'vite';
Expand All @@ -22,6 +22,7 @@ describe('@vaadin/hilla-file-router', () => {
runtimeUrls = {
json: new URL('server/file-routes.json', tmp),
code: new URL('generated/file-routes.ts', tmp),
layouts: new URL('generated/layouts.json', tmp),
};

await createTestingRouteFiles(viewsDir);
Expand All @@ -36,6 +37,10 @@ describe('@vaadin/hilla-file-router', () => {
});

it('should generate the runtime files', async () => {
await mkdir(new URL('generated', tmp));
await writeFile(runtimeUrls.layouts, '[{"path": "/profile"}]', 'utf-8');
expect(existsSync(runtimeUrls.layouts)).to.be.true;

await generateRuntimeFiles(viewsDir, runtimeUrls, ['.tsx', '.jsx'], logger, true);
expect(existsSync(runtimeUrls.json)).to.be.true;
expect(existsSync(runtimeUrls.code)).to.be.true;
Expand Down

0 comments on commit f3a11c7

Please sign in to comment.