patram 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/patram.js +169 -0
- package/bin/patram.test.js +184 -0
- package/lib/build-graph.js +222 -0
- package/lib/build-graph.test.js +141 -0
- package/lib/build-graph.types.ts +23 -0
- package/lib/check-graph.js +125 -0
- package/lib/check-graph.test.js +103 -0
- package/lib/list-source-files.js +44 -0
- package/lib/list-source-files.test.js +101 -0
- package/lib/load-patram-config.js +244 -0
- package/lib/load-patram-config.test.js +211 -0
- package/lib/load-patram-config.types.ts +23 -0
- package/lib/parse-claims.js +178 -0
- package/lib/parse-claims.test.js +113 -0
- package/lib/parse-claims.types.ts +27 -0
- package/lib/patram-config.js +194 -0
- package/lib/patram-config.test.js +147 -0
- package/lib/patram-config.types.ts +33 -0
- package/package.json +64 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import graph_v0_config from '../docs/graph-v0.config.json' with { type: 'json' };
|
|
8
|
+
import { buildGraph } from './build-graph.js';
|
|
9
|
+
import { parseClaims } from './parse-claims.js';
|
|
10
|
+
import { parsePatramConfig } from './patram-config.js';
|
|
11
|
+
|
|
12
|
+
const patram_config = parsePatramConfig(graph_v0_config);
|
|
13
|
+
|
|
14
|
+
it('builds document nodes, configured target nodes and edges from claims', () => {
|
|
15
|
+
const graph = buildGraph(patram_config, createMarkdownClaims());
|
|
16
|
+
|
|
17
|
+
expect(graph).toEqual(createExpectedGraph());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps source document nodes even when a claim has no mapping', () => {
|
|
21
|
+
const graph = buildGraph(patram_config, [
|
|
22
|
+
{
|
|
23
|
+
document_id: 'doc:docs/orphan.md',
|
|
24
|
+
id: 'claim:doc:docs/orphan.md:1',
|
|
25
|
+
origin: {
|
|
26
|
+
column: 1,
|
|
27
|
+
line: 2,
|
|
28
|
+
path: 'docs/orphan.md',
|
|
29
|
+
},
|
|
30
|
+
type: 'directive',
|
|
31
|
+
value: 'ignored',
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
expect(graph).toEqual({
|
|
36
|
+
edges: [],
|
|
37
|
+
nodes: {
|
|
38
|
+
'doc:docs/orphan.md': {
|
|
39
|
+
id: 'doc:docs/orphan.md',
|
|
40
|
+
kind: 'document',
|
|
41
|
+
path: 'docs/orphan.md',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create markdown claims for graph materialization tests.
|
|
49
|
+
*
|
|
50
|
+
* @returns {PatramClaim[]}
|
|
51
|
+
*/
|
|
52
|
+
function createMarkdownClaims() {
|
|
53
|
+
return parseClaims({
|
|
54
|
+
path: 'docs/patram.md',
|
|
55
|
+
source: createMarkdownSource(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create markdown input for graph materialization tests.
|
|
61
|
+
*
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function createMarkdownSource() {
|
|
65
|
+
return [
|
|
66
|
+
'# Patram',
|
|
67
|
+
'',
|
|
68
|
+
'Read the [graph design](./graph-v0.md).',
|
|
69
|
+
'Defined by: terms/patram.md',
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create the expected graph for the materialization test.
|
|
75
|
+
*
|
|
76
|
+
* @returns {object}
|
|
77
|
+
*/
|
|
78
|
+
function createExpectedGraph() {
|
|
79
|
+
return {
|
|
80
|
+
edges: createExpectedEdges(),
|
|
81
|
+
nodes: createExpectedNodes(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create the expected edges for the materialization test.
|
|
87
|
+
*
|
|
88
|
+
* @returns {object[]}
|
|
89
|
+
*/
|
|
90
|
+
function createExpectedEdges() {
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
from: 'doc:docs/patram.md',
|
|
94
|
+
id: 'edge:1',
|
|
95
|
+
origin: {
|
|
96
|
+
column: 10,
|
|
97
|
+
line: 3,
|
|
98
|
+
path: 'docs/patram.md',
|
|
99
|
+
},
|
|
100
|
+
relation: 'links_to',
|
|
101
|
+
to: 'doc:docs/graph-v0.md',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
from: 'doc:docs/patram.md',
|
|
105
|
+
id: 'edge:2',
|
|
106
|
+
origin: {
|
|
107
|
+
column: 1,
|
|
108
|
+
line: 4,
|
|
109
|
+
path: 'docs/patram.md',
|
|
110
|
+
},
|
|
111
|
+
relation: 'defines',
|
|
112
|
+
to: 'term:docs/terms/patram.md',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create the expected nodes for the materialization test.
|
|
119
|
+
*
|
|
120
|
+
* @returns {object}
|
|
121
|
+
*/
|
|
122
|
+
function createExpectedNodes() {
|
|
123
|
+
return {
|
|
124
|
+
'doc:docs/graph-v0.md': {
|
|
125
|
+
id: 'doc:docs/graph-v0.md',
|
|
126
|
+
kind: 'document',
|
|
127
|
+
path: 'docs/graph-v0.md',
|
|
128
|
+
},
|
|
129
|
+
'doc:docs/patram.md': {
|
|
130
|
+
id: 'doc:docs/patram.md',
|
|
131
|
+
kind: 'document',
|
|
132
|
+
path: 'docs/patram.md',
|
|
133
|
+
title: 'Patram',
|
|
134
|
+
},
|
|
135
|
+
'term:docs/terms/patram.md': {
|
|
136
|
+
id: 'term:docs/terms/patram.md',
|
|
137
|
+
key: 'docs/terms/patram.md',
|
|
138
|
+
kind: 'term',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClaimOrigin } from './parse-claims.types.ts';
|
|
2
|
+
|
|
3
|
+
export interface GraphNode {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
key?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
[field: string]: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GraphEdge {
|
|
13
|
+
from: string;
|
|
14
|
+
id: string;
|
|
15
|
+
origin: ClaimOrigin;
|
|
16
|
+
relation: string;
|
|
17
|
+
to: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BuildGraphResult {
|
|
21
|
+
edges: GraphEdge[];
|
|
22
|
+
nodes: Record<string, GraphNode>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult, GraphEdge, GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check a materialized graph for broken document links and missing edge nodes.
|
|
8
|
+
*
|
|
9
|
+
* @param {BuildGraphResult} graph
|
|
10
|
+
* @param {string[]} source_file_paths
|
|
11
|
+
* @returns {PatramDiagnostic[]}
|
|
12
|
+
*/
|
|
13
|
+
export function checkGraph(graph, source_file_paths) {
|
|
14
|
+
/** @type {PatramDiagnostic[]} */
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
const source_file_path_set = new Set(source_file_paths);
|
|
17
|
+
|
|
18
|
+
for (const graph_edge of graph.edges) {
|
|
19
|
+
const source_node = graph.nodes[graph_edge.from];
|
|
20
|
+
const target_node = graph.nodes[graph_edge.to];
|
|
21
|
+
|
|
22
|
+
collectMissingNodeDiagnostics(
|
|
23
|
+
diagnostics,
|
|
24
|
+
graph_edge,
|
|
25
|
+
source_node,
|
|
26
|
+
target_node,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!target_node) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
collectBrokenLinkDiagnostics(
|
|
34
|
+
diagnostics,
|
|
35
|
+
graph_edge,
|
|
36
|
+
target_node,
|
|
37
|
+
source_file_path_set,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return diagnostics;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
46
|
+
* @param {GraphEdge} graph_edge
|
|
47
|
+
* @param {GraphNode | undefined} source_node
|
|
48
|
+
* @param {GraphNode | undefined} target_node
|
|
49
|
+
*/
|
|
50
|
+
function collectMissingNodeDiagnostics(
|
|
51
|
+
diagnostics,
|
|
52
|
+
graph_edge,
|
|
53
|
+
source_node,
|
|
54
|
+
target_node,
|
|
55
|
+
) {
|
|
56
|
+
if (!source_node) {
|
|
57
|
+
diagnostics.push(
|
|
58
|
+
createDiagnostic(
|
|
59
|
+
graph_edge,
|
|
60
|
+
'graph.edge_missing_from',
|
|
61
|
+
`Graph edge "${graph_edge.id}" references missing source node "${graph_edge.from}".`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!target_node) {
|
|
67
|
+
diagnostics.push(
|
|
68
|
+
createDiagnostic(
|
|
69
|
+
graph_edge,
|
|
70
|
+
'graph.edge_missing_to',
|
|
71
|
+
`Graph edge "${graph_edge.id}" references missing target node "${graph_edge.to}".`,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
79
|
+
* @param {GraphEdge} graph_edge
|
|
80
|
+
* @param {GraphNode} target_node
|
|
81
|
+
* @param {Set<string>} source_file_path_set
|
|
82
|
+
*/
|
|
83
|
+
function collectBrokenLinkDiagnostics(
|
|
84
|
+
diagnostics,
|
|
85
|
+
graph_edge,
|
|
86
|
+
target_node,
|
|
87
|
+
source_file_path_set,
|
|
88
|
+
) {
|
|
89
|
+
if (graph_edge.relation !== 'links_to') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (target_node.kind !== 'document' || !target_node.path) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (source_file_path_set.has(target_node.path)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
diagnostics.push(
|
|
102
|
+
createDiagnostic(
|
|
103
|
+
graph_edge,
|
|
104
|
+
'graph.link_broken',
|
|
105
|
+
`Document link target "${target_node.path}" was not found.`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {GraphEdge} graph_edge
|
|
112
|
+
* @param {string} code
|
|
113
|
+
* @param {string} message
|
|
114
|
+
* @returns {PatramDiagnostic}
|
|
115
|
+
*/
|
|
116
|
+
function createDiagnostic(graph_edge, code, message) {
|
|
117
|
+
return {
|
|
118
|
+
code,
|
|
119
|
+
column: graph_edge.origin.column,
|
|
120
|
+
level: 'error',
|
|
121
|
+
line: graph_edge.origin.line,
|
|
122
|
+
message,
|
|
123
|
+
path: graph_edge.origin.path,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult } from './build-graph.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
import { expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { checkGraph } from './check-graph.js';
|
|
7
|
+
|
|
8
|
+
it('reports broken document links', () => {
|
|
9
|
+
const diagnostics = checkGraph(createGraphWithBrokenLink(), [
|
|
10
|
+
'docs/patram.md',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
expect(diagnostics).toEqual([
|
|
14
|
+
{
|
|
15
|
+
code: 'graph.link_broken',
|
|
16
|
+
column: 10,
|
|
17
|
+
level: 'error',
|
|
18
|
+
line: 3,
|
|
19
|
+
message: 'Document link target "docs/missing.md" was not found.',
|
|
20
|
+
path: 'docs/patram.md',
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports graph edges that reference missing nodes', () => {
|
|
26
|
+
const diagnostics = checkGraph(createGraphWithMissingTargetNode(), [
|
|
27
|
+
'docs/patram.md',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
expect(diagnostics).toEqual([
|
|
31
|
+
{
|
|
32
|
+
code: 'graph.edge_missing_to',
|
|
33
|
+
column: 8,
|
|
34
|
+
level: 'error',
|
|
35
|
+
line: 6,
|
|
36
|
+
message:
|
|
37
|
+
'Graph edge "edge:1" references missing target node "doc:docs/missing.md".',
|
|
38
|
+
path: 'docs/patram.md',
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @returns {BuildGraphResult}
|
|
45
|
+
*/
|
|
46
|
+
function createGraphWithBrokenLink() {
|
|
47
|
+
return {
|
|
48
|
+
edges: [
|
|
49
|
+
{
|
|
50
|
+
from: 'doc:docs/patram.md',
|
|
51
|
+
id: 'edge:1',
|
|
52
|
+
origin: {
|
|
53
|
+
column: 10,
|
|
54
|
+
line: 3,
|
|
55
|
+
path: 'docs/patram.md',
|
|
56
|
+
},
|
|
57
|
+
relation: 'links_to',
|
|
58
|
+
to: 'doc:docs/missing.md',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
nodes: {
|
|
62
|
+
'doc:docs/missing.md': {
|
|
63
|
+
id: 'doc:docs/missing.md',
|
|
64
|
+
kind: 'document',
|
|
65
|
+
path: 'docs/missing.md',
|
|
66
|
+
},
|
|
67
|
+
'doc:docs/patram.md': {
|
|
68
|
+
id: 'doc:docs/patram.md',
|
|
69
|
+
kind: 'document',
|
|
70
|
+
path: 'docs/patram.md',
|
|
71
|
+
title: 'Patram',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @returns {BuildGraphResult}
|
|
79
|
+
*/
|
|
80
|
+
function createGraphWithMissingTargetNode() {
|
|
81
|
+
return {
|
|
82
|
+
edges: [
|
|
83
|
+
{
|
|
84
|
+
from: 'doc:docs/patram.md',
|
|
85
|
+
id: 'edge:1',
|
|
86
|
+
origin: {
|
|
87
|
+
column: 8,
|
|
88
|
+
line: 6,
|
|
89
|
+
path: 'docs/patram.md',
|
|
90
|
+
},
|
|
91
|
+
relation: 'links_to',
|
|
92
|
+
to: 'doc:docs/missing.md',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
nodes: {
|
|
96
|
+
'doc:docs/patram.md': {
|
|
97
|
+
id: 'doc:docs/patram.md',
|
|
98
|
+
kind: 'document',
|
|
99
|
+
path: 'docs/patram.md',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { glob } from 'node:fs/promises';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* List source files matched by Patram include globs.
|
|
6
|
+
*
|
|
7
|
+
* @param {string[]} include_patterns
|
|
8
|
+
* @param {string} [project_directory]
|
|
9
|
+
* @returns {Promise<string[]>}
|
|
10
|
+
*/
|
|
11
|
+
export async function listSourceFiles(
|
|
12
|
+
include_patterns,
|
|
13
|
+
project_directory = process.cwd(),
|
|
14
|
+
) {
|
|
15
|
+
/** @type {Set<string>} */
|
|
16
|
+
const source_file_paths = new Set();
|
|
17
|
+
|
|
18
|
+
for (const include_pattern of include_patterns) {
|
|
19
|
+
for await (const matched_path of glob(include_pattern, {
|
|
20
|
+
cwd: project_directory,
|
|
21
|
+
})) {
|
|
22
|
+
source_file_paths.add(normalizeRepoRelativePath(matched_path));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [...source_file_paths].sort(comparePaths);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} source_path
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function normalizeRepoRelativePath(source_path) {
|
|
34
|
+
return source_path.replaceAll('\\', '/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} left_path
|
|
39
|
+
* @param {string} right_path
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
function comparePaths(left_path, right_path) {
|
|
43
|
+
return left_path.localeCompare(right_path, 'en');
|
|
44
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { listSourceFiles } from './list-source-files.js';
|
|
8
|
+
|
|
9
|
+
const test_context = createTestContext();
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await cleanupTestContext(test_context);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('lists matching source files as sorted repo-relative paths', async () => {
|
|
16
|
+
test_context.project_directory = await createTempProjectDirectory();
|
|
17
|
+
|
|
18
|
+
await writeProjectFile(test_context.project_directory, 'docs/zeta.md');
|
|
19
|
+
await writeProjectFile(test_context.project_directory, 'docs/alpha.md');
|
|
20
|
+
await writeProjectFile(
|
|
21
|
+
test_context.project_directory,
|
|
22
|
+
'docs/nested/overview.md',
|
|
23
|
+
);
|
|
24
|
+
await writeProjectFile(test_context.project_directory, 'docs/notes.txt');
|
|
25
|
+
await writeProjectFile(test_context.project_directory, 'notes/todo.md');
|
|
26
|
+
|
|
27
|
+
const source_files = await listSourceFiles(
|
|
28
|
+
['docs/**/*.md'],
|
|
29
|
+
test_context.project_directory,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(source_files).toEqual([
|
|
33
|
+
'docs/alpha.md',
|
|
34
|
+
'docs/nested/overview.md',
|
|
35
|
+
'docs/zeta.md',
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('deduplicates paths matched by multiple include globs', async () => {
|
|
40
|
+
test_context.project_directory = await createTempProjectDirectory();
|
|
41
|
+
|
|
42
|
+
await writeProjectFile(test_context.project_directory, 'docs/patram.md');
|
|
43
|
+
|
|
44
|
+
const source_files = await listSourceFiles(
|
|
45
|
+
['docs/**/*.md', 'docs/patram.md'],
|
|
46
|
+
test_context.project_directory,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(source_files).toEqual(['docs/patram.md']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a temporary project directory.
|
|
54
|
+
*
|
|
55
|
+
* @returns {Promise<string>}
|
|
56
|
+
*/
|
|
57
|
+
async function createTempProjectDirectory() {
|
|
58
|
+
return mkdtemp(join(tmpdir(), 'patram-list-source-files-'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write a file under the temporary project directory.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} project_directory
|
|
65
|
+
* @param {string} relative_path
|
|
66
|
+
*/
|
|
67
|
+
async function writeProjectFile(project_directory, relative_path) {
|
|
68
|
+
const file_path = join(project_directory, relative_path);
|
|
69
|
+
const parent_directory = file_path.slice(0, file_path.lastIndexOf('/'));
|
|
70
|
+
|
|
71
|
+
await mkdir(parent_directory, { recursive: true });
|
|
72
|
+
await writeFile(file_path, `# ${relative_path}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove a temporary directory tree.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} project_directory
|
|
79
|
+
*/
|
|
80
|
+
async function removeDirectory(project_directory) {
|
|
81
|
+
await rm(project_directory, { force: true, recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @returns {{ project_directory: string | null }}
|
|
86
|
+
*/
|
|
87
|
+
function createTestContext() {
|
|
88
|
+
return {
|
|
89
|
+
project_directory: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {{ project_directory: string | null }} test_context
|
|
95
|
+
*/
|
|
96
|
+
async function cleanupTestContext(test_context) {
|
|
97
|
+
if (test_context.project_directory) {
|
|
98
|
+
await removeDirectory(test_context.project_directory);
|
|
99
|
+
test_context.project_directory = null;
|
|
100
|
+
}
|
|
101
|
+
}
|