Skip to content

Commit b0d19eb

Browse files
committed
feat: util path deepTrimStartDir
1 parent c5c4bb8 commit b0d19eb

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed

src/util/deep-map.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
interface DeepMapContext {
3+
mapper: (v: any) => unknown;
4+
processed: Map<unknown, unknown>;
5+
cachedForTypes: Set<CacheForType>;
6+
}
7+
8+
function deepMapImpl(value: unknown, ctx: DeepMapContext) {
9+
if (ctx.processed.has(value)) return ctx.processed.get(value);
10+
11+
if (Array.isArray(value)) {
12+
const res: unknown[] = [];
13+
ctx.processed.set(value, res);
14+
res.push(...value.map((v) => deepMapImpl(v, ctx)));
15+
return res;
16+
}
17+
18+
if (typeof value === "object" && value !== null) {
19+
const res = {};
20+
ctx.processed.set(value, res);
21+
22+
for (const k of Reflect.ownKeys(value)) {
23+
const v: unknown = Reflect.get(value, k);
24+
const r = deepMapImpl(v, ctx);
25+
Reflect.set(res, k, r);
26+
}
27+
28+
return res;
29+
}
30+
31+
const res = ctx.mapper(value);
32+
33+
if (
34+
(value === null && ctx.cachedForTypes.has("null")) ||
35+
ctx.cachedForTypes.has(typeof value as never)
36+
) {
37+
ctx.processed.set(value, res);
38+
}
39+
40+
return res;
41+
}
42+
43+
export type CacheForType =
44+
| "string"
45+
| "number"
46+
| "symbol"
47+
| "function"
48+
| "undefined"
49+
| "null"
50+
| "bigint"
51+
| "boolean";
52+
53+
export interface DeepMapOptions {
54+
cacheForTypes?: Iterable<CacheForType>;
55+
}
56+
57+
export type AnyObjectOrArray = Record<string, unknown> | any[];
58+
59+
export type DeepMappedByMapper<
60+
T,
61+
M extends (v: Exclude<T, AnyObjectOrArray>) => unknown,
62+
> = T extends AnyObjectOrArray
63+
? { [K in keyof T]: DeepMappedByMapper<T[K], M> }
64+
: M extends (v: T) => infer R
65+
? R
66+
: unknown;
67+
68+
export type DeepMapped<TV, TVR> = TV extends AnyObjectOrArray
69+
? { [K in keyof TV]: DeepMapped<TV[K], TVR> }
70+
: TVR;
71+
72+
export function deepMap<TV, TVR>(
73+
value: TV,
74+
mapper: (
75+
v: Exclude<
76+
| TV
77+
| (TV extends (infer TI)[]
78+
? TI
79+
: TV extends Record<PropertyKey, infer TP>
80+
? TP
81+
: never),
82+
AnyObjectOrArray
83+
>,
84+
) => TVR,
85+
options?: DeepMapOptions,
86+
): DeepMapped<TV, TVR> {
87+
return deepMapImpl(value, {
88+
mapper,
89+
cachedForTypes: new Set(options?.cacheForTypes),
90+
processed: new Map(),
91+
}) as never;
92+
}

src/util/path.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { trimStartDir } from "./path";
1+
import { deepTrimStartDir, trimStartDir } from "./path";
22

33
test("trimStartDir", () => {
44
const startDirVariants = [
@@ -35,3 +35,60 @@ test("trimStartDir", () => {
3535
}
3636
}
3737
});
38+
39+
test("deepTrimStartDir", () => {
40+
expect(deepTrimStartDir("./some-dir/some-file", "some-dir")).toBe(
41+
"some-file",
42+
);
43+
44+
expect(
45+
deepTrimStartDir(
46+
[
47+
"./some-dir/some-file",
48+
"dir/file2",
49+
"./some-dir/file3",
50+
{ prop: "./some-dir/file4" },
51+
],
52+
"some-dir",
53+
),
54+
).toStrictEqual(["some-file", "dir/file2", "file3", { prop: "file4" }]);
55+
56+
expect(
57+
deepTrimStartDir(
58+
{ prop: "./some-dir/file", propNum: 1, propNull: null, propObj: {} },
59+
"some-dir",
60+
),
61+
).toStrictEqual({
62+
prop: "file",
63+
propNum: 1,
64+
propNull: null,
65+
propObj: {},
66+
});
67+
68+
const nested = {
69+
num: 1,
70+
path: "./some-dir/file",
71+
obj: {
72+
path: "./some-dir/dir2/file2",
73+
propNull: null,
74+
nested: undefined as unknown,
75+
},
76+
};
77+
78+
nested.obj.nested = nested;
79+
80+
const res = {
81+
num: 1,
82+
path: "file",
83+
obj: {
84+
path: "dir2/file2",
85+
propNull: null,
86+
nested: undefined as unknown,
87+
},
88+
};
89+
90+
res.obj.nested = res;
91+
92+
expect(deepTrimStartDir(nested, "some-dir")).toStrictEqual(res);
93+
expect(res.obj.nested).toBe(res);
94+
});

src/util/path.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { posix as pp } from "path";
2+
import { deepMap } from "./deep-map";
23

34
export function joinPath(...paths: string[]): string {
45
return pp.join(...paths);
@@ -10,3 +11,29 @@ export function trimStartDir(p: string, startDir: string): string {
1011
if (res.startsWith("../")) return p;
1112
else return res || ".";
1213
}
14+
15+
export type NestedPathValue =
16+
| string
17+
| NestedPathValue[]
18+
| {
19+
[K in PropertyKey]: unknown;
20+
}
21+
| undefined
22+
| null
23+
| number
24+
| boolean
25+
| bigint
26+
| symbol
27+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28+
| ((...args: any[]) => any);
29+
30+
export function deepTrimStartDir<T extends NestedPathValue>(
31+
p: T,
32+
startDir: string,
33+
): T {
34+
return deepMap<T, unknown>(
35+
p,
36+
(v: unknown) => (typeof v === "string" ? trimStartDir(v, startDir) : v),
37+
{ cacheForTypes: ["string", "null"] },
38+
) as never;
39+
}

0 commit comments

Comments
 (0)