@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.
Files changed (45) hide show
  1. package/package.json +40 -0
  2. package/policy.json +29 -0
  3. package/src/cli/check-deps.mjs +61 -0
  4. package/src/cli/install-husky.mjs +23 -0
  5. package/src/config/create-config.mjs +75 -0
  6. package/src/husky/commit-msg +1 -0
  7. package/src/husky/pre-commit +1 -0
  8. package/src/husky/pre-push +6 -0
  9. package/src/scripts/add-api-doc-links.mjs +92 -0
  10. package/src/scripts/check-examples.mjs +81 -0
  11. package/src/scripts/check-public-api-docs.mjs +128 -0
  12. package/src/scripts/lib/api-doc-checks.mjs +147 -0
  13. package/src/scripts/lib/api-doc-links-config.mjs +32 -0
  14. package/src/scripts/lib/api-doc-routes.mjs +118 -0
  15. package/src/scripts/lib/example-checks.mjs +389 -0
  16. package/src/scripts/lib/jsdoc-comments.mjs +78 -0
  17. package/src/scripts/lib/reflection-policy.mjs +106 -0
  18. package/src/scripts/lib/reporting.mjs +23 -0
  19. package/src/scripts/lib/typedoc-project.mjs +25 -0
  20. package/src/typedoc-plugins/accessor-docs-normalizer/README.md +45 -0
  21. package/src/typedoc-plugins/accessor-docs-normalizer/accessor-comments.js +53 -0
  22. package/src/typedoc-plugins/accessor-docs-normalizer/index.js +32 -0
  23. package/src/typedoc-plugins/accessor-docs-normalizer/render-accessor.js +144 -0
  24. package/src/typedoc-plugins/accessor-docs-normalizer/theme-patch.js +28 -0
  25. package/src/typedoc-plugins/all-member-pages-router/README.md +57 -0
  26. package/src/typedoc-plugins/all-member-pages-router/child-pages.js +65 -0
  27. package/src/typedoc-plugins/all-member-pages-router/constants.js +58 -0
  28. package/src/typedoc-plugins/all-member-pages-router/index.js +28 -0
  29. package/src/typedoc-plugins/all-member-pages-router/member-paths.js +48 -0
  30. package/src/typedoc-plugins/all-member-pages-router/member-reflections.js +164 -0
  31. package/src/typedoc-plugins/all-member-pages-router/namespace-paths.js +44 -0
  32. package/src/typedoc-plugins/all-member-pages-router/router.js +58 -0
  33. package/src/typedoc-plugins/api-frontmatter/README.md +66 -0
  34. package/src/typedoc-plugins/api-frontmatter/constants.js +52 -0
  35. package/src/typedoc-plugins/api-frontmatter/descriptions.js +80 -0
  36. package/src/typedoc-plugins/api-frontmatter/frontmatter.js +56 -0
  37. package/src/typedoc-plugins/api-frontmatter/index.js +38 -0
  38. package/src/typedoc-plugins/api-frontmatter/library-context.js +62 -0
  39. package/src/typedoc-plugins/api-frontmatter/reflection-metadata.js +95 -0
  40. package/src/typedoc-plugins/include-code-examples/README.md +40 -0
  41. package/src/typedoc-plugins/include-code-examples/code-fences.js +55 -0
  42. package/src/typedoc-plugins/include-code-examples/example-metadata.js +41 -0
  43. package/src/typedoc-plugins/include-code-examples/index.js +23 -0
  44. package/src/typedoc-plugins/include-code-examples/transform-markdown.js +33 -0
  45. package/src/typedoc-plugins/strip-api-self-links/index.js +126 -0
@@ -0,0 +1,62 @@
1
+ // @ts-check
2
+
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, resolve } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ import { TEXTMODE_ECOSYSTEM_NAME, TEXTMODE_PACKAGE_PREFIX, UNKNOWN_LIBRARY_NAME } from './constants.js';
8
+
9
+ /**
10
+ * Library context extracted from package.json and the TypeDoc project.
11
+ *
12
+ * @typedef {Object} LibraryContext
13
+ * @property {string} name
14
+ * @property {string} description
15
+ * @property {string} ecosystem
16
+ */
17
+
18
+ /**
19
+ * Read and parse package.json from the project root.
20
+ *
21
+ * @returns {{ name?: string; description?: string } | null}
22
+ */
23
+ export function readPackageJson() {
24
+ try {
25
+ const directory = dirname(fileURLToPath(import.meta.url));
26
+ const packagePath = resolve(directory, '..', '..', 'package.json');
27
+ const content = readFileSync(packagePath, 'utf-8');
28
+ return JSON.parse(content);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get the ecosystem name for a package.
36
+ *
37
+ * @param {string} packageName
38
+ * @returns {string}
39
+ */
40
+ export function getEcosystemName(packageName) {
41
+ return packageName.startsWith(TEXTMODE_PACKAGE_PREFIX) ? TEXTMODE_ECOSYSTEM_NAME : packageName;
42
+ }
43
+
44
+ /**
45
+ * Extract library context from TypeDoc project metadata and package.json.
46
+ *
47
+ * @param {import('typedoc').ProjectReflection} project
48
+ * @returns {LibraryContext}
49
+ */
50
+ export function extractLibraryContext(project) {
51
+ const name = project.packageName ?? project.name ?? UNKNOWN_LIBRARY_NAME;
52
+
53
+ let description = `API reference for ${name}`;
54
+ const packageJson = readPackageJson();
55
+ if (packageJson?.description) {
56
+ description = packageJson.description;
57
+ }
58
+
59
+ const ecosystem = getEcosystemName(name);
60
+
61
+ return { name, description, ecosystem };
62
+ }
@@ -0,0 +1,95 @@
1
+ // @ts-check
2
+
3
+ import { ReflectionKind } from 'typedoc';
4
+
5
+ import { API_REFERENCE_CATEGORY, CATEGORY_BY_KIND } from './constants.js';
6
+
7
+ /**
8
+ * Get the frontmatter category for a reflection kind.
9
+ *
10
+ * @param {import('typedoc').ReflectionKind} kind
11
+ * @returns {string}
12
+ */
13
+ export function getCategoryForKind(kind) {
14
+ return CATEGORY_BY_KIND.get(kind) ?? API_REFERENCE_CATEGORY;
15
+ }
16
+
17
+ /**
18
+ * Check if a reflection belongs to a namespace.
19
+ *
20
+ * @param {import('typedoc').Reflection} model
21
+ * @returns {boolean}
22
+ */
23
+ export function isInNamespace(model) {
24
+ let parent = model.parent;
25
+ while (parent) {
26
+ if (parent.kind === ReflectionKind.Namespace) {
27
+ return true;
28
+ }
29
+ parent = parent.parent;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ /**
35
+ * Get the full namespace path for a reflection.
36
+ *
37
+ * @param {import('typedoc').Reflection} model
38
+ * @returns {string | undefined}
39
+ */
40
+ export function getNamespacePath(model) {
41
+ /** @type {string[]} */
42
+ const parts = [];
43
+ let parent = model.parent;
44
+
45
+ while (parent) {
46
+ if (parent.kind === ReflectionKind.Namespace) {
47
+ parts.unshift(parent.name);
48
+ }
49
+ parent = parent.parent;
50
+ }
51
+
52
+ return parts.length > 0 ? parts.join('.') : undefined;
53
+ }
54
+
55
+ /**
56
+ * Get the owning class or interface name for member reflections.
57
+ *
58
+ * @param {import('typedoc').Reflection} model
59
+ * @returns {string | undefined}
60
+ */
61
+ export function getOwnerName(model) {
62
+ let parent = model.parent;
63
+ while (parent) {
64
+ if (parent.kind === ReflectionKind.Class || parent.kind === ReflectionKind.Interface) {
65
+ return parent.name;
66
+ }
67
+ parent = parent.parent;
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Check if a class reflection has a constructor child.
75
+ *
76
+ * @param {import('typedoc').Reflection} model
77
+ * @returns {boolean | undefined}
78
+ */
79
+ export function getHasConstructor(model) {
80
+ if (model.kind !== ReflectionKind.Class || !('children' in model)) {
81
+ return undefined;
82
+ }
83
+
84
+ return model.children?.some((child) => child.kind === ReflectionKind.Constructor);
85
+ }
86
+
87
+ /**
88
+ * Check if a reflection is an interface.
89
+ *
90
+ * @param {import('typedoc').Reflection} model
91
+ * @returns {true | undefined}
92
+ */
93
+ export function getIsInterface(model) {
94
+ return model.kind === ReflectionKind.Interface ? true : undefined;
95
+ }
@@ -0,0 +1,40 @@
1
+ # Include Code Examples
2
+
3
+ ## Purpose
4
+
5
+ This TypeDoc plugin cleans up example sketches included in API docs with `{@includeCode ...}`.
6
+
7
+ Example sketches keep leading gallery metadata such as `@title` and `@author` so the examples gallery and validation
8
+ scripts can read it. API reference pages should show only the runnable sketch code, so this plugin removes a leading
9
+ JSDoc metadata block from supported fenced code blocks after TypeDoc has rendered markdown.
10
+
11
+ The plugin also normalizes transformed `js` fences to `javascript` for VitePress syntax highlighting. That normalization
12
+ is intentionally gated by metadata stripping: a plain `js` fence without a leading metadata block is left unchanged.
13
+
14
+ This is a post-render markdown transform. It does not edit source examples, change TypeDoc reflections, or add new
15
+ TypeDoc options.
16
+
17
+ ## Preserved Output Contract
18
+
19
+ - Only fenced `js`, `jsx`, `ts`, `tsx`, `javascript`, and `typescript` blocks are considered.
20
+ - Only a leading JSDoc metadata block is stripped.
21
+ - Plain code fences without stripped metadata are left byte-for-byte unchanged.
22
+ - `js` fences are normalized to `javascript` only when metadata was stripped.
23
+ - Unsupported and language-less fences are left unchanged.
24
+
25
+ ## Contributor Notes
26
+
27
+ - Safe to edit: helper names, supported-language tests, and comments.
28
+ - Be careful with: `EXAMPLE_CODE_FENCE_PATTERN`, because it intentionally operates after TypeDoc renders markdown.
29
+ - Do not casually change: supported language gating, `js` normalization timing, or the “leading docblock only” rule.
30
+
31
+ ## Maintenance
32
+
33
+ This plugin depends on TypeDoc rendering `{@includeCode ...}` examples as fenced code blocks before
34
+ `MarkdownPageEvent.END`. Re-check the transform after upgrading TypeDoc or `typedoc-plugin-markdown`.
35
+
36
+ Upgrade checklist:
37
+
38
+ - Confirm included examples still arrive in `page.contents` as fenced markdown.
39
+ - Run `npm run test:tooling`.
40
+ - Inspect a generated API example and verify gallery metadata such as `@title` is absent.
@@ -0,0 +1,55 @@
1
+ // @ts-check
2
+
3
+ export const EXAMPLE_CODE_FENCE_PATTERN = /```([^\n]*)\n([\s\S]*?)```/g;
4
+
5
+ export const SUPPORTED_INCLUDED_EXAMPLE_LANGUAGES = new Set(['js', 'jsx', 'ts', 'tsx', 'javascript', 'typescript']);
6
+
7
+ /**
8
+ * Get the language token from a fenced code block info string.
9
+ *
10
+ * @param {string} infoString
11
+ * @returns {string}
12
+ */
13
+ export function getFenceLanguage(infoString) {
14
+ return infoString.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? '';
15
+ }
16
+
17
+ /**
18
+ * Check whether the language can contain an included sketch.
19
+ *
20
+ * @param {string} language
21
+ * @returns {boolean}
22
+ */
23
+ export function isSupportedExampleLanguage(language) {
24
+ return SUPPORTED_INCLUDED_EXAMPLE_LANGUAGES.has(language);
25
+ }
26
+
27
+ /**
28
+ * Normalize the fence language for transformed included examples.
29
+ *
30
+ * @param {string} infoString
31
+ * @returns {string}
32
+ */
33
+ export function normalizeIncludedExampleFenceInfo(infoString) {
34
+ const trimmedInfoString = infoString.trim();
35
+ if (!trimmedInfoString) {
36
+ return infoString;
37
+ }
38
+
39
+ const parts = trimmedInfoString.split(/\s+/);
40
+ if (parts[0].toLowerCase() === 'js') {
41
+ parts[0] = 'javascript';
42
+ }
43
+
44
+ return parts.join(' ');
45
+ }
46
+
47
+ /**
48
+ * Ensure transformed code ends with one newline before the closing fence.
49
+ *
50
+ * @param {string} code
51
+ * @returns {string}
52
+ */
53
+ export function ensureTrailingNewline(code) {
54
+ return code.endsWith('\n') ? code : `${code}\n`;
55
+ }
@@ -0,0 +1,41 @@
1
+ // @ts-check
2
+
3
+ const LEADING_EXAMPLE_METADATA_PATTERN = /^\s*\/\*\*([\s\S]*?)\*\/\s*\n?/;
4
+
5
+ /**
6
+ * Strip the leading gallery metadata docblock from an included example.
7
+ *
8
+ * @param {string} source
9
+ * @returns {string}
10
+ */
11
+ export function stripLeadingExampleMetadataDocblock(source) {
12
+ const match = source.match(LEADING_EXAMPLE_METADATA_PATTERN);
13
+ if (!match) {
14
+ return source;
15
+ }
16
+
17
+ return source.slice(match[0].length).replace(/^\n+/, '');
18
+ }
19
+
20
+ /**
21
+ * Backwards-compatible helper name retained for copied add-on plugin stacks.
22
+ *
23
+ * @param {string} source
24
+ * @returns {string}
25
+ */
26
+ export function stripLeadingExampleMetadata(source) {
27
+ return stripLeadingExampleMetadataDocblock(source);
28
+ }
29
+
30
+ /**
31
+ * Preserve the old helper shape while keeping metadata parsing intentionally minimal.
32
+ *
33
+ * @param {string} source
34
+ * @returns {{ metadata: null; code: string }}
35
+ */
36
+ export function extractExampleMetadata(source) {
37
+ return {
38
+ metadata: null,
39
+ code: stripLeadingExampleMetadataDocblock(source),
40
+ };
41
+ }
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+
3
+ import { MarkdownPageEvent } from 'typedoc-plugin-markdown';
4
+
5
+ import { transformExampleCodeBlocks } from './transform-markdown.js';
6
+
7
+ /**
8
+ * TypeDoc plugin load function.
9
+ *
10
+ * @param {import('typedoc-plugin-markdown').MarkdownApplication} app
11
+ * @returns {void}
12
+ */
13
+ export function load(app) {
14
+ app.renderer.on(MarkdownPageEvent.END, (page) => {
15
+ if (!page.contents) {
16
+ return;
17
+ }
18
+
19
+ page.contents = transformExampleCodeBlocks(page.contents);
20
+ });
21
+
22
+ app.logger.verbose('[typedoc] Registered includeCode example normalizer');
23
+ }
@@ -0,0 +1,33 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ ensureTrailingNewline,
5
+ EXAMPLE_CODE_FENCE_PATTERN,
6
+ getFenceLanguage,
7
+ isSupportedExampleLanguage,
8
+ normalizeIncludedExampleFenceInfo,
9
+ } from './code-fences.js';
10
+ import { stripLeadingExampleMetadataDocblock } from './example-metadata.js';
11
+
12
+ /**
13
+ * Remove gallery metadata from included example code blocks.
14
+ *
15
+ * @param {string} markdown
16
+ * @returns {string}
17
+ */
18
+ export function transformExampleCodeBlocks(markdown) {
19
+ return markdown.replace(EXAMPLE_CODE_FENCE_PATTERN, (fullMatch, infoString, code) => {
20
+ const language = getFenceLanguage(infoString);
21
+ if (!isSupportedExampleLanguage(language)) {
22
+ return fullMatch;
23
+ }
24
+
25
+ const strippedCode = stripLeadingExampleMetadataDocblock(code);
26
+ if (strippedCode === code) {
27
+ return fullMatch;
28
+ }
29
+
30
+ const normalizedInfoString = normalizeIncludedExampleFenceInfo(infoString);
31
+ return `\`\`\`${normalizedInfoString}\n${ensureTrailingNewline(strippedCode)}\`\`\``;
32
+ });
33
+ }
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+
3
+ import { Converter, ParameterType, ReflectionKind } from 'typedoc';
4
+ import { MarkdownPageEvent } from 'typedoc-plugin-markdown';
5
+
6
+ let apiBaseUrl = 'https://code.textmode.art/api/textmode.js';
7
+ let stripMode = 'full';
8
+
9
+ /**
10
+ * Check whether a comment display part links to the hosted textmode.js API.
11
+ *
12
+ * @param {import('typedoc').CommentDisplayPart} part
13
+ * @returns {boolean}
14
+ */
15
+ function isHostedApiLink(part) {
16
+ return part.kind === 'inline-tag' && part.tag === '@link' && String(part.target ?? '').startsWith(apiBaseUrl);
17
+ }
18
+
19
+ /**
20
+ * Remove `@see` tags that point back to the generated API site itself.
21
+ *
22
+ * These links are useful in emitted declaration hover docs, but they are redundant
23
+ * inside the TypeDoc API pages hosted at the same URLs.
24
+ *
25
+ * @param {import('typedoc').Comment | undefined} comment
26
+ * @returns {void}
27
+ */
28
+ function stripHostedApiSeeTags(comment) {
29
+ if (!comment) {
30
+ return;
31
+ }
32
+
33
+ comment.blockTags = comment.blockTags.filter(
34
+ (tag) => tag.tag !== '@see' || !tag.content.some((part) => isHostedApiLink(part))
35
+ );
36
+ }
37
+
38
+ /**
39
+ * @param {import('typedoc').Reflection} reflection
40
+ * @returns {void}
41
+ */
42
+ function stripReflectionHostedApiSeeTags(reflection) {
43
+ stripHostedApiSeeTags(reflection.comment);
44
+
45
+ if (!reflection.isDeclaration()) {
46
+ return;
47
+ }
48
+
49
+ for (const signature of reflection.signatures ?? []) {
50
+ stripHostedApiSeeTags(signature.comment);
51
+ }
52
+
53
+ stripHostedApiSeeTags(reflection.getSignature?.comment);
54
+ stripHostedApiSeeTags(reflection.setSignature?.comment);
55
+ }
56
+
57
+ /**
58
+ * Remove generated Markdown links back to the same hosted API reference.
59
+ *
60
+ * @param {string} contents
61
+ * @returns {string}
62
+ */
63
+ function stripHostedApiMarkdownLinks(contents) {
64
+ const escapedBaseUrl = apiBaseUrl.replaceAll('.', '\\.');
65
+ const hostedApiLinkPattern = `\\[[^\\]]+ API reference\\]\\(${escapedBaseUrl}[^)]*\\)`;
66
+ const standaloneSeeSection = new RegExp(`\\n#{2,6} See\\n\\n${hostedApiLinkPattern}\\n`, 'g');
67
+ const inlineSeeLink = new RegExp(`\\s*\\*\\*See\\*\\*\\s*${hostedApiLinkPattern}`, 'g');
68
+ const standaloneLinkLine =
69
+ stripMode === 'minimal'
70
+ ? new RegExp(`^${hostedApiLinkPattern}\\n`, 'gm')
71
+ : new RegExp(`^\\s*${hostedApiLinkPattern}\\n?`, 'gm');
72
+
73
+ let nextContents = contents.replace(standaloneSeeSection, '\n').replace(inlineSeeLink, '').replace(standaloneLinkLine, '');
74
+
75
+ if (stripMode === 'full') {
76
+ const listItemLinkLine = new RegExp(`^\\s*-\\s*${hostedApiLinkPattern}\\n?`, 'gm');
77
+ const emptySeeSection = /\n#{2,6} See\n(?:\s*\n)+(?=#{1,6}\s|\n*$)/g;
78
+ nextContents = nextContents.replace(listItemLinkLine, '').replace(emptySeeSection, '\n');
79
+ }
80
+
81
+ return nextContents;
82
+ }
83
+
84
+ /**
85
+ * TypeDoc plugin load function.
86
+ *
87
+ * @param {import('typedoc').Application} app
88
+ * @returns {void}
89
+ */
90
+ export function load(app) {
91
+ app.options.addDeclaration({
92
+ name: 'textmodeApiBaseUrl',
93
+ help: 'Base URL of the hosted textmode API for self-link stripping.',
94
+ type: ParameterType.String,
95
+ defaultValue: apiBaseUrl,
96
+ });
97
+ app.options.addDeclaration({
98
+ name: 'textmodeStripApiSelfLinksMode',
99
+ help: 'Hosted API self-link Markdown stripping mode.',
100
+ type: ParameterType.String,
101
+ defaultValue: stripMode,
102
+ });
103
+
104
+ app.converter.on(Converter.EVENT_BEGIN, () => {
105
+ apiBaseUrl = app.options.getValue('textmodeApiBaseUrl');
106
+ stripMode = app.options.getValue('textmodeStripApiSelfLinksMode');
107
+ });
108
+
109
+ app.converter.on(Converter.EVENT_RESOLVE_END, (context) => {
110
+ const reflections = context.project.getReflectionsByKind(ReflectionKind.All);
111
+
112
+ for (const reflection of reflections) {
113
+ stripReflectionHostedApiSeeTags(reflection);
114
+ }
115
+ });
116
+
117
+ app.renderer.on(MarkdownPageEvent.END, (page) => {
118
+ if (!page.contents) {
119
+ return;
120
+ }
121
+
122
+ page.contents = stripHostedApiMarkdownLinks(page.contents);
123
+ });
124
+
125
+ app.logger.verbose('[typedoc] Registered hosted API self-link stripper');
126
+ }