diff --git a/add-examples-to-dts.ts b/add-examples-to-dts.ts index 4c03c060..42c98fe7 100644 --- a/add-examples-to-dts.ts +++ b/add-examples-to-dts.ts @@ -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'; @@ -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); @@ -37,95 +86,33 @@ 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) { @@ -133,6 +120,10 @@ if (examplesAdded === 0) { 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'); diff --git a/package-lock.json b/package-lock.json index 2ab4f431..5479c857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,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", @@ -1385,6 +1387,34 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2665,6 +2695,13 @@ "node": ">=6" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -6644,6 +6681,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8014,6 +8058,17 @@ "typescript": ">=4.0.0" } }, + "node_modules/ts-morph": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.28.1", + "code-block-writer": "^13.0.3" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8022,6 +8077,26 @@ "license": "0BSD", "optional": true }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 65067444..17696cb2 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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",