Skip to content

Commit 70994f7

Browse files
Copilotfregante
andcommitted
Replace custom code in add-examples-to-dts.ts with ts-morph
Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
1 parent 484c119 commit 70994f7

File tree

3 files changed

+176
-82
lines changed

3 files changed

+176
-82
lines changed

add-examples-to-dts.ts

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */
2-
import {readFileSync, writeFileSync} from 'node:fs';
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 */
2+
import {readFileSync} from 'node:fs';
33
import {execSync} from 'node:child_process';
4+
import {Project} 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,21 @@ 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('temp.d.ts', 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+
// Process each exported variable declaration (these are the function declarations)
28+
for (const statement of sourceFile.getVariableStatements()) {
29+
// Only process exported statements
30+
if (!statement.isExported()) {
31+
continue;
32+
}
33+
34+
for (const declaration of statement.getDeclarations()) {
35+
const functionName = declaration.getName();
3036

3137
// Get the tests/examples for this function
3238
const examples = getTests(functionName);
@@ -37,104 +43,115 @@ for (const line of lines) {
3743
const urlExamples = examples.filter((url: string) => url.startsWith('http'));
3844

3945
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
46+
// Get or create JSDoc for this statement (not the declaration)
47+
const jsDoc = statement.getJsDocs()[0];
48+
49+
if (jsDoc) {
50+
// Add @example tags to existing JSDoc
51+
const existingTags = jsDoc.getTags();
52+
const description = jsDoc.getDescription().trim();
53+
54+
// Build new JSDoc content
55+
const newJsDocLines: string[] = [];
56+
if (description) {
57+
newJsDocLines.push(description);
5258
}
5359

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+
// Add existing tags (that aren't @example tags)
61+
for (const tag of existingTags) {
62+
if (tag.getTagName() !== 'example') {
63+
newJsDocLines.push(tag.getText());
64+
}
6065
}
6166

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-
}
67+
// Add new @example tags
68+
for (const url of urlExamples) {
69+
newJsDocLines.push(`@example ${url}`);
70+
}
7271

73-
break;
72+
// Replace the JSDoc
73+
jsDoc.remove();
74+
statement.addJsDoc(newJsDocLines.join('\n'));
75+
} else {
76+
// Create new JSDoc with examples
77+
const jsDocLines: string[] = [];
78+
for (const url of urlExamples) {
79+
jsDocLines.push(`@example ${url}`);
7480
}
7581

76-
// If we hit a non-JSDoc line, there's no JSDoc block
77-
break;
82+
statement.addJsDoc(jsDocLines.join('\n'));
7883
}
7984

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-
}
85+
examplesAdded += urlExamples.length;
86+
}
87+
}
88+
}
89+
}
9390

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-
}
91+
// Also process exported type aliases (like RepoExplorerInfo)
92+
for (const typeAlias of sourceFile.getTypeAliases()) {
93+
if (!typeAlias.isExported()) {
94+
continue;
95+
}
9996

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-
}
97+
const typeName = typeAlias.getName();
10798

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}`);
99+
// Get the tests/examples for this type (unlikely but keeping consistency)
100+
const examples = getTests(typeName);
101+
102+
if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') {
103+
const urlExamples = examples.filter((url: string) => url.startsWith('http'));
104+
105+
if (urlExamples.length > 0) {
106+
const jsDoc = typeAlias.getJsDocs()[0];
107+
108+
if (jsDoc) {
109+
const existingTags = jsDoc.getTags();
110+
const description = jsDoc.getDescription().trim();
111+
112+
const newJsDocLines: string[] = [];
113+
if (description) {
114+
newJsDocLines.push(description);
115+
}
116+
117+
for (const tag of existingTags) {
118+
if (tag.getTagName() !== 'example') {
119+
newJsDocLines.push(tag.getText());
115120
}
121+
}
122+
123+
for (const url of urlExamples) {
124+
newJsDocLines.push(`@example ${url}`);
125+
}
116126

117-
outputLines.push(' */');
118-
examplesAdded += urlExamples.length;
127+
jsDoc.remove();
128+
typeAlias.addJsDoc(newJsDocLines.join('\n'));
129+
} else {
130+
const jsDocLines: string[] = [];
131+
for (const url of urlExamples) {
132+
jsDocLines.push(`@example ${url}`);
119133
}
134+
135+
typeAlias.addJsDoc(jsDocLines.join('\n'));
120136
}
137+
138+
examplesAdded += urlExamples.length;
121139
}
122140
}
123-
124-
outputLines.push(line);
125141
}
126142

127-
// Add marker at the beginning
128-
const finalContent = `${marker}\n${outputLines.join('\n')}`;
129-
130143
// Validate that we added some examples
131144
if (examplesAdded === 0) {
132145
console.error('❌ Error: No examples were added. This likely indicates a problem with the script.');
133146
process.exit(1);
134147
}
135148

149+
// Get the modified content and add marker
150+
const modifiedContent = sourceFile.getFullText();
151+
const finalContent = `${marker}\n${modifiedContent}`;
152+
136153
// Write the modified content back
137-
writeFileSync(dtsPath, finalContent, 'utf8');
154+
sourceFile.getProject().createSourceFile(dtsPath, finalContent, {overwrite: true}).saveSync();
138155

139156
console.log(`✓ Added ${examplesAdded} example URLs to index.d.ts`);
140157

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)