Skip to content

Commit 577f7b4

Browse files
Copilotfregante
andauthored
Add example URLs to JSDoc comments (#214)
Co-authored-by: fregante <me@fregante.com>
1 parent d5ec8ca commit 577f7b4

File tree

6 files changed

+167
-14
lines changed

6 files changed

+167
-14
lines changed

.github/workflows/esm-lint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ jobs:
1919
runs-on: ubuntu-latest
2020
steps:
2121
- uses: actions/checkout@v6
22+
- uses: actions/setup-node@v6
23+
with:
24+
node-version-file: package.json
2225
- run: npm install
2326
- run: npm run build --if-present
2427
- run: npm pack --dry-run

add-examples-to-dts.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */
2+
import {readFileSync, writeFileSync} from 'node:fs';
3+
import {execSync} from 'node:child_process';
4+
// Import index.ts to populate the test data via side effect
5+
// eslint-disable-next-line import-x/no-unassigned-import
6+
import './index.ts';
7+
import {getTests} from './collector.ts';
8+
9+
// Read the generated .d.ts file
10+
const dtsPath = './distribution/index.d.ts';
11+
const dtsContent = readFileSync(dtsPath, 'utf8');
12+
13+
// Check if script has already been run
14+
const marker = '/* Examples added by add-examples-to-dts.ts */';
15+
if (dtsContent.includes(marker)) {
16+
console.error('❌ Error: Examples have already been added to this file');
17+
process.exit(1);
18+
}
19+
20+
// Process each exported function
21+
const lines = dtsContent.split('\n');
22+
const outputLines: string[] = [];
23+
let examplesAdded = 0;
24+
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];
30+
31+
// Get the tests/examples for this function
32+
const examples = getTests(functionName);
33+
34+
// Only add examples if they exist and aren't the special 'combinedTestOnly' marker
35+
if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') {
36+
// Filter to only include actual URLs (not references to other functions)
37+
const urlExamples = examples.filter((url: string) => url.startsWith('http'));
38+
39+
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+
}
120+
}
121+
}
122+
}
123+
124+
outputLines.push(line);
125+
}
126+
127+
// Add marker at the beginning
128+
const finalContent = `${marker}\n${outputLines.join('\n')}`;
129+
130+
// Validate that we added some examples
131+
if (examplesAdded === 0) {
132+
console.error('❌ Error: No examples were added. This likely indicates a problem with the script.');
133+
process.exit(1);
134+
}
135+
136+
// Write the modified content back
137+
writeFileSync(dtsPath, finalContent, 'utf8');
138+
139+
console.log(`✓ Added ${examplesAdded} example URLs to index.d.ts`);
140+
141+
// Validate with TypeScript
142+
try {
143+
execSync('npx tsc --noEmit distribution/index.d.ts', {
144+
cwd: process.cwd(),
145+
stdio: 'pipe',
146+
});
147+
console.log('✓ TypeScript validation passed');
148+
} catch (error: unknown) {
149+
console.error('❌ TypeScript validation failed:');
150+
const execError = error as {stdout?: Uint8Array; stderr?: Uint8Array; message?: string};
151+
console.error(execError.stdout?.toString() ?? execError.stderr?.toString() ?? execError.message);
152+
process.exit(1);
153+
}

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'};
2-
import {addTests} from './collector.js';
2+
import {addTests} from './collector.ts';
33

44
const $ = <E extends Element>(selector: string) => document.querySelector<E>(selector);
55
const exists = (selector: string) => Boolean($(selector));

package-lock.json

Lines changed: 5 additions & 8 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
@@ -26,7 +26,8 @@
2626
"scripts": {
2727
"build": "run-p build:*",
2828
"build:esbuild": "esbuild index.ts --bundle --external:github-reserved-names --outdir=distribution --format=esm --drop-labels=TEST",
29-
"build:typescript": "tsc --declaration --emitDeclarationOnly",
29+
"build:typescript": "tsc",
30+
"postbuild:typescript": "node add-examples-to-dts.ts",
3031
"build:demo": "vite build demo",
3132
"try": "esbuild index.ts --bundle --global-name=x --format=iife | pbcopy && echo 'Copied to clipboard'",
3233
"fix": "xo --fix",
@@ -46,6 +47,7 @@
4647
"devDependencies": {
4748
"@sindresorhus/tsconfig": "^8.1.0",
4849
"@sveltejs/vite-plugin-svelte": "^6.2.4",
50+
"@types/node": "^25.0.8",
4951
"esbuild": "^0.27.2",
5052
"globals": "^17.0.0",
5153
"npm-run-all": "^4.1.5",

tsconfig.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
{
22
"extends": "@sindresorhus/tsconfig",
33
"compilerOptions": {
4-
// TODO: Drop after https://github.com/sindresorhus/tsconfig/issues/29
5-
"resolveJsonModule": true,
6-
"moduleResolution": "Node",
7-
"module": "Preserve"
4+
"emitDeclarationOnly": true,
5+
"allowImportingTsExtensions": true
86
},
97
"include": [
108
"index.ts",

0 commit comments

Comments
 (0)