diff --git a/packages/simple-router/src/matcher/index.ts b/packages/simple-router/src/matcher/index.ts index ff07a9e..9b5e543 100644 --- a/packages/simple-router/src/matcher/index.ts +++ b/packages/simple-router/src/matcher/index.ts @@ -1,9 +1,10 @@ import type { ElegantConstRoute } from '@ohh-889/react-auto-route'; import type { Location } from 'react-router-dom'; import type { RouteLocationNamedRaw } from '../types'; +import { stringifyQuery } from '../query'; import type { RouteRecordRaw } from './types'; import { createRouteRecordMatcher } from './pathMatcher'; -import { generatePath, getQueryParams, mergeMetaFields, normalizeRouteRecord, objectToQueryParams } from './shared'; +import { generatePath, getQueryParams, mergeMetaFields, normalizeRouteRecord } from './shared'; class CreateRouterMatcher { // Internal routes maintained for react-router @@ -155,7 +156,8 @@ class CreateRouterMatcher { if ('query' in location) { query = location.query || {}; - const queryParams = objectToQueryParams(query); + const queryParams = stringifyQuery(query); + console.log(queryParams); fullPath += queryParams ? `?${queryParams}` : ''; } diff --git a/packages/simple-router/src/query.ts b/packages/simple-router/src/query.ts new file mode 100644 index 0000000..d42985e --- /dev/null +++ b/packages/simple-router/src/query.ts @@ -0,0 +1,135 @@ +export type LocationQueryValue = string | null; + +/** + * Normalized query object that appears in {@link RouteLocationNormalized} + * + * @public + */ +export type LocationQuery = Record; +export type LocationQueryValueRaw = LocationQueryValue | number | undefined; + +export type LocationQueryRaw = Record; + +/** + * Transforms a queryString into a {@link LocationQuery} object. Accept both, a version with the leading `?` and without + * Should work as URLSearchParams + * + * @param search - search string to parse + * @returns a query object + * @internal + */ + +export const PLUS_RE = /\+/g; // %2B + +const EQUAL_RE = /[=]/g; // %3D + +const ENC_BRACKET_OPEN_RE = /%5B/g; // [ +const ENC_BRACKET_CLOSE_RE = /%5D/g; // ] +const ENC_CARET_RE = /%5E/g; // ^ +const ENC_BACKTICK_RE = /%60/g; // ` +const ENC_CURLY_OPEN_RE = /%7B/g; // { +const ENC_PIPE_RE = /%7C/g; // | +const ENC_CURLY_CLOSE_RE = /%7D/g; // } +const ENC_SPACE_RE = /%20/g; // } +const HASH_RE = /#/g; // %23 +const AMPERSAND_RE = /&/g; // %26 +export function parseQuery(search: string): LocationQuery { + const query: LocationQuery = {}; + // avoid creating an object with an empty key and empty value + // because of split('&') + if (search === '' || search === '?') return query; + const hasLeadingIM = search[0] === '?'; + const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&'); + // eslint-disable-next-line no-plusplus + for (let i = 0; i < searchParams.length; ++i) { + // pre decode the + into space + const searchParam = searchParams[i].replace(PLUS_RE, ' '); + // allow the = character + const eqPos = searchParam.indexOf('='); + const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)); + const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)); + + if (key in query) { + // an extra variable for ts types + let currentValue = query[key]; + if (!Array.isArray(currentValue)) { + // eslint-disable-next-line no-multi-assign + currentValue = query[key] = [currentValue]; + } + // we force the modification + (currentValue as LocationQueryValue[]).push(value); + } else { + query[key] = value; + } + } + return query; +} + +export function stringifyQuery(query: LocationQueryRaw): string { + let search = ''; + // eslint-disable-next-line guard-for-in + for (let key in query) { + const value = query[key]; + key = encodeQueryKey(key); + if (value === null) { + // only null adds the value + if (value !== undefined) { + search += (search.length ? '&' : '') + key; + } + // eslint-disable-next-line no-continue + continue; + } + // keep null values + const values: LocationQueryValueRaw[] = Array.isArray(value) + ? value.map(v => v && encodeQueryValue(v)) + : [value && encodeQueryValue(value)]; + + // eslint-disable-next-line no-loop-func + values.forEach(v => { + // skip undefined values in arrays as if they were not present + // smaller code than using filter + if (v !== undefined) { + // only append & with non-empty search + search += (search.length ? '&' : '') + key; + if (v !== null) search += `=${v}`; + } + }); + } + + return search; +} + +export function decode(text: string | number): string { + try { + return decodeURIComponent(`${text}`); + } catch (err) { + console.warn(`Error decoding "${text}". Using original value`); + } + return `${text}`; +} + +export function encodeQueryKey(text: string | number): string { + return encodeQueryValue(text).replace(EQUAL_RE, '%3D'); +} + +export function encodeQueryValue(text: string | number): string { + return ( + commonEncode(text) + // Encode the space as +, encode the + to differentiate it from the space + .replace(PLUS_RE, '%2B') + .replace(ENC_SPACE_RE, '+') + .replace(HASH_RE, '%23') + .replace(AMPERSAND_RE, '%26') + .replace(ENC_BACKTICK_RE, '`') + .replace(ENC_CURLY_OPEN_RE, '{') + .replace(ENC_CURLY_CLOSE_RE, '}') + .replace(ENC_CARET_RE, '^') + ); +} + +function commonEncode(text: string | number): string { + return encodeURI(`${text}`) + .replace(ENC_PIPE_RE, '|') + .replace(ENC_BRACKET_OPEN_RE, '[') + .replace(ENC_BRACKET_CLOSE_RE, ']'); +} diff --git a/vercel.json b/vercel.json index e2dd4a8..d3d09c7 100644 --- a/vercel.json +++ b/vercel.json @@ -1,95 +1,92 @@ { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }], "headers": [ - { - "source": "/sw.js", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=0, must-revalidate" - } - ] - }, - { - "source": "(.*)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=86400, max-age=86400" - } - ] - }, - { - "source": "/medias/(.*)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=2592000, max-age=2592000" - } - ] - }, - { - "source": "/medias_webp/(.*)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=2592000, max-age=2592000" - } - ] - }, - { - "source": "(.*).html", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=1800, max-age=1800" - } - ] - }, - { - "source": "(.*).js", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=2592000, max-age=2592000" - } - ] - }, - { - "source": "(.*).css", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=2592000, max-age=2592000" - } - ] - }, - { - "source": "(.*).json", - "headers": [ - { - "key": "Cache-Control", - "value": "public, s-maxage=2592000, max-age=2592000" - } - ] - } - ], - "regions": [ - "hkg1" + { + "source": "/sw.js", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=0, must-revalidate" + } + ] + }, + { + "source": "(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=86400, max-age=86400" + } + ] + }, + { + "source": "/medias/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=2592000, max-age=2592000" + } + ] + }, + { + "source": "/medias_webp/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=2592000, max-age=2592000" + } + ] + }, + { + "source": "(.*).html", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=1800, max-age=1800" + } + ] + }, + { + "source": "(.*).js", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=2592000, max-age=2592000" + } + ] + }, + { + "source": "(.*).css", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=2592000, max-age=2592000" + } + ] + }, + { + "source": "(.*).json", + "headers": [ + { + "key": "Cache-Control", + "value": "public, s-maxage=2592000, max-age=2592000" + } + ] + } ], + "regions": ["hkg1"], "redirects": [ - { - "source": "/gtag/js", - "destination": "https://www.googletagmanager.com/gtag/js" - }, - { - "source": "/atom.xml", - "destination": "https://cfblog.17lai.site/atom.xml" - }, - { - "source": "/rss.xml", - "destination": "https://cfblog.17lai.site/rss.xml" - } + { + "source": "/gtag/js", + "destination": "https://www.googletagmanager.com/gtag/js" + }, + { + "source": "/atom.xml", + "destination": "https://cfblog.17lai.site/atom.xml" + }, + { + "source": "/rss.xml", + "destination": "https://cfblog.17lai.site/rss.xml" + } ] } -