UNPKG

react-router-typesafe-routes

Version:

Enhanced type safety via validation for all route params in React Router v7.

302 lines (301 loc) 12 kB
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, };