Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 84 additions & 93 deletions add-examples-to-dts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */
/* 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 */
import {readFileSync, writeFileSync} from 'node:fs';
import {execSync} from 'node:child_process';
import {Project, type JSDocableNode} from 'ts-morph';
// Import index.ts to populate the test data via side effect
// eslint-disable-next-line import-x/no-unassigned-import
import './index.ts';
Expand All @@ -17,16 +18,64 @@ if (dtsContent.includes(marker)) {
process.exit(1);
}

// Process each exported function
const lines = dtsContent.split('\n');
const outputLines: string[] = [];
// Create a ts-morph project and load the file
const project = new Project();
const sourceFile = project.createSourceFile(dtsPath, dtsContent, {overwrite: true});

let examplesAdded = 0;

for (const line of lines) {
// Check if this is a function declaration
const match = /^export declare const (\w+):/.exec(line);
if (match) {
const functionName = match[1];
/**
* Add example URLs to a JSDocable node (e.g., variable statement or type alias)
*/
function addExamplesToNode(node: JSDocableNode, urlExamples: string[]): void {
const jsDoc = node.getJsDocs().at(0);

if (jsDoc) {
// Add @example tags to existing JSDoc
const existingTags = jsDoc.getTags();
const description = jsDoc.getDescription().trim();

// Build new JSDoc content
const newJsDocLines: string[] = [];
if (description) {
newJsDocLines.push(description);
}

// Add existing tags (that aren't @example tags)
for (const tag of existingTags) {
if (tag.getTagName() !== 'example') {
newJsDocLines.push(tag.getText());
}
}

// Add new @example tags
for (const url of urlExamples) {
newJsDocLines.push(`@example ${url}`);
}

// Replace the JSDoc
jsDoc.remove();
node.addJsDoc(newJsDocLines.join('\n'));
} else {
// Create new JSDoc with examples
const jsDocLines: string[] = [];
for (const url of urlExamples) {
jsDocLines.push(`@example ${url}`);
}

node.addJsDoc(jsDocLines.join('\n'));
}
}

// Process each exported variable declaration (these are the function declarations)
for (const statement of sourceFile.getVariableStatements()) {
// Only process exported statements
if (!statement.isExported()) {
continue;
}

for (const declaration of statement.getDeclarations()) {
const functionName = declaration.getName();

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

if (urlExamples.length > 0) {
// Check if there's an existing JSDoc block immediately before this line
let jsDocumentEndIndex = -1;
let jsDocumentStartIndex = -1;
let isSingleLineJsDocument = false;

// Look backwards from outputLines to find JSDoc
for (let index = outputLines.length - 1; index >= 0; index--) {
const previousLine = outputLines[index];
const trimmed = previousLine.trim();

if (trimmed === '') {
continue; // Skip empty lines
}

// Check for single-line JSDoc: /** ... */
if (trimmed.startsWith('/**') && trimmed.endsWith('*/') && trimmed.length > 5) {
jsDocumentStartIndex = index;
jsDocumentEndIndex = index;
isSingleLineJsDocument = true;
break;
}

// Check for multi-line JSDoc ending
if (trimmed === '*/') {
jsDocumentEndIndex = index;
// Now find the start of this JSDoc
for (let k = index - 1; k >= 0; k--) {
if (outputLines[k].trim().startsWith('/**')) {
jsDocumentStartIndex = k;
break;
}
}

break;
}

// If we hit a non-JSDoc line, there's no JSDoc block
break;
}

if (jsDocumentStartIndex >= 0 && jsDocumentEndIndex >= 0) {
// Extend existing JSDoc block
if (isSingleLineJsDocument) {
// Convert single-line to multi-line and add examples
const singleLineContent = outputLines[jsDocumentStartIndex];
// Extract the comment text without /** and */
const commentText = singleLineContent.trim().slice(3, -2).trim();

// Replace the single line with multi-line format
outputLines[jsDocumentStartIndex] = '/**';
if (commentText) {
outputLines.splice(jsDocumentStartIndex + 1, 0, ` * ${commentText}`);
}

// Add examples after the existing content
const insertIndex = jsDocumentStartIndex + (commentText ? 2 : 1);
for (const url of urlExamples) {
outputLines.splice(insertIndex + urlExamples.indexOf(url), 0, ` * @example ${url}`);
}

outputLines.splice(insertIndex + urlExamples.length, 0, ' */');
examplesAdded += urlExamples.length;
} else {
// Insert @example lines before the closing */
for (const url of urlExamples) {
outputLines.splice(jsDocumentEndIndex, 0, ` * @example ${url}`);
}

examplesAdded += urlExamples.length;
}
} else {
// Add new JSDoc comment with examples before the declaration
outputLines.push('/**');
for (const url of urlExamples) {
outputLines.push(` * @example ${url}`);
}

outputLines.push(' */');
examplesAdded += urlExamples.length;
}
addExamplesToNode(statement, urlExamples);
examplesAdded += urlExamples.length;
}
}
}

outputLines.push(line);
}

// Add marker at the beginning
const finalContent = `${marker}\n${outputLines.join('\n')}`;
// Also process exported type aliases (like RepoExplorerInfo)
for (const typeAlias of sourceFile.getTypeAliases()) {
if (!typeAlias.isExported()) {
continue;
}

const typeName = typeAlias.getName();

// Get the tests/examples for this type (unlikely but keeping consistency)
const examples = getTests(typeName);

if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') {
const urlExamples = examples.filter((url: string) => url.startsWith('http'));

if (urlExamples.length > 0) {
addExamplesToNode(typeAlias, urlExamples);
examplesAdded += urlExamples.length;
}
}
}

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

// Get the modified content and add marker
const modifiedContent = sourceFile.getFullText();
const finalContent = `${marker}\n${modifiedContent}`;

// Write the modified content back
writeFileSync(dtsPath, finalContent, 'utf8');

Expand Down
75 changes: 75 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build": "run-p build:*",
"build:esbuild": "esbuild index.ts --bundle --external:github-reserved-names --outdir=distribution --format=esm --drop-labels=TEST",
"build:typescript": "tsc",
"postbuild:typescript": "node add-examples-to-dts.ts",
"postbuild:typescript": "tsx add-examples-to-dts.ts",
"build:demo": "vite build demo",
"try": "esbuild index.ts --bundle --global-name=x --format=iife | pbcopy && echo 'Copied to clipboard'",
"fix": "xo --fix",
Expand All @@ -54,6 +54,8 @@
"strip-indent": "^4.1.1",
"svelte": "^5.46.1",
"svelte-check": "^4.3.5",
"ts-morph": "^27.0.2",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.17",
Expand Down