Skip to content

Commit acdd96c

Browse files
committed
chore: add script to check generated d.ts jsdoc links
1 parent 81820dc commit acdd96c

File tree

6 files changed

+195
-11
lines changed

6 files changed

+195
-11
lines changed

.github/actions/build/action.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ runs:
3333
cd lib
3434
npm run check-exports
3535
shell: bash
36+
37+
- name: Check generated jsdoc links
38+
run: npm run check:jsdoc-links
39+
shell: bash

.github/actions/lint/action.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ runs:
2929
git config --global --add safe.directory "$(pwd)"
3030
npm run lint:commits
3131
shell: bash
32+
33+
- name: Check markdown links
34+
run: npm run check:md-links
35+
shell: bash

lib/src/markers/Markers.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const MarkersRenderFunction = (
1919
* @param props - The properties for the markers.
2020
* @param ref - The ref to access the markers API.
2121
* @returns A React component that renders the markers.
22-
* @see {@link https://tradingview.github.io/lightweight-charts/tutorials/how_to/series-markers | Markers documentation}
23-
* @see {@link https://tradingview.github.io/lightweight-charts/docs/markers | TradingView documentation for markers}
22+
* @see {@link https://ukorvl.github.io/lightweight-charts-react-components/docs/markers | Documentation for Markers}
23+
* @see {@link https://tradingview.github.io/lightweight-charts/tutorials/how_to/series-markers | TradingView documentation for markers}
2424
* @example
2525
* ```tsx
2626
* <Markers

package-lock.json

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

package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"lint": "eslint . --ext .ts,.tsx",
66
"lint:fix": "eslint . --ext .ts,.tsx --fix",
77
"lint:commits": "bash ./scripts/lint-commits.sh",
8-
"lint:md-links": "node ./scripts/check-md-links.mts",
8+
"check:md-links": "node ./scripts/check-md-links.mts",
9+
"check:jsdoc-links": "node ./scripts/check-jsdoc-links.mts",
910
"format": "prettier --check './**/*.{ts,tsx,json,mjs,md}'",
1011
"format:fix": "prettier --write './**/*.{ts,tsx,json,mjs,md}'",
1112
"dev": "concurrently \"npm run dev -w lib\" \"npm run dev -w examples\" --kill-others --success last",
@@ -36,6 +37,7 @@
3637
"@testing-library/react": "^16.3.0",
3738
"@types/jsdom": "^21.1.7",
3839
"@types/node": "^24.0.13",
40+
"chalk": "^5.5.0",
3941
"concurrently": "^9.1.2",
4042
"dotenv": "^17.2.0",
4143
"eslint": "^9.28.0",
@@ -53,6 +55,7 @@
5355
"lint-staged": "^15.5.2",
5456
"markdown-it-anchor": "^9.2.0",
5557
"npm-run-all": "^4.1.5",
58+
"p-limit": "^6.2.0",
5659
"prettier": "^3.6.2",
5760
"simple-git-hooks": "^2.11.1",
5861
"size-limit": "^11.2.0",
@@ -93,7 +96,8 @@
9396
"lib/rslib.config.ts",
9497
"lib/tests/**",
9598
"scripts/check-md-links.mts",
96-
"examples/scripts/prelhci.ts"
99+
"examples/scripts/prelhci.ts",
100+
"scripts/check-jsdoc-links.mts"
97101
],
98102
"ignoreBinaries": [
99103
"lint-staged",
@@ -102,9 +106,9 @@
102106
]
103107
},
104108
"optionalDependencies": {
105-
"@rollup/rollup-linux-x64-gnu": "4.37.0",
106-
"@rspack/binding-linux-x64-gnu": "1.3.0",
107109
"@ast-grep/napi-linux-x64-gnu": "0.36.2",
108-
"@esbuild/linux-x64": "0.25.8"
110+
"@esbuild/linux-x64": "0.25.8",
111+
"@rollup/rollup-linux-x64-gnu": "4.37.0",
112+
"@rspack/binding-linux-x64-gnu": "1.3.0"
109113
}
110114
}

scripts/check-jsdoc-links.mts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
4+
/**
5+
* This script checks external links in generated jsdoc files.
6+
*/
7+
8+
import { readFile } from "fs/promises";
9+
import path from "node:path";
10+
import chalk from "chalk";
11+
import pLimit from "p-limit";
12+
import ts from "typescript";
13+
14+
/** Key is domain name, value is an array of links, using Set to avoid duplicates */
15+
type LinksMap = Map<string, Set<string>>;
16+
17+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
18+
const rootDir = path.join(scriptDir, "..");
19+
const libOutputDir = path.join(rootDir, "lib", "dist");
20+
const fileName = "index.d.ts";
21+
const potentiallyDTSBundleFile = path.join(libOutputDir, fileName);
22+
23+
const limitPerTCPConnection = pLimit(10);
24+
25+
/** Avoid throwing errors to check all links even if one of them is broken, push errors to an array instead */
26+
const scriptErrors: string[] = [];
27+
28+
// TODO: remove after deploying docs page
29+
const ignoreDomains = new Set(["ukorvl.github.io"]);
30+
31+
const sourceFile = ts.createSourceFile(
32+
potentiallyDTSBundleFile,
33+
await readFile(potentiallyDTSBundleFile, "utf8"),
34+
ts.ScriptTarget.ESNext,
35+
true,
36+
ts.ScriptKind.TS
37+
);
38+
39+
const extractUrlsFromSeeTag = (tag: ts.JSDocTag): string[] => {
40+
const results: string[] = [];
41+
42+
if (!Array.isArray(tag.comment)) return results;
43+
44+
for (const fragment of tag.comment) {
45+
if ("name" in fragment && fragment.name) {
46+
const prefix = ts.isIdentifier(fragment.name)
47+
? fragment.name.escapedText.toString()
48+
: "";
49+
50+
const full = prefix + fragment.text;
51+
const [url] = full.split("|");
52+
53+
if (url.trim().startsWith("http")) {
54+
results.push(url.trim());
55+
}
56+
}
57+
58+
if (!("name" in fragment) && typeof fragment.text === "string") {
59+
const possibleUrl = fragment.text.trim();
60+
61+
if (possibleUrl.startsWith("http")) {
62+
results.push(possibleUrl);
63+
}
64+
65+
if (possibleUrl.startsWith("://")) {
66+
results.push("https" + possibleUrl);
67+
}
68+
}
69+
}
70+
71+
return results;
72+
};
73+
74+
const main = async () => {
75+
const linksMap: LinksMap = new Map();
76+
77+
const visitNode = (node: ts.Node) => {
78+
const tags = ts.getJSDocTags(node);
79+
80+
for (const tag of tags) {
81+
if (tag.tagName.escapedText === "see" && tag.comment) {
82+
const urls = extractUrlsFromSeeTag(tag);
83+
84+
for (const url of urls) {
85+
const urlObj = new URL(url);
86+
const domain = urlObj.hostname;
87+
88+
if (ignoreDomains.has(domain)) {
89+
continue;
90+
}
91+
92+
if (!linksMap.has(domain)) {
93+
linksMap.set(domain, new Set());
94+
}
95+
96+
linksMap.get(domain)?.add(url);
97+
}
98+
}
99+
}
100+
101+
ts.forEachChild(node, visitNode);
102+
};
103+
104+
visitNode(sourceFile);
105+
106+
const domains = Array.from(linksMap.keys());
107+
108+
return Promise.all(
109+
domains.map(domain => {
110+
const links = Array.from(linksMap.get(domain) || []);
111+
112+
return limitPerTCPConnection(async () => {
113+
for (const link of links) {
114+
try {
115+
const response = await fetch(link, { method: "HEAD" });
116+
117+
if (!response.ok) {
118+
scriptErrors.push(`Invalid link: ${link} (status: ${response.status})`);
119+
}
120+
} catch (error) {
121+
scriptErrors.push(`Error fetching link: ${link} (${error})`);
122+
}
123+
}
124+
});
125+
})
126+
);
127+
};
128+
129+
main()
130+
.then(() => {
131+
if (scriptErrors.length > 0) {
132+
console.log(
133+
chalk.red.bold(
134+
`Found ${scriptErrors.length} errors in ${potentiallyDTSBundleFile}:`
135+
)
136+
);
137+
scriptErrors.forEach(error => console.error(`- ${error}`));
138+
process.exit(1);
139+
} else {
140+
console.log(chalk.green(`All links in ${potentiallyDTSBundleFile} are valid.`));
141+
process.exit(0);
142+
}
143+
})
144+
.catch(error => {
145+
console.error(
146+
`An error occurred while checking jsdoc links in ${potentiallyDTSBundleFile}:`,
147+
error
148+
);
149+
process.exit(1);
150+
});

0 commit comments

Comments
 (0)