@rtorcato/repo-tooling 3.13.2 → 3.15.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.
@@ -0,0 +1,109 @@
1
+ // Canonical docs-generator helpers for @rtorcato/* repos (shipped by
2
+ // @rtorcato/repo-tooling — copy via `repo-tooling copy docusaurus-docs-helpers`).
3
+ //
4
+ // The pure pieces every subpath-exports package's doc generator needs, so the
5
+ // generator script above them stays small and project-specific:
6
+ //
7
+ // escapeForMarkdownTable(text) — make a JSDoc summary safe for a table cell
8
+ // collectExportNames(file) — recursive `export` parser over a module graph
9
+ // spliceGeneratedBlock(existing, block) — rewrite only the fenced generated region
10
+ //
11
+ // Zero-config and side-effect free: no paths, no package names, no I/O beyond
12
+ // reading the files you hand it, so this file is copied unmodified.
13
+ //
14
+ // import {
15
+ // collectExportNames,
16
+ // escapeForMarkdownTable,
17
+ // MARKER_END,
18
+ // MARKER_START,
19
+ // spliceGeneratedBlock,
20
+ // } from './docs-helpers.mjs'
21
+
22
+ import { existsSync, readFileSync } from 'node:fs'
23
+ import { dirname, join, resolve } from 'node:path'
24
+
25
+ const EXPORT_NAMED =
26
+ /export\s+(?:async\s+)?(?:function|const|let|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g
27
+ const EXPORT_BRACE = /export\s*\{\s*([^}]+)\}/g
28
+ // Both re-export forms, any specifier — `export * from …` and
29
+ // `export { … } from …`. Non-relative specifiers are filtered below rather than
30
+ // in the pattern, so a bare-package re-export is recognised and then skipped
31
+ // instead of silently parsed as nothing.
32
+ const REEXPORT_FROM = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g
33
+
34
+ /**
35
+ * Escape `text` so it survives a markdown table cell.
36
+ *
37
+ * Neutralises raw HTML-ish tags outside code spans, which would otherwise
38
+ * confuse Docusaurus's MDX parser — but keeps them inside backticks, where
39
+ * `Success<T>` and `<br>` are the point. Then escapes pipes everywhere, since
40
+ * one anywhere in the cell breaks the row.
41
+ *
42
+ * Escaping the `<` beats stripping `/<[^>]*>/`: a one-pass tag strip can leave
43
+ * a tag behind on nested input (`<<b>>` -> `<b>`) and silently eats text like
44
+ * `Success<T>` when the JSDoc forgot the backticks.
45
+ *
46
+ * Escape the backslash in the same pass as the pipe, not after it: escaping
47
+ * only `|` turns the input `a\|b` into `a\\|b`, which markdown reads as an
48
+ * escaped backslash followed by a live pipe, and the row breaks anyway.
49
+ */
50
+ export function escapeForMarkdownTable(text) {
51
+ return text
52
+ .split(/(`[^`]*`)/)
53
+ .map((part, i) => (i % 2 === 1 ? part : part.replace(/</g, '&lt;')))
54
+ .join('')
55
+ .replace(/[\\|]/g, '\\$&')
56
+ }
57
+
58
+ /**
59
+ * Collect every name `file` exports, following relative re-exports into the
60
+ * files they name. Returns the accumulating `names` set; `seen` guards against
61
+ * an import cycle re-entering a file.
62
+ *
63
+ * For an aliased export the *alias* is the exported name — `export { foo as
64
+ * bar }` exports `bar`, which is what a consumer imports — so this takes the
65
+ * last segment, not the first.
66
+ */
67
+ export function collectExportNames(file, names = new Set(), seen = new Set()) {
68
+ if (seen.has(file) || !existsSync(file)) return names
69
+ seen.add(file)
70
+ const src = readFileSync(file, 'utf8')
71
+
72
+ for (const m of src.matchAll(EXPORT_NAMED)) names.add(m[1])
73
+ for (const m of src.matchAll(EXPORT_BRACE)) {
74
+ for (const part of m[1].split(',')) {
75
+ const name = part
76
+ .trim()
77
+ .split(/\s+as\s+/)
78
+ .pop()
79
+ ?.trim()
80
+ if (name) names.add(name)
81
+ }
82
+ }
83
+ for (const m of src.matchAll(REEXPORT_FROM)) {
84
+ if (!m[1].startsWith('.')) continue
85
+ const base = resolve(dirname(file), m[1].replace(/\.js$/, ''))
86
+ for (const candidate of [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts')]) {
87
+ if (existsSync(candidate)) {
88
+ collectExportNames(candidate, names, seen)
89
+ break
90
+ }
91
+ }
92
+ }
93
+ return names
94
+ }
95
+
96
+ export const MARKER_START =
97
+ '<!-- generated:exports — do not edit; `pnpm docs:generate` rewrites this block -->'
98
+ export const MARKER_END = '<!-- /generated:exports -->'
99
+
100
+ /**
101
+ * Splice `block` into `existing` between the markers. Returns null when the
102
+ * page has no markers, which means "hand-written, leave it alone".
103
+ */
104
+ export function spliceGeneratedBlock(existing, block) {
105
+ const start = existing.indexOf(MARKER_START)
106
+ const end = existing.indexOf(MARKER_END)
107
+ if (start === -1 || end === -1 || end < start) return null
108
+ return existing.slice(0, start) + block + existing.slice(end + MARKER_END.length)
109
+ }