@vaadin/hilla-file-router
Version:
Hilla file-based router
106 lines • 3.84 kB
JavaScript
import { opendir, readFile } from "node:fs/promises";
import { basename, extname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { RouteParamType } from "../shared/routeParamType.js";
import { cleanUp, routeParamTypeMap } from "./utils.js";
async function checkFile(url, logger) {
if (url) {
const contents = await readFile(url, "utf-8");
if (contents.trim() === "") {
return undefined;
} else if (!contents.includes("export default")) {
logger.error(`The file "${String(url)}" should contain a default export of a component`);
return undefined;
}
}
return url;
}
const collator = new Intl.Collator("en-US");
const warningFor = [".ts", ".js"];
/**
* Collect route metadata from the file system and build a route tree.
*
* It accepts files that start with `@` as special files.
* - `@layout` contains a component that wraps the child components.
* - `@index` contains a component that will be used as the index page of the directory.
*
* It accepts files that start with `_` as private files. They will be ignored.
*
* @param dir - The directory to collect routes from.
* @param options - The options object.
*
* @returns The route metadata array.
*/
export default async function collectRoutesFromFS(dir, { extensions, logger, parent = dir }) {
const path = relative(fileURLToPath(parent), fileURLToPath(dir));
let children = [];
let layout;
for await (const d of await opendir(dir)) {
if (d.name.startsWith("_")) {
continue;
}
const extension = extname(d.name);
const name = basename(d.name, extension);
if (extension !== "" && !extensions.includes(extension)) {
if (warningFor.includes(extension)) {
logger.warn(`File System based router expects only JSX files in 'Frontend/views/' directory, such as '*.tsx' and '*.jsx'. The file '${d.name}' will be ignored by router, as it doesn't match this convention. Please consider storing it in another directory.`);
}
continue;
}
if (children.some(({ path: p }) => p === name)) {
throw new Error(`You cannot create a file and a directory with the same name ("${name}"). Use "@index" instead`);
}
if (d.isDirectory()) {
const directoryRoutes = await collectRoutesFromFS(new URL(`${name}/`, dir), {
extensions,
logger,
parent: dir
});
if (directoryRoutes.length === 1 && directoryRoutes[0]?.layout) {
const [layoutRoute] = directoryRoutes;
children.push(layoutRoute);
} else if (directoryRoutes.length > 0) {
children.push({
path: name,
children: directoryRoutes
});
}
continue;
}
const file = new URL(d.name, dir);
const url = await checkFile(file, logger);
if (url === undefined) {
continue;
}
const optionalParamType = routeParamTypeMap.get(RouteParamType.Optional);
if (name === "@index" && children.some(({ path: p }) => p.search(optionalParamType) >= 0) || name.search(optionalParamType) >= 0 && children.some(({ path: p }) => p === "")) {
throw new Error("You cannot create an `@index` file in a directory with optional parameters");
} else if (name === "@layout") {
layout = file;
} else if (name === "@index") {
children.push({
path: "",
file
});
} else if (name.startsWith("@")) {
throw new Error("Symbol \"@\" is reserved for special directories and files; only \"@layout\" and \"@index\" are allowed");
} else {
children.push({
path: name,
file
});
}
}
[children, layout] = await Promise.all([Promise.all(children.map(async (child) => ({
...child,
file: child.file,
layout: await checkFile(child.layout, logger)
}))), checkFile(layout, logger)]);
children = children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b)));
return layout ? [{
path,
layout,
children
}] : children;
}
//# sourceMappingURL=./collectRoutesFromFS.js.map