@textmode/tooling-scripts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +40 -0
- package/policy.json +29 -0
- package/src/cli/check-deps.mjs +61 -0
- package/src/cli/install-husky.mjs +23 -0
- package/src/config/create-config.mjs +75 -0
- package/src/husky/commit-msg +1 -0
- package/src/husky/pre-commit +1 -0
- package/src/husky/pre-push +6 -0
- package/src/scripts/add-api-doc-links.mjs +92 -0
- package/src/scripts/check-examples.mjs +81 -0
- package/src/scripts/check-public-api-docs.mjs +128 -0
- package/src/scripts/lib/api-doc-checks.mjs +147 -0
- package/src/scripts/lib/api-doc-links-config.mjs +32 -0
- package/src/scripts/lib/api-doc-routes.mjs +118 -0
- package/src/scripts/lib/example-checks.mjs +389 -0
- package/src/scripts/lib/jsdoc-comments.mjs +78 -0
- package/src/scripts/lib/reflection-policy.mjs +106 -0
- package/src/scripts/lib/reporting.mjs +23 -0
- package/src/scripts/lib/typedoc-project.mjs +25 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/README.md +45 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/accessor-comments.js +53 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/index.js +32 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/render-accessor.js +144 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/theme-patch.js +28 -0
- package/src/typedoc-plugins/all-member-pages-router/README.md +57 -0
- package/src/typedoc-plugins/all-member-pages-router/child-pages.js +65 -0
- package/src/typedoc-plugins/all-member-pages-router/constants.js +58 -0
- package/src/typedoc-plugins/all-member-pages-router/index.js +28 -0
- package/src/typedoc-plugins/all-member-pages-router/member-paths.js +48 -0
- package/src/typedoc-plugins/all-member-pages-router/member-reflections.js +164 -0
- package/src/typedoc-plugins/all-member-pages-router/namespace-paths.js +44 -0
- package/src/typedoc-plugins/all-member-pages-router/router.js +58 -0
- package/src/typedoc-plugins/api-frontmatter/README.md +66 -0
- package/src/typedoc-plugins/api-frontmatter/constants.js +52 -0
- package/src/typedoc-plugins/api-frontmatter/descriptions.js +80 -0
- package/src/typedoc-plugins/api-frontmatter/frontmatter.js +56 -0
- package/src/typedoc-plugins/api-frontmatter/index.js +38 -0
- package/src/typedoc-plugins/api-frontmatter/library-context.js +62 -0
- package/src/typedoc-plugins/api-frontmatter/reflection-metadata.js +95 -0
- package/src/typedoc-plugins/include-code-examples/README.md +40 -0
- package/src/typedoc-plugins/include-code-examples/code-fences.js +55 -0
- package/src/typedoc-plugins/include-code-examples/example-metadata.js +41 -0
- package/src/typedoc-plugins/include-code-examples/index.js +23 -0
- package/src/typedoc-plugins/include-code-examples/transform-markdown.js +33 -0
- package/src/typedoc-plugins/strip-api-self-links/index.js +126 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@textmode/tooling-scripts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./config": "./src/config/create-config.mjs",
|
|
7
|
+
"./typedoc-plugins/accessor-docs-normalizer/index.js": "./src/typedoc-plugins/accessor-docs-normalizer/index.js",
|
|
8
|
+
"./typedoc-plugins/all-member-pages-router/index.js": "./src/typedoc-plugins/all-member-pages-router/index.js",
|
|
9
|
+
"./typedoc-plugins/api-frontmatter/index.js": "./src/typedoc-plugins/api-frontmatter/index.js",
|
|
10
|
+
"./typedoc-plugins/include-code-examples/index.js": "./src/typedoc-plugins/include-code-examples/index.js",
|
|
11
|
+
"./typedoc-plugins/strip-api-self-links/index.js": "./src/typedoc-plugins/strip-api-self-links/index.js"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"install-husky": "src/cli/install-husky.mjs",
|
|
15
|
+
"check-deps": "src/cli/check-deps.mjs",
|
|
16
|
+
"textmode-add-api-links": "src/scripts/add-api-doc-links.mjs",
|
|
17
|
+
"textmode-check-api-docs": "src/scripts/check-public-api-docs.mjs",
|
|
18
|
+
"textmode-check-examples": "src/scripts/check-examples.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"policy.json"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"husky": ">=9",
|
|
26
|
+
"typedoc": ">=0.28",
|
|
27
|
+
"typedoc-plugin-markdown": ">=4.11",
|
|
28
|
+
"typescript": ">=5.9"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typedoc": "^0.28.19",
|
|
32
|
+
"typedoc-plugin-markdown": "^4.11.0",
|
|
33
|
+
"typescript": "^5.9.2"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"check": "node --check src/cli/check-deps.mjs && node --check src/scripts/check-examples.mjs",
|
|
37
|
+
"lint": "node --check src/cli/check-deps.mjs",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/policy.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
4
|
+
"@semantic-release/exec": "^7.1.0",
|
|
5
|
+
"@semantic-release/git": "^10.0.1",
|
|
6
|
+
"@semantic-release/github": "^12.0.6",
|
|
7
|
+
"@semantic-release/npm": "^13.1.5",
|
|
8
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
9
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
10
|
+
"@vitest/ui": "^3.1.2",
|
|
11
|
+
"eslint": "^9.39.2",
|
|
12
|
+
"eslint-plugin-jsdoc": "^62.9.0",
|
|
13
|
+
"globals": "^17.5.0",
|
|
14
|
+
"jsdom": "^27.4.0",
|
|
15
|
+
"prettier": "^3.8.3",
|
|
16
|
+
"semantic-release": "^25.0.3",
|
|
17
|
+
"typedoc": "^0.28.19",
|
|
18
|
+
"typedoc-plugin-frontmatter": "^1.3.1",
|
|
19
|
+
"typedoc-plugin-markdown": "^4.11.0",
|
|
20
|
+
"typedoc-vitepress-theme": "^1.1.2",
|
|
21
|
+
"typescript": "^5.9.2",
|
|
22
|
+
"typescript-eslint": "^8.59.0",
|
|
23
|
+
"vite": "^6.3.4",
|
|
24
|
+
"vitest": "^3.1.2"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"textmode.js": ">=0.16.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
9
|
+
const policyPath = path.join(packageDir, 'policy.json');
|
|
10
|
+
|
|
11
|
+
function readJson(filePath) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getPackageVersion(packageJson, section, dependencyName) {
|
|
16
|
+
return packageJson[section]?.[dependencyName];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectIssues(packageJson, policy) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const dependencySections = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
22
|
+
|
|
23
|
+
for (const [dependencyName, expectedRange] of Object.entries(policy.dependencies ?? {})) {
|
|
24
|
+
const actual = dependencySections
|
|
25
|
+
.map((section) => [section, getPackageVersion(packageJson, section, dependencyName)])
|
|
26
|
+
.find(([, version]) => version !== undefined);
|
|
27
|
+
|
|
28
|
+
if (!actual) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [section, version] = actual;
|
|
33
|
+
if (version !== expectedRange) {
|
|
34
|
+
issues.push(`${section}.${dependencyName} must be ${expectedRange}, found ${version}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [dependencyName, expectedRange] of Object.entries(policy.peerDependencies ?? {})) {
|
|
39
|
+
const version = getPackageVersion(packageJson, 'peerDependencies', dependencyName);
|
|
40
|
+
if (version !== undefined && version !== expectedRange) {
|
|
41
|
+
issues.push(`peerDependencies.${dependencyName} must be ${expectedRange}, found ${version}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return issues;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
49
|
+
const packageJson = readJson(packageJsonPath);
|
|
50
|
+
const policy = readJson(policyPath);
|
|
51
|
+
const issues = collectIssues(packageJson, policy);
|
|
52
|
+
|
|
53
|
+
if (issues.length === 0) {
|
|
54
|
+
console.log('Dependency policy check passed.');
|
|
55
|
+
} else {
|
|
56
|
+
console.error('Dependency policy check failed:');
|
|
57
|
+
for (const issue of issues) {
|
|
58
|
+
console.error(`- ${issue}`);
|
|
59
|
+
}
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
8
|
+
const sourceDir = path.join(packageDir, 'src/husky');
|
|
9
|
+
const targetDir = path.resolve(process.cwd(), '.husky');
|
|
10
|
+
|
|
11
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
for (const hookName of fs.readdirSync(sourceDir)) {
|
|
14
|
+
const sourcePath = path.join(sourceDir, hookName);
|
|
15
|
+
const targetPath = path.join(targetDir, hookName);
|
|
16
|
+
|
|
17
|
+
if (!fs.statSync(sourcePath).isFile()) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
22
|
+
fs.chmodSync(targetPath, 0o755);
|
|
23
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TARGET_KINDS =
|
|
4
|
+
128 | // ReflectionKind.Class
|
|
5
|
+
256 | // ReflectionKind.Interface
|
|
6
|
+
2097152 | // ReflectionKind.TypeAlias
|
|
7
|
+
8 | // ReflectionKind.Enum
|
|
8
|
+
32 | // ReflectionKind.Variable
|
|
9
|
+
64 | // ReflectionKind.Function
|
|
10
|
+
2048 | // ReflectionKind.Method
|
|
11
|
+
262144 | // ReflectionKind.Accessor
|
|
12
|
+
1024; // ReflectionKind.Property
|
|
13
|
+
|
|
14
|
+
const DEFAULT_EXAMPLE_TARGET_KINDS = 64 | 2048;
|
|
15
|
+
const DEFAULT_ACCESSOR_TARGET_KIND = 262144;
|
|
16
|
+
|
|
17
|
+
function sourceRootsForEntryPoints(entryPoints) {
|
|
18
|
+
return Array.from(
|
|
19
|
+
new Set(
|
|
20
|
+
entryPoints.map((entryPoint) => {
|
|
21
|
+
const firstSegment = entryPoint.path.split(/[\\/]/)[0];
|
|
22
|
+
return path.resolve(firstSegment);
|
|
23
|
+
})
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createConfig({
|
|
29
|
+
apiBaseUrl,
|
|
30
|
+
apiDocsDir,
|
|
31
|
+
checkExampleGallerySourceBase = true,
|
|
32
|
+
checkExampleSketches,
|
|
33
|
+
docstringTargetKinds,
|
|
34
|
+
docstringTargetKindsWithAccessors,
|
|
35
|
+
entryPoints,
|
|
36
|
+
exampleTargetKinds,
|
|
37
|
+
exampleTargetKindsWithAccessors,
|
|
38
|
+
packageName,
|
|
39
|
+
sourceRoots,
|
|
40
|
+
targetKinds = DEFAULT_TARGET_KINDS,
|
|
41
|
+
}) {
|
|
42
|
+
if (!packageName) {
|
|
43
|
+
throw new Error('textmode tooling config requires packageName.');
|
|
44
|
+
}
|
|
45
|
+
if (!apiBaseUrl) {
|
|
46
|
+
throw new Error('textmode tooling config requires apiBaseUrl.');
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(entryPoints) || entryPoints.length === 0) {
|
|
49
|
+
throw new Error('textmode tooling config requires at least one entry point.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const primaryEntryPoint = entryPoints[0];
|
|
53
|
+
const resolvedExampleTargetKinds = exampleTargetKinds ?? DEFAULT_EXAMPLE_TARGET_KINDS;
|
|
54
|
+
const resolvedExampleTargetKindsWithAccessors =
|
|
55
|
+
exampleTargetKindsWithAccessors ?? (resolvedExampleTargetKinds | DEFAULT_ACCESSOR_TARGET_KIND);
|
|
56
|
+
const resolvedDocstringTargetKinds = docstringTargetKinds ?? resolvedExampleTargetKinds;
|
|
57
|
+
const resolvedDocstringTargetKindsWithAccessors =
|
|
58
|
+
docstringTargetKindsWithAccessors ?? resolvedExampleTargetKindsWithAccessors;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
API_BASE_URL: apiBaseUrl,
|
|
62
|
+
API_DOCS_DIR: path.resolve(apiDocsDir ?? `api/${packageName}`),
|
|
63
|
+
CHECK_EXAMPLE_GALLERY_SOURCE_BASE: checkExampleGallerySourceBase,
|
|
64
|
+
CHECK_EXAMPLE_SKETCHES: Boolean(checkExampleSketches),
|
|
65
|
+
DOCSTRING_TARGET_KINDS: resolvedDocstringTargetKinds,
|
|
66
|
+
DOCSTRING_TARGET_KINDS_WITH_ACCESSORS: resolvedDocstringTargetKindsWithAccessors,
|
|
67
|
+
ENTRY_POINT: primaryEntryPoint.path,
|
|
68
|
+
ENTRY_POINTS: entryPoints,
|
|
69
|
+
EXAMPLE_TARGET_KINDS: resolvedExampleTargetKinds,
|
|
70
|
+
EXAMPLE_TARGET_KINDS_WITH_ACCESSORS: resolvedExampleTargetKindsWithAccessors,
|
|
71
|
+
PACKAGE_LABEL: packageName,
|
|
72
|
+
SOURCE_ROOTS: sourceRoots ?? sourceRootsForEntryPoints(entryPoints),
|
|
73
|
+
TARGET_KINDS: targetKinds,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit "$1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
|
|
6
|
+
import { API_BASE_URL, API_DOCS_DIR, ENTRY_POINTS, SOURCE_ROOTS, TARGET_KINDS } from './lib/api-doc-links-config.mjs';
|
|
7
|
+
import { createApiLinkTargets } from './lib/api-doc-checks.mjs';
|
|
8
|
+
import { readMarkdownRoutes } from './lib/api-doc-routes.mjs';
|
|
9
|
+
import { findJsdocBeforeLine, getLineStartOffsets, upsertSeeLine } from './lib/jsdoc-comments.mjs';
|
|
10
|
+
import {
|
|
11
|
+
getIncludedReflections,
|
|
12
|
+
shouldProcessEntryPointReflection,
|
|
13
|
+
sourceIsInRoots,
|
|
14
|
+
} from './lib/reflection-policy.mjs';
|
|
15
|
+
import { loadTypeDocProject } from './lib/typedoc-project.mjs';
|
|
16
|
+
|
|
17
|
+
async function collectPendingEdits(routes) {
|
|
18
|
+
const pendingByFile = new Map();
|
|
19
|
+
|
|
20
|
+
for (const entryPoint of ENTRY_POINTS) {
|
|
21
|
+
const project = await loadTypeDocProject({
|
|
22
|
+
entryPoint: entryPoint.path,
|
|
23
|
+
tsconfig: entryPoint.tsconfig,
|
|
24
|
+
});
|
|
25
|
+
const reflections = getIncludedReflections(project, TARGET_KINDS);
|
|
26
|
+
|
|
27
|
+
for (const reflection of reflections) {
|
|
28
|
+
if (!shouldProcessEntryPointReflection(entryPoint, reflection)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const target of createApiLinkTargets(reflection, routes, API_BASE_URL)) {
|
|
33
|
+
if (!target.source?.line || !sourceIsInRoots(target.source, SOURCE_ROOTS)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries = pendingByFile.get(target.source.fullFileName) ?? [];
|
|
38
|
+
entries.push({ line: target.source.line, seeLine: target.seeLine });
|
|
39
|
+
pendingByFile.set(target.source.fullFileName, entries);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return pendingByFile;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function applyPendingEdits(pendingByFile) {
|
|
48
|
+
let changedFiles = 0;
|
|
49
|
+
let insertedLinks = 0;
|
|
50
|
+
|
|
51
|
+
for (const [fileName, entries] of pendingByFile) {
|
|
52
|
+
const originalText = fs.readFileSync(fileName, 'utf8');
|
|
53
|
+
let text = originalText;
|
|
54
|
+
const uniqueEntries = Array.from(
|
|
55
|
+
new Map(entries.map((entry) => [`${entry.line}\0${entry.seeLine}`, entry])).values()
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
for (const entry of uniqueEntries.sort((left, right) => right.line - left.line)) {
|
|
59
|
+
const before = text;
|
|
60
|
+
const lineStarts = getLineStartOffsets(text);
|
|
61
|
+
const comment = findJsdocBeforeLine(text, lineStarts, entry.line);
|
|
62
|
+
if (!comment) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
text = upsertSeeLine(text, comment, entry.seeLine, API_BASE_URL);
|
|
67
|
+
if (text !== before) {
|
|
68
|
+
insertedLinks += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (text !== originalText) {
|
|
73
|
+
fs.writeFileSync(fileName, text);
|
|
74
|
+
changedFiles += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { changedFiles, insertedLinks };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
const routes = readMarkdownRoutes(API_DOCS_DIR);
|
|
83
|
+
const pendingByFile = await collectPendingEdits(routes);
|
|
84
|
+
const { changedFiles, insertedLinks } = applyPendingEdits(pendingByFile);
|
|
85
|
+
|
|
86
|
+
console.log(`Inserted ${insertedLinks} API reference link(s) in ${changedFiles} file(s).`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await main().catch((error) => {
|
|
90
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
import { CHECK_EXAMPLE_SKETCHES, PACKAGE_LABEL } from './lib/api-doc-links-config.mjs';
|
|
6
|
+
import {
|
|
7
|
+
checkSketch,
|
|
8
|
+
EXAMPLES_MANIFEST,
|
|
9
|
+
getManifestExamples,
|
|
10
|
+
readExamplesIndex,
|
|
11
|
+
readExamplesManifest,
|
|
12
|
+
validateExamplesGallery,
|
|
13
|
+
validateExamplesManifest,
|
|
14
|
+
} from './lib/example-checks.mjs';
|
|
15
|
+
import { renderList } from './lib/reporting.mjs';
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
try {
|
|
19
|
+
console.log(`Checking ${PACKAGE_LABEL} example sketches...`);
|
|
20
|
+
const indexContent = readExamplesIndex();
|
|
21
|
+
const manifest = readExamplesManifest();
|
|
22
|
+
const galleryIssues = validateExamplesGallery(indexContent);
|
|
23
|
+
const manifestIssues = validateExamplesManifest(manifest);
|
|
24
|
+
const validationIssues = [...galleryIssues, ...manifestIssues];
|
|
25
|
+
|
|
26
|
+
if (validationIssues.length > 0) {
|
|
27
|
+
renderList('Examples validation failed:', validationIssues);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const files = getManifestExamples(manifest).map((example) => example.sourceFile);
|
|
33
|
+
console.log(`Found ${files.length} manifest examples from ${EXAMPLES_MANIFEST}.`);
|
|
34
|
+
|
|
35
|
+
if (!CHECK_EXAMPLE_SKETCHES) {
|
|
36
|
+
console.log('Sketch convention checks are disabled for this package.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let passedCount = 0;
|
|
41
|
+
let failedCount = 0;
|
|
42
|
+
let skippedCount = 0;
|
|
43
|
+
const failures = [];
|
|
44
|
+
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const { skipped, issues } = checkSketch(file);
|
|
47
|
+
if (skipped) {
|
|
48
|
+
skippedCount += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (issues.length === 0) {
|
|
53
|
+
passedCount += 1;
|
|
54
|
+
} else {
|
|
55
|
+
failedCount += 1;
|
|
56
|
+
failures.push({ file, issues });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\nResults: ${passedCount} passed, ${failedCount} failed, ${skippedCount} skipped.\n`);
|
|
61
|
+
|
|
62
|
+
if (failures.length > 0) {
|
|
63
|
+
console.error('Validation failed for the following sketches:');
|
|
64
|
+
for (const failure of failures) {
|
|
65
|
+
console.error(`\n- ${failure.file}:`);
|
|
66
|
+
for (const issue of failure.issues) {
|
|
67
|
+
console.error(` * ${issue}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
} else {
|
|
72
|
+
console.log('All checked sketches meet the requirements successfully.');
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Error running examples check:');
|
|
76
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
77
|
+
process.exitCode = 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await main();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
import { ReflectionKind } from 'typedoc';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
API_BASE_URL,
|
|
9
|
+
API_DOCS_DIR,
|
|
10
|
+
DOCSTRING_TARGET_KINDS,
|
|
11
|
+
DOCSTRING_TARGET_KINDS_WITH_ACCESSORS,
|
|
12
|
+
ENTRY_POINT,
|
|
13
|
+
EXAMPLE_TARGET_KINDS,
|
|
14
|
+
EXAMPLE_TARGET_KINDS_WITH_ACCESSORS,
|
|
15
|
+
TARGET_KINDS,
|
|
16
|
+
} from './lib/api-doc-links-config.mjs';
|
|
17
|
+
import { collectApiDocIssues, countApiDocIssues } from './lib/api-doc-checks.mjs';
|
|
18
|
+
import { readOrGenerateMarkdownRoutes } from './lib/api-doc-routes.mjs';
|
|
19
|
+
import { getIncludedReflections } from './lib/reflection-policy.mjs';
|
|
20
|
+
import { renderIssueSection } from './lib/reporting.mjs';
|
|
21
|
+
import { loadTypeDocProject } from './lib/typedoc-project.mjs';
|
|
22
|
+
|
|
23
|
+
function parseOptions(argv) {
|
|
24
|
+
return {
|
|
25
|
+
help: argv.includes('--help') || argv.includes('-h'),
|
|
26
|
+
includeAccessors: argv.includes('--include-accessors'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getTargetKinds(options) {
|
|
31
|
+
if (!options.includeAccessors) {
|
|
32
|
+
return {
|
|
33
|
+
docstringTargetKinds: DOCSTRING_TARGET_KINDS,
|
|
34
|
+
exampleTargetKinds: EXAMPLE_TARGET_KINDS,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
docstringTargetKinds: DOCSTRING_TARGET_KINDS_WITH_ACCESSORS,
|
|
40
|
+
exampleTargetKinds: EXAMPLE_TARGET_KINDS_WITH_ACCESSORS,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function exampleScopeLabel(targetKinds) {
|
|
45
|
+
return targetKinds & ReflectionKind.Accessor
|
|
46
|
+
? 'exported functions, methods, and accessors'
|
|
47
|
+
: 'exported functions and methods';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printHelp() {
|
|
51
|
+
console.log('Usage: node ./scripts/check-public-api-docs.mjs [--include-accessors]');
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(`Checks the documented public API reachable from ${ENTRY_POINT}.`);
|
|
54
|
+
console.log('Skips private/protected/inherited members and any API path segment starting with "_".');
|
|
55
|
+
console.log('Requires public API JSDoc comments to link to their code.textmode.art API reference.');
|
|
56
|
+
console.log('Also requires at least one @example tag for configured example target kinds.');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('Options:');
|
|
59
|
+
console.log(' --include-accessors Include accessors/getters when the repository config supports it.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
try {
|
|
64
|
+
const options = parseOptions(process.argv.slice(2));
|
|
65
|
+
|
|
66
|
+
if (options.help) {
|
|
67
|
+
printHelp();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const routes = await readOrGenerateMarkdownRoutes(API_DOCS_DIR);
|
|
72
|
+
const { docstringTargetKinds, exampleTargetKinds } = getTargetKinds(options);
|
|
73
|
+
const project = await loadTypeDocProject({
|
|
74
|
+
entryPoint: ENTRY_POINT,
|
|
75
|
+
tsconfig: ENTRY_POINT.startsWith('typedoc-entrypoints/') ? 'tsconfig.typedoc.json' : 'tsconfig.json',
|
|
76
|
+
errorMessage: 'TypeDoc failed to resolve the public API surface.',
|
|
77
|
+
});
|
|
78
|
+
const apiLinkReflections = getIncludedReflections(project, TARGET_KINDS);
|
|
79
|
+
const docstringReflections = getIncludedReflections(project, docstringTargetKinds);
|
|
80
|
+
const exampleReflections = getIncludedReflections(project, exampleTargetKinds);
|
|
81
|
+
const scopeLabel = exampleScopeLabel(exampleTargetKinds);
|
|
82
|
+
const issues = collectApiDocIssues({
|
|
83
|
+
apiBaseUrl: API_BASE_URL,
|
|
84
|
+
apiLinkReflections,
|
|
85
|
+
docstringReflections,
|
|
86
|
+
exampleReflections,
|
|
87
|
+
routes,
|
|
88
|
+
});
|
|
89
|
+
const issueCount = countApiDocIssues(issues);
|
|
90
|
+
|
|
91
|
+
if (issueCount === 0) {
|
|
92
|
+
console.log('Public API documentation check passed.');
|
|
93
|
+
console.log(
|
|
94
|
+
`Scanned ${docstringReflections.length} documented public API reflections from ${ENTRY_POINT}.`
|
|
95
|
+
);
|
|
96
|
+
console.log('Policy: skip private/protected/inherited members and any API path segment starting with "_".');
|
|
97
|
+
console.log('Every included API item has a JSDoc docstring and a valid code.textmode.art API link.');
|
|
98
|
+
console.log(`Every included ${scopeLabel} has at least one @example tag.`);
|
|
99
|
+
if (!options.includeAccessors && EXAMPLE_TARGET_KINDS_WITH_ACCESSORS !== EXAMPLE_TARGET_KINDS) {
|
|
100
|
+
console.log('Accessors are excluded by default. Re-run with --include-accessors to check them too.');
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.error('Public API documentation check failed.');
|
|
106
|
+
console.error(`Scanned ${docstringReflections.length} documented public API reflections from ${ENTRY_POINT}.`);
|
|
107
|
+
console.error('Policy: skip private/protected/inherited members and any API path segment starting with "_".');
|
|
108
|
+
console.error(
|
|
109
|
+
`Found ${issueCount} issue(s): ${issues.missingDocstrings.length} missing docstring, ${issues.missingExamples.length} missing @example, ${issues.missingApiLinks.length} missing API link, ${issues.invalidApiLinks.length} invalid API link.`
|
|
110
|
+
);
|
|
111
|
+
if (!options.includeAccessors && EXAMPLE_TARGET_KINDS_WITH_ACCESSORS !== EXAMPLE_TARGET_KINDS) {
|
|
112
|
+
console.error('Accessors are excluded by default. Re-run with --include-accessors to check them too.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
renderIssueSection('Missing JSDoc docstring', issues.missingDocstrings);
|
|
116
|
+
renderIssueSection('Missing @example tag', issues.missingExamples);
|
|
117
|
+
renderIssueSection('Missing code.textmode.art API link', issues.missingApiLinks);
|
|
118
|
+
renderIssueSection('Invalid code.textmode.art API link', issues.invalidApiLinks);
|
|
119
|
+
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Public API documentation check could not complete.');
|
|
123
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
124
|
+
process.exitCode = 2;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await main();
|