reminist
Version:
Blazing fast, zero-dependency, type-safe Radix Tree router for TypeScript/JavaScript.
341 lines (337 loc) • 10.3 kB
JavaScript
// src/utils/nullPrototype.ts
var d = /* @__PURE__ */ (() => {
let o = function() {
};
return o.prototype = /* @__PURE__ */ Object.create(null), Object.freeze(o.prototype), o;
})();
// src/types/node.ts
var f = /* @__PURE__ */ ((e) => (e[e.Static = 0] = "Static", e[e.Dynamic = 1] = "Dynamic", e[e.CatchAll = 2] = "CatchAll", e[e.OptionalCatchAll = 3] = "OptionalCatchAll", e[e.Wildcard = 4] = "Wildcard", e))(f || {});
// src/controllers/Node.ts
var h = class o {
/** The name of this path segment */
name;
/** The data stored in this node if it's an endpoint */
store;
/** Indicates if this node marks the end of a valid route */
endpoint;
/** The classification of this node (Static, Dynamic, CatchAll, etc.) */
type;
/** The name of the parameter extracted from this segment, if any */
paramName;
/** Direct child nodes with static names */
staticChildren = new d();
/** Child node for simple dynamic segments (e.g., ':id') */
dynamicChild = null;
/** Child node for catch-all segments (e.g., '[...slug]') */
catchAllChild = null;
/** Child node for optional catch-all segments (e.g., '[[...slug]]') */
optionalCatchAllChild = null;
/** Child node for wildcard segments (e.g., '*') */
wildcardChild = null;
/** Total number of direct children */
childCount = 0;
/** Number of non-static children */
dynamicChildCount = 0;
/**
* Creates a new Node instance.
* @param params Initialization parameters including name and endpoint status.
*/
constructor(t) {
this.name = t.name, this.endpoint = t.endpoint, this.store = t.store;
let { type: s, paramName: a } = o.resolveSegmentType(this.name);
this.type = s, this.paramName = a;
}
/**
* Derives the NodeType and extracted parameter name from a raw path segment.
*
* Supported segment syntaxes:
* - `:id` → Dynamic (paramName: 'id')
* - `*` → Wildcard (paramName: '*')
* - `[id]` → Dynamic (paramName: 'id')
* - `[...id]` → CatchAll (paramName: 'id')
* - `[[...id]]` → OptionalCatchAll (paramName: 'id')
* - Otherwise → Static (paramName: '')
*
* @param segment The raw segment string to analyze.
* @returns An object containing the derived type and parameter name.
*/
static resolveSegmentType(t) {
let s = t.charCodeAt(0);
if (s === 58)
return {
type: 1,
paramName: t.substring(1)
};
if (s === 42)
return {
type: 4,
paramName: "*"
};
if (s === 91) {
let a = t.charCodeAt(1);
return a === 91 ? {
type: 3,
paramName: t.substring(5, t.length - 2)
} : a === 46 ? {
type: 2,
paramName: t.substring(4, t.length - 1)
} : {
type: 1,
paramName: t.substring(1, t.length - 1)
};
}
return {
type: 0,
paramName: ""
};
}
/**
* Adds a child node to this node.
* @param child The node to add as a child.
*/
addChild(t) {
switch (this.childCount++, t.type !== 0 && this.dynamicChildCount++, t.type) {
case 0:
this.staticChildren[t.name] = t;
break;
case 1:
this.dynamicChild = t;
break;
case 2:
this.catchAllChild = t;
break;
case 3:
this.optionalCatchAllChild = t;
break;
case 4:
this.wildcardChild = t;
break;
}
}
/**
* Removes a child node from this node.
* @param child The node to remove.
*/
removeChild(t) {
switch (this.childCount--, t.type !== 0 && this.dynamicChildCount--, t.type) {
case 0:
this.staticChildren && delete this.staticChildren[t.name];
break;
case 1:
this.dynamicChild = null;
break;
case 2:
this.catchAllChild = null;
break;
case 3:
this.optionalCatchAllChild = null;
break;
case 4:
this.wildcardChild = null;
break;
}
}
};
// src/controllers/Reminist.ts
var N = new d();
function P(o) {
let t = N[o];
if (t) return t;
let s = 0, a = o.length;
o.charCodeAt(0) === 47 && (s = 1), a > 1 && o.charCodeAt(a - 1) === 47 && a--;
let i = o.substring(s, a).split("/");
return i.length === 1 && i[0] === "" ? (N[o] = [], []) : (N[o] = i, i);
}
function A(o) {
let t = o.charCodeAt(0);
return t === 58 || t === 42 || t === 91;
}
function K(o, t) {
let { type: s } = h.resolveSegmentType(t);
switch (s) {
case 1:
return o.dynamicChild ?? void 0;
case 2:
return o.catchAllChild ?? void 0;
case 3:
return o.optionalCatchAllChild ?? void 0;
case 4:
return o.wildcardChild ?? void 0;
default:
return;
}
}
var w = class {
keys = [];
rootNodes = new d();
staticNodeIndex = new d();
registeredPaths = new d();
/**
* Initializes a new Reminist router instance.
* @param options Configuration options including top-level keys.
*/
constructor(t) {
t?.keys && (this.keys = t.keys);
for (let s = 0, a = this.keys.length; s < a; s++) {
let i = this.keys[s];
this.rootNodes[i] = new h({ name: "/", endpoint: !1 }), this.staticNodeIndex[i] = new d(), this.registeredPaths[i] = new d();
}
}
/**
* Gets or initializes the root node for a specific key.
* @param key The key (e.g., 'GET') to retrieve the root for.
* @returns The root Node for the given key.
*/
getRoot(t) {
return this.rootNodes[t] ??= new h({ name: "/", endpoint: !1 });
}
/**
* Registers a new route in the router.
*
* @template P The path template string.
* @template S The type of data to store with this route.
* @param key The key group for this route (e.g., 'GET').
* @param path The route path template (e.g., '/users/:id').
* @param store The data/handler to associate with this route.
* @returns A new Reminist instance with updated context types.
*/
add(t, s, a) {
let i = P(s), e = i.every((r) => !A(r));
if (this.registeredPaths[t] && (this.registeredPaths[t][s] = !0), e) {
this.staticNodeIndex[t] ??= new d();
let r = new h({ name: s, endpoint: !0, store: a });
return this.staticNodeIndex[t][s] = r, this;
}
let n = this.getRoot(t);
for (let r = 0, l = i.length; r < l; r++) {
let c = i[r], m = n.staticChildren[c];
if (!m && A(c)) {
let u = K(n, c);
if (u && u.name !== c) {
let p = `/${i.slice(0, r).concat(u.name).join("/")}`;
throw new Error(
`\x1B[33m[Reminist] There are two conflicting routes: /${i.join("/")} and ${p} use different dynamic parameters.\x1B[0m`
);
}
m = u;
}
if (m) {
n = m;
continue;
}
let C = new h({
name: c,
endpoint: !1
});
n.addChild(C), n = C;
}
return n.endpoint = !0, n.store = a, this;
}
/**
* Finds a registered route matching the given path.
*
* @template P The literal path string to search for.
* @param key The key group to search within (e.g., 'GET').
* @param path The actual path to match.
* @returns An object containing the matched node (if any) and extracted parameters.
*/
find(t, s) {
let a = this.staticNodeIndex[t]?.[s];
if (a)
return { node: a, params: {} };
let i = P(s), e = this.getRoot(t), n;
for (let l = 0; l < i.length; l++) {
let c = i[l], m = e.staticChildren[c];
if (m) {
e = m;
continue;
}
if (e.dynamicChildCount === 0)
return { node: null, params: n ?? {} };
let { dynamicChild: C, catchAllChild: u, optionalCatchAllChild: p, wildcardChild: b } = e;
if (C) {
n ??= new d(), n[C.paramName] = c, e = C;
continue;
}
return u ? (n ??= new d(), n[u.paramName] = i.slice(l).join("/"), {
node: u,
params: n
}) : p ? (n ??= new d(), n[p.paramName] = i.slice(l).join("/"), {
node: p,
params: n
}) : b ? (n ??= new d(), n["*"] = i.slice(l).join("/"), {
node: b,
params: n
}) : { node: null, params: n ?? {} };
}
if (e.endpoint)
return {
node: e,
params: n ?? {}
};
let { optionalCatchAllChild: r } = e;
return r?.endpoint ? (n ??= new d(), n[r.paramName] = "", {
node: r,
params: n
}) : { node: null, params: n ?? {} };
}
/**
* Checks if a route exists for the given path.
*
* @param key The key group to check.
* @param path The path to check for.
* @returns True if the path is registered.
*/
has(t, s) {
let { node: a } = this.find(t, s);
return a !== null && a.endpoint;
}
/**
* Deletes a registered route.
*
* @template P The route path to delete.
* @param key The key group containing the route.
* @param path The path template to remove.
* @returns The Reminist instance with updated context types.
*/
delete(t, s) {
if (this.registeredPaths[t] && delete this.registeredPaths[t][s], this.staticNodeIndex[t]?.[s])
return delete this.staticNodeIndex[t][s], this;
let a = P(s);
if (a.length === 0 && s !== "/") return this;
let i = [this.getRoot(t)], e = i[0];
for (let n = 0, r = a.length; n < r; n++) {
let l = a[n], c = e.staticChildren[l] ?? (e.dynamicChild?.name === l ? e.dynamicChild : void 0) ?? (e.catchAllChild?.name === l ? e.catchAllChild : void 0) ?? (e.optionalCatchAllChild?.name === l ? e.optionalCatchAllChild : void 0) ?? (e.wildcardChild?.name === l ? e.wildcardChild : void 0);
if (!c) return this;
e = c, i.push(e);
}
if (!e.endpoint) return this;
e.endpoint = !1, e.store = void 0;
for (let n = i.length - 1; n > 0; n--) {
let r = i[n], l = i[n - 1];
if (!r.endpoint && r.childCount === 0)
l.removeChild(r);
else
break;
}
return this;
}
getRoutes(t) {
if (t) {
let a = this.registeredPaths[t];
return a ? Object.keys(a) : [];
}
let s = new d();
for (let a = 0, i = this.keys.length; a < i; a++) {
let e = this.keys[a], n = this.registeredPaths[e];
s[e] = n ? Object.keys(n) : [];
}
return s;
}
};
export {
h as Node,
f as NodeType,
d as NullProtoObj,
w as Reminist
};