patram 0.0.2 → 0.1.1
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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +238 -0
- package/lib/build-graph.js +143 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/command-output.js +83 -0
- package/lib/layout-stored-queries.js +213 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +106 -18
- package/lib/load-patram-config.types.ts +9 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +73 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +273 -0
- package/lib/parse-cli-arguments.js +114 -0
- package/lib/parse-cli-arguments.types.ts +24 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +312 -0
- package/lib/patram-cli.js +337 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/query-graph.js +256 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-json-output.js +108 -0
- package/lib/render-output-view.js +193 -0
- package/lib/render-plain-output.js +237 -0
- package/lib/render-rich-output.js +293 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +51 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +21 -10
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { ResolvedOutputMode } from './output-view.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Ansis } from 'ansis';
|
|
7
|
+
import stringWidth from 'string-width';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render check diagnostics for one output mode.
|
|
11
|
+
*
|
|
12
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
13
|
+
* @param {ResolvedOutputMode} output_mode
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function renderCheckDiagnostics(diagnostics, output_mode) {
|
|
17
|
+
if (output_mode.renderer_name === 'json') {
|
|
18
|
+
return `${JSON.stringify(
|
|
19
|
+
{
|
|
20
|
+
diagnostics: diagnostics.map(formatJsonDiagnostic),
|
|
21
|
+
},
|
|
22
|
+
null,
|
|
23
|
+
2,
|
|
24
|
+
)}\n`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (output_mode.renderer_name === 'plain') {
|
|
28
|
+
return renderPlainCheckDiagnostics(diagnostics);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return renderRichCheckDiagnostics(diagnostics, createAnsi(output_mode));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render the success summary for one check run.
|
|
36
|
+
*
|
|
37
|
+
* @param {number} source_file_count
|
|
38
|
+
* @param {ResolvedOutputMode} output_mode
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function renderCheckSuccess(source_file_count, output_mode) {
|
|
42
|
+
if (output_mode.renderer_name === 'json') {
|
|
43
|
+
return `${JSON.stringify({ diagnostics: [] }, null, 2)}\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (output_mode.renderer_name === 'plain') {
|
|
47
|
+
return formatCheckSuccess(source_file_count);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ansi = createAnsi(output_mode);
|
|
51
|
+
return `${ansi.green('Check passed.')}\n${ansi.gray(`Scanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.`)}\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function renderPlainCheckDiagnostics(diagnostics) {
|
|
59
|
+
const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
|
|
60
|
+
const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
|
|
61
|
+
return `${grouped_diagnostics
|
|
62
|
+
.map((diagnostic_group) =>
|
|
63
|
+
formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width),
|
|
64
|
+
)
|
|
65
|
+
.join('\n\n')}\n\n${formatPlainSummary(diagnostics)}\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
70
|
+
* @param {Ansis} ansi
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function renderRichCheckDiagnostics(diagnostics, ansi) {
|
|
74
|
+
const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
|
|
75
|
+
const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
|
|
76
|
+
return `${grouped_diagnostics
|
|
77
|
+
.map((diagnostic_group) =>
|
|
78
|
+
formatRichDiagnosticGroup(
|
|
79
|
+
diagnostic_group,
|
|
80
|
+
diagnostic_prefix_width,
|
|
81
|
+
ansi,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
.join('\n\n')}\n\n${formatRichSummary(diagnostics, ansi)}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
89
|
+
* @returns {Array<{ diagnostics: PatramDiagnostic[], path: string }>}
|
|
90
|
+
*/
|
|
91
|
+
function groupDiagnosticsByPath(diagnostics) {
|
|
92
|
+
/** @type {Array<{ diagnostics: PatramDiagnostic[], path: string }>} */
|
|
93
|
+
const grouped_diagnostics = [];
|
|
94
|
+
/** @type {Map<string, { diagnostics: PatramDiagnostic[], path: string }>} */
|
|
95
|
+
const grouped_diagnostics_by_path = new Map();
|
|
96
|
+
|
|
97
|
+
for (const diagnostic of diagnostics) {
|
|
98
|
+
let diagnostic_group = grouped_diagnostics_by_path.get(diagnostic.path);
|
|
99
|
+
|
|
100
|
+
if (!diagnostic_group) {
|
|
101
|
+
diagnostic_group = {
|
|
102
|
+
diagnostics: [],
|
|
103
|
+
path: diagnostic.path,
|
|
104
|
+
};
|
|
105
|
+
grouped_diagnostics_by_path.set(diagnostic.path, diagnostic_group);
|
|
106
|
+
grouped_diagnostics.push(diagnostic_group);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
diagnostic_group.diagnostics.push(diagnostic);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return grouped_diagnostics;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
|
|
117
|
+
* @param {number} diagnostic_prefix_width
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
function formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width) {
|
|
121
|
+
return `${formatDiagnosticGroupHeader(diagnostic_group.path)}\n${diagnostic_group.diagnostics
|
|
122
|
+
.map((diagnostic) =>
|
|
123
|
+
formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width),
|
|
124
|
+
)
|
|
125
|
+
.join('\n')}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {PatramDiagnostic} diagnostic
|
|
130
|
+
* @param {number} diagnostic_prefix_width
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width) {
|
|
134
|
+
const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
|
|
135
|
+
return `${diagnostic_prefix}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${diagnostic.code}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
function formatPlainSummary(diagnostics) {
|
|
143
|
+
const error_count = countDiagnosticsByLevel(diagnostics, 'error');
|
|
144
|
+
const warning_count = countDiagnosticsByLevel(diagnostics, 'warning');
|
|
145
|
+
const problem_count = diagnostics.length;
|
|
146
|
+
|
|
147
|
+
return `\u2716 ${problem_count} ${pluralize('problem', problem_count)} (${error_count} ${pluralize('error', error_count)}, ${warning_count} ${pluralize('warning', warning_count)})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
|
|
152
|
+
* @param {number} diagnostic_prefix_width
|
|
153
|
+
* @param {Ansis} ansi
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
function formatRichDiagnosticGroup(
|
|
157
|
+
diagnostic_group,
|
|
158
|
+
diagnostic_prefix_width,
|
|
159
|
+
ansi,
|
|
160
|
+
) {
|
|
161
|
+
return `${ansi.green(formatDiagnosticGroupHeader(diagnostic_group.path))}\n${diagnostic_group.diagnostics
|
|
162
|
+
.map((diagnostic) =>
|
|
163
|
+
formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi),
|
|
164
|
+
)
|
|
165
|
+
.join('\n')}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {PatramDiagnostic} diagnostic
|
|
170
|
+
* @param {number} diagnostic_prefix_width
|
|
171
|
+
* @param {Ansis} ansi
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi) {
|
|
175
|
+
const location = `${diagnostic.line}:${diagnostic.column}`;
|
|
176
|
+
const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
|
|
177
|
+
return ` ${ansi.gray(location)} ${formatRichDiagnosticLevel(diagnostic.level, ansi)} ${diagnostic.message}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${ansi.gray(diagnostic.code)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
182
|
+
* @param {Ansis} ansi
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
function formatRichSummary(diagnostics, ansi) {
|
|
186
|
+
return ansi.red(formatPlainSummary(diagnostics));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {PatramDiagnostic} diagnostic
|
|
191
|
+
* @returns {{ code: string, column: number, level: 'error', line: number, message: string, path: string }}
|
|
192
|
+
*/
|
|
193
|
+
function formatJsonDiagnostic(diagnostic) {
|
|
194
|
+
return {
|
|
195
|
+
path: diagnostic.path,
|
|
196
|
+
line: diagnostic.line,
|
|
197
|
+
column: diagnostic.column,
|
|
198
|
+
level: diagnostic.level,
|
|
199
|
+
code: diagnostic.code,
|
|
200
|
+
message: diagnostic.message,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {number} source_file_count
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function formatCheckSuccess(source_file_count) {
|
|
209
|
+
return `Check passed.\nScanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.\n`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {PatramDiagnostic} diagnostic
|
|
214
|
+
*/
|
|
215
|
+
function formatDiagnosticPrefix(diagnostic) {
|
|
216
|
+
return ` ${diagnostic.line}:${diagnostic.column} ${diagnostic.level} ${diagnostic.message}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
221
|
+
*/
|
|
222
|
+
function measureMaxDiagnosticPrefixWidth(diagnostics) {
|
|
223
|
+
let max_width = 0;
|
|
224
|
+
|
|
225
|
+
for (const diagnostic of diagnostics) {
|
|
226
|
+
const diagnostic_prefix_width = stringWidth(
|
|
227
|
+
formatDiagnosticPrefix(diagnostic),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (diagnostic_prefix_width > max_width) {
|
|
231
|
+
max_width = diagnostic_prefix_width;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return max_width;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {string} diagnostic_prefix
|
|
240
|
+
* @param {number} diagnostic_prefix_width
|
|
241
|
+
*/
|
|
242
|
+
function createDiagnosticCodePadding(
|
|
243
|
+
diagnostic_prefix,
|
|
244
|
+
diagnostic_prefix_width,
|
|
245
|
+
) {
|
|
246
|
+
return ' '.repeat(
|
|
247
|
+
Math.max(0, diagnostic_prefix_width - stringWidth(diagnostic_prefix)),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
253
|
+
* @param {'error' | 'warning'} level
|
|
254
|
+
*/
|
|
255
|
+
function countDiagnosticsByLevel(diagnostics, level) {
|
|
256
|
+
let count = 0;
|
|
257
|
+
|
|
258
|
+
for (const diagnostic of diagnostics) {
|
|
259
|
+
if (diagnostic.level === level) {
|
|
260
|
+
count += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return count;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {'error' | 'warning'} level
|
|
269
|
+
* @param {Ansis} ansi
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
function formatRichDiagnosticLevel(level, ansi) {
|
|
273
|
+
if (level === 'warning') {
|
|
274
|
+
return ansi.yellow(level);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return ansi.red(level);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} noun
|
|
282
|
+
* @param {number} count
|
|
283
|
+
*/
|
|
284
|
+
function pluralize(noun, count) {
|
|
285
|
+
if (count === 1) {
|
|
286
|
+
return noun;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return `${noun}s`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {ResolvedOutputMode} output_mode
|
|
294
|
+
*/
|
|
295
|
+
function createAnsi(output_mode) {
|
|
296
|
+
return new Ansis(output_mode.color_enabled ? 3 : 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} path
|
|
301
|
+
*/
|
|
302
|
+
function formatDiagnosticGroupHeader(path) {
|
|
303
|
+
return `${resolveDiagnosticPathType(path)} ${path}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {string} path
|
|
308
|
+
*/
|
|
309
|
+
function resolveDiagnosticPathType(path) {
|
|
310
|
+
if (path.endsWith('.json')) {
|
|
311
|
+
return 'file';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return 'document';
|
|
315
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView } from './output-view.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render structured JSON output for one output view.
|
|
7
|
+
*
|
|
8
|
+
* @param {OutputView} output_view
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function renderJsonOutput(output_view) {
|
|
12
|
+
if (output_view.command === 'query') {
|
|
13
|
+
return `${JSON.stringify(
|
|
14
|
+
{
|
|
15
|
+
results: output_view.items.map(formatJsonQueryItem),
|
|
16
|
+
summary: {
|
|
17
|
+
shown_count: output_view.summary.count,
|
|
18
|
+
total_count: output_view.summary.total_count,
|
|
19
|
+
offset: output_view.summary.offset,
|
|
20
|
+
limit: output_view.summary.limit,
|
|
21
|
+
},
|
|
22
|
+
hints: output_view.hints,
|
|
23
|
+
},
|
|
24
|
+
null,
|
|
25
|
+
2,
|
|
26
|
+
)}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (output_view.command === 'queries') {
|
|
30
|
+
return `${JSON.stringify(
|
|
31
|
+
{
|
|
32
|
+
queries: output_view.items.map(formatJsonStoredQuery),
|
|
33
|
+
},
|
|
34
|
+
null,
|
|
35
|
+
2,
|
|
36
|
+
)}\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (output_view.command === 'show') {
|
|
40
|
+
return `${JSON.stringify(
|
|
41
|
+
{
|
|
42
|
+
source: output_view.source,
|
|
43
|
+
resolved_links: output_view.items.map(formatJsonResolvedLink),
|
|
44
|
+
},
|
|
45
|
+
null,
|
|
46
|
+
2,
|
|
47
|
+
)}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error('Unsupported output view command.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {OutputNodeItem} output_item
|
|
55
|
+
* @returns {{ id: string, kind: string, title: string, path: string, status?: string }}
|
|
56
|
+
*/
|
|
57
|
+
function formatJsonQueryItem(output_item) {
|
|
58
|
+
/** @type {{ id: string, kind: string, title: string, path: string, status?: string }} */
|
|
59
|
+
const query_item = {
|
|
60
|
+
id: output_item.id,
|
|
61
|
+
kind: output_item.node_kind,
|
|
62
|
+
title: output_item.title,
|
|
63
|
+
path: output_item.path,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (output_item.status) {
|
|
67
|
+
query_item.status = output_item.status;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return query_item;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {OutputStoredQueryItem} output_item
|
|
75
|
+
* @returns {{ name: string, where: string }}
|
|
76
|
+
*/
|
|
77
|
+
function formatJsonStoredQuery(output_item) {
|
|
78
|
+
return {
|
|
79
|
+
name: output_item.name,
|
|
80
|
+
where: output_item.where,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {OutputResolvedLinkItem} output_item
|
|
86
|
+
* @returns {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }}
|
|
87
|
+
*/
|
|
88
|
+
function formatJsonResolvedLink(output_item) {
|
|
89
|
+
/** @type {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }} */
|
|
90
|
+
const resolved_link = {
|
|
91
|
+
reference: output_item.reference,
|
|
92
|
+
label: output_item.label,
|
|
93
|
+
target: {
|
|
94
|
+
title: output_item.target.title,
|
|
95
|
+
path: output_item.target.path,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (output_item.target.kind) {
|
|
100
|
+
resolved_link.target.kind = output_item.target.kind;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (output_item.target.status) {
|
|
104
|
+
resolved_link.target.status = output_item.target.status;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return resolved_link;
|
|
108
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
|
|
2
|
+
/**
|
|
3
|
+
* @import { GraphNode } from './build-graph.types.ts';
|
|
4
|
+
* @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
|
|
5
|
+
* @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderJsonOutput } from './render-json-output.js';
|
|
9
|
+
import { renderPlainOutput } from './render-plain-output.js';
|
|
10
|
+
import { renderRichOutput } from './render-rich-output.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared command output views.
|
|
14
|
+
*
|
|
15
|
+
* Normalizes `query`, `queries`, and `show` results into renderer-specific
|
|
16
|
+
* output models.
|
|
17
|
+
*
|
|
18
|
+
* Kind: output
|
|
19
|
+
* Status: active
|
|
20
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
21
|
+
* Decided by: ../docs/decisions/cli-output-architecture.md
|
|
22
|
+
* @patram
|
|
23
|
+
* @see {@link ./show-document.js}
|
|
24
|
+
* @see {@link ../docs/decisions/cli-output-architecture.md}
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a shared output view from one command result.
|
|
29
|
+
*
|
|
30
|
+
* @param {'query' | 'queries'} command_name
|
|
31
|
+
* @param {GraphNode[] | { name: string, where: string }[]} command_items
|
|
32
|
+
* @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
|
|
33
|
+
* @returns {OutputView}
|
|
34
|
+
*/
|
|
35
|
+
export function createOutputView(command_name, command_items, command_options) {
|
|
36
|
+
if (command_name === 'query') {
|
|
37
|
+
return createQueryOutputView(
|
|
38
|
+
/** @type {GraphNode[]} */ (command_items),
|
|
39
|
+
command_options,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (command_name === 'queries') {
|
|
44
|
+
return createStoredQueriesOutputView(
|
|
45
|
+
/** @type {OutputStoredQueryItem[]} */ (command_items),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(`Unsupported output view command "${command_name}".`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a shared output view for the show command.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }} show_output
|
|
56
|
+
* @returns {ShowOutputView}
|
|
57
|
+
*/
|
|
58
|
+
export function createShowOutputView(show_output) {
|
|
59
|
+
return {
|
|
60
|
+
command: 'show',
|
|
61
|
+
hints: [],
|
|
62
|
+
items: show_output.resolved_links.map((resolved_link) => ({
|
|
63
|
+
kind: 'resolved_link',
|
|
64
|
+
label: resolved_link.label,
|
|
65
|
+
reference: resolved_link.reference,
|
|
66
|
+
target: createResolvedLinkTarget(resolved_link.target),
|
|
67
|
+
})),
|
|
68
|
+
path: show_output.path,
|
|
69
|
+
rendered_source: show_output.rendered_source,
|
|
70
|
+
source: show_output.source,
|
|
71
|
+
summary: {
|
|
72
|
+
count: show_output.resolved_links.length,
|
|
73
|
+
kind: 'resolved_link_list',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render one shared output view through the resolved renderer.
|
|
80
|
+
*
|
|
81
|
+
* @param {OutputView} output_view
|
|
82
|
+
* @param {ResolvedOutputMode} output_mode
|
|
83
|
+
* @param {ParsedCliArguments} parsed_arguments
|
|
84
|
+
* @returns {Promise<string>}
|
|
85
|
+
*/
|
|
86
|
+
export async function renderOutputView(
|
|
87
|
+
output_view,
|
|
88
|
+
output_mode,
|
|
89
|
+
parsed_arguments,
|
|
90
|
+
) {
|
|
91
|
+
if (output_mode.renderer_name === 'json') {
|
|
92
|
+
return renderJsonOutput(output_view);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (output_mode.renderer_name === 'plain') {
|
|
96
|
+
return renderPlainOutput(output_view);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return renderRichOutput(output_view, {
|
|
100
|
+
color_enabled: output_mode.color_enabled,
|
|
101
|
+
color_mode: parsed_arguments.color_mode,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {GraphNode[]} graph_nodes
|
|
107
|
+
* @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
|
|
108
|
+
* @returns {OutputView}
|
|
109
|
+
*/
|
|
110
|
+
function createQueryOutputView(graph_nodes, command_options = {}) {
|
|
111
|
+
const total_count = command_options.total_count ?? graph_nodes.length;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
command: 'query',
|
|
115
|
+
hints:
|
|
116
|
+
command_options.hints ??
|
|
117
|
+
(total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
|
|
118
|
+
items: graph_nodes.map(createOutputNodeItem),
|
|
119
|
+
summary: {
|
|
120
|
+
count: graph_nodes.length,
|
|
121
|
+
kind: 'result_list',
|
|
122
|
+
limit: command_options.limit ?? graph_nodes.length,
|
|
123
|
+
offset: command_options.offset ?? 0,
|
|
124
|
+
total_count,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {{ name: string, where: string }[]} stored_queries
|
|
131
|
+
* @returns {OutputView}
|
|
132
|
+
*/
|
|
133
|
+
function createStoredQueriesOutputView(stored_queries) {
|
|
134
|
+
return {
|
|
135
|
+
command: 'queries',
|
|
136
|
+
hints: [],
|
|
137
|
+
items: stored_queries.map((stored_query) => ({
|
|
138
|
+
kind: 'stored_query',
|
|
139
|
+
name: stored_query.name,
|
|
140
|
+
where: stored_query.where,
|
|
141
|
+
})),
|
|
142
|
+
summary: {
|
|
143
|
+
count: stored_queries.length,
|
|
144
|
+
kind: 'stored_query_list',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {GraphNode} graph_node
|
|
151
|
+
* @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
|
|
152
|
+
*/
|
|
153
|
+
function createOutputNodeItem(graph_node) {
|
|
154
|
+
const title =
|
|
155
|
+
graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
|
|
156
|
+
|
|
157
|
+
if (!title || !graph_node.path) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Expected graph node "${graph_node.id}" to have a title and path.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: graph_node.id,
|
|
165
|
+
kind: 'node',
|
|
166
|
+
node_kind: graph_node.kind,
|
|
167
|
+
path: graph_node.path,
|
|
168
|
+
status: graph_node.status,
|
|
169
|
+
title,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {{ kind?: string, path: string, status?: string, title: string }} target
|
|
175
|
+
* @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
|
|
176
|
+
*/
|
|
177
|
+
function createResolvedLinkTarget(target) {
|
|
178
|
+
/** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
|
|
179
|
+
const resolved_target = {
|
|
180
|
+
path: target.path,
|
|
181
|
+
title: target.title,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (target.kind && target.kind !== 'document') {
|
|
185
|
+
resolved_target.kind = target.kind;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (target.status) {
|
|
189
|
+
resolved_target.status = target.status;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return resolved_target;
|
|
193
|
+
}
|