@redpanda-data/docs-extensions-and-macros 5.55.0 → 5.56.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/bin/doc-tools.js +315 -7
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +37 -3
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +90 -4
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +2 -2
- package/extensions/url-to-xref.js +92 -22
- package/extensions/validate-attributes.js +10 -1
- package/package.json +2 -1
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +29 -8
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +19 -24
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +1 -1
package/bin/doc-tools.js
CHANGED
|
@@ -43,6 +43,7 @@ const { getAntoraValue, setAntoraValue } = require('../cli-utils/antora-utils')
|
|
|
43
43
|
const programCli = new Command()
|
|
44
44
|
|
|
45
45
|
const pkg = require('../package.json')
|
|
46
|
+
const { generatedMarker } = require('../cli-utils/generated-marker')
|
|
46
47
|
programCli
|
|
47
48
|
.name('doc-tools')
|
|
48
49
|
.description('Redpanda Document Automation CLI')
|
|
@@ -755,6 +756,111 @@ programCli
|
|
|
755
756
|
console.log(' mcp/README.adoc')
|
|
756
757
|
})
|
|
757
758
|
|
|
759
|
+
/**
|
|
760
|
+
* check-dispatch-graph
|
|
761
|
+
*
|
|
762
|
+
* @description
|
|
763
|
+
* Health check for the repository_dispatch graph that connects Redpanda's
|
|
764
|
+
* engineering repos to its docs repos. Reads every workflow file on each
|
|
765
|
+
* configured repo's default branch, pairs the senders (peter-evans/repository-dispatch,
|
|
766
|
+
* `gh api .../dispatches`, curl, github-script) with the listeners
|
|
767
|
+
* (`on.repository_dispatch.types`), and reads each workflow's recent runs from
|
|
768
|
+
* the Actions API. Reports senders with no listener, docs listeners with no
|
|
769
|
+
* sender (informational when a cron backstops them), senders that have never
|
|
770
|
+
* fired, receivers failing more than the threshold, receiver payload variants
|
|
771
|
+
* no sender dispatches, and edges dispatched from more than one repo.
|
|
772
|
+
*
|
|
773
|
+
* @why
|
|
774
|
+
* A repository_dispatch is accepted with HTTP 204 whether or not a listener
|
|
775
|
+
* exists, and a receiver that fails every run looks, from the sender's side,
|
|
776
|
+
* exactly like one that works. Breaks in this graph went unnoticed for weeks
|
|
777
|
+
* until someone read the receivers' run history by hand. This command reads it
|
|
778
|
+
* by script, and .github/workflows/check-dispatch-graph.yml runs it weekly,
|
|
779
|
+
* files one issue, and closes it on the first run that finds the graph healthy.
|
|
780
|
+
*
|
|
781
|
+
* Exit codes: 0 clean, 1 findings (the output ends with the line
|
|
782
|
+
* DISPATCH_GRAPH_FINDINGS_CONFIRMED), 2 could not run (a repo or runs query
|
|
783
|
+
* could not be read, so the graph is incomplete; never reported as clean).
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* # Check the default repo set against the live org
|
|
787
|
+
* export REDPANDA_GITHUB_TOKEN=... # or GH_TOKEN; must read the private docs repos
|
|
788
|
+
* npx doc-tools check-dispatch-graph
|
|
789
|
+
*
|
|
790
|
+
* # Machine-readable
|
|
791
|
+
* npx doc-tools check-dispatch-graph --json > dispatch-graph.json
|
|
792
|
+
*
|
|
793
|
+
* # A narrower set of repos (role decides which side a missing counterpart is reported on)
|
|
794
|
+
* npx doc-tools check-dispatch-graph --repos redpanda-data/helm-charts:engineering,redpanda-data/docs:docs
|
|
795
|
+
*
|
|
796
|
+
* @requirements
|
|
797
|
+
* - A GitHub token that can read every configured repo and its Actions runs
|
|
798
|
+
* (REDPANDA_GITHUB_TOKEN, ACTIONS_BOT_TOKEN, GITHUB_TOKEN or GH_TOKEN). The
|
|
799
|
+
* docs repos are private, so an unauthenticated run exits 2.
|
|
800
|
+
*/
|
|
801
|
+
programCli
|
|
802
|
+
.command('check-dispatch-graph')
|
|
803
|
+
.description('Check the repository_dispatch graph between engineering and docs repos: senders without listeners, listeners without senders, never-fired senders, failing receivers (exit 0 clean, 1 findings, 2 could not run)')
|
|
804
|
+
.option('--repos <list>', 'Comma-separated owner/name[:role] entries (role engineering or docs) replacing the default repo set')
|
|
805
|
+
.option('--fail-threshold <fraction>', 'Flag a receiver whose fail rate over the window is above this fraction', '0.5')
|
|
806
|
+
.option('--window <n>', 'How many of the newest runs the fail rate is computed over', '20')
|
|
807
|
+
.option('--per-page <n>', 'How many runs to fetch per workflow (max 100)', '50')
|
|
808
|
+
.option('--json', 'Print the full result as JSON instead of the table')
|
|
809
|
+
.action(async (options) => {
|
|
810
|
+
const { runCheck, formatReport, createGitHubApi, DEFAULT_REPOS, SENTINEL } = require('../tools/dispatch-graph/check-dispatch-graph')
|
|
811
|
+
|
|
812
|
+
let repos = DEFAULT_REPOS
|
|
813
|
+
if (options.repos) {
|
|
814
|
+
repos = String(options.repos).split(',').map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
815
|
+
const [repo, role = 'docs'] = entry.split(':')
|
|
816
|
+
if (!/^[^/]+\/[^/]+$/.test(repo) || !['engineering', 'docs'].includes(role)) {
|
|
817
|
+
console.error(`Error: --repos entry "${entry}" must be owner/name or owner/name:engineering|docs`)
|
|
818
|
+
process.exit(2)
|
|
819
|
+
}
|
|
820
|
+
return { repo, role }
|
|
821
|
+
})
|
|
822
|
+
}
|
|
823
|
+
const threshold = Number(options.failThreshold)
|
|
824
|
+
const window = Number(options.window)
|
|
825
|
+
const perPage = Number(options.perPage)
|
|
826
|
+
if (!(threshold >= 0 && threshold <= 1) || !(window > 0) || !(perPage > 0 && perPage <= 100)) {
|
|
827
|
+
console.error('Error: --fail-threshold must be 0..1, --window > 0, --per-page 1..100')
|
|
828
|
+
process.exit(2)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Exit 2 is reserved for "could not find out", so the weekly job can tell
|
|
832
|
+
// "an edge is broken" apart from "the API was down" and not file a false
|
|
833
|
+
// issue. An unauthenticated run cannot read the private docs repos and
|
|
834
|
+
// lands here too; it must never pass as clean.
|
|
835
|
+
let result
|
|
836
|
+
try {
|
|
837
|
+
const octokit = require('../cli-utils/octokit-client')
|
|
838
|
+
result = await runCheck({ repos, api: createGitHubApi(octokit), threshold, window, perPage })
|
|
839
|
+
} catch (err) {
|
|
840
|
+
console.error(`Error: Could not check the dispatch graph: ${err.message}`)
|
|
841
|
+
process.exit(2)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (options.json) {
|
|
845
|
+
console.log(JSON.stringify(result, null, 2))
|
|
846
|
+
} else {
|
|
847
|
+
console.log(formatReport(result))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (result.exitCode === 2) {
|
|
851
|
+
console.error(`\nError: ${result.unreadable.length} repo(s) or run record(s) could not be read, so the graph is incomplete. Not a clean result.`)
|
|
852
|
+
process.exit(2)
|
|
853
|
+
}
|
|
854
|
+
if (result.exitCode === 1) {
|
|
855
|
+
// Printed only where findings were actually established; the weekly
|
|
856
|
+
// script requires this line before filing an issue, so a usage error or
|
|
857
|
+
// a crash (both exit 1 under Commander) can never file one.
|
|
858
|
+
if (!options.json) console.log(`\n${SENTINEL}`)
|
|
859
|
+
process.exit(1)
|
|
860
|
+
}
|
|
861
|
+
process.exit(0)
|
|
862
|
+
})
|
|
863
|
+
|
|
758
864
|
// ====================================================================
|
|
759
865
|
// GENERATE SUBCOMMAND GROUP
|
|
760
866
|
// ====================================================================
|
|
@@ -1824,17 +1930,25 @@ automation
|
|
|
1824
1930
|
const outFile = path.join(outDir, `k-${name}${opts.outputSuffix}`)
|
|
1825
1931
|
console.log(`pandoc ${md} → ${outFile}`)
|
|
1826
1932
|
fs.mkdirSync(path.dirname(outFile), { recursive: true })
|
|
1827
|
-
|
|
1828
|
-
|
|
1933
|
+
const { markdownToAsciiDoc } = require('../cli-utils/helm-commented-values')
|
|
1934
|
+
let converted
|
|
1935
|
+
try {
|
|
1936
|
+
converted = markdownToAsciiDoc(fs.readFileSync(md, 'utf8')) + '\n'
|
|
1937
|
+
} catch (err) {
|
|
1938
|
+
console.error(`Error: ${err.message}`)
|
|
1939
|
+
process.exit(1)
|
|
1940
|
+
}
|
|
1829
1941
|
|
|
1830
1942
|
const { formatHelmSpec } = require('../cli-utils/format-helm-spec')
|
|
1831
|
-
let doc = formatHelmSpec(
|
|
1943
|
+
let doc = formatHelmSpec(converted)
|
|
1832
1944
|
|
|
1833
1945
|
// helm-docs only renders keys that exist in the YAML tree, so optional
|
|
1834
1946
|
// values that ship commented out never appear in the reference. Pull
|
|
1835
1947
|
// documented commented-out keys from values.yaml and inject them. This
|
|
1836
1948
|
// runs after formatHelmSpec so the injected body sections are not affected
|
|
1837
|
-
// by the blank-line normalization that its header fix performs.
|
|
1949
|
+
// by the blank-line normalization that its header fix performs. The
|
|
1950
|
+
// comments are Markdown, so each one goes through markdownToAsciiDoc on
|
|
1951
|
+
// the way in, like the README did.
|
|
1838
1952
|
const valuesFile = path.join(chartPath, 'values.yaml')
|
|
1839
1953
|
if (fs.existsSync(valuesFile)) {
|
|
1840
1954
|
const { extractCommentedValueDocs, injectIntoAsciiDoc, filterEntriesBySchema } = require('../cli-utils/helm-commented-values')
|
|
@@ -1856,7 +1970,7 @@ automation
|
|
|
1856
1970
|
}
|
|
1857
1971
|
|
|
1858
1972
|
if (entries.length > 0) {
|
|
1859
|
-
const { doc: withInjected, injected, sectionsFound } = injectIntoAsciiDoc(doc, entries)
|
|
1973
|
+
const { doc: withInjected, injected, sectionsFound } = injectIntoAsciiDoc(doc, entries, { toAsciiDoc: markdownToAsciiDoc })
|
|
1860
1974
|
doc = withInjected
|
|
1861
1975
|
if (injected.length > 0) {
|
|
1862
1976
|
console.log(`Documented ${injected.length} commented-out value(s): ${injected.join(', ')}`)
|
|
@@ -1871,6 +1985,10 @@ automation
|
|
|
1871
1985
|
}
|
|
1872
1986
|
}
|
|
1873
1987
|
|
|
1988
|
+
// Same marker shape as every doc-tools generator; convert-to-markdown
|
|
1989
|
+
// reads it and reports the provenance in the Markdown twins.
|
|
1990
|
+
const helmMarker = generatedMarker('helm-docs', `the ${name} chart's values.yaml and README in redpanda-data/redpanda-operator`) + '\n\n'
|
|
1991
|
+
doc = helmMarker + doc
|
|
1874
1992
|
fs.writeFileSync(outFile, doc, 'utf8')
|
|
1875
1993
|
|
|
1876
1994
|
console.log(`Done: Wrote ${outFile}`)
|
|
@@ -2134,6 +2252,12 @@ automation
|
|
|
2134
2252
|
* - Use --tag for released content (GA or beta releases). Tags reference specific release points.
|
|
2135
2253
|
* - Use --branch for in-progress content (unreleased features). Branches track ongoing development.
|
|
2136
2254
|
*
|
|
2255
|
+
* Links: the Go doc comments link to docs.redpanda.com with absolute URLs. When the command
|
|
2256
|
+
* runs inside a docs checkout (or is given --docs-root), every URL that matches a page of that
|
|
2257
|
+
* checkout, directly or through a page alias, is rewritten as an xref so the committed page
|
|
2258
|
+
* keeps one link form across regenerations. URLs that match no page are left as written and
|
|
2259
|
+
* listed in the output.
|
|
2260
|
+
*
|
|
2137
2261
|
* @why
|
|
2138
2262
|
* Kubernetes CRDs define complex APIs for deploying and managing Redpanda. The API schema
|
|
2139
2263
|
* is defined in Go code with hundreds of fields across nested structures. Manual documentation
|
|
@@ -2168,6 +2292,7 @@ automation
|
|
|
2168
2292
|
* - For GitHub URLs: Git and internet connection
|
|
2169
2293
|
* - crd-ref-docs tool (automatically installed if missing)
|
|
2170
2294
|
* - Go toolchain for running crd-ref-docs
|
|
2295
|
+
* - To convert links: a docs checkout with Antora installed (npm install in redpanda-data/docs)
|
|
2171
2296
|
*/
|
|
2172
2297
|
automation
|
|
2173
2298
|
.command('crd-spec')
|
|
@@ -2178,6 +2303,7 @@ automation
|
|
|
2178
2303
|
.option('-d, --depth <n>', 'How many levels deep', '10')
|
|
2179
2304
|
.option('--templates-dir <dir>', 'Asciidoctor templates dir', '.github/crd-config/templates/asciidoctor/operator')
|
|
2180
2305
|
.option('--output <file>', 'Where to write the generated AsciiDoc file', 'modules/reference/pages/k-crd.adoc')
|
|
2306
|
+
.option('--docs-root <dir>', 'Docs checkout whose pages docs.redpanda.com URLs are converted into xrefs against (default: the current repo when run inside redpanda-data/docs)')
|
|
2181
2307
|
.action(async (opts) => {
|
|
2182
2308
|
verifyCrdDependencies()
|
|
2183
2309
|
|
|
@@ -2286,8 +2412,28 @@ automation
|
|
|
2286
2412
|
process.exit(1)
|
|
2287
2413
|
}
|
|
2288
2414
|
|
|
2289
|
-
// docs.redpanda.com URLs
|
|
2290
|
-
//
|
|
2415
|
+
// Go doc comments carry absolute docs.redpanda.com URLs (a Go comment
|
|
2416
|
+
// cannot hold an xref). Convert them against the docs checkout so the
|
|
2417
|
+
// committed page keeps one link form across regenerations, instead of
|
|
2418
|
+
// flipping between xrefs and URLs and burying the real changes in the
|
|
2419
|
+
// review diff (docs 2256). A URL that matches no page in the checkout is
|
|
2420
|
+
// left as written and listed; the url-to-xref extension still converts
|
|
2421
|
+
// cross-component URLs at site build time and warns about broken ones.
|
|
2422
|
+
const docsRoot = opts.docsRoot || (inDocs ? repoRoot : null)
|
|
2423
|
+
if (docsRoot) {
|
|
2424
|
+
const { buildDocsResolverContext, convertDocsLinksInFile, formatDocsLinkReport } = require('../cli-utils/docs-links-to-xrefs.js')
|
|
2425
|
+
let linkContext
|
|
2426
|
+
try {
|
|
2427
|
+
linkContext = await buildDocsResolverContext(docsRoot)
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
console.error(`Error: Cannot map docs URLs to xrefs: ${err.message}`)
|
|
2430
|
+
process.exit(1)
|
|
2431
|
+
}
|
|
2432
|
+
console.log(`Mapping docs URLs against ${linkContext.pageCount} pages of the ${linkContext.component} component in ${docsRoot}`)
|
|
2433
|
+
console.log(formatDocsLinkReport(convertDocsLinksInFile(opts.output, linkContext), opts.output))
|
|
2434
|
+
} else {
|
|
2435
|
+
console.warn('⚠️ No docs checkout to map against (pass --docs-root); docs.redpanda.com URLs are left as written.')
|
|
2436
|
+
}
|
|
2291
2437
|
|
|
2292
2438
|
if (tmpSrc) fs.rmSync(tmpSrc, { recursive: true, force: true })
|
|
2293
2439
|
fs.rmSync(configTmp, { recursive: true, force: true })
|
|
@@ -2875,12 +3021,67 @@ programCli
|
|
|
2875
3021
|
.option('--skip-rules <list>', 'Comma-separated rule ids to skip')
|
|
2876
3022
|
.option('--only-rules <list>', 'Comma-separated rule ids to run exclusively')
|
|
2877
3023
|
.option('--reviewed <file>', 'Diff mode: skip declarations whose fingerprint is listed in <file> (already reviewed on an earlier push)')
|
|
3024
|
+
.option('--reviewer-comments <file>', 'Diff mode: skip declarations whose text a human reviewer wrote in one of the comment bodies in <file> (a JSON array of strings)')
|
|
2878
3025
|
.option('--strict', 'Exit 1 when any error-severity finding exists (default: always exit 0 - suggest, never block)')
|
|
2879
3026
|
.action((options) => {
|
|
2880
3027
|
const { runCli } = require('../tools/lint-strings')
|
|
2881
3028
|
runCli(options)
|
|
2882
3029
|
})
|
|
2883
3030
|
|
|
3031
|
+
/**
|
|
3032
|
+
* detect-enterprise-gating
|
|
3033
|
+
*
|
|
3034
|
+
* @description
|
|
3035
|
+
* Find enterprise gating changes in a Redpanda core diff that no doc string
|
|
3036
|
+
* announces: a value added to or removed from license_required_feature in
|
|
3037
|
+
* src/v/features/enterprise_features.h, a property wrapped in or unwrapped
|
|
3038
|
+
* from config::enterprise<> in src/v/config/configuration.h, or a changed
|
|
3039
|
+
* restricted value of an enterprise<> property in
|
|
3040
|
+
* src/v/config/configuration.cc. Prints them as a
|
|
3041
|
+
* doc-impact report, the JSON the doc-strings review dispatches to the docs
|
|
3042
|
+
* team's Jira router, so a licensing change reaches a DOC ticket even when
|
|
3043
|
+
* the review has no doc string to look at.
|
|
3044
|
+
*
|
|
3045
|
+
* @example
|
|
3046
|
+
* npx doc-tools detect-enterprise-gating --repo . --diff origin/dev
|
|
3047
|
+
*
|
|
3048
|
+
* @exit 0 always: `{"findings":[]}` means nothing to report. Errors from git
|
|
3049
|
+
* print to stderr and still exit 0 with an empty report, so a caller's
|
|
3050
|
+
* fail-open gate stays closed rather than red.
|
|
3051
|
+
*/
|
|
3052
|
+
programCli
|
|
3053
|
+
.command('detect-enterprise-gating')
|
|
3054
|
+
.description('Find enterprise gating changes (license_required_feature values, config::enterprise<> wrappers and restricted values) in a core diff and print them as a doc-impact report')
|
|
3055
|
+
.requiredOption('--repo <path>', 'Path to a Redpanda core checkout. Nothing is cloned.')
|
|
3056
|
+
.requiredOption('--diff <base>', 'Base ref: changes in <base>...HEAD are inspected')
|
|
3057
|
+
.option('--format <format>', 'Output format: json or human', 'json')
|
|
3058
|
+
.action((options) => {
|
|
3059
|
+
const { spawnSync } = require('child_process')
|
|
3060
|
+
const { detectGatingChanges, buildDocImpact, enterprisePropertyNames, ENUM_HEADER, CONFIG_HEADER, CONFIG_SOURCE } = require('../tools/enterprise-features/detect-gating-changes')
|
|
3061
|
+
const empty = { findings: [] }
|
|
3062
|
+
const result = spawnSync('git', ['diff', '--no-color', '--no-ext-diff', `${options.diff}...HEAD`, '--', ENUM_HEADER, CONFIG_HEADER, CONFIG_SOURCE], {
|
|
3063
|
+
cwd: options.repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024
|
|
3064
|
+
})
|
|
3065
|
+
if (result.status !== 0) {
|
|
3066
|
+
console.error(`git diff ${options.diff}...HEAD failed in ${options.repo}: ${(result.stderr || '').trim()}`)
|
|
3067
|
+
console.log(JSON.stringify(empty))
|
|
3068
|
+
return
|
|
3069
|
+
}
|
|
3070
|
+
// A configuration.cc change is tied to an enterprise property through
|
|
3071
|
+
// the header at HEAD. Without it, header and enum changes still report.
|
|
3072
|
+
const header = spawnSync('git', ['show', `HEAD:${CONFIG_HEADER}`], { cwd: options.repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
|
|
3073
|
+
if (header.status !== 0) console.error(`git show HEAD:${CONFIG_HEADER} failed in ${options.repo}; configuration.cc changes are not checked: ${(header.stderr || '').trim()}`)
|
|
3074
|
+
const enterpriseNames = header.status === 0 ? enterprisePropertyNames(header.stdout) : []
|
|
3075
|
+
const findings = detectGatingChanges(result.stdout, { enterpriseNames })
|
|
3076
|
+
const report = buildDocImpact(findings) || empty
|
|
3077
|
+
if (options.format === 'human') {
|
|
3078
|
+
if (findings.length === 0) { console.log('No enterprise gating changes.'); return }
|
|
3079
|
+
for (const f of findings) console.log(`${f.change_kind}: ${f.name}`)
|
|
3080
|
+
return
|
|
3081
|
+
}
|
|
3082
|
+
console.log(JSON.stringify(report, null, 2))
|
|
3083
|
+
})
|
|
3084
|
+
|
|
2884
3085
|
/**
|
|
2885
3086
|
* doc-impact-recording
|
|
2886
3087
|
*
|
|
@@ -3008,6 +3209,113 @@ programCli
|
|
|
3008
3209
|
runCli(log, options)
|
|
3009
3210
|
})
|
|
3010
3211
|
|
|
3212
|
+
/**
|
|
3213
|
+
* check-metadata
|
|
3214
|
+
*
|
|
3215
|
+
* @description
|
|
3216
|
+
* Check the taxonomy metadata in AsciiDoc page headers against the docs
|
|
3217
|
+
* taxonomy (taxonomy.yml in the docs repository). On every page,
|
|
3218
|
+
* `:page-categories:` values must resolve to a concept: an unknown value, a
|
|
3219
|
+
* deprecated value, or two values that are the same concept warn, and each
|
|
3220
|
+
* message names the replacement. On a solution overview
|
|
3221
|
+
* (`:page-layout: solution`), `:page-solution-use-cases:`,
|
|
3222
|
+
* `:page-solution-industries:`, `:page-solution-technologies:`, and
|
|
3223
|
+
* `:page-solution-platforms:` must resolve within their own facet, and an
|
|
3224
|
+
* unknown value or a duplicate concept is an error. `:page-topic-type:` is
|
|
3225
|
+
* checked against the content types, as a warning. --strict turns every
|
|
3226
|
+
* warning into an error. The taxonomy comes from --taxonomy (a path or URL),
|
|
3227
|
+
* else the docs checkout at the current directory, else $REDPANDA_DOCS_PATH,
|
|
3228
|
+
* else the published file. The build runs the same rules
|
|
3229
|
+
* (extension-utils/metadata-check.js), so the two never disagree.
|
|
3230
|
+
* --fix first rewrites `:page-categories:` in place: each value becomes its
|
|
3231
|
+
* concept's preferred label, a deprecated value becomes its replacements, and
|
|
3232
|
+
* a replacement that is a task is dropped (the task comes from the module).
|
|
3233
|
+
* Values that need a judgment call (no replacement, or a content type) are
|
|
3234
|
+
* left as they are and still warn.
|
|
3235
|
+
* --suggest also lists, for each page, the topics, use cases, technologies,
|
|
3236
|
+
* and industries its text names that its header does not carry yet, so
|
|
3237
|
+
* authors and agents choose values from the taxonomy instead of inventing
|
|
3238
|
+
* them. Matching is deterministic, with no model: every live concept's
|
|
3239
|
+
* prefLabel and altLabels are matched case-insensitively on whole words
|
|
3240
|
+
* against the title, `:description:`, the page path (module and directories),
|
|
3241
|
+
* section headings, and body, code included and included partials followed.
|
|
3242
|
+
* Words split on anything that is not a letter or digit, so the code literal
|
|
3243
|
+
* `postgres_cdc` names PostgreSQL (altLabel Postgres) and Change data capture
|
|
3244
|
+
* (altLabel CDC). A two-letter label such as Go matches only in its exact
|
|
3245
|
+
* case and never as the first word of a sentence. Hits are weighted title 8,
|
|
3246
|
+
* path 6, description 5, heading 1.5 each (up to 3), and body 1 + log2(n)
|
|
3247
|
+
* (at most 3). A suggestion never names a deprecated concept, never takes a
|
|
3248
|
+
* facet past its cardinality, and skips what the page carries or what that
|
|
3249
|
+
* implies; on a solution overview a technology that a topic implies (Iceberg
|
|
3250
|
+
* Topics implies Apache Iceberg) is suggested in page-solution-technologies.
|
|
3251
|
+
* Each suggestion has a score, a reason, and evidence (the matched term,
|
|
3252
|
+
* where, and the line). --min-score defaults to 5 for Product Docs pages and
|
|
3253
|
+
* 3 for solution overviews; --limit (per facet) to 3 and 6. Suggestions do
|
|
3254
|
+
* not change the exit code.
|
|
3255
|
+
* --suggest also lists candidates: frequent, distinctive terms on a page that
|
|
3256
|
+
* no concept's label or altLabel matches, in any scheme, for proposing new
|
|
3257
|
+
* concepts or altLabels. They are repeated capitalized terms in prose (Amazon
|
|
3258
|
+
* S3, Debezium) and component identifiers (`aws_s3` output, xrefs to Connect
|
|
3259
|
+
* component pages), each with a count, the lines, and a likely facet from a
|
|
3260
|
+
* cue word after it (a connector, database, format, or product word). Terms
|
|
3261
|
+
* in the terminology lists (--terminology, default the docs glossary in the
|
|
3262
|
+
* checkout), common English, and words the page also writes in lowercase are
|
|
3263
|
+
* dropped. --candidates-only skips the check and the suggestions and
|
|
3264
|
+
* aggregates candidates across the pages: one entry per term, with the pages
|
|
3265
|
+
* it is on, leaving out terms on more than a fifth of the pages
|
|
3266
|
+
* (boilerplate).
|
|
3267
|
+
* Exit 0 without errors, 1 with errors, 2 when the check could not run.
|
|
3268
|
+
*
|
|
3269
|
+
* @why
|
|
3270
|
+
* Categories, use cases, and technologies drive search facets and solution
|
|
3271
|
+
* recommendations, and a free-text value becomes a near-duplicate the moment
|
|
3272
|
+
* a second author writes one. This catches it on the PR, without a model, in
|
|
3273
|
+
* a form the docs agents and the PR review can read.
|
|
3274
|
+
*
|
|
3275
|
+
* @example
|
|
3276
|
+
* # Pages changed on this branch (what the docs PR check runs)
|
|
3277
|
+
* npx doc-tools check-metadata --changed origin/main
|
|
3278
|
+
*
|
|
3279
|
+
* # Specific pages or directories, JSON for a grader
|
|
3280
|
+
* npx doc-tools check-metadata modules/manage/pages/iceberg --format json
|
|
3281
|
+
*
|
|
3282
|
+
* # Against a local taxonomy, failing on warnings
|
|
3283
|
+
* npx doc-tools check-metadata --taxonomy ../docs/shared/modules/ROOT/attachments/taxonomy.yml --strict
|
|
3284
|
+
*
|
|
3285
|
+
* # Rewrite deprecated categories to their replacements, then check
|
|
3286
|
+
* npx doc-tools check-metadata --fix modules
|
|
3287
|
+
*
|
|
3288
|
+
* # Suggest taxonomy values for new pages, as JSON for an agent
|
|
3289
|
+
* npx doc-tools check-metadata --suggest --format json modules/manage/pages/new-page.adoc
|
|
3290
|
+
*
|
|
3291
|
+
* # Suggest only for changed pages that carry no categories, with a lower bar
|
|
3292
|
+
* npx doc-tools check-metadata --changed origin/main --suggest --suggest-empty-only --min-score 3
|
|
3293
|
+
*
|
|
3294
|
+
* # Terms across a repository that no concept covers, filtered by the style guide's word lists
|
|
3295
|
+
* npx doc-tools check-metadata --candidates-only --terminology shared/modules/terms/partials,../docs-team-standards/resources/writing-style/terminology.md --format json modules
|
|
3296
|
+
*/
|
|
3297
|
+
programCli
|
|
3298
|
+
.command('check-metadata')
|
|
3299
|
+
.description('Check page-categories and solution facet values in AsciiDoc page headers against the docs taxonomy')
|
|
3300
|
+
.argument('[paths...]', 'Files, directories, or globs to check, relative to --root (default: modules/**/*.adoc). With --changed, filters the changed files')
|
|
3301
|
+
.option('--changed <base>', 'Check only .adoc files changed between the merge base with <base> and HEAD, for example origin/main')
|
|
3302
|
+
.option('--taxonomy <path|url>', 'taxonomy.yml to use (default: the docs checkout, $REDPANDA_DOCS_PATH, then https://docs.redpanda.com/shared/_attachments/taxonomy.yml)')
|
|
3303
|
+
.option('--format <format>', 'Output format: text or json', 'text')
|
|
3304
|
+
.option('--strict', 'Treat warnings as errors')
|
|
3305
|
+
.option('--fix', 'Rewrite page-categories in place to current taxonomy labels, then check')
|
|
3306
|
+
.option('--root <path>', 'Repository root (default: current directory)')
|
|
3307
|
+
.option('--output <path>', 'Also write the JSON result to this file')
|
|
3308
|
+
.option('--suggest', 'Also suggest taxonomy values each page does not carry yet, matched from its title, description, path, headings, and body')
|
|
3309
|
+
.option('--min-score <n>', 'With --suggest, the lowest score to suggest (default: 5 on Product Docs pages, 3 on solution overviews)')
|
|
3310
|
+
.option('--limit <n>', 'With --suggest, the most suggestions per facet and page (default: 3 on Product Docs pages, 6 on solution overviews; never past the facet cardinality)')
|
|
3311
|
+
.option('--suggest-empty-only', 'With --suggest, suggest only for pages without :page-categories:')
|
|
3312
|
+
.option('--candidates-only', 'List only candidate terms, frequent distinctive terms that no concept matches, aggregated across the pages with the pages each appears in')
|
|
3313
|
+
.option('--terminology <paths>', 'With --suggest or --candidates-only, comma-separated terminology lists whose terms are never candidates: a glossary partials directory, a Markdown word list, or a text file (default: the docs glossary, shared/modules/terms/partials, in the checkout or $REDPANDA_DOCS_PATH)')
|
|
3314
|
+
.action(async (paths, options) => {
|
|
3315
|
+
const { runCli } = require('../tools/check-metadata')
|
|
3316
|
+
await runCli(paths, options)
|
|
3317
|
+
})
|
|
3318
|
+
|
|
3011
3319
|
/**
|
|
3012
3320
|
* check-rendered-html
|
|
3013
3321
|
*
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts docs.redpanda.com URLs in a generated page into xrefs before the
|
|
5
|
+
* page is committed to a docs repository.
|
|
6
|
+
*
|
|
7
|
+
* Reference pages generated from engineering sources (the CRD reference from
|
|
8
|
+
* Go doc comments, for example) arrive with absolute docs URLs, because a Go
|
|
9
|
+
* comment cannot carry an AsciiDoc xref. The url-to-xref Antora extension
|
|
10
|
+
* rewrites those at site build time, but the committed source then flips
|
|
11
|
+
* between xrefs and URLs on every regeneration, which buries the real content
|
|
12
|
+
* changes in a review diff. Converting here keeps the committed page stable.
|
|
13
|
+
*
|
|
14
|
+
* The URL-to-resource mapping is the one the extension uses: this module
|
|
15
|
+
* builds a real Antora content catalog from the docs checkout (aggregator plus
|
|
16
|
+
* classifier, worktree files, HEAD only) and hands it to the extension's
|
|
17
|
+
* buildUrlMap and convertContent. Nothing about the URL structure is guessed:
|
|
18
|
+
* a URL is converted only when it matches a page the checkout publishes,
|
|
19
|
+
* directly or through a page alias, and its fragment, if any, matches an
|
|
20
|
+
* anchor on that page; everything else is left as written and reported to the
|
|
21
|
+
* caller.
|
|
22
|
+
*
|
|
23
|
+
* Targets in the checkout's own component are written as module:page xrefs,
|
|
24
|
+
* the form the docs repos use by hand for same-component links. Pages of other
|
|
25
|
+
* components (Connect, Cloud) are not in a single-repo catalog, so URLs that
|
|
26
|
+
* point at them stay raw and are listed in the run output; the build-time
|
|
27
|
+
* extension still converts them on the site.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs')
|
|
31
|
+
const os = require('os')
|
|
32
|
+
const path = require('path')
|
|
33
|
+
const yaml = require('js-yaml')
|
|
34
|
+
|
|
35
|
+
const { buildUrlMap, convertContent } = require('../extensions/url-to-xref')
|
|
36
|
+
|
|
37
|
+
const DEFAULT_HOSTNAMES = ['docs.redpanda.com']
|
|
38
|
+
// Mirrors the extension's default ignore list: docs-domain paths that Antora
|
|
39
|
+
// does not publish (the Bump.sh API reference and the MCP endpoint).
|
|
40
|
+
const DEFAULT_IGNORE = ['^/api/', '^/mcp(/|$)']
|
|
41
|
+
const LATEST_VERSION_SEGMENT = 'current'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Loads an Antora package, preferring the copy installed in the docs checkout
|
|
45
|
+
* (the site's own Antora version) over the one this package ships for its own
|
|
46
|
+
* preview build. The aggregator and classifier are dependencies of
|
|
47
|
+
* @antora/site-generator, so they are resolved through it.
|
|
48
|
+
*/
|
|
49
|
+
function requireAntora (name, searchPaths) {
|
|
50
|
+
const errors = []
|
|
51
|
+
for (const searchPath of searchPaths) {
|
|
52
|
+
try {
|
|
53
|
+
const siteGenerator = path.dirname(require.resolve('@antora/site-generator/package.json', { paths: [searchPath] }))
|
|
54
|
+
return require(require.resolve(name, { paths: [siteGenerator, searchPath] }))
|
|
55
|
+
} catch (err) {
|
|
56
|
+
errors.push(`${searchPath}: ${err.message.split('\n')[0]}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Cannot load ${name}; install Antora in the docs repository (npm install) so generated links can be converted.\n${errors.join('\n')}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads the component name a docs checkout declares in antora.yml. A checkout
|
|
64
|
+
* without one is not an Antora component and cannot be mapped.
|
|
65
|
+
*/
|
|
66
|
+
function readComponentName (docsRoot) {
|
|
67
|
+
const descriptor = path.join(docsRoot, 'antora.yml')
|
|
68
|
+
if (!fs.existsSync(descriptor)) {
|
|
69
|
+
throw new Error(`No antora.yml in ${docsRoot}; a docs checkout is needed to map docs URLs to pages`)
|
|
70
|
+
}
|
|
71
|
+
const { name } = yaml.load(fs.readFileSync(descriptor, 'utf8')) || {}
|
|
72
|
+
if (!name) throw new Error(`${descriptor} declares no component name`)
|
|
73
|
+
return name
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds the resolver context for a docs checkout: the extension's URL map,
|
|
78
|
+
* computed from an Antora content catalog of the checkout's worktree.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} docsRoot - Root of a docs repository checkout (holds antora.yml).
|
|
81
|
+
* @param {Object} [options]
|
|
82
|
+
* @param {string[]} [options.hostnames] - Docs hostnames to convert.
|
|
83
|
+
* @param {string[]} [options.ignore] - URL path patterns to leave alone silently.
|
|
84
|
+
* @param {string} [options.cacheDir] - Antora cache directory.
|
|
85
|
+
* @returns {Promise<Object>} A context for convertContent, plus `component`
|
|
86
|
+
* (the checkout's component name) and `pageCount`.
|
|
87
|
+
*/
|
|
88
|
+
async function buildDocsResolverContext (docsRoot, options = {}) {
|
|
89
|
+
const root = path.resolve(docsRoot)
|
|
90
|
+
const component = readComponentName(root)
|
|
91
|
+
const searchPaths = [root, path.join(__dirname, '..')]
|
|
92
|
+
const aggregateContent = requireAntora('@antora/content-aggregator', searchPaths)
|
|
93
|
+
const classifyContent = requireAntora('@antora/content-classifier', searchPaths)
|
|
94
|
+
const playbook = {
|
|
95
|
+
runtime: {
|
|
96
|
+
quiet: true,
|
|
97
|
+
fetch: false,
|
|
98
|
+
cacheDir: options.cacheDir || fs.mkdtempSync(path.join(os.tmpdir(), 'docs-links-antora-cache-')),
|
|
99
|
+
log: { level: 'silent' },
|
|
100
|
+
},
|
|
101
|
+
site: {},
|
|
102
|
+
// The published shape of the live site; normalizeUrlPath in the extension
|
|
103
|
+
// makes the extension style irrelevant to matching, and candidatePaths
|
|
104
|
+
// covers both the symbolic and the real version segment.
|
|
105
|
+
urls: { htmlExtensionStyle: 'indexify', latestVersionSegment: LATEST_VERSION_SEGMENT },
|
|
106
|
+
// HEAD of the local checkout, read from the worktree, so an unpushed
|
|
107
|
+
// branch and uncommitted pages count as published.
|
|
108
|
+
content: { sources: [{ url: root, branches: 'HEAD' }] },
|
|
109
|
+
}
|
|
110
|
+
const contentCatalog = classifyContent(playbook, await aggregateContent(playbook))
|
|
111
|
+
const pageCount = contentCatalog.getPages((page) => page.out).length
|
|
112
|
+
return Object.assign(buildUrlMap(contentCatalog), {
|
|
113
|
+
hostnames: new Set(options.hostnames || DEFAULT_HOSTNAMES),
|
|
114
|
+
ignore: (options.ignore || DEFAULT_IGNORE).map((pattern) => new RegExp(pattern)),
|
|
115
|
+
latestVersionSegment: LATEST_VERSION_SEGMENT,
|
|
116
|
+
localComponent: component,
|
|
117
|
+
resolveInclude: (spec, page) => contentCatalog.resolveResource(spec, page.src, 'partial', ['partial', 'page']),
|
|
118
|
+
component,
|
|
119
|
+
pageCount,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Converts the docs URLs in a string. Thin wrapper over the extension's
|
|
125
|
+
* convertContent so callers do not depend on the extension module directly.
|
|
126
|
+
*/
|
|
127
|
+
function convertDocsLinks (content, context) {
|
|
128
|
+
return convertContent(content, context)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Converts the docs URLs in a file in place. The file is rewritten only when
|
|
133
|
+
* something changed.
|
|
134
|
+
*
|
|
135
|
+
* @returns {Object} `{ converted, unmapped, withQueryString, withoutLinkText, missingAnchor }`
|
|
136
|
+
* with the raw URLs in each list deduplicated.
|
|
137
|
+
*/
|
|
138
|
+
function convertDocsLinksInFile (filePath, context) {
|
|
139
|
+
const before = fs.readFileSync(filePath, 'utf8')
|
|
140
|
+
const result = convertDocsLinks(before, context)
|
|
141
|
+
if (result.converted) fs.writeFileSync(filePath, result.content, 'utf8')
|
|
142
|
+
return {
|
|
143
|
+
converted: result.converted,
|
|
144
|
+
unmapped: [...new Set(result.unmapped)],
|
|
145
|
+
withQueryString: [...new Set(result.withQueryString)],
|
|
146
|
+
withoutLinkText: [...new Set(result.withoutLinkText)],
|
|
147
|
+
missingAnchor: [...new Set(result.missingAnchor)],
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Formats the outcome of a file conversion for the run output: one summary
|
|
153
|
+
* line, then every URL that was left as written and why.
|
|
154
|
+
*/
|
|
155
|
+
function formatDocsLinkReport (result, filePath) {
|
|
156
|
+
const lines = [`Converted ${result.converted} docs URL${result.converted === 1 ? '' : 's'} to xrefs in ${filePath}`]
|
|
157
|
+
const leftRaw = (urls, reason) => {
|
|
158
|
+
if (!urls.length) return
|
|
159
|
+
lines.push(`Left ${urls.length} docs URL${urls.length === 1 ? '' : 's'} as written (${reason}):`)
|
|
160
|
+
for (const url of urls) lines.push(` ${url}`)
|
|
161
|
+
}
|
|
162
|
+
leftRaw(result.unmapped, 'no page in this checkout matches; the url-to-xref extension converts cross-component URLs at build time and warns about broken ones')
|
|
163
|
+
leftRaw(result.withQueryString, 'an xref cannot carry a query string')
|
|
164
|
+
leftRaw(result.withoutLinkText, 'the target page could not be read to check the fragment')
|
|
165
|
+
leftRaw(result.missingAnchor || [], 'anchor not found: the page exists but has no section or anchor with that id')
|
|
166
|
+
return lines.join('\n')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
buildDocsResolverContext,
|
|
171
|
+
convertDocsLinks,
|
|
172
|
+
convertDocsLinksInFile,
|
|
173
|
+
formatDocsLinkReport,
|
|
174
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// The one comment line every doc-tools generator writes at the top of each
|
|
4
|
+
// file it produces. convert-to-markdown reads it and reports the provenance
|
|
5
|
+
// in the Markdown twins (page-generated-content, page-generated-by,
|
|
6
|
+
// page-generated-from, page-generated-sources), so a reader or an agent can
|
|
7
|
+
// tell generated text from hand-written text and knows where a fix goes.
|
|
8
|
+
//
|
|
9
|
+
// The first two sentences are the legacy banner that existing tests and the
|
|
10
|
+
// guard-generated-files check already look for, so they never change.
|
|
11
|
+
//
|
|
12
|
+
// The marker names no doc-tools version. Generated files are committed, so a
|
|
13
|
+
// version in the marker would rewrite the first line of every generated file
|
|
14
|
+
// on every release even when nothing else changed, and every auto-docs PR
|
|
15
|
+
// would carry hundreds of one-line diffs. DOC_TOOLS_VERSION is for provenance
|
|
16
|
+
// recorded at build time (pages generated during the site build), where no
|
|
17
|
+
// file is committed.
|
|
18
|
+
const pkg = require('../package.json')
|
|
19
|
+
|
|
20
|
+
const GENERATED_MARKER_LEGACY = 'This content is autogenerated. Do not edit manually.'
|
|
21
|
+
const DOC_TOOLS_VERSION = pkg.version
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the marker line, without a trailing newline.
|
|
25
|
+
* @param {string} generator - Short generator name, for example `property-docs`
|
|
26
|
+
* @param {string} source - Where the text comes from, as a noun phrase, for
|
|
27
|
+
* example `the configuration property definitions in redpanda-data/streaming-enterprise`
|
|
28
|
+
* @param {string} [hint] - How to change the text, as one or more sentences
|
|
29
|
+
* @returns {string} `// This content is autogenerated. ... Generated by doc-tools (<generator>) from <source>. <hint>`
|
|
30
|
+
*/
|
|
31
|
+
function generatedMarker (generator, source, hint) {
|
|
32
|
+
if (!generator || !source) throw new Error('generatedMarker needs a generator and a source')
|
|
33
|
+
const parts = [
|
|
34
|
+
GENERATED_MARKER_LEGACY,
|
|
35
|
+
`Generated by doc-tools (${generator}) from ${source.replace(/\.$/, '')}.`,
|
|
36
|
+
]
|
|
37
|
+
// Handlebars passes its options object as the last argument when a
|
|
38
|
+
// template omits the hint, so only a string counts as one.
|
|
39
|
+
if (typeof hint === 'string' && hint.trim()) parts.push(hint.trim())
|
|
40
|
+
return `// ${parts.join(' ')}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { generatedMarker, GENERATED_MARKER_LEGACY, DOC_TOOLS_VERSION }
|