Skip to content

Commit 57247e6

Browse files
authored
Improve compile performance (#436)
1 parent 5844988 commit 57247e6

2 files changed

Lines changed: 67 additions & 31 deletions

File tree

src/index.bench.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, bench } from "vitest";
2-
import { match, parse } from "./index.js";
2+
import { compile, match, parse } from "./index.js";
33

44
describe("parse", () => {
55
const PATHS: string[] = [
@@ -57,3 +57,26 @@ describe("match", () => {
5757
for (const path of PATHS) ASTERISK_MATCH(path);
5858
});
5959
});
60+
61+
describe("compile", () => {
62+
const PATH_FNS = [
63+
"/api",
64+
"/user/:id",
65+
"/user/:id{/:extra}",
66+
"/files/*path",
67+
"/:param1-:param2",
68+
'/quoted-:"param1"',
69+
"/complex/:param1-:param2/*path",
70+
].map((path) => compile(path));
71+
72+
bench("compiling paths", () => {
73+
for (const fn of PATH_FNS) {
74+
fn({
75+
id: "123",
76+
param1: "param1",
77+
param2: "param2",
78+
path: ["path", "to", "file"],
79+
});
80+
}
81+
});
82+
});

src/index.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -275,33 +275,39 @@ export function compile<P extends ParamData = ParamData>(
275275
const fn = tokensToFunction(data.tokens, delimiter, encode);
276276

277277
return function path(params: P = {} as P) {
278-
const [path, ...missing] = fn(params);
278+
const missing: string[] = [];
279+
const path = fn(params, missing);
280+
279281
if (missing.length) {
280282
throw new TypeError(`Missing parameters: ${missing.join(", ")}`);
281283
}
284+
282285
return path;
283286
};
284287
}
285288

286289
export type ParamData = Partial<Record<string, string | string[]>>;
287290
export type PathFunction<P extends ParamData> = (data?: P) => string;
288291

292+
/**
293+
* Internal path builder function.
294+
*/
295+
type TokenEncoder = (data: ParamData, missing: string[]) => string;
296+
289297
function tokensToFunction(
290298
tokens: Token[],
291299
delimiter: string,
292300
encode: Encode | false,
293-
) {
301+
): TokenEncoder {
294302
const encoders = tokens.map((token) =>
295303
tokenToFunction(token, delimiter, encode),
296304
);
297305

298-
return (data: ParamData) => {
299-
const result: string[] = [""];
306+
return (data: ParamData, missing: string[]) => {
307+
let result = "";
300308

301309
for (const encoder of encoders) {
302-
const [value, ...extras] = encoder(data);
303-
result[0] += value;
304-
result.push(...extras);
310+
result += encoder(data, missing);
305311
}
306312

307313
return result;
@@ -315,55 +321,62 @@ function tokenToFunction(
315321
token: Token,
316322
delimiter: string,
317323
encode: Encode | false,
318-
): (data: ParamData) => string[] {
319-
if (token.type === "text") return () => [token.value];
324+
): TokenEncoder {
325+
if (token.type === "text") return () => token.value;
320326

321327
if (token.type === "group") {
322328
const fn = tokensToFunction(token.tokens, delimiter, encode);
323329

324-
return (data) => {
325-
const [value, ...missing] = fn(data);
326-
if (!missing.length) return [value];
327-
return [""];
330+
return (data, missing) => {
331+
const len = missing.length;
332+
const value = fn(data, missing);
333+
if (missing.length === len) return value;
334+
335+
missing.length = len; // Reset optional group.
336+
return "";
328337
};
329338
}
330339

331340
const encodeValue = encode || NOOP_VALUE;
332341

333342
if (token.type === "wildcard" && encode !== false) {
334-
return (data) => {
343+
return (data, missing) => {
335344
const value = data[token.name];
336-
if (value == null) return ["", token.name];
345+
if (value == null) {
346+
missing.push(token.name);
347+
return "";
348+
}
337349

338350
if (!Array.isArray(value) || value.length === 0) {
339351
throw new TypeError(`Expected "${token.name}" to be a non-empty array`);
340352
}
341353

342-
return [
343-
value
344-
.map((value, index) => {
345-
if (typeof value !== "string") {
346-
throw new TypeError(
347-
`Expected "${token.name}/${index}" to be a string`,
348-
);
349-
}
354+
return value
355+
.map((value, index) => {
356+
if (typeof value !== "string") {
357+
throw new TypeError(
358+
`Expected "${token.name}/${index}" to be a string`,
359+
);
360+
}
350361

351-
return encodeValue(value);
352-
})
353-
.join(delimiter),
354-
];
362+
return encodeValue(value);
363+
})
364+
.join(delimiter);
355365
};
356366
}
357367

358-
return (data) => {
368+
return (data, missing) => {
359369
const value = data[token.name];
360-
if (value == null) return ["", token.name];
370+
if (value == null) {
371+
missing.push(token.name);
372+
return "";
373+
}
361374

362375
if (typeof value !== "string") {
363376
throw new TypeError(`Expected "${token.name}" to be a string`);
364377
}
365378

366-
return [encodeValue(value)];
379+
return encodeValue(value);
367380
};
368381
}
369382

0 commit comments

Comments
 (0)