react-router-typesafe-routes
Version:
Enhanced type safety via validation for all route params in React Router v7.
302 lines (301 loc) • 12 kB
JavaScript
import { string } from "../types/index.mjs";
import { createSearchParams, generatePath } from "react-router";
function route(opts) {
var _a, _b, _c, _d, _e;
const composedSpecList = ((_a = opts.compose) !== null && _a !== void 0 ? _a : []).map(({ $spec }) => $spec);
const ownSpec = {
path: opts.path,
params: (_b = opts === null || opts === void 0 ? void 0 : opts.params) !== null && _b !== void 0 ? _b : {},
searchParams: (_c = opts === null || opts === void 0 ? void 0 : opts.searchParams) !== null && _c !== void 0 ? _c : {},
hash: (_d = opts === null || opts === void 0 ? void 0 : opts.hash) !== null && _d !== void 0 ? _d : [],
state: (_e = opts === null || opts === void 0 ? void 0 : opts.state) !== null && _e !== void 0 ? _e : {},
};
const resolvedSpec = mergeSpecList([...composedSpecList, ownSpec], "compose");
return Object.assign(Object.assign(Object.assign({}, decorateChildren(resolvedSpec, opts.children)), getRouteApi(resolvedSpec)), { $: decorateChildren(omitPath(resolvedSpec), opts.children) });
}
function omitPath(spec) {
return Object.assign(Object.assign({}, spec), { path: "" });
}
function mergeSpecList(specList, mode) {
return specList.reduce((acc, item) => {
return {
path: mode === "compose"
? item.path
: ["", undefined].includes(acc.path)
? item.path
: ["", undefined].includes(item.path)
? acc.path
: `${acc.path}/${item.path}`,
params: Object.assign(Object.assign({}, acc.params), item.params),
searchParams: Object.assign(Object.assign({}, acc.searchParams), item.searchParams),
hash: isHashType(item.hash)
? item.hash
: isHashType(acc.hash)
? acc.hash
: [...(acc.hash || []), ...(item.hash || [])],
state: Object.assign(Object.assign({}, acc.state), item.state),
};
});
}
function isHashType(value) {
return Boolean(value) && !Array.isArray(value);
}
function decorateChildren(spec, children) {
const result = {};
if (children) {
Object.keys(children).forEach((key) => {
// Explicit unknown is required for the type guard to work in TS 5.1 for some reason
const value = children[key];
result[key] = isRoute(value)
? Object.assign(Object.assign(Object.assign({}, decorateChildren(spec, value)), getRouteApi(mergeSpecList([spec, value.$spec], "inherit"))), { $: decorateChildren(omitPath(spec), value.$) }) : value;
});
}
return result;
}
function getRouteApi(spec) {
const [allPathParams] = getPathParams(spec.path);
const absolutePath = makeAbsolute(spec.path);
const relativePath = removeIntermediateStars(spec.path);
const resolvedSpec = Object.assign(Object.assign({}, spec), { params: Object.assign(Object.assign({}, getInferredPathnameTypes(spec.path)), (spec.path === undefined ? spec.params : pickKnownKeys(spec.params, allPathParams))) });
function serializeParams(opts) {
return serializeParamsByTypes(allPathParams, opts.params, resolvedSpec.params);
}
function buildPathname(opts) {
const rawBuiltPath = generatePath(relativePath !== null && relativePath !== void 0 ? relativePath : "", serializeParams(opts));
const relativePathname = rawBuiltPath.startsWith("/") ? rawBuiltPath.substring(1) : rawBuiltPath;
return `${(opts === null || opts === void 0 ? void 0 : opts.relative) ? "" : "/"}${relativePathname}`;
}
function buildPath(opts) {
var _a, _b;
const params = (_a = opts.params) !== null && _a !== void 0 ? _a : {};
const searchParams = (_b = opts.searchParams) !== null && _b !== void 0 ? _b : {};
const hash = opts.hash;
return `${buildPathname(Object.assign({ params }, opts))}${buildSearch(Object.assign({ searchParams }, opts))}${hash !== undefined ? buildHash({ hash }) : ""}`;
}
function serializeSearchParams(opts) {
const plainParams = createSearchParams(serializeSearchParamsByTypes(opts.searchParams, resolvedSpec.searchParams));
if (opts === null || opts === void 0 ? void 0 : opts.untypedSearchParams) {
appendSearchParams(plainParams, getUntypedSearchParams(opts === null || opts === void 0 ? void 0 : opts.untypedSearchParams));
}
return plainParams;
}
function buildSearch(opts) {
const searchString = createSearchParams(serializeSearchParams(opts)).toString();
return searchString ? `?${searchString}` : "";
}
function buildHash(opts) {
if (isHashType(resolvedSpec.hash)) {
return `#${resolvedSpec.hash.serializeHash(opts.hash)}`;
}
return `#${String(opts.hash)}`;
}
function buildState(opts) {
return (isStateType(resolvedSpec.state)
? serializeStateByType(opts.state, resolvedSpec.state)
: Object.assign(serializeStateParamsByTypes(opts.state, resolvedSpec.state), getUntypedState(opts === null || opts === void 0 ? void 0 : opts.untypedState)));
}
function deserializeParams(params) {
return deserializeParamsByTypes(params, resolvedSpec);
}
function deserializeSearchParams(params) {
return deserializeSearchParamsByTypes(params, resolvedSpec);
}
function getUntypedSearchParams(params) {
const result = createSearchParams(params);
if (!resolvedSpec.searchParams)
return result;
Object.keys(resolvedSpec.searchParams).forEach((key) => {
result.delete(key);
});
return result;
}
function deserializeState(state) {
return deserializeStateByTypes(state, resolvedSpec);
}
function getUntypedState(state) {
const result = (isStateType(resolvedSpec.state) ? undefined : {});
if (!isRecord(state) || !result)
return result;
const typedKeys = resolvedSpec.state ? Object.keys(resolvedSpec.state) : [];
Object.keys(state).forEach((key) => {
if (typedKeys.indexOf(key) === -1) {
result[key] = state[key];
}
});
return result;
}
function deserializeHash(hash) {
const normalizedHash = hash === null || hash === void 0 ? void 0 : hash.substring(1, hash === null || hash === void 0 ? void 0 : hash.length);
if (isHashType(resolvedSpec.hash)) {
return resolvedSpec.hash.deserializeHash(normalizedHash);
}
if (normalizedHash && resolvedSpec.hash.indexOf(normalizedHash) !== -1) {
return normalizedHash;
}
return undefined;
}
return {
$path: ((opts) => ((opts === null || opts === void 0 ? void 0 : opts.relative) ? relativePath : absolutePath)),
$buildPath: buildPath,
$buildPathname: buildPathname,
$buildSearch: buildSearch,
$buildHash: buildHash,
$buildState: buildState,
$serializeParams: serializeParams,
$serializeSearchParams: serializeSearchParams,
$deserializeParams: deserializeParams,
$deserializeSearchParams: deserializeSearchParams,
$deserializeHash: deserializeHash,
$deserializeState: deserializeState,
$spec: spec,
};
}
function getInferredPathnameTypes(path) {
const [allPathParams, optionalPathParams] = getPathParams(path);
const params = {};
optionalPathParams.forEach((optionalParam) => {
params[optionalParam] = string();
});
allPathParams.forEach((param) => {
if (!params[param]) {
params[param] = string().defined();
}
});
return params;
}
function pickKnownKeys(obj, keys) {
const result = {};
keys.forEach((key) => {
if (obj[key] !== undefined) {
result[key] = obj[key];
}
});
return result;
}
function serializeParamsByTypes(keys, params, types) {
const result = {};
Object.keys(params).forEach((key) => {
const type = types[key];
const value = params[key];
if (type && keys.indexOf(key) !== -1 && value !== undefined) {
result[key] = type.serializeParam(value);
}
});
return result;
}
function serializeSearchParamsByTypes(params, types) {
const result = {};
Object.keys(params).forEach((key) => {
const type = types[key];
if (type && params[key] !== undefined) {
result[key] = type.serializeSearchParam(params[key]);
}
});
return result;
}
function serializeStateParamsByTypes(params, types) {
const result = {};
Object.keys(params).forEach((key) => {
const type = types[key];
const value = params[key];
if (type && value !== undefined) {
result[key] = type.serializeState(value);
}
});
return result;
}
function serializeStateByType(state, type) {
return type.serializeState(state);
}
function deserializeParamsByTypes(params, spec) {
const types = spec.params;
const result = {};
Object.keys(types).forEach((key) => {
const type = types[key];
if (type) {
const typedSearchParam = type.deserializeParam(params[key]);
if (typedSearchParam !== undefined) {
result[key] = typedSearchParam;
}
}
});
return result;
}
function deserializeSearchParamsByTypes(searchParams, spec) {
const result = {};
const types = spec.searchParams;
Object.keys(types).forEach((key) => {
const type = types[key];
if (type) {
const typedSearchParam = type.deserializeSearchParam(searchParams.getAll(key));
if (typedSearchParam !== undefined) {
result[key] = typedSearchParam;
}
}
});
return result;
}
function deserializeStateByTypes(state, spec) {
if (isStateType(spec.state)) {
return spec.state.deserializeState(state);
}
const result = {};
const types = spec.state;
if (isRecord(state)) {
Object.keys(types).forEach((key) => {
const type = types[key];
if (type) {
const typedStateParam = type.deserializeState(state[key]);
if (typedStateParam !== undefined) {
result[key] = typedStateParam;
}
}
});
}
return result;
}
function getPathParams(path) {
const allParams = [];
const optionalParams = [];
path === null || path === void 0 ? void 0 : path.split(":").filter((_, index) => Boolean(index)).forEach((part) => {
const rawParam = part.split("/")[0];
if (rawParam.endsWith("?")) {
const param = rawParam.replace("?", "");
allParams.push(param);
optionalParams.push(param);
}
else {
allParams.push(rawParam);
}
});
if (path === null || path === void 0 ? void 0 : path.includes("*?")) {
allParams.push("*");
optionalParams.push("*");
}
else if (path === null || path === void 0 ? void 0 : path.includes("*")) {
allParams.push("*");
}
return [allParams, optionalParams];
}
function removeIntermediateStars(path) {
return path === null || path === void 0 ? void 0 : path.replace(/\*\??\//g, "");
}
function makeAbsolute(path) {
return (typeof path === "string" ? `/${path}` : path);
}
function isRoute(value) {
return Boolean(value && typeof value === "object" && "$spec" in value);
}
function isRecord(value) {
return Boolean(value && typeof value === "object");
}
function isStateType(value) {
return typeof value.serializeState === "function";
}
function appendSearchParams(target, source) {
for (const [key, val] of source.entries()) {
target.append(key, val);
}
return target;
}
export { route, };