@ndp-software/lit-md 0.3.0
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.md +3 -0
- package/README.md +125 -0
- package/dist/cli.js +1273 -0
- package/dist/cli.js.map +7 -0
- package/docs/cli.md +74 -0
- package/docs/how-wait-mode-works.md +381 -0
- package/docs/shell-examples.md +254 -0
- package/package.json +58 -0
- package/src/cli.ts +327 -0
- package/src/describe-format.ts +53 -0
- package/src/docs/README.lit-md.ts +126 -0
- package/src/docs/cli.lit-md.ts +44 -0
- package/src/docs/shell-examples.lit-md.ts +243 -0
- package/src/index.ts +7 -0
- package/src/parser.ts +970 -0
- package/src/renderer.ts +130 -0
- package/src/resolver.ts +65 -0
- package/src/shell.ts +362 -0
- package/src/typecheck.ts +51 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync } from "node:fs";
|
|
5
|
+
import { join as join5, dirname as dirname2, basename, extname as extname2, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// src/parser.ts
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { writeFileSync, mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join, isAbsolute } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
function parse(src, lang = "typescript") {
|
|
15
|
+
if (!src.trim()) return [];
|
|
16
|
+
const sf = ts.createSourceFile("input.ts", src, ts.ScriptTarget.Latest, true);
|
|
17
|
+
const nodes = [];
|
|
18
|
+
const processedCommentRanges = /* @__PURE__ */ new Set();
|
|
19
|
+
let pendingFileLabel = void 0;
|
|
20
|
+
let lastCommentEnd = 0;
|
|
21
|
+
let pendingNewParagraph = false;
|
|
22
|
+
function extractLeadingComments(pos) {
|
|
23
|
+
const ranges = ts.getLeadingCommentRanges(src, pos) ?? [];
|
|
24
|
+
for (const r of ranges) {
|
|
25
|
+
if (processedCommentRanges.has(r.pos)) continue;
|
|
26
|
+
processedCommentRanges.add(r.pos);
|
|
27
|
+
const raw = src.slice(r.pos, r.end);
|
|
28
|
+
const gap = lastCommentEnd > 0 ? src.slice(lastCommentEnd, r.pos) : "";
|
|
29
|
+
const hasBlankLineBefore = gap !== "" && /^[ \t\n]*$/.test(gap) && /\n[ \t]*\n/.test(gap);
|
|
30
|
+
lastCommentEnd = r.end;
|
|
31
|
+
if (r.kind === ts.SyntaxKind.SingleLineCommentTrivia) {
|
|
32
|
+
const fileMatch = raw.match(/^\/\/\s*file:\s*(.+)$/);
|
|
33
|
+
if (fileMatch) {
|
|
34
|
+
pendingFileLabel = fileMatch[1].trim();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const prose = commentToProse(raw, r.kind);
|
|
39
|
+
if (prose !== null) {
|
|
40
|
+
if (hasBlankLineBefore && prose === "") {
|
|
41
|
+
pendingNewParagraph = true;
|
|
42
|
+
} else if ((hasBlankLineBefore || pendingNewParagraph) && prose !== "") {
|
|
43
|
+
nodes.push({ kind: "prose", text: prose });
|
|
44
|
+
pendingNewParagraph = false;
|
|
45
|
+
} else {
|
|
46
|
+
mergeOrPushProse(nodes, prose);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function visitStatements(statements, depth = 0, parentBlock) {
|
|
52
|
+
for (const stmt of statements) {
|
|
53
|
+
extractLeadingComments(stmt.getFullStart());
|
|
54
|
+
processStatement(stmt, depth);
|
|
55
|
+
}
|
|
56
|
+
if (statements.length > 0 && parentBlock) {
|
|
57
|
+
const lastStatement = statements[statements.length - 1];
|
|
58
|
+
extractLeadingComments(lastStatement.end);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function processStatement(stmt, depth = 0) {
|
|
62
|
+
const fullStmt = getFullStatement(stmt, src);
|
|
63
|
+
if (fullStmt !== null) {
|
|
64
|
+
const title = pendingFileLabel;
|
|
65
|
+
pendingFileLabel = void 0;
|
|
66
|
+
mergeOrPushCode(nodes, fullStmt, lang, title);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const lineText = getStatementLine(stmt, src);
|
|
70
|
+
if (lineText !== null && hasKeepComment(lineText)) {
|
|
71
|
+
const title = pendingFileLabel;
|
|
72
|
+
pendingFileLabel = void 0;
|
|
73
|
+
const cleanedLine = lineText.replace(/\s*\/\/\s*keep\b.*$/, "");
|
|
74
|
+
mergeOrPushCode(nodes, cleanedLine, lang, title);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
81
|
+
const expr = stmt.expression;
|
|
82
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
83
|
+
const name = expr.expression.text;
|
|
84
|
+
if (name === "test" || name === "it" || name === "example") {
|
|
85
|
+
const testName = getStringArg(expr, 0);
|
|
86
|
+
const body = getFnBody(expr, 1);
|
|
87
|
+
if (body) {
|
|
88
|
+
const code = extractBodyCode(src, body);
|
|
89
|
+
if (code.trim()) {
|
|
90
|
+
const title = pendingFileLabel;
|
|
91
|
+
pendingFileLabel = void 0;
|
|
92
|
+
let proseNodeIdx = -1;
|
|
93
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
94
|
+
if (nodes[i].kind === "prose") {
|
|
95
|
+
proseNodeIdx = i;
|
|
96
|
+
break;
|
|
97
|
+
} else if (nodes[i].kind !== "describe") {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (proseNodeIdx >= 0) {
|
|
102
|
+
const proseNode = nodes[proseNodeIdx];
|
|
103
|
+
const fenceMatch = extractTrailingFence(proseNode.text);
|
|
104
|
+
if (fenceMatch) {
|
|
105
|
+
proseNode.text = fenceMatch.prose;
|
|
106
|
+
proseNode.noBlankAfter = true;
|
|
107
|
+
nodes.splice(proseNodeIdx + 1);
|
|
108
|
+
const mergedCode = fenceMatch.fenceCode + "\n" + code;
|
|
109
|
+
nodes.push(codeNode(lang, mergedCode, title));
|
|
110
|
+
} else if (proseNodeIdx === nodes.length - 1) {
|
|
111
|
+
proseNode.noBlankAfter = true;
|
|
112
|
+
nodes.push(codeNode(lang, code, title));
|
|
113
|
+
} else {
|
|
114
|
+
nodes.push(codeNode(lang, code, title));
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
nodes.push(codeNode(lang, code, title));
|
|
118
|
+
}
|
|
119
|
+
} else if (body && !ts.isBlock(body)) {
|
|
120
|
+
const lines = src.slice(body.getFullStart(), body.getEnd()).split("\n").length;
|
|
121
|
+
if (lines > 2) {
|
|
122
|
+
console.warn(`\u26A0 Warning: ${name}('${testName}') expression body (${lines} lines) did not produce output`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (name === "describe") {
|
|
129
|
+
const descName = getStringArg(expr, 0);
|
|
130
|
+
const body = getFnBody(expr, 1);
|
|
131
|
+
if (body && ts.isBlock(body) && descName !== null) {
|
|
132
|
+
nodes.push({ kind: "describe", name: descName, depth });
|
|
133
|
+
visitStatements(body.statements, depth + 1, body);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (name === "metaExample") {
|
|
138
|
+
const body = getFnBody(expr, 1);
|
|
139
|
+
if (body) {
|
|
140
|
+
const code = extractBodyCode(src, body);
|
|
141
|
+
if (code.trim()) {
|
|
142
|
+
const title = pendingFileLabel;
|
|
143
|
+
pendingFileLabel = void 0;
|
|
144
|
+
const rawCall = dedentCallSource(src.slice(expr.getStart(), expr.getEnd())).replace(/^metaExample\b/, "example");
|
|
145
|
+
nodes.push(codeNode(lang, rawCall, title));
|
|
146
|
+
nodes.push({ kind: "prose", text: "becomes", noBlankBefore: true, noBlankAfter: true });
|
|
147
|
+
const langAlias = lang === "typescript" ? "ts" : lang === "javascript" ? "js" : lang;
|
|
148
|
+
const innerFence = `\`\`\`${langAlias}
|
|
149
|
+
${code}
|
|
150
|
+
\`\`\``;
|
|
151
|
+
nodes.push(codeNode("md", innerFence));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (name === "shellExample") {
|
|
157
|
+
const cmd = getStringArg(expr, 0);
|
|
158
|
+
if (cmd !== null) {
|
|
159
|
+
const title = pendingFileLabel;
|
|
160
|
+
pendingFileLabel = void 0;
|
|
161
|
+
const optsArg = expr.arguments[1];
|
|
162
|
+
const opts = optsArg && ts.isObjectLiteralExpression(optsArg) ? optsArg : void 0;
|
|
163
|
+
const metaProp = opts ? getProp(opts, "meta") : void 0;
|
|
164
|
+
const hasMeta = metaProp && metaProp.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
165
|
+
if (hasMeta) {
|
|
166
|
+
const reconstructed = reconstructShellExampleWithoutMeta(src, expr, cmd, opts);
|
|
167
|
+
nodes.push(codeNode("ts", reconstructed));
|
|
168
|
+
nodes.push({ kind: "prose", text: "becomes", noBlankBefore: true, noBlankAfter: true });
|
|
169
|
+
}
|
|
170
|
+
if (opts) processShellExampleInputFiles(opts, nodes);
|
|
171
|
+
const inputFiles = opts ? extractStaticInputFiles(opts) : [];
|
|
172
|
+
const exitCodeProp = opts ? getProp(opts, "exitCode") : void 0;
|
|
173
|
+
const expectedExitCode = exitCodeProp && ts.isNumericLiteral(exitCodeProp.initializer) ? parseInt(exitCodeProp.initializer.text, 10) : 0;
|
|
174
|
+
let execution = null;
|
|
175
|
+
if (opts && isExecutionNeeded(opts)) {
|
|
176
|
+
const outputPaths = extractOutputFilePaths(opts);
|
|
177
|
+
execution = executeShellCommand(cmd, inputFiles, outputPaths, expectedExitCode);
|
|
178
|
+
}
|
|
179
|
+
const displayCommand = opts ? readBoolOption(getProp(opts, "displayCommand")) : true;
|
|
180
|
+
const lines = displayCommand ? [`$ ${cmd}`] : [];
|
|
181
|
+
if (opts) appendShellExampleAnnotations(opts, lines, execution);
|
|
182
|
+
if (lines.length > 0) {
|
|
183
|
+
nodes.push(codeNode("sh", lines.join("\n"), title));
|
|
184
|
+
}
|
|
185
|
+
if (opts) processShellExampleOutputFiles(src, opts, nodes, cmd, inputFiles, execution);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
visitStatements(sf.statements);
|
|
193
|
+
extractLeadingComments(sf.endOfFileToken.getFullStart());
|
|
194
|
+
return nodes;
|
|
195
|
+
}
|
|
196
|
+
function getProp(obj, name) {
|
|
197
|
+
return obj.properties.find(
|
|
198
|
+
(p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
function readBoolOption(prop) {
|
|
202
|
+
if (!prop) return true;
|
|
203
|
+
const init = prop.initializer;
|
|
204
|
+
if (init.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
205
|
+
if (ts.isStringLiteralLike(init) && init.text === "hidden") return false;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
function readFlag(prop) {
|
|
209
|
+
if (!prop) return true;
|
|
210
|
+
return prop.initializer.kind !== ts.SyntaxKind.FalseKeyword;
|
|
211
|
+
}
|
|
212
|
+
function escapeForSingleQuotedString(s) {
|
|
213
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
214
|
+
}
|
|
215
|
+
function reconstructShellExampleWithoutMeta(src, expr, cmd, opts) {
|
|
216
|
+
const escapedCmd = escapeForSingleQuotedString(cmd);
|
|
217
|
+
if (!opts) {
|
|
218
|
+
return `shellExample('${escapedCmd}')`;
|
|
219
|
+
}
|
|
220
|
+
const optsText = src.slice(opts.getStart(), opts.getEnd());
|
|
221
|
+
let cleanedOpts = optsText.replace(/,?\s*meta:\s*true\s*,?/g, ",").replace(/^\{\s*,/, "{").replace(/,\s*\}$/, "}");
|
|
222
|
+
return `shellExample('${escapedCmd}'${cleanedOpts !== "{}" ? `, ${cleanedOpts}` : ""})`;
|
|
223
|
+
}
|
|
224
|
+
function codeNode(lang, text, title) {
|
|
225
|
+
return title !== void 0 ? { kind: "code", lang, text, title } : { kind: "code", lang, text };
|
|
226
|
+
}
|
|
227
|
+
function getStringArg(call, index) {
|
|
228
|
+
const arg = call.arguments[index];
|
|
229
|
+
if (arg && ts.isStringLiteralLike(arg)) return arg.text;
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function getFnBody(call, index) {
|
|
233
|
+
const arg = call.arguments[index];
|
|
234
|
+
if (!arg) return null;
|
|
235
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
236
|
+
return arg.body;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function extractBodyCode(src, bodyOrBlock) {
|
|
241
|
+
if (!ts.isBlock(bodyOrBlock)) {
|
|
242
|
+
return src.slice(bodyOrBlock.getFullStart(), bodyOrBlock.getEnd()).trim();
|
|
243
|
+
}
|
|
244
|
+
const stmts = bodyOrBlock.statements;
|
|
245
|
+
if (!stmts.length) return "";
|
|
246
|
+
const firstStart = stmts[0].getStart();
|
|
247
|
+
const lineStart = src.lastIndexOf("\n", firstStart - 1) + 1;
|
|
248
|
+
const indent = firstStart - lineStart;
|
|
249
|
+
const lastStmt = stmts[stmts.length - 1];
|
|
250
|
+
const lastEnd = lastStmt.getEnd();
|
|
251
|
+
const lastLineEnd = src.indexOf("\n", lastEnd);
|
|
252
|
+
const textAfterLast = src.slice(lastEnd, lastLineEnd === -1 ? src.length : lastLineEnd);
|
|
253
|
+
const extractEnd = /^\s*\/\//.test(textAfterLast) ? lastLineEnd === -1 ? src.length : lastLineEnd : lastEnd;
|
|
254
|
+
const base = stmts[0].getFullStart();
|
|
255
|
+
let raw = src.slice(base, extractEnd);
|
|
256
|
+
const replacements = [];
|
|
257
|
+
for (const stmt of stmts) {
|
|
258
|
+
const rewritten = tryRewriteAssertion(src, stmt, indent);
|
|
259
|
+
if (rewritten !== null) {
|
|
260
|
+
replacements.push({ start: stmt.getStart() - base, end: stmt.getEnd() - base, text: rewritten });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
264
|
+
for (const r of replacements) {
|
|
265
|
+
raw = raw.slice(0, r.start) + r.text + raw.slice(r.end);
|
|
266
|
+
}
|
|
267
|
+
let result = raw.split("\n").map((line) => line.length >= indent && line.slice(0, indent).trim() === "" ? line.slice(indent) : line).join("\n").trim();
|
|
268
|
+
result = result.replace(/\n\n\n+/g, "\n\n");
|
|
269
|
+
result = transformNestedAssertOk(result);
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function dedentCallSource(callText) {
|
|
273
|
+
const lines = callText.split("\n");
|
|
274
|
+
if (lines.length <= 1) return callText;
|
|
275
|
+
const lastLine = lines[lines.length - 1];
|
|
276
|
+
const closingIndent = lastLine.length - lastLine.trimStart().length;
|
|
277
|
+
if (closingIndent === 0) return callText;
|
|
278
|
+
return lines.map((line, i) => {
|
|
279
|
+
if (i === 0) return line;
|
|
280
|
+
return line.length >= closingIndent && line.slice(0, closingIndent).trim() === "" ? line.slice(closingIndent) : line;
|
|
281
|
+
}).join("\n");
|
|
282
|
+
}
|
|
283
|
+
function commentToProse(raw, kind) {
|
|
284
|
+
if (kind === ts.SyntaxKind.SingleLineCommentTrivia) {
|
|
285
|
+
return raw.replace(/^\/\/\s?/, "");
|
|
286
|
+
}
|
|
287
|
+
if (kind === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
288
|
+
const inner = raw.replace(/^\/\*+/, "").replace(/\*+\/$/, "").split("\n").map((l) => l.replace(/^\s*\*\s?/, "")).join("\n").trim();
|
|
289
|
+
return inner || null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function hasKeepComment(text) {
|
|
294
|
+
return /\/\/\s*keep\b/.test(text);
|
|
295
|
+
}
|
|
296
|
+
function isFullKeep(text) {
|
|
297
|
+
return /\/\/\s*keep:full\b/.test(text);
|
|
298
|
+
}
|
|
299
|
+
function getStatementLine(stmt, src) {
|
|
300
|
+
const lineEnd = src.indexOf("\n", stmt.getEnd());
|
|
301
|
+
const lineText = src.slice(stmt.getStart(), lineEnd === -1 ? src.length : lineEnd).trimEnd();
|
|
302
|
+
const lines = lineText.split("\n");
|
|
303
|
+
if (lines.length > 1) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return lineText;
|
|
307
|
+
}
|
|
308
|
+
function getFullStatement(stmt, src) {
|
|
309
|
+
const stmtStart = stmt.getStart();
|
|
310
|
+
const stmtEnd = stmt.getEnd();
|
|
311
|
+
const firstLineEnd = src.indexOf("\n", stmtStart);
|
|
312
|
+
const firstLine = src.slice(stmtStart, firstLineEnd === -1 ? src.length : firstLineEnd);
|
|
313
|
+
if (!isFullKeep(firstLine)) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const stmtText = src.slice(stmtStart, stmtEnd);
|
|
317
|
+
const firstLineMatch = stmtText.match(/^(\s*)/);
|
|
318
|
+
const baseIndent = firstLineMatch ? firstLineMatch[1].length : 0;
|
|
319
|
+
const dedented = stmtText.split("\n").map((line) => {
|
|
320
|
+
if (line.length >= baseIndent && line.slice(0, baseIndent).trim() === "") {
|
|
321
|
+
return line.slice(baseIndent);
|
|
322
|
+
}
|
|
323
|
+
return line;
|
|
324
|
+
}).join("\n").trimEnd();
|
|
325
|
+
const cleaned = dedented.replace(/\s*\/\/\s*keep:full\b.*$/gm, "");
|
|
326
|
+
return cleaned;
|
|
327
|
+
}
|
|
328
|
+
function tryRewriteAssertion(src, stmt, bodyIndent) {
|
|
329
|
+
if (!ts.isExpressionStatement(stmt)) return null;
|
|
330
|
+
const expr = stmt.expression;
|
|
331
|
+
if (!ts.isCallExpression(expr)) return null;
|
|
332
|
+
if (!ts.isPropertyAccessExpression(expr.expression)) return null;
|
|
333
|
+
const obj = expr.expression.expression;
|
|
334
|
+
const method = expr.expression.name.text;
|
|
335
|
+
if (!ts.isIdentifier(obj) || obj.text !== "assert") return null;
|
|
336
|
+
if (method === "ok") {
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
if (method === "throws") {
|
|
340
|
+
const fn = expr.arguments[0];
|
|
341
|
+
if (!fn) return null;
|
|
342
|
+
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) {
|
|
343
|
+
const exprText = src.slice(fn.body.getStart(), fn.body.getEnd());
|
|
344
|
+
const patternArg = expr.arguments[1];
|
|
345
|
+
const patternText = patternArg ? src.slice(patternArg.getStart(), patternArg.getEnd()) : null;
|
|
346
|
+
return patternText ? `${exprText} // throws ${patternText}` : `${exprText} // throws`;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const [actual, expected] = expr.arguments;
|
|
351
|
+
if (!actual || !expected) return null;
|
|
352
|
+
if (["equal", "strictEqual", "deepEqual", "deepStrictEqual"].includes(method)) {
|
|
353
|
+
return formatComparison(src, actual, expected, "=>", bodyIndent);
|
|
354
|
+
}
|
|
355
|
+
if (["notEqual", "notStrictEqual", "notDeepEqual", "notDeepStrictEqual"].includes(method)) {
|
|
356
|
+
return formatComparison(src, actual, expected, "!=", bodyIndent);
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function formatComparison(src, actual, expected, op, bodyIndent) {
|
|
361
|
+
const actualText = src.slice(actual.getStart(), actual.getEnd());
|
|
362
|
+
const expectedRaw = src.slice(expected.getStart(), expected.getEnd());
|
|
363
|
+
const expectedText = dedentContinuationLines(expectedRaw);
|
|
364
|
+
const lines = expectedText.split("\n");
|
|
365
|
+
if (lines.length === 1) {
|
|
366
|
+
return `${actualText} // ${op} ${expectedText}`;
|
|
367
|
+
}
|
|
368
|
+
const first = lines[0];
|
|
369
|
+
const indent = " ".repeat(actualText.length + 1 + bodyIndent);
|
|
370
|
+
const rest = lines.slice(1).map((l) => `${indent}// ${l}`);
|
|
371
|
+
return [`${actualText} // ${op} ${first}`, ...rest].join("\n");
|
|
372
|
+
}
|
|
373
|
+
function dedentContinuationLines(text) {
|
|
374
|
+
const lines = text.split("\n");
|
|
375
|
+
if (lines.length === 1) return text;
|
|
376
|
+
const contLines = lines.slice(1).filter((l) => l.trim().length > 0);
|
|
377
|
+
if (!contLines.length) return text;
|
|
378
|
+
const minInd = Math.min(...contLines.map((l) => l.length - l.trimStart().length));
|
|
379
|
+
if (minInd === 0) return text;
|
|
380
|
+
return [
|
|
381
|
+
lines[0],
|
|
382
|
+
...lines.slice(1).map((l) => l.length >= minInd && l.slice(0, minInd).trim() === "" ? l.slice(minInd) : l)
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
function extractTrailingFence(prose) {
|
|
386
|
+
const match = prose.match(/^([\s\S]*?)\n?```[^\n]*\n([\s\S]*?)```\s*$/);
|
|
387
|
+
if (!match) return null;
|
|
388
|
+
return {
|
|
389
|
+
prose: match[1].trimEnd(),
|
|
390
|
+
fenceCode: match[2].trimEnd()
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function mergeOrPushCode(nodes, text, lang, title) {
|
|
394
|
+
const last = nodes[nodes.length - 1];
|
|
395
|
+
if (last?.kind === "code" && last.title === void 0 && title === void 0) {
|
|
396
|
+
last.text = last.text + "\n" + text;
|
|
397
|
+
} else {
|
|
398
|
+
nodes.push(codeNode(lang, text, title));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function mergeOrPushProse(nodes, text) {
|
|
402
|
+
const last = nodes[nodes.length - 1];
|
|
403
|
+
if (last?.kind === "prose" && !last.terminal) {
|
|
404
|
+
last.text = last.text + "\n" + text;
|
|
405
|
+
} else {
|
|
406
|
+
nodes.push({ kind: "prose", text });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function transformNestedAssertOk(code) {
|
|
410
|
+
return code.replace(/assert\.ok\(([^)]+)\)/g, "$1 // OK");
|
|
411
|
+
}
|
|
412
|
+
function getLanguageFromExtension(filePath) {
|
|
413
|
+
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
414
|
+
const langMap = {
|
|
415
|
+
ts: "typescript",
|
|
416
|
+
tsx: "typescript",
|
|
417
|
+
js: "javascript",
|
|
418
|
+
jsx: "javascript",
|
|
419
|
+
json: "json",
|
|
420
|
+
md: "markdown",
|
|
421
|
+
yaml: "yaml",
|
|
422
|
+
yml: "yaml",
|
|
423
|
+
sh: "sh",
|
|
424
|
+
bash: "bash",
|
|
425
|
+
py: "python",
|
|
426
|
+
rs: "rust",
|
|
427
|
+
go: "go",
|
|
428
|
+
java: "java",
|
|
429
|
+
cs: "csharp",
|
|
430
|
+
rb: "ruby",
|
|
431
|
+
php: "php",
|
|
432
|
+
html: "html",
|
|
433
|
+
css: "css",
|
|
434
|
+
xml: "xml",
|
|
435
|
+
txt: "text"
|
|
436
|
+
};
|
|
437
|
+
return langMap[ext] || ext || "text";
|
|
438
|
+
}
|
|
439
|
+
function supportsCStyleComments(lang) {
|
|
440
|
+
const cStyleLangs = /* @__PURE__ */ new Set([
|
|
441
|
+
"typescript",
|
|
442
|
+
"javascript",
|
|
443
|
+
"java",
|
|
444
|
+
"csharp",
|
|
445
|
+
"go",
|
|
446
|
+
"rust",
|
|
447
|
+
"cpp",
|
|
448
|
+
"c",
|
|
449
|
+
"objc",
|
|
450
|
+
"swift",
|
|
451
|
+
"kotlin",
|
|
452
|
+
"scala",
|
|
453
|
+
"groovy"
|
|
454
|
+
]);
|
|
455
|
+
return cStyleLangs.has(lang);
|
|
456
|
+
}
|
|
457
|
+
function processShellExampleInputFiles(opts, nodes) {
|
|
458
|
+
const inputFilesProp = getProp(opts, "inputFiles");
|
|
459
|
+
if (!inputFilesProp || !ts.isArrayLiteralExpression(inputFilesProp.initializer)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
for (const el of inputFilesProp.initializer.elements) {
|
|
463
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
464
|
+
const pathProp = getProp(el, "path");
|
|
465
|
+
const contentProp = getProp(el, "content");
|
|
466
|
+
const displayPathProp = getProp(el, "displayPath");
|
|
467
|
+
const summaryProp = getProp(el, "summary");
|
|
468
|
+
const displayProp = getProp(el, "display");
|
|
469
|
+
const displayContent = readBoolOption(displayProp);
|
|
470
|
+
if (!pathProp || !ts.isStringLiteralLike(pathProp.initializer)) continue;
|
|
471
|
+
const filePath = pathProp.initializer.text;
|
|
472
|
+
const displayPath = readBoolOption(displayPathProp);
|
|
473
|
+
const summary = readFlag(summaryProp);
|
|
474
|
+
if (contentProp && displayContent && ts.isStringLiteralLike(contentProp.initializer)) {
|
|
475
|
+
const content = contentProp.initializer.text;
|
|
476
|
+
const lang = getLanguageFromExtension(filePath);
|
|
477
|
+
if (!supportsCStyleComments(lang) && displayPath && summary) {
|
|
478
|
+
nodes.push({ kind: "prose", text: `With input file \`${filePath}\`:`, noBlankAfter: true });
|
|
479
|
+
}
|
|
480
|
+
let blockText = content;
|
|
481
|
+
if (supportsCStyleComments(lang) && displayPath && summary) {
|
|
482
|
+
blockText = `// Input file "${filePath}":
|
|
483
|
+
${content}`;
|
|
484
|
+
}
|
|
485
|
+
nodes.push({ kind: "code", lang, text: blockText });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
var OUTPUT_FILE_INLINE_LIMIT = 60;
|
|
490
|
+
function processShellExampleOutputFiles(src, opts, nodes, cmd, inputFiles, execution) {
|
|
491
|
+
const outputFilesProp = getProp(opts, "outputFiles");
|
|
492
|
+
if (!outputFilesProp || !ts.isArrayLiteralExpression(outputFilesProp.initializer)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
for (const el of outputFilesProp.initializer.elements) {
|
|
496
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
497
|
+
const pathProp = getProp(el, "path");
|
|
498
|
+
const containsProp = getProp(el, "contains");
|
|
499
|
+
const matchesProp = getProp(el, "matches");
|
|
500
|
+
const displayProp = getProp(el, "display");
|
|
501
|
+
const displayPathProp = getProp(el, "displayPath");
|
|
502
|
+
const summaryProp = getProp(el, "summary");
|
|
503
|
+
const display = displayProp && ts.isStringLiteralLike(displayProp.initializer) ? displayProp.initializer.text : void 0;
|
|
504
|
+
if (!pathProp || !ts.isStringLiteralLike(pathProp.initializer)) continue;
|
|
505
|
+
const filePath = pathProp.initializer.text;
|
|
506
|
+
const lang = getLanguageFromExtension(filePath);
|
|
507
|
+
const displayPath = readBoolOption(displayPathProp);
|
|
508
|
+
const summary = readFlag(summaryProp);
|
|
509
|
+
let emitDisplayNode = display !== "none";
|
|
510
|
+
if (matchesProp && ts.isRegularExpressionLiteral(matchesProp.initializer)) {
|
|
511
|
+
if (summary) {
|
|
512
|
+
const regexText = src.slice(matchesProp.initializer.getStart(), matchesProp.initializer.getEnd());
|
|
513
|
+
const proseText = displayPath ? `Output file \`${filePath}\` matches \`${regexText}\`.` : `Matches \`${regexText}\`.`;
|
|
514
|
+
nodes.push({ kind: "prose", text: proseText, terminal: true });
|
|
515
|
+
}
|
|
516
|
+
} else if (containsProp && ts.isStringLiteralLike(containsProp.initializer)) {
|
|
517
|
+
const text = containsProp.initializer.text;
|
|
518
|
+
const isMultiLine = text.includes("\n");
|
|
519
|
+
if (!isMultiLine && text.length < OUTPUT_FILE_INLINE_LIMIT) {
|
|
520
|
+
if (summary) {
|
|
521
|
+
const proseText = displayPath ? `Output file \`${filePath}\` contains \`${text}\`.` : `Contains \`${text}\`.`;
|
|
522
|
+
nodes.push({ kind: "prose", text: proseText, terminal: true });
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
const firstNewline = text.indexOf("\n");
|
|
526
|
+
const truncateAt = isMultiLine ? Math.min(firstNewline, OUTPUT_FILE_INLINE_LIMIT) : OUTPUT_FILE_INLINE_LIMIT;
|
|
527
|
+
const truncated = text.slice(0, truncateAt);
|
|
528
|
+
if (isMultiLine) {
|
|
529
|
+
if (summary) {
|
|
530
|
+
const proseText = displayPath ? `Output file \`${filePath}\` contains ${truncated}...:` : `Contains ${truncated}...:`;
|
|
531
|
+
nodes.push({ kind: "prose", text: proseText, terminal: true, noBlankAfter: true });
|
|
532
|
+
}
|
|
533
|
+
nodes.push({ kind: "code", lang, text: `...
|
|
534
|
+
${text}
|
|
535
|
+
...` });
|
|
536
|
+
emitDisplayNode = false;
|
|
537
|
+
} else {
|
|
538
|
+
if (summary) {
|
|
539
|
+
const proseText = displayPath ? `Output file \`${filePath}\` contains ${truncated}....` : `Contains ${truncated}....`;
|
|
540
|
+
nodes.push({ kind: "prose", text: proseText, terminal: true });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
if (summary) {
|
|
546
|
+
const proseText = displayPath ? `Output file \`${filePath}\`:` : `Output:`;
|
|
547
|
+
nodes.push({ kind: "prose", text: proseText, terminal: true, noBlankAfter: true });
|
|
548
|
+
}
|
|
549
|
+
emitDisplayNode = true;
|
|
550
|
+
}
|
|
551
|
+
if (emitDisplayNode) {
|
|
552
|
+
const node = { kind: "output-file-display", path: filePath, lang, cmd, inputFiles };
|
|
553
|
+
if (execution) {
|
|
554
|
+
node.execution = execution;
|
|
555
|
+
}
|
|
556
|
+
nodes.push(node);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function extractStaticInputFiles(opts) {
|
|
561
|
+
const inputFilesProp = getProp(opts, "inputFiles");
|
|
562
|
+
if (!inputFilesProp || !ts.isArrayLiteralExpression(inputFilesProp.initializer)) return [];
|
|
563
|
+
const result = [];
|
|
564
|
+
for (const el of inputFilesProp.initializer.elements) {
|
|
565
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
566
|
+
const pathProp = getProp(el, "path");
|
|
567
|
+
const contentProp = getProp(el, "content");
|
|
568
|
+
if (!pathProp || !ts.isStringLiteralLike(pathProp.initializer)) continue;
|
|
569
|
+
if (!contentProp || !ts.isStringLiteralLike(contentProp.initializer)) continue;
|
|
570
|
+
result.push({ path: pathProp.initializer.text, content: contentProp.initializer.text });
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
function isExecutionNeeded(opts) {
|
|
575
|
+
const stdoutProp = getProp(opts, "stdout");
|
|
576
|
+
if (stdoutProp && ts.isObjectLiteralExpression(stdoutProp.initializer)) {
|
|
577
|
+
const displayProp = getProp(stdoutProp.initializer, "display");
|
|
578
|
+
if (displayProp && displayProp.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const outputFilesProp = getProp(opts, "outputFiles");
|
|
583
|
+
if (!outputFilesProp || !ts.isArrayLiteralExpression(outputFilesProp.initializer)) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
for (const el of outputFilesProp.initializer.elements) {
|
|
587
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
588
|
+
const displayProp = getProp(el, "display");
|
|
589
|
+
const containsProp = getProp(el, "contains");
|
|
590
|
+
const matchesProp = getProp(el, "matches");
|
|
591
|
+
const display = displayProp && ts.isStringLiteralLike(displayProp.initializer) ? displayProp.initializer.text : void 0;
|
|
592
|
+
if (display !== "none") {
|
|
593
|
+
const hasMatches = matchesProp && ts.isRegularExpressionLiteral(matchesProp.initializer);
|
|
594
|
+
if (!containsProp && !hasMatches) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
if (containsProp && ts.isStringLiteralLike(containsProp.initializer)) {
|
|
598
|
+
const text = containsProp.initializer.text;
|
|
599
|
+
if (text.includes("\n") || text.length >= OUTPUT_FILE_INLINE_LIMIT) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
function extractOutputFilePaths(opts) {
|
|
608
|
+
const outputFilesProp = getProp(opts, "outputFiles");
|
|
609
|
+
if (!outputFilesProp || !ts.isArrayLiteralExpression(outputFilesProp.initializer)) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const paths = [];
|
|
613
|
+
for (const el of outputFilesProp.initializer.elements) {
|
|
614
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
615
|
+
const pathProp = getProp(el, "path");
|
|
616
|
+
if (pathProp && ts.isStringLiteralLike(pathProp.initializer)) {
|
|
617
|
+
paths.push(pathProp.initializer.text);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return paths;
|
|
621
|
+
}
|
|
622
|
+
function executeShellCommand(cmd, inputFiles, outputFilePaths, expectedExitCode = 0) {
|
|
623
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "lit-md-exec-"));
|
|
624
|
+
const resolvePath = (p) => isAbsolute(p) ? p : join(tmpDir, p);
|
|
625
|
+
try {
|
|
626
|
+
for (const f of inputFiles) {
|
|
627
|
+
writeFileSync(resolvePath(f.path), f.content, "utf8");
|
|
628
|
+
}
|
|
629
|
+
const result = spawnSync(cmd, { shell: true, encoding: "utf8", cwd: tmpDir });
|
|
630
|
+
const outputFiles = /* @__PURE__ */ new Map();
|
|
631
|
+
if ((result.status ?? 1) === expectedExitCode) {
|
|
632
|
+
for (const filePath of outputFilePaths) {
|
|
633
|
+
try {
|
|
634
|
+
const content = readFileSync(resolvePath(filePath), "utf8");
|
|
635
|
+
outputFiles.set(filePath, content);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
stdout: result.stdout.trimEnd(),
|
|
642
|
+
outputFiles,
|
|
643
|
+
exitCode: result.status ?? 1
|
|
644
|
+
};
|
|
645
|
+
} catch (e) {
|
|
646
|
+
return null;
|
|
647
|
+
} finally {
|
|
648
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function appendShellExampleAnnotations(opts, lines, execution) {
|
|
652
|
+
for (const prop of opts.properties) {
|
|
653
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
654
|
+
const key = prop.name.text;
|
|
655
|
+
if (key === "exitCode" && ts.isNumericLiteral(prop.initializer)) {
|
|
656
|
+
const code = parseInt(prop.initializer.text, 10);
|
|
657
|
+
if (code !== 0) {
|
|
658
|
+
lines.push(`# exits: ${code}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (key === "stdout" && ts.isObjectLiteralExpression(prop.initializer)) {
|
|
662
|
+
const containsProp = getProp(prop.initializer, "contains");
|
|
663
|
+
const displayProp = getProp(prop.initializer, "display");
|
|
664
|
+
if (displayProp && displayProp.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
665
|
+
if (execution && execution.exitCode === 0) {
|
|
666
|
+
lines.push(execution.stdout);
|
|
667
|
+
}
|
|
668
|
+
} else if (containsProp && ts.isStringLiteralLike(containsProp.initializer)) {
|
|
669
|
+
lines.push(containsProp.initializer.text);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (key === "inputFiles" && ts.isArrayLiteralExpression(prop.initializer)) {
|
|
673
|
+
for (const el of prop.initializer.elements) {
|
|
674
|
+
if (!ts.isObjectLiteralExpression(el)) continue;
|
|
675
|
+
const pathProp = getProp(el, "path");
|
|
676
|
+
const contentProp = getProp(el, "content");
|
|
677
|
+
const displayPathProp = getProp(el, "displayPath");
|
|
678
|
+
const summaryProp = getProp(el, "summary");
|
|
679
|
+
if (!pathProp || !ts.isStringLiteralLike(pathProp.initializer)) continue;
|
|
680
|
+
const filePath = pathProp.initializer.text;
|
|
681
|
+
const displayPath = readBoolOption(displayPathProp);
|
|
682
|
+
const summary = readFlag(summaryProp);
|
|
683
|
+
if (contentProp && ts.isStringLiteralLike(contentProp.initializer)) {
|
|
684
|
+
const content = contentProp.initializer.text;
|
|
685
|
+
const lang = getLanguageFromExtension(filePath);
|
|
686
|
+
if (!content.includes("\n") && supportsCStyleComments(lang) && displayPath && summary) {
|
|
687
|
+
lines.push(`# Input file \`${filePath}\` contains \`${content}\``);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/renderer.ts
|
|
696
|
+
var langAliases = {
|
|
697
|
+
typescript: "ts",
|
|
698
|
+
javascript: "js"
|
|
699
|
+
};
|
|
700
|
+
function mergeConsecutiveCodeBlocks(nodes) {
|
|
701
|
+
if (nodes.length === 0) return nodes;
|
|
702
|
+
const result = [];
|
|
703
|
+
let currentCodeBlock = null;
|
|
704
|
+
for (const node of nodes) {
|
|
705
|
+
if (node.kind === "code") {
|
|
706
|
+
if (currentCodeBlock && currentCodeBlock.lang === node.lang && !currentCodeBlock.title && !node.title) {
|
|
707
|
+
currentCodeBlock.text += "\n\n" + node.text;
|
|
708
|
+
} else {
|
|
709
|
+
if (currentCodeBlock) result.push(currentCodeBlock);
|
|
710
|
+
currentCodeBlock = { ...node };
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
if (currentCodeBlock) {
|
|
714
|
+
result.push(currentCodeBlock);
|
|
715
|
+
currentCodeBlock = null;
|
|
716
|
+
}
|
|
717
|
+
result.push(node);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (currentCodeBlock) result.push(currentCodeBlock);
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
function render(nodes, describeFormat2 = "hidden") {
|
|
724
|
+
const merged = mergeConsecutiveCodeBlocks(nodes.filter((n) => n.kind !== "output-file-display"));
|
|
725
|
+
if (!merged.length) return "";
|
|
726
|
+
let out = "";
|
|
727
|
+
let lastHeaderLevel = 0;
|
|
728
|
+
for (let i = 0; i < merged.length; i++) {
|
|
729
|
+
if (i > 0) {
|
|
730
|
+
const prev = merged[i - 1];
|
|
731
|
+
const curr = merged[i];
|
|
732
|
+
const noBlank = prev.kind === "prose" && prev.noBlankAfter || curr.kind === "prose" && curr.noBlankBefore;
|
|
733
|
+
out += noBlank ? "\n" : "\n\n";
|
|
734
|
+
}
|
|
735
|
+
const rendered = renderNode(merged[i], describeFormat2, lastHeaderLevel);
|
|
736
|
+
out += rendered;
|
|
737
|
+
const node = merged[i];
|
|
738
|
+
if (node.kind === "describe" && describeFormat2 !== "hidden") {
|
|
739
|
+
const baseLevel = describeFormat2 === "auto" ? lastHeaderLevel : describeFormat2.length;
|
|
740
|
+
lastHeaderLevel = baseLevel + node.depth;
|
|
741
|
+
lastHeaderLevel = Math.min(lastHeaderLevel, 6);
|
|
742
|
+
} else if (node.kind === "prose") {
|
|
743
|
+
const proseHeaderLevel = getMaxHeaderLevelInProse(node.text);
|
|
744
|
+
if (proseHeaderLevel > 0) {
|
|
745
|
+
lastHeaderLevel = proseHeaderLevel;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
function renderNode(node, describeFormat2 = "hidden", lastHeaderLevel = 0) {
|
|
752
|
+
if (node.kind === "prose") return node.text;
|
|
753
|
+
if (node.kind === "output-file-display") return "";
|
|
754
|
+
if (node.kind === "describe") {
|
|
755
|
+
if (describeFormat2 === "hidden") return "";
|
|
756
|
+
let baseLevel;
|
|
757
|
+
if (describeFormat2 === "auto") {
|
|
758
|
+
baseLevel = lastHeaderLevel === 0 ? 1 : lastHeaderLevel + 1;
|
|
759
|
+
} else {
|
|
760
|
+
baseLevel = describeFormat2.length > 0 ? describeFormat2.length : 1;
|
|
761
|
+
}
|
|
762
|
+
const level = baseLevel + node.depth;
|
|
763
|
+
const hashes = "#".repeat(Math.min(level, 6));
|
|
764
|
+
return `${hashes} ${node.name}`;
|
|
765
|
+
}
|
|
766
|
+
const lang = langAliases[node.lang] ?? node.lang;
|
|
767
|
+
const info = lang === "text" ? node.title ?? "" : node.title ? `${lang} ${node.title}` : lang;
|
|
768
|
+
const maxBackticks = findMaxBacktickSequence(node.text);
|
|
769
|
+
const fenceLength = Math.max(3, maxBackticks + 1);
|
|
770
|
+
const fence = "`".repeat(fenceLength);
|
|
771
|
+
return `${fence}${info}
|
|
772
|
+
${node.text}
|
|
773
|
+
${fence}`;
|
|
774
|
+
}
|
|
775
|
+
function findMaxBacktickSequence(text) {
|
|
776
|
+
let maxSeq = 0;
|
|
777
|
+
let currentSeq = 0;
|
|
778
|
+
for (const char of text) {
|
|
779
|
+
if (char === "`") {
|
|
780
|
+
currentSeq++;
|
|
781
|
+
maxSeq = Math.max(maxSeq, currentSeq);
|
|
782
|
+
} else {
|
|
783
|
+
currentSeq = 0;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return maxSeq;
|
|
787
|
+
}
|
|
788
|
+
function getMaxHeaderLevelInProse(text) {
|
|
789
|
+
let maxLevel = 0;
|
|
790
|
+
const lines = text.split("\n");
|
|
791
|
+
for (const line of lines) {
|
|
792
|
+
const match = line.match(/^(#+)\s/);
|
|
793
|
+
if (match) {
|
|
794
|
+
const level = match[1].length;
|
|
795
|
+
maxLevel = Math.max(maxLevel, Math.min(level, 6));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return maxLevel;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/typecheck.ts
|
|
802
|
+
import ts2 from "typescript";
|
|
803
|
+
import { join as join2 } from "path";
|
|
804
|
+
function typecheck(files) {
|
|
805
|
+
const tsConfigPath = join2(process.cwd(), "tsconfig.json");
|
|
806
|
+
const configPath = ts2.sys.fileExists(tsConfigPath) ? tsConfigPath : void 0;
|
|
807
|
+
let compilerOptions;
|
|
808
|
+
if (configPath) {
|
|
809
|
+
const configFile = ts2.readConfigFile(configPath, ts2.sys.readFile);
|
|
810
|
+
if (configFile.error) {
|
|
811
|
+
return { ok: false, messages: [ts2.flattenDiagnosticMessageText(configFile.error.messageText, "\n")] };
|
|
812
|
+
}
|
|
813
|
+
const parsed = ts2.parseJsonConfigFileContent(configFile.config, ts2.sys, ts2.sys.getCurrentDirectory());
|
|
814
|
+
compilerOptions = { ...parsed.options, noEmit: true };
|
|
815
|
+
} else {
|
|
816
|
+
compilerOptions = {
|
|
817
|
+
strict: true,
|
|
818
|
+
noEmit: true,
|
|
819
|
+
module: ts2.ModuleKind.NodeNext,
|
|
820
|
+
moduleResolution: ts2.ModuleResolutionKind.NodeNext,
|
|
821
|
+
target: ts2.ScriptTarget.ESNext,
|
|
822
|
+
allowImportingTsExtensions: true
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
const program = ts2.createProgram(files, compilerOptions);
|
|
826
|
+
const diagnostics = ts2.getPreEmitDiagnostics(program);
|
|
827
|
+
if (!diagnostics.length) return { ok: true, messages: [] };
|
|
828
|
+
const formatted = ts2.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
829
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
830
|
+
getCurrentDirectory: () => process.cwd(),
|
|
831
|
+
getNewLine: () => "\n"
|
|
832
|
+
});
|
|
833
|
+
return { ok: false, messages: [formatted] };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/shell.ts
|
|
837
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdtempSync as mkdtempSync2, rmSync as rmSync2, existsSync } from "node:fs";
|
|
838
|
+
import { isAbsolute as isAbsolute2, resolve, join as join3, dirname } from "node:path";
|
|
839
|
+
import { test } from "node:test";
|
|
840
|
+
import { test as test2, test as test3, describe as describe2 } from "node:test";
|
|
841
|
+
function stripTypesFlag() {
|
|
842
|
+
const parts = process.versions.node.split(".");
|
|
843
|
+
const major = parseInt(parts[0] ?? "0", 10);
|
|
844
|
+
const minor = parseInt(parts[1] ?? "0", 10);
|
|
845
|
+
if (major === 22 && minor >= 6) return "--experimental-strip-types";
|
|
846
|
+
if (major === 23 && minor < 6) return "--experimental-strip-types";
|
|
847
|
+
return "";
|
|
848
|
+
}
|
|
849
|
+
function extractImportPaths(filePath, baseDir) {
|
|
850
|
+
const importedPaths = /* @__PURE__ */ new Set();
|
|
851
|
+
try {
|
|
852
|
+
const content = readFileSync2(filePath, "utf8");
|
|
853
|
+
const importRegex = /import\s+(?:(?:{[^}]*})|(?:\*\s+as\s+\w+)|(?:\w+))?(?:\s*,\s*(?:{[^}]*}|\*\s+as\s+\w+|\w+))*\s+from\s+['"]([^'"]+)['"]/g;
|
|
854
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
855
|
+
let match;
|
|
856
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
857
|
+
const importPath = match[1];
|
|
858
|
+
if (importPath && !importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const resolved = resolveImportPath(importPath, baseDir);
|
|
862
|
+
if (resolved) importedPaths.add(resolved);
|
|
863
|
+
}
|
|
864
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
865
|
+
const importPath = match[1];
|
|
866
|
+
if (importPath && !importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const resolved = resolveImportPath(importPath, baseDir);
|
|
870
|
+
if (resolved) importedPaths.add(resolved);
|
|
871
|
+
}
|
|
872
|
+
} catch {
|
|
873
|
+
}
|
|
874
|
+
return importedPaths;
|
|
875
|
+
}
|
|
876
|
+
function resolveImportPath(importPath, baseDir) {
|
|
877
|
+
const basePath = resolve(baseDir, importPath);
|
|
878
|
+
if (existsSync(basePath)) return basePath;
|
|
879
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
880
|
+
if (existsSync(basePath + ext)) {
|
|
881
|
+
return basePath + ext;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const indexFile of ["index.ts", "index.tsx", "index.js", "index.jsx"]) {
|
|
885
|
+
const indexPath = join3(basePath, indexFile);
|
|
886
|
+
if (existsSync(indexPath)) {
|
|
887
|
+
return indexPath;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
function collectAllDependencies(inputPaths2, visited = /* @__PURE__ */ new Set()) {
|
|
893
|
+
const allDeps = new Set(inputPaths2.map((p) => resolve(p)));
|
|
894
|
+
const toProcess = [...inputPaths2];
|
|
895
|
+
while (toProcess.length > 0) {
|
|
896
|
+
const current = toProcess.shift();
|
|
897
|
+
const resolvedCurrent = resolve(current);
|
|
898
|
+
if (visited.has(resolvedCurrent)) continue;
|
|
899
|
+
visited.add(resolvedCurrent);
|
|
900
|
+
const deps = extractImportPaths(resolvedCurrent, dirname(resolvedCurrent));
|
|
901
|
+
for (const dep of deps) {
|
|
902
|
+
allDeps.add(dep);
|
|
903
|
+
if (!visited.has(dep)) {
|
|
904
|
+
toProcess.push(dep);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return allDeps;
|
|
909
|
+
}
|
|
910
|
+
async function watchFilesAndWait(inputPaths2) {
|
|
911
|
+
if (!process.stdin.isTTY) {
|
|
912
|
+
return "spacebar";
|
|
913
|
+
}
|
|
914
|
+
const { watch } = await import("node:fs");
|
|
915
|
+
let lastChangeTime = 0;
|
|
916
|
+
const DEBOUNCE_MS = 300;
|
|
917
|
+
let changeDetected = false;
|
|
918
|
+
const filesToWatch = collectAllDependencies(inputPaths2);
|
|
919
|
+
const watchers = Array.from(filesToWatch).map((filePath) => {
|
|
920
|
+
return watch(filePath, (eventType) => {
|
|
921
|
+
const now = Date.now();
|
|
922
|
+
if (now - lastChangeTime >= DEBOUNCE_MS) {
|
|
923
|
+
lastChangeTime = now;
|
|
924
|
+
changeDetected = true;
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
const exitHandler = () => {
|
|
929
|
+
watchers.forEach((w) => w.close());
|
|
930
|
+
process.stdin.setRawMode(false);
|
|
931
|
+
process.exit(0);
|
|
932
|
+
};
|
|
933
|
+
process.on("SIGINT", exitHandler);
|
|
934
|
+
process.on("SIGTERM", exitHandler);
|
|
935
|
+
console.error("Press space to regenerate, q to quit...");
|
|
936
|
+
process.stdin.setRawMode(true);
|
|
937
|
+
process.stdin.resume();
|
|
938
|
+
return new Promise((resolve3) => {
|
|
939
|
+
const onData = (data) => {
|
|
940
|
+
const char = data[0];
|
|
941
|
+
if (char === 32) {
|
|
942
|
+
cleanup();
|
|
943
|
+
resolve3("spacebar");
|
|
944
|
+
} else if (char === 3 || char === 113 || char === 120 || char === 27) {
|
|
945
|
+
cleanup();
|
|
946
|
+
process.exit(0);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const checkForChanges = setInterval(() => {
|
|
950
|
+
if (changeDetected) {
|
|
951
|
+
cleanup();
|
|
952
|
+
console.error("Files changed, regenerating...");
|
|
953
|
+
resolve3("filechange");
|
|
954
|
+
}
|
|
955
|
+
}, 50);
|
|
956
|
+
const cleanup = () => {
|
|
957
|
+
process.stdin.off("data", onData);
|
|
958
|
+
clearInterval(checkForChanges);
|
|
959
|
+
process.stdin.setRawMode(false);
|
|
960
|
+
process.stdin.pause();
|
|
961
|
+
process.removeListener("SIGINT", exitHandler);
|
|
962
|
+
process.removeListener("SIGTERM", exitHandler);
|
|
963
|
+
watchers.forEach((w) => w.close());
|
|
964
|
+
};
|
|
965
|
+
process.stdin.on("data", onData);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/resolver.ts
|
|
970
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
971
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "node:fs";
|
|
972
|
+
import { join as join4 } from "node:path";
|
|
973
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
974
|
+
function resolveOutputFiles(nodes) {
|
|
975
|
+
const result = [];
|
|
976
|
+
for (const node of nodes) {
|
|
977
|
+
if (node.kind !== "output-file-display") {
|
|
978
|
+
result.push(node);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const content = runAndCapture(node);
|
|
982
|
+
if (content !== null) {
|
|
983
|
+
const prev = result[result.length - 1];
|
|
984
|
+
if (content.trim()) {
|
|
985
|
+
if (prev?.kind === "prose" && prev.text.endsWith(".")) {
|
|
986
|
+
result[result.length - 1] = { ...prev, text: prev.text.slice(0, -1) + ":", noBlankAfter: true };
|
|
987
|
+
}
|
|
988
|
+
result.push({ kind: "code", lang: node.lang, text: content.trimEnd() });
|
|
989
|
+
} else {
|
|
990
|
+
if (prev?.kind === "prose" && (prev.text.endsWith(":") || prev.text.endsWith("."))) {
|
|
991
|
+
result[result.length - 1] = { ...prev, text: prev.text.slice(0, -1) + " is empty." };
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
}
|
|
998
|
+
function runAndCapture(node) {
|
|
999
|
+
if (node.execution) {
|
|
1000
|
+
if (node.execution.exitCode !== 0) return null;
|
|
1001
|
+
const content = node.execution.outputFiles.get(node.path);
|
|
1002
|
+
return content ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
const tmpDir = mkdtempSync3(join4(tmpdir2(), "lit-md-cap-"));
|
|
1005
|
+
try {
|
|
1006
|
+
for (const f of node.inputFiles) {
|
|
1007
|
+
writeFileSync3(join4(tmpDir, f.path), f.content, "utf8");
|
|
1008
|
+
}
|
|
1009
|
+
const result = spawnSync2(node.cmd, { shell: true, encoding: "utf8", cwd: tmpDir });
|
|
1010
|
+
if (result.status !== 0) return null;
|
|
1011
|
+
try {
|
|
1012
|
+
return readFileSync3(join4(tmpDir, node.path), "utf8");
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
} finally {
|
|
1017
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/describe-format.ts
|
|
1022
|
+
var overrideFormat = void 0;
|
|
1023
|
+
function resetDescribeFormat() {
|
|
1024
|
+
overrideFormat = void 0;
|
|
1025
|
+
}
|
|
1026
|
+
function resolveDescribeFormat(cliFormat) {
|
|
1027
|
+
return overrideFormat ?? cliFormat;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/cli.ts
|
|
1031
|
+
var args = process.argv.slice(2);
|
|
1032
|
+
function extractFlag(flag) {
|
|
1033
|
+
const idx = args.indexOf(flag);
|
|
1034
|
+
if (idx === -1) return false;
|
|
1035
|
+
args.splice(idx, 1);
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
function extractFlagValue(flag) {
|
|
1039
|
+
const idx = args.indexOf(flag);
|
|
1040
|
+
if (idx === -1) {
|
|
1041
|
+
const eqIdx = args.findIndex((arg) => arg.startsWith(flag + "="));
|
|
1042
|
+
if (eqIdx === -1) return void 0;
|
|
1043
|
+
const value2 = args[eqIdx].slice(flag.length + 1);
|
|
1044
|
+
args.splice(eqIdx, 1);
|
|
1045
|
+
return value2;
|
|
1046
|
+
}
|
|
1047
|
+
const value = args[idx + 1];
|
|
1048
|
+
args.splice(idx, 2);
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1051
|
+
var showHelp = extractFlag("--help") || extractFlag("-h");
|
|
1052
|
+
var dryrun = extractFlag("--dryrun");
|
|
1053
|
+
var runTests = extractFlag("--test");
|
|
1054
|
+
var runTypecheck = extractFlag("--typecheck");
|
|
1055
|
+
var updateSnapshots = extractFlag("--update-snapshots") || extractFlag("-u");
|
|
1056
|
+
var wait = extractFlag("--wait");
|
|
1057
|
+
var outFlag = extractFlagValue("--out");
|
|
1058
|
+
var outputDir = extractFlagValue("--outDir");
|
|
1059
|
+
var describeFormat = extractFlagValue("--describe") || "##";
|
|
1060
|
+
function parseTestSummary(output) {
|
|
1061
|
+
const lines = output.split("\n");
|
|
1062
|
+
let stats = { passed: 0, failed: 0, total: 0, hasFailed: false };
|
|
1063
|
+
for (const line of lines) {
|
|
1064
|
+
if (line.includes("\u2139 pass")) {
|
|
1065
|
+
const match = line.match(/pass\s+(\d+)/);
|
|
1066
|
+
if (match) stats.passed = parseInt(match[1]);
|
|
1067
|
+
}
|
|
1068
|
+
if (line.includes("\u2139 fail")) {
|
|
1069
|
+
const match = line.match(/fail\s+(\d+)/);
|
|
1070
|
+
if (match) stats.failed = parseInt(match[1]);
|
|
1071
|
+
}
|
|
1072
|
+
if (line.includes("\u2139 tests")) {
|
|
1073
|
+
const match = line.match(/tests\s+(\d+)/);
|
|
1074
|
+
if (match) stats.total = parseInt(match[1]);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
stats.hasFailed = stats.failed > 0;
|
|
1078
|
+
return stats;
|
|
1079
|
+
}
|
|
1080
|
+
var unknownOptions = args.filter((a) => a.startsWith("--") || a.startsWith("-") && a.length > 1 && a !== "-");
|
|
1081
|
+
if (unknownOptions.length > 0) {
|
|
1082
|
+
console.error(`error: unknown option${unknownOptions.length > 1 ? "s" : ""}: ${unknownOptions.join(", ")}`);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
var inputPaths = args.filter((a) => !a.startsWith("--"));
|
|
1086
|
+
function getOutputFileName(inputPath) {
|
|
1087
|
+
const base = basename(inputPath);
|
|
1088
|
+
if (base.endsWith(".lit-md.ts") || base.endsWith(".lit-md.js")) {
|
|
1089
|
+
return base.slice(0, -(base.endsWith(".lit-md.ts") ? ".lit-md.ts".length : ".lit-md.js".length)) + ".md";
|
|
1090
|
+
}
|
|
1091
|
+
return basename(inputPath, extname2(inputPath)) + ".md";
|
|
1092
|
+
}
|
|
1093
|
+
if (showHelp) {
|
|
1094
|
+
console.log(`lit-md - Generate markdown documentation from test files
|
|
1095
|
+
|
|
1096
|
+
Usage: lit-md [options] <file.ts|js> [file2 ...]
|
|
1097
|
+
|
|
1098
|
+
Options:
|
|
1099
|
+
--help, -h Show this help message
|
|
1100
|
+
--test Run tests before generating markdown
|
|
1101
|
+
--typecheck Run type checking before generating markdown
|
|
1102
|
+
--dryrun Show what would be written without writing files
|
|
1103
|
+
-u, --update-snapshots Update snapshot files instead of generating markdown
|
|
1104
|
+
--wait After generating, keep the process alive and watch for file
|
|
1105
|
+
changes. Press space to manually regenerate, Ctrl+C to exit.
|
|
1106
|
+
Works with --test and --typecheck (reruns on each change).
|
|
1107
|
+
--out <output.md> Write to a specific output file (requires single input)
|
|
1108
|
+
--outDir <dir> Write generated markdown files to this directory
|
|
1109
|
+
--describe <format> Control describe() block rendering (default: ##)
|
|
1110
|
+
Formats:
|
|
1111
|
+
hidden - Omit describes
|
|
1112
|
+
# - Render as h1 headers, nested as h2, h3, etc.
|
|
1113
|
+
## - Render as h2 headers, nested as h3, h4, etc. (default)
|
|
1114
|
+
### - Render as h3 headers, nested as h4, h5, etc.
|
|
1115
|
+
#### - Render as h4 headers, nested as h5, h6, etc.
|
|
1116
|
+
auto - Dynamically determine level based on document structure
|
|
1117
|
+
(h1 if no headers exist, else one level deeper than last header)
|
|
1118
|
+
|
|
1119
|
+
By default, output is written to stdout. Use --out or --outDir to write to files.
|
|
1120
|
+
|
|
1121
|
+
Examples:
|
|
1122
|
+
lit-md README.md.test.ts # outputs to stdout
|
|
1123
|
+
lit-md --test --typecheck README.md.test.ts # outputs to stdout after testing
|
|
1124
|
+
lit-md --wait README.md.test.ts # outputs to stdout, then waits for changes
|
|
1125
|
+
lit-md --out /tmp/docs.md README.md.test.ts # writes to file
|
|
1126
|
+
lit-md --outDir ./docs src/**/*.md.test.ts # writes to directory
|
|
1127
|
+
lit-md --describe="#" README.md.test.ts # outputs to stdout with custom format
|
|
1128
|
+
lit-md --describe="auto" README.md.test.ts # outputs to stdout with auto format
|
|
1129
|
+
`);
|
|
1130
|
+
process.exit(0);
|
|
1131
|
+
}
|
|
1132
|
+
if (!inputPaths.length) {
|
|
1133
|
+
console.error("Usage: lit-md [--test] [--typecheck] [--dryrun] [-u|--update-snapshots] [--out <output.md>] [--outDir <dir>] <file.ts|js> [file2 ...]");
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
var validDescribeFormats = ["hidden", "auto", "#", "##", "###", "####"];
|
|
1137
|
+
if (!validDescribeFormats.includes(describeFormat)) {
|
|
1138
|
+
console.error(`error: invalid --describe format: ${describeFormat}. Valid formats: ${validDescribeFormats.join(", ")}`);
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
1141
|
+
if (outFlag && outputDir) {
|
|
1142
|
+
console.error("error: --out and --outDir are mutually exclusive");
|
|
1143
|
+
process.exit(1);
|
|
1144
|
+
}
|
|
1145
|
+
if (outFlag && inputPaths.length > 1) {
|
|
1146
|
+
console.error("error: --out can only be used with a single input file");
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
if (runTypecheck) {
|
|
1150
|
+
const jsFiles = inputPaths.filter((f) => extname2(f) === ".js");
|
|
1151
|
+
if (jsFiles.length) {
|
|
1152
|
+
console.error(`error: --typecheck requires .ts files; received: ${jsFiles.join(", ")}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function generateMarkdown() {
|
|
1157
|
+
let filesGenerated = 0;
|
|
1158
|
+
for (const inputPath of inputPaths) {
|
|
1159
|
+
resetDescribeFormat();
|
|
1160
|
+
const absolutePath = resolve2(inputPath);
|
|
1161
|
+
try {
|
|
1162
|
+
const origStdoutWrite = process.stdout.write;
|
|
1163
|
+
const origStderrWrite = process.stderr.write;
|
|
1164
|
+
const origLog = console.log;
|
|
1165
|
+
const origInfo = console.info;
|
|
1166
|
+
const origWarn = console.warn;
|
|
1167
|
+
try {
|
|
1168
|
+
process.stdout.write = () => true;
|
|
1169
|
+
process.stderr.write = () => true;
|
|
1170
|
+
console.log = () => {
|
|
1171
|
+
};
|
|
1172
|
+
console.info = () => {
|
|
1173
|
+
};
|
|
1174
|
+
console.warn = () => {
|
|
1175
|
+
};
|
|
1176
|
+
await import(absolutePath);
|
|
1177
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
1178
|
+
} finally {
|
|
1179
|
+
process.stdout.write = origStdoutWrite;
|
|
1180
|
+
process.stderr.write = origStderrWrite;
|
|
1181
|
+
console.log = origLog;
|
|
1182
|
+
console.info = origInfo;
|
|
1183
|
+
console.warn = origWarn;
|
|
1184
|
+
}
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
const src = readFileSync4(inputPath, "utf8");
|
|
1188
|
+
const lang = extname2(inputPath) === ".js" ? "javascript" : "typescript";
|
|
1189
|
+
let nodes = parse(src, lang);
|
|
1190
|
+
if (!dryrun) {
|
|
1191
|
+
nodes = resolveOutputFiles(nodes);
|
|
1192
|
+
}
|
|
1193
|
+
const finalDescribeFormat = resolveDescribeFormat(describeFormat);
|
|
1194
|
+
const md = render(nodes, finalDescribeFormat);
|
|
1195
|
+
let outPath = null;
|
|
1196
|
+
let isStdout = false;
|
|
1197
|
+
if (updateSnapshots) {
|
|
1198
|
+
const outputFileName = getOutputFileName(inputPath);
|
|
1199
|
+
const fileNameWithoutMd = outputFileName.slice(0, -3);
|
|
1200
|
+
outPath = join5(dirname2(resolve2(inputPath)), `${fileNameWithoutMd}.snapshot.md`);
|
|
1201
|
+
} else if (outFlag) {
|
|
1202
|
+
outPath = outFlag;
|
|
1203
|
+
} else if (outputDir) {
|
|
1204
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1205
|
+
outPath = join5(outputDir, getOutputFileName(inputPath));
|
|
1206
|
+
} else {
|
|
1207
|
+
isStdout = true;
|
|
1208
|
+
}
|
|
1209
|
+
if (dryrun) {
|
|
1210
|
+
if (isStdout) {
|
|
1211
|
+
console.error(`dry run: would write to stdout`);
|
|
1212
|
+
} else {
|
|
1213
|
+
console.error(`dry run: would write ${outPath}`);
|
|
1214
|
+
}
|
|
1215
|
+
} else if (isStdout) {
|
|
1216
|
+
process.stdout.write(md + "\n");
|
|
1217
|
+
} else {
|
|
1218
|
+
writeFileSync4(outPath, md + "\n", "utf8");
|
|
1219
|
+
filesGenerated++;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!dryrun && filesGenerated > 0) {
|
|
1223
|
+
const fileWord = filesGenerated === 1 ? "file" : "files";
|
|
1224
|
+
console.error(`\u2705 Generated ${filesGenerated} ${fileWord}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async function executeTasks() {
|
|
1228
|
+
if (runTypecheck) {
|
|
1229
|
+
const result = typecheck(inputPaths.map((p) => resolve2(p)));
|
|
1230
|
+
if (!result.ok) {
|
|
1231
|
+
for (const msg of result.messages) console.error(msg);
|
|
1232
|
+
console.error("\u274C Typecheck failed");
|
|
1233
|
+
if (!wait) process.exit(1);
|
|
1234
|
+
} else {
|
|
1235
|
+
console.error("\u2705 Typecheck passed");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (runTests) {
|
|
1239
|
+
const stripFlag = stripTypesFlag();
|
|
1240
|
+
const nodeArgs = ["--test", ...stripFlag ? [stripFlag] : [], ...inputPaths.map((p) => resolve2(p))];
|
|
1241
|
+
const spawnOptions = wait ? { encoding: "utf-8" } : { stdio: "inherit", env: process.env };
|
|
1242
|
+
const result = spawnSync3(process.execPath, nodeArgs, spawnOptions);
|
|
1243
|
+
if (wait && result.stdout) {
|
|
1244
|
+
const output = result.stdout.toString();
|
|
1245
|
+
const stats = parseTestSummary(output);
|
|
1246
|
+
if (stats.hasFailed) {
|
|
1247
|
+
const failureStart = output.indexOf("\u2716 failing tests");
|
|
1248
|
+
if (failureStart !== -1) {
|
|
1249
|
+
const failureSection = output.substring(failureStart);
|
|
1250
|
+
console.error(failureSection);
|
|
1251
|
+
}
|
|
1252
|
+
console.error(`
|
|
1253
|
+
\u274C Tests failed: ${stats.failed}/${stats.total} failed`);
|
|
1254
|
+
} else {
|
|
1255
|
+
console.log(`\u2705 Tests passed: ${stats.passed} passed`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (result.status !== 0) {
|
|
1259
|
+
if (!wait) process.exit(result.status ?? 1);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
await generateMarkdown();
|
|
1263
|
+
}
|
|
1264
|
+
(async () => {
|
|
1265
|
+
await executeTasks();
|
|
1266
|
+
if (wait && process.stdin.isTTY) {
|
|
1267
|
+
while (true) {
|
|
1268
|
+
const trigger = await watchFilesAndWait(inputPaths);
|
|
1269
|
+
await executeTasks();
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
})();
|
|
1273
|
+
//# sourceMappingURL=cli.js.map
|