Skip to content

Commit e5e9077

Browse files
authored
Refactor render map helpers (#833)
1 parent b47e3cb commit e5e9077

File tree

10 files changed

+348
-181
lines changed

10 files changed

+348
-181
lines changed

.changeset/fresh-sides-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/renderers-core': minor
3+
---
4+
5+
Add fragment support to `createRenderMap` and remove `fragmentToRenderMap` helper

.changeset/hip-moles-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/renderers-core': minor
3+
---
4+
5+
Rename `renderMap` helper to `createRenderMap` and return frozen objects in all render map helpers

packages/renderers-core/src/renderMap.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,57 @@ import { mapVisitor, Visitor } from '@codama/visitors-core';
44

55
import { BaseFragment } from './fragment';
66
import { writeFile } from './fs';
7-
import { Path } from './path';
7+
import { joinPath, Path } from './path';
88

99
export type RenderMap = ReadonlyMap<Path, string>;
1010

11-
export function renderMap(): RenderMap {
12-
return new Map<Path, string>();
13-
}
14-
15-
export function fragmentToRenderMap(fragment: BaseFragment, path: Path): RenderMap {
16-
const newMap = new Map<Path, string>();
17-
newMap.set(path, fragment.content);
18-
return newMap;
11+
export function createRenderMap(): RenderMap;
12+
export function createRenderMap(path: Path, content: BaseFragment | string): RenderMap;
13+
export function createRenderMap(entries: Record<Path, BaseFragment | string | undefined>): RenderMap;
14+
export function createRenderMap(
15+
pathOrEntries?: Path | Record<Path, BaseFragment | string | undefined>,
16+
content?: BaseFragment | string,
17+
): RenderMap {
18+
let entries: [Path, string][] = [];
19+
if (typeof pathOrEntries === 'string' && pathOrEntries !== undefined && content !== undefined) {
20+
entries = [[pathOrEntries, typeof content === 'string' ? content : content.content]];
21+
} else if (typeof pathOrEntries === 'object' && pathOrEntries !== null) {
22+
entries = Object.entries(pathOrEntries).flatMap(([key, value]) => {
23+
if (value === undefined) return [];
24+
return [[key, typeof value === 'string' ? value : value.content]] as const;
25+
});
26+
}
27+
return Object.freeze(new Map<Path, string>(entries));
1928
}
2029

2130
export function addToRenderMap(renderMap: RenderMap, path: Path, content: BaseFragment | string): RenderMap {
22-
const newMap = new Map(renderMap);
23-
newMap.set(path, typeof content === 'string' ? content : content.content);
24-
return newMap;
31+
return mergeRenderMaps([renderMap, createRenderMap(path, content)]);
2532
}
2633

2734
export function removeFromRenderMap(renderMap: RenderMap, path: Path): RenderMap {
2835
const newMap = new Map(renderMap);
2936
newMap.delete(path);
30-
return newMap;
37+
return Object.freeze(newMap);
3138
}
3239

3340
export function mergeRenderMaps(renderMaps: RenderMap[]): RenderMap {
41+
if (renderMaps.length === 0) return createRenderMap();
42+
if (renderMaps.length === 1) return renderMaps[0];
3443
const merged = new Map(renderMaps[0]);
3544
for (const map of renderMaps.slice(1)) {
3645
for (const [key, value] of map) {
3746
merged.set(key, value);
3847
}
3948
}
40-
return merged;
49+
return Object.freeze(merged);
4150
}
4251

4352
export function mapRenderMapContent(renderMap: RenderMap, fn: (content: string) => string): RenderMap {
4453
const newMap = new Map<Path, string>();
4554
for (const [key, value] of renderMap) {
4655
newMap.set(key, fn(value));
4756
}
48-
return newMap;
57+
return Object.freeze(newMap);
4958
}
5059

5160
export async function mapRenderMapContentAsync(
@@ -55,7 +64,7 @@ export async function mapRenderMapContentAsync(
5564
const entries = await Promise.all([
5665
...[...renderMap.entries()].map(async ([key, value]) => [key, await fn(value)] as const),
5766
]);
58-
return new Map<Path, string>(entries);
67+
return Object.freeze(new Map<Path, string>(entries));
5968
}
6069

6170
export function getFromRenderMap(renderMap: RenderMap, path: Path): string {
@@ -71,9 +80,9 @@ export function renderMapContains(renderMap: RenderMap, path: Path, value: RegEx
7180
return typeof value === 'string' ? content.includes(value) : value.test(content);
7281
}
7382

74-
export function writeRenderMap(renderMap: RenderMap, basePath: string): void {
83+
export function writeRenderMap(renderMap: RenderMap, basePath: Path): void {
7584
renderMap.forEach((content, relativePath) => {
76-
writeFile(`${basePath}/${relativePath}`, content);
85+
writeFile(joinPath(basePath, relativePath), content);
7786
});
7887
}
7988

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { assert, describe, expect, test } from 'vitest';
2+
3+
import {
4+
addToRenderMap,
5+
BaseFragment,
6+
createRenderMap,
7+
mapRenderMapContent,
8+
mapRenderMapContentAsync,
9+
mergeRenderMaps,
10+
removeFromRenderMap,
11+
} from '../src';
12+
13+
describe('createRenderMap', () => {
14+
test('it creates an empty render map', () => {
15+
expect(createRenderMap()).toStrictEqual(new Map());
16+
});
17+
18+
test('it creates a render map from a path and a string', () => {
19+
expect(createRenderMap('some/path', 'Some content')).toStrictEqual(new Map([['some/path', 'Some content']]));
20+
});
21+
22+
test('it creates a render map from a path and a fragment', () => {
23+
const fragment: BaseFragment = { content: 'Some fragment content' };
24+
expect(createRenderMap('some/fragment/path', fragment)).toStrictEqual(
25+
new Map([['some/fragment/path', 'Some fragment content']]),
26+
);
27+
});
28+
29+
test('it creates a render map from a record of entries', () => {
30+
expect(
31+
createRenderMap({
32+
'some/fragment/path': { content: 'Some fragment content' },
33+
'some/path': 'Some content',
34+
}),
35+
).toStrictEqual(
36+
new Map([
37+
['some/fragment/path', 'Some fragment content'],
38+
['some/path', 'Some content'],
39+
]),
40+
);
41+
});
42+
43+
test('it removes undefined entries from the provided record', () => {
44+
expect(
45+
createRenderMap({
46+
'some/path': 'Some content',
47+
'some/path/undefined': undefined,
48+
}),
49+
).toStrictEqual(new Map([['some/path', 'Some content']]));
50+
});
51+
52+
test('it freezes the returned render map', () => {
53+
assert.isFrozen(createRenderMap());
54+
assert.isFrozen(createRenderMap('some/path', 'Some content'));
55+
assert.isFrozen(createRenderMap({ 'some/path': 'Some content' }));
56+
});
57+
});
58+
59+
describe('addToRenderMap', () => {
60+
test('it adds new entries to render map', () => {
61+
const renderMap = createRenderMap('some/path', 'Some content');
62+
expect(addToRenderMap(renderMap, 'some/new/path', 'Some new content')).toStrictEqual(
63+
new Map([
64+
['some/path', 'Some content'],
65+
['some/new/path', 'Some new content'],
66+
]),
67+
);
68+
});
69+
70+
test('it overwrites existing entries in the render map', () => {
71+
const renderMap = createRenderMap('some/path', 'Some content');
72+
expect(addToRenderMap(renderMap, 'some/path', 'Some new content')).toStrictEqual(
73+
new Map([['some/path', 'Some new content']]),
74+
);
75+
});
76+
77+
test('it freezes the returned render map', () => {
78+
assert.isFrozen(addToRenderMap(createRenderMap(), 'some/new/path', 'Some new content'));
79+
});
80+
});
81+
82+
describe('removeFromRenderMap', () => {
83+
test('it removes existing entries from a render map', () => {
84+
const renderMap = createRenderMap({ pathA: 'Content A', pathB: 'Content B' });
85+
expect(removeFromRenderMap(renderMap, 'pathA')).toStrictEqual(new Map([['pathB', 'Content B']]));
86+
});
87+
88+
test('it can remove the last entry of a render map', () => {
89+
const renderMap = createRenderMap('pathA', 'Content A');
90+
expect(removeFromRenderMap(renderMap, 'pathA')).toStrictEqual(new Map());
91+
});
92+
93+
test('it ignores missing paths', () => {
94+
const renderMap = createRenderMap();
95+
expect(removeFromRenderMap(renderMap, 'missingPaths')).toStrictEqual(new Map());
96+
});
97+
98+
test('it freezes the returned render map', () => {
99+
assert.isFrozen(removeFromRenderMap(createRenderMap(), 'some/path'));
100+
});
101+
});
102+
103+
describe('mergeRenderMaps', () => {
104+
test('it returns an empty render map when no maps are provided', () => {
105+
expect(mergeRenderMaps([])).toStrictEqual(new Map());
106+
});
107+
108+
test('it returns the first render map as-is when only one map is provided', () => {
109+
const renderMap = createRenderMap('pathA', 'ContentA');
110+
expect(mergeRenderMaps([renderMap])).toBe(renderMap);
111+
});
112+
113+
test('it merges the entries of two render maps', () => {
114+
expect(
115+
mergeRenderMaps([createRenderMap('pathA', 'ContentA'), createRenderMap('pathB', 'ContentB')]),
116+
).toStrictEqual(
117+
new Map([
118+
['pathA', 'ContentA'],
119+
['pathB', 'ContentB'],
120+
]),
121+
);
122+
});
123+
124+
test('later entries overwrite earlier entries', () => {
125+
expect(
126+
mergeRenderMaps([createRenderMap('samePath', 'Old content'), createRenderMap('samePath', 'New content')]),
127+
).toStrictEqual(new Map([['samePath', 'New content']]));
128+
});
129+
130+
test('it merges the entries of two render maps', () => {
131+
expect(
132+
mergeRenderMaps([createRenderMap('pathA', 'ContentA'), createRenderMap('pathB', 'ContentB')]),
133+
).toStrictEqual(
134+
new Map([
135+
['pathA', 'ContentA'],
136+
['pathB', 'ContentB'],
137+
]),
138+
);
139+
});
140+
141+
test('it freezes the returned render map', () => {
142+
assert.isFrozen(mergeRenderMaps([]));
143+
assert.isFrozen(mergeRenderMaps([createRenderMap('pathA', 'ContentA')]));
144+
assert.isFrozen(mergeRenderMaps([createRenderMap('pathA', 'ContentA'), createRenderMap('pathB', 'ContentB')]));
145+
});
146+
});
147+
148+
describe('mapRenderMapContent', () => {
149+
test('it maps the content of all entries inside a render map', () => {
150+
expect(
151+
mapRenderMapContent(
152+
createRenderMap({
153+
pathA: 'ContentA',
154+
pathB: 'ContentB',
155+
}),
156+
content => `Mapped: ${content}`,
157+
),
158+
).toStrictEqual(
159+
new Map([
160+
['pathA', 'Mapped: ContentA'],
161+
['pathB', 'Mapped: ContentB'],
162+
]),
163+
);
164+
});
165+
166+
test('it freezes the returned render map', () => {
167+
assert.isFrozen(mapRenderMapContent(createRenderMap(), c => c));
168+
});
169+
});
170+
171+
describe('mapRenderMapContentAsync', () => {
172+
test('it maps the content of all entries inside a render map', async () => {
173+
expect(
174+
await mapRenderMapContentAsync(
175+
createRenderMap({
176+
pathA: 'ContentA',
177+
pathB: 'ContentB',
178+
}),
179+
content => Promise.resolve(`Mapped: ${content}`),
180+
),
181+
).toStrictEqual(
182+
new Map([
183+
['pathA', 'Mapped: ContentA'],
184+
['pathB', 'Mapped: ContentB'],
185+
]),
186+
);
187+
});
188+
189+
test('it freezes the returned render map', async () => {
190+
assert.isFrozen(await mapRenderMapContentAsync(createRenderMap(), c => Promise.resolve(c)));
191+
});
192+
});

packages/renderers-demo/src/visitors/getRenderMapVisitor.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { camelCase } from '@codama/nodes';
2-
import { addToRenderMap, fragmentToRenderMap, mergeRenderMaps, renderMap } from '@codama/renderers-core';
2+
import { createRenderMap, mergeRenderMaps } from '@codama/renderers-core';
33
import {
44
extendVisitor,
55
getByteSizeVisitor,
@@ -34,49 +34,49 @@ export function getRenderMapVisitor(options: RenderMapOptions = {}) {
3434
const byteSizeVisitor = getByteSizeVisitor(linkables, { stack });
3535

3636
return pipe(
37-
staticVisitor(() => renderMap(), {
37+
staticVisitor(() => createRenderMap(), {
3838
keys: ['rootNode', 'programNode', 'pdaNode', 'accountNode', 'definedTypeNode', 'instructionNode'],
3939
}),
4040
v =>
4141
extendVisitor(v, {
4242
visitAccount(node) {
4343
const pda = node.pda ? linkables.get([...stack.getPath(), node.pda]) : undefined;
4444
const size = visit(node, byteSizeVisitor);
45-
return fragmentToRenderMap(
46-
getAccountPageFragment(node, typeVisitor, size ?? undefined, pda),
45+
return createRenderMap(
4746
`accounts/${camelCase(node.name)}.${extension}`,
47+
getAccountPageFragment(node, typeVisitor, size ?? undefined, pda),
4848
);
4949
},
5050

5151
visitDefinedType(node) {
52-
return fragmentToRenderMap(
53-
getDefinedTypePageFragment(node, typeVisitor),
52+
return createRenderMap(
5453
`definedTypes/${camelCase(node.name)}.${extension}`,
54+
getDefinedTypePageFragment(node, typeVisitor),
5555
);
5656
},
5757

5858
visitInstruction(node) {
59-
return fragmentToRenderMap(
60-
getInstructionPageFragment(node, typeVisitor),
59+
return createRenderMap(
6160
`instructions/${camelCase(node.name)}.${extension}`,
61+
getInstructionPageFragment(node, typeVisitor),
6262
);
6363
},
6464

6565
visitPda(node) {
66-
return fragmentToRenderMap(
67-
getPdaPageFragment(node, typeVisitor, valueVisitor),
66+
return createRenderMap(
6867
`pdas/${camelCase(node.name)}.${extension}`,
68+
getPdaPageFragment(node, typeVisitor, valueVisitor),
6969
);
7070
},
7171

7272
visitProgram(node, { self }) {
73-
const children = mergeRenderMaps([
73+
return mergeRenderMaps([
74+
createRenderMap(`${indexFilename}.${extension}`, getProgramPageFragment(node)),
7475
...node.accounts.map(n => visit(n, self)),
7576
...node.definedTypes.map(n => visit(n, self)),
7677
...node.instructions.map(n => visit(n, self)),
7778
...node.pdas.map(n => visit(n, self)),
7879
]);
79-
return addToRenderMap(children, `${indexFilename}.${extension}`, getProgramPageFragment(node));
8080
},
8181

8282
visitRoot(node, { self }) {

0 commit comments

Comments
 (0)