stack/apps/backend/src/smart-router.tsx
BilalG1 609579abab
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482)
2026-05-29 15:21:59 -07:00

185 lines
6.7 KiB
TypeScript

import { isTruthy } from "@hexclave/shared/dist/utils/booleans";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { numberCompare } from "@hexclave/shared/dist/utils/numbers";
// SmartRouter may be imported on the edge, so we can't import fs at the top level
// hence, we define some wrapper functions
const listRecursively: typeof import("@hexclave/shared/dist/utils/fs").listRecursively = async (...args) => {
// SmartRouter may be imported on the edge, so we can't import fs at the top level
// hence, this wrapper function
const m = await import("@hexclave/shared/dist/utils/fs");
return await m.listRecursively(...args);
};
const readFile = async (path: string) => {
const fs = await import("fs");
return await fs.promises.readFile(path, "utf-8");
};
export const SmartRouter = {
listRoutes: async () => {
const routePaths = await listRecursively("src/app", { excludeDirectories: true });
const res = [];
for (const path of routePaths) {
const isRoute = !!path.match(/route\.[^/]+$/);
const isPage = !!path.match(/page\.[^/]+$/);
if (!isRoute && !isPage) {
continue;
}
const normalizedPath = normalizeAppPath(path.replace("src/app", ""));
if (normalizedPath.includes("/_")) {
continue;
}
res.push({
filePath: path,
normalizedPath,
match: (path: string) => matchPath(path, normalizedPath),
isRoute,
isPage,
});
}
return res;
},
listApiVersions: async () => {
const allFiles = await listRecursively("src/app/api/migrations", { excludeDirectories: true });
return await Promise.all([
"v1",
...new Set(
allFiles.map((path) => path.match(/src\/app\/api\/migrations\/(v[^/]+)/)?.[1])
.filter(isTruthy)
.sort((a, b) => {
const parsedA = parseApiVersionStringToArray(a!);
const parsedB = parseApiVersionStringToArray(b!);
return numberCompare(parsedA[0], parsedB[0]) * 2 + numberCompare(parsedA[1], parsedB[1]);
})
),
"latest",
].map(async (version) => {
if (version === "latest") {
return {
name: version,
changes: undefined,
betaChanges: undefined,
migrationFolder: "/api/latest",
servedRoute: "/api/latest",
} as const;
} else if (version === "v1") {
return {
name: version,
changes: "Initial release.\n",
betaChanges: "Initial release.\n",
migrationFolder: undefined,
servedRoute: "/api/v1",
} as const;
} else {
if (!allFiles.includes(`src/app/api/migrations/${version}/beta-changes.txt`)) {
throw new HexclaveAssertionError(`API version ${version} does not have a beta-changes.txt file. The beta-changes.txt file should contain the changes since the last beta release.`);
}
if (!version.includes("beta") && !allFiles.includes(`src/app/api/migrations/${version}/changes.txt`)) {
throw new HexclaveAssertionError(`API version ${version} does not have a changes.txt file. The changes.txt file should contain the changes since the last full (non-beta) release.`);
}
return {
name: version,
changes: !version.includes("beta") ? await readFile(`src/app/api/migrations/${version}/changes.txt`) : undefined,
betaChanges: await readFile(`src/app/api/migrations/${version}/beta-changes.txt`),
migrationFolder: `/api/migrations/${version}`,
servedRoute: `/api/${version}`,
};
}
}));
},
matchNormalizedPath: (path: string, normalizedPath: string) => matchPath(path, normalizedPath),
};
function parseApiVersionStringToArray(version: string): [number, number] {
const matchResult = version.match(/^v(\d+)(?:beta(\d+))?$/);
if (!matchResult) throw new HexclaveAssertionError(`Invalid API version string: ${version}`);
return [+matchResult[1], matchResult[2] === "" ? Number.POSITIVE_INFINITY : +matchResult[2]];
}
function matchPath(path: string, toMatchWith: string): Record<string, string | string[]> | false {
// get the relative part, and modify it to have a leading slash, without a trailing slash, without ./.., etc.
const url = new URL(path + "/", "http://example.com");
const modifiedPath = url.pathname.slice(1, -1);
const modifiedToMatchWith = toMatchWith.slice(1);
if (modifiedPath === "" && modifiedToMatchWith === "") {
return {};
}
const pathFirst = modifiedPath.split("/")[0];
const toMatchWithFirst = modifiedToMatchWith.split("/")[0];
const recurse = () => matchPath(modifiedPath.slice((modifiedPath + "/").indexOf("/")), modifiedToMatchWith.slice((modifiedToMatchWith + "/").indexOf("/")));
if (toMatchWithFirst.startsWith("[[...") && toMatchWithFirst.endsWith("]]")) {
if (modifiedToMatchWith.includes("/")) {
throw new HexclaveAssertionError("Optional catch-all routes must be at the end of the path", { modifiedPath, modifiedToMatchWith });
}
return {
[toMatchWithFirst.slice(5, -2)]: modifiedPath === "" ? [] : modifiedPath.split("/"),
};
} else if (toMatchWithFirst.startsWith("[...") && toMatchWithFirst.endsWith("]")) {
if (modifiedToMatchWith.includes("/")) {
throw new HexclaveAssertionError("Catch-all routes must be at the end of the path", { modifiedPath, modifiedToMatchWith });
}
if (modifiedPath === "") return false;
return {
[toMatchWithFirst.slice(4, -1)]: modifiedPath.split("/"),
};
} else if (toMatchWithFirst.startsWith("[") && toMatchWithFirst.endsWith("]")) {
if (modifiedPath === "") return false;
const recurseResult = recurse();
if (!recurseResult) return false;
return {
[toMatchWithFirst.slice(1, -1)]: pathFirst,
...recurseResult,
};
} else if (toMatchWithFirst === pathFirst) {
return recurse();
} else {
return false;
}
}
/**
* Modified from: https://github.com/vercel/next.js/blob/6ff13369bb18045657d0f84ddc86b540340603a1/packages/next/src/shared/lib/router/utils/app-paths.ts#L23
*/
function normalizeAppPath(route: string) {
let res = route.split('/').reduce((pathname, segment, index, segments) => {
// Empty segments are ignored.
if (!segment) {
return pathname;
}
// Groups are ignored.
if (segment.startsWith('(') && segment.endsWith(')')) {
return pathname;
}
// Parallel segments are ignored.
if (segment[0] === '@') {
return pathname;
}
// The last segment (if it's a leaf) should be ignored.
if (
(segment.startsWith('page.') || segment.startsWith('route.')) &&
index === segments.length - 1
) {
return pathname;
}
return `${pathname}/${segment}`;
}, '');
if (!res.startsWith('/')) {
res = `/${res}`;
}
return res;
}