Skip to content

Commit 54f9b25

Browse files
authored
Replace manual string parsing in add-examples-to-dts.ts with ts-morph (#218)
1 parent e81e3cc commit 54f9b25

File tree

3 files changed

+162
-94
lines changed

3 files changed

+162
-94
lines changed

add-examples-to-dts.ts

Lines changed: 84 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */
1+
/* eslint-disable n/prefer-global/process, unicorn/no-process-exit, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument */
22
import {readFileSync, writeFileSync} from 'node:fs';
33
import {execSync} from 'node:child_process';
4+
import {Project, type JSDocableNode} from 'ts-morph';
45
// Import index.ts to populate the test data via side effect
56
// eslint-disable-next-line import-x/no-unassigned-import
67
import './index.ts';
@@ -17,16 +18,64 @@ if (dtsContent.includes(marker)) {
1718
process.exit(1);
1819
}
1920

20-
// Process each exported function
21-
const lines = dtsContent.split('\n');
22-
const outputLines: string[] = [];
21+
// Create a ts-morph project and load the file
22+
const project = new Project();
23+
const sourceFile = project.createSourceFile(dtsPath, dtsContent, {overwrite: true});
24+
2325
let examplesAdded = 0;
2426

25-
for (const line of lines) {
26-
// Check if this is a function declaration
27-
const match = /^export declare const (\w+):/.exec(line);
28-
if (match) {
29-
const functionName = match[1];
27+
/**
28+
* Add example URLs to a JSDocable node (e.g., variable statement or type alias)
29+
*/
30+
function addExamplesToNode(node: JSDocableNode, urlExamples: string[]): void {
31+
const jsDoc = node.getJsDocs().at(0);
32+
33+
if (jsDoc) {
34+
// Add @example tags to existing JSDoc
35+
const existingTags = jsDoc.getTags();
36+
const description = jsDoc.getDescription().trim();
37+
38+
// Build new JSDoc content
39+
const newJsDocLines: string[] = [];
40+
if (description) {
41+
newJsDocLines.push(description);
42+
}
43+
44+
// Add existing tags (that aren't @example tags)
45+
for (const tag of existingTags) {
46+
if (tag.getTagName() !== 'example') {
47+
newJsDocLines.push(tag.getText());
48+
}
49+
}
50+
51+
// Add new @example tags
52+
for (const url of urlExamples) {
53+
newJsDocLines.push(`@example ${url}`);
54+
}
55+
56+
// Replace the JSDoc
57+
jsDoc.remove();
58+
node.addJsDoc(newJsDocLines.join('\n'));
59+
} else {
60+
// Create new JSDoc with examples
61+
const jsDocLines: string[] = [];
62+
for (const url of urlExamples) {
63+
jsDocLines.push(`@example ${url}`);
64+
}
65+
66+
node.addJsDoc(jsDocLines.join('\n'));
67+
}
68+
}
69+
70+
// Process each exported variable declaration (these are the function declarations)
71+
for (const statement of sourceFile.getVariableStatements()) {
72+
// Only process exported statements
73+
if (!statement.isExported()) {
74+
continue;
75+
}
76+
77+
for (const declaration of statement.getDeclarations()) {
78+
const functionName = declaration.getName();
3079

3180
// Get the tests/examples for this function
3281
const examples = getTests(functionName);
@@ -37,102 +86,44 @@ for (const line of lines) {
3786
const urlExamples = examples.filter((url: string) => url.startsWith('http'));
3887

3988
if (urlExamples.length > 0) {
40-
// Check if there's an existing JSDoc block immediately before this line
41-
let jsDocumentEndIndex = -1;
42-
let jsDocumentStartIndex = -1;
43-
let isSingleLineJsDocument = false;
44-
45-
// Look backwards from outputLines to find JSDoc
46-
for (let index = outputLines.length - 1; index >= 0; index--) {
47-
const previousLine = outputLines[index];
48-
const trimmed = previousLine.trim();
49-
50-
if (trimmed === '') {
51-
continue; // Skip empty lines
52-
}
53-
54-
// Check for single-line JSDoc: /** ... */
55-
if (trimmed.startsWith('/**') && trimmed.endsWith('*/') && trimmed.length > 5) {
56-
jsDocumentStartIndex = index;
57-
jsDocumentEndIndex = index;
58-
isSingleLineJsDocument = true;
59-
break;
60-
}
61-
62-
// Check for multi-line JSDoc ending
63-
if (trimmed === '*/') {
64-
jsDocumentEndIndex = index;
65-
// Now find the start of this JSDoc
66-
for (let k = index - 1; k >= 0; k--) {
67-
if (outputLines[k].trim().startsWith('/**')) {
68-
jsDocumentStartIndex = k;
69-
break;
70-
}
71-
}
72-
73-
break;
74-
}
75-
76-
// If we hit a non-JSDoc line, there's no JSDoc block
77-
break;
78-
}
79-
80-
if (jsDocumentStartIndex >= 0 && jsDocumentEndIndex >= 0) {
81-
// Extend existing JSDoc block
82-
if (isSingleLineJsDocument) {
83-
// Convert single-line to multi-line and add examples
84-
const singleLineContent = outputLines[jsDocumentStartIndex];
85-
// Extract the comment text without /** and */
86-
const commentText = singleLineContent.trim().slice(3, -2).trim();
87-
88-
// Replace the single line with multi-line format
89-
outputLines[jsDocumentStartIndex] = '/**';
90-
if (commentText) {
91-
outputLines.splice(jsDocumentStartIndex + 1, 0, ` * ${commentText}`);
92-
}
93-
94-
// Add examples after the existing content
95-
const insertIndex = jsDocumentStartIndex + (commentText ? 2 : 1);
96-
for (const url of urlExamples) {
97-
outputLines.splice(insertIndex + urlExamples.indexOf(url), 0, ` * @example ${url}`);
98-
}
99-
100-
outputLines.splice(insertIndex + urlExamples.length, 0, ' */');
101-
examplesAdded += urlExamples.length;
102-
} else {
103-
// Insert @example lines before the closing */
104-
for (const url of urlExamples) {
105-
outputLines.splice(jsDocumentEndIndex, 0, ` * @example ${url}`);
106-
}
107-
108-
examplesAdded += urlExamples.length;
109-
}
110-
} else {
111-
// Add new JSDoc comment with examples before the declaration
112-
outputLines.push('/**');
113-
for (const url of urlExamples) {
114-
outputLines.push(` * @example ${url}`);
115-
}
116-
117-
outputLines.push(' */');
118-
examplesAdded += urlExamples.length;
119-
}
89+
addExamplesToNode(statement, urlExamples);
90+
examplesAdded += urlExamples.length;
12091
}
12192
}
12293
}
123-
124-
outputLines.push(line);
12594
}
12695

127-
// Add marker at the beginning
128-
const finalContent = `${marker}\n${outputLines.join('\n')}`;
96+
// Also process exported type aliases (like RepoExplorerInfo)
97+
for (const typeAlias of sourceFile.getTypeAliases()) {
98+
if (!typeAlias.isExported()) {
99+
continue;
100+
}
101+
102+
const typeName = typeAlias.getName();
103+
104+
// Get the tests/examples for this type (unlikely but keeping consistency)
105+
const examples = getTests(typeName);
106+
107+
if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') {
108+
const urlExamples = examples.filter((url: string) => url.startsWith('http'));
109+
110+
if (urlExamples.length > 0) {
111+
addExamplesToNode(typeAlias, urlExamples);
112+
examplesAdded += urlExamples.length;
113+
}
114+
}
115+
}
129116

130117
// Validate that we added some examples
131118
if (examplesAdded === 0) {
132119
console.error('❌ Error: No examples were added. This likely indicates a problem with the script.');
133120
process.exit(1);
134121
}
135122

123+
// Get the modified content and add marker
124+
const modifiedContent = sourceFile.getFullText();
125+
const finalContent = `${marker}\n${modifiedContent}`;
126+
136127
// Write the modified content back
137128
writeFileSync(dtsPath, finalContent, 'utf8');
138129

package-lock.json

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"build": "run-p build:*",
2828
"build:esbuild": "esbuild index.ts --bundle --external:github-reserved-names --outdir=distribution --format=esm --drop-labels=TEST",
2929
"build:typescript": "tsc",
30-
"postbuild:typescript": "node add-examples-to-dts.ts",
30+
"postbuild:typescript": "tsx add-examples-to-dts.ts",
3131
"build:demo": "vite build demo",
3232
"try": "esbuild index.ts --bundle --global-name=x --format=iife | pbcopy && echo 'Copied to clipboard'",
3333
"fix": "xo --fix",
@@ -54,6 +54,8 @@
5454
"strip-indent": "^4.1.1",
5555
"svelte": "^5.46.1",
5656
"svelte-check": "^4.3.5",
57+
"ts-morph": "^27.0.2",
58+
"tsx": "^4.21.0",
5759
"typescript": "5.9.3",
5860
"vite": "^7.3.1",
5961
"vitest": "^4.0.17",

0 commit comments

Comments
 (0)