@clidoc/docusaurus 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/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/index.cjs +5 -0
- package/package.json +58 -0
- package/src/index.ts +138 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Houston
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @clidoc/docusaurus
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@clidoc/docusaurus)
|
|
4
|
+
[](https://www.npmjs.com/package/@clidoc/docusaurus)
|
|
5
|
+
[](https://github.com/bhouston/clidoc/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/bhouston/clidoc)
|
|
7
|
+
[](https://clidoc.dev)
|
|
8
|
+
|
|
9
|
+
Generate Docusaurus documentation and sidebar entries from an OpenCLI document.
|
|
10
|
+
The output is ordinary Markdown with CommonMark front matter, so CLI descriptions
|
|
11
|
+
remain plain Markdown even when they contain JSX-like text.
|
|
12
|
+
|
|
13
|
+
Install the publisher alongside Docusaurus and its classic preset:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @clidoc/docusaurus @docusaurus/core @docusaurus/preset-classic react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Export an OpenCLI document from your CLI, for example with
|
|
20
|
+
`mycli docgen --format yaml --output cli.ocs.yaml`.
|
|
21
|
+
|
|
22
|
+
In `docusaurus.config.js`:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const clidocPlugin = require('@clidoc/docusaurus');
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
title: 'My CLI',
|
|
29
|
+
url: 'https://example.com',
|
|
30
|
+
baseUrl: '/',
|
|
31
|
+
markdown: { format: 'md' },
|
|
32
|
+
presets: [['classic', { docs: { routeBasePath: '/', sidebarPath: './sidebars.js' }, blog: false }]],
|
|
33
|
+
plugins: [
|
|
34
|
+
[
|
|
35
|
+
clidocPlugin,
|
|
36
|
+
{
|
|
37
|
+
input: 'cli.ocs.yaml',
|
|
38
|
+
outputDir: 'docs/generated-cli',
|
|
39
|
+
basePath: '/cli',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
In `sidebars.js`:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
module.exports = { docs: [{ type: 'autogenerated', dirName: '.' }] };
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep your handwritten pages in `docs/` (for example, `docs/index.md`). The
|
|
53
|
+
generated CLI pages appear beneath `docs/generated-cli/` and are included by
|
|
54
|
+
the autogenerated sidebar.
|
|
55
|
+
|
|
56
|
+
Run `npx docusaurus start` to preview the site.
|
|
57
|
+
|
|
58
|
+
Docusaurus 3 loads site configuration with CommonJS. The package includes a small
|
|
59
|
+
CommonJS entrypoint that imports the ESM generator when the plugin runs.
|
|
60
|
+
`docusaurus.config.js` itself is loaded through jiti, which does not support a
|
|
61
|
+
top-level `await`; load the OpenCLI document from a file path via `input`
|
|
62
|
+
instead of awaiting a parse at the top of the config.
|
|
63
|
+
|
|
64
|
+
Set `input` to a parsed OpenCLI document or a JSON/YAML path relative to the
|
|
65
|
+
site directory. The plugin writes files during initialization, before the docs plugin
|
|
66
|
+
scans content, so the docs plugin's autogenerated sidebar discovers the pages.
|
|
67
|
+
`title` is used as each page's sidebar label. The
|
|
68
|
+
plugin path relies on that autogenerated sidebar and does not return one of
|
|
69
|
+
its own; for a hand-built sidebar array, call `writeDocusaurus()` directly
|
|
70
|
+
from `sidebars.js` instead of using the plugin.
|
|
71
|
+
|
|
72
|
+
`writeDocusaurus(document, {outputDir, basePath})` offers direct generation
|
|
73
|
+
for build scripts. Generated files have readable names based on the executable and
|
|
74
|
+
command, such as `mycli.md` for the landing page and `mycli-validate.md` for a
|
|
75
|
+
command page. Names that contain unsafe characters, exceed the length limit, or
|
|
76
|
+
would otherwise collide retain a readable prefix and add a deterministic digest.
|
|
77
|
+
Page IDs and routes are defined separately in front matter and do not change with
|
|
78
|
+
the filenames.
|
|
79
|
+
|
|
80
|
+
Regeneration removes files listed in `.clidoc-generated.json`. Generation stops
|
|
81
|
+
instead of overwriting a matching filename that is not owned by that manifest.
|
|
82
|
+
On the first run after upgrading from OpenCLI, it also removes old generated
|
|
83
|
+
pages listed in `.opencli-generated.json` and deletes that legacy manifest.
|
|
84
|
+
Other documents in the directory are preserved. Landing-page links to command
|
|
85
|
+
pages point at the generated filenames directly, so routes resolve correctly
|
|
86
|
+
regardless of the docs plugin's `routeBasePath`.
|
|
87
|
+
|
|
88
|
+
Add `docs/generated-cli/` to `.gitignore` if generated pages should stay out of
|
|
89
|
+
version control.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT. See [LICENSE](../../LICENSE).
|
|
94
|
+
|
|
95
|
+
## Author
|
|
96
|
+
|
|
97
|
+
[Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type OpenCliDocument } from '@clidoc/core';
|
|
2
|
+
export interface DocusaurusOptions {
|
|
3
|
+
/** An OpenCLI document or a JSON/YAML filename relative to siteDir. */
|
|
4
|
+
input: OpenCliDocument | string;
|
|
5
|
+
/** Dedicated generated directory consumed by the Docusaurus docs plugin. */
|
|
6
|
+
outputDir: string;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Write CommonMark docs and remove only files recorded as generated by this package. */
|
|
10
|
+
export declare function writeDocusaurus(document: OpenCliDocument, options: {
|
|
11
|
+
outputDir: string;
|
|
12
|
+
basePath?: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
type: 'doc';
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}[]>;
|
|
18
|
+
/** Generate before content loading, so plugin-content-docs can discover the files. */
|
|
19
|
+
export default function clidocPlugin(context: {
|
|
20
|
+
siteDir: string;
|
|
21
|
+
}, options: DocusaurusOptions): Promise<{
|
|
22
|
+
name: string;
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;IAChC,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmED,yFAAyF;AACzF,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE;UA4C5E,KAAK;;;KAC1C;AAED,sFAAsF;AACtF,wBAA8B,YAAY,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,iBAAiB;;GAUlG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { generatePages, parse } from '@clidoc/core';
|
|
5
|
+
const manifestName = '.clidoc-generated.json';
|
|
6
|
+
const legacyManifestName = '.opencli-generated.json';
|
|
7
|
+
const legacyFilename = /^(?:clidoc|opencli)-[a-f0-9]{20}\.md$/;
|
|
8
|
+
function filenamePart(value) {
|
|
9
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) && value.length <= 48)
|
|
10
|
+
return value;
|
|
11
|
+
const readable = value
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.normalize('NFKD')
|
|
14
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
15
|
+
.replace(/^-+|-+$/g, '')
|
|
16
|
+
.slice(0, 32) || 'page';
|
|
17
|
+
return `${readable}-${createHash('sha256').update(value).digest('hex').slice(0, 16)}`;
|
|
18
|
+
}
|
|
19
|
+
function filename(binary, id, title) {
|
|
20
|
+
const prefix = filenamePart(binary);
|
|
21
|
+
if (id === 'index')
|
|
22
|
+
return `${prefix}.md`;
|
|
23
|
+
const command = title.startsWith(`${binary} `) ? title.slice(binary.length + 1) : title;
|
|
24
|
+
const words = command.trim().split(/\s+/);
|
|
25
|
+
const syntax = words.findIndex((word) => /^(?:<|\[|\{|--)/.test(word));
|
|
26
|
+
const staticWords = syntax === -1 ? words : words.slice(0, syntax);
|
|
27
|
+
const staticCommand = staticWords.length ? staticWords.join('-') : command;
|
|
28
|
+
return `${prefix}-${filenamePart(staticCommand)}.md`;
|
|
29
|
+
}
|
|
30
|
+
function filenames(binary, pages) {
|
|
31
|
+
const candidates = pages.map((page) => filename(binary, page.id, page.title));
|
|
32
|
+
const counts = new Map();
|
|
33
|
+
for (const candidate of candidates)
|
|
34
|
+
counts.set(candidate, (counts.get(candidate) ?? 0) + 1);
|
|
35
|
+
return candidates.map((candidate, position) => {
|
|
36
|
+
if (counts.get(candidate) === 1)
|
|
37
|
+
return candidate;
|
|
38
|
+
const stem = candidate.slice(0, -'.md'.length);
|
|
39
|
+
const digest = createHash('sha256').update(pages[position].id).digest('hex').slice(0, 16);
|
|
40
|
+
return `${stem}--${digest}.md`;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function previousFiles(outputDir, name) {
|
|
44
|
+
try {
|
|
45
|
+
const value = JSON.parse(await readFile(join(outputDir, name), 'utf8'));
|
|
46
|
+
// Array manifests were written by older releases. Only accept their opaque generated
|
|
47
|
+
// names so a malformed old manifest cannot claim an unrelated Markdown document.
|
|
48
|
+
if (Array.isArray(value))
|
|
49
|
+
return value.filter((entry) => typeof entry === 'string' && legacyFilename.test(entry));
|
|
50
|
+
if (typeof value !== 'object' ||
|
|
51
|
+
value === null ||
|
|
52
|
+
!('version' in value) ||
|
|
53
|
+
value.version !== 1 ||
|
|
54
|
+
!('files' in value) ||
|
|
55
|
+
!Array.isArray(value.files) ||
|
|
56
|
+
!value.files.every((entry) => typeof entry === 'string' && /^[a-z0-9][a-z0-9-]*\.md$/.test(entry) && entry.length <= 120))
|
|
57
|
+
throw new Error('Invalid generated file manifest');
|
|
58
|
+
return value.files;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT')
|
|
62
|
+
return [];
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Write CommonMark docs and remove only files recorded as generated by this package. */
|
|
67
|
+
export async function writeDocusaurus(document, options) {
|
|
68
|
+
const pages = generatePages(document, { basePath: options.basePath });
|
|
69
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
70
|
+
const previous = await previousFiles(options.outputDir, manifestName);
|
|
71
|
+
const legacy = await previousFiles(options.outputDir, legacyManifestName);
|
|
72
|
+
const existing = await readdir(options.outputDir);
|
|
73
|
+
const current = filenames(document.info.binary, pages);
|
|
74
|
+
if (new Set(current).size !== current.length)
|
|
75
|
+
throw new Error('Generated Docusaurus filenames collide');
|
|
76
|
+
const owned = new Set([...previous, ...legacy]);
|
|
77
|
+
const unownedCollision = current.find((name) => !owned.has(name) && existing.includes(name));
|
|
78
|
+
if (unownedCollision)
|
|
79
|
+
throw new Error(`Refusing to overwrite unowned file: ${unownedCollision}`);
|
|
80
|
+
// Map each page's route to the generated filename holding it, so the landing page can link by
|
|
81
|
+
// file instead of by route: Docusaurus then resolves the URL itself regardless of routeBasePath.
|
|
82
|
+
const filenameByPath = new Map(pages.map((page, position) => [page.path, current[position]]));
|
|
83
|
+
for (const [position, page] of pages.entries()) {
|
|
84
|
+
const frontmatter = [
|
|
85
|
+
'---',
|
|
86
|
+
'mdx:',
|
|
87
|
+
' format: md',
|
|
88
|
+
`id: ${JSON.stringify(page.id)}`,
|
|
89
|
+
`title: ${JSON.stringify(page.title)}`,
|
|
90
|
+
`slug: ${JSON.stringify(page.path)}`,
|
|
91
|
+
`sidebar_position: ${position + 1}`,
|
|
92
|
+
'---',
|
|
93
|
+
'',
|
|
94
|
+
].join('\n');
|
|
95
|
+
const content = page.id === 'index'
|
|
96
|
+
? pages
|
|
97
|
+
.slice(1)
|
|
98
|
+
.reduce((text, target) => text.replaceAll(`](${target.path})`, `](./${filenameByPath.get(target.path)})`), page.content)
|
|
99
|
+
: page.content;
|
|
100
|
+
await writeFile(join(options.outputDir, current[position]), frontmatter + content);
|
|
101
|
+
}
|
|
102
|
+
for (const name of [...previous, ...legacy])
|
|
103
|
+
if (!current.includes(name))
|
|
104
|
+
await rm(join(options.outputDir, name), { force: true });
|
|
105
|
+
await writeFile(join(options.outputDir, manifestName), JSON.stringify({ version: 1, files: current }, null, 2) + '\n');
|
|
106
|
+
await rm(join(options.outputDir, legacyManifestName), { force: true });
|
|
107
|
+
return pages.map((page) => ({ type: 'doc', id: page.id, label: page.title }));
|
|
108
|
+
}
|
|
109
|
+
/** Generate before content loading, so plugin-content-docs can discover the files. */
|
|
110
|
+
export default async function clidocPlugin(context, options) {
|
|
111
|
+
const document = typeof options.input === 'string'
|
|
112
|
+
? parse(await readFile(resolve(context.siteDir, options.input), 'utf8'))
|
|
113
|
+
: options.input;
|
|
114
|
+
await writeDocusaurus(document, {
|
|
115
|
+
outputDir: resolve(context.siteDir, options.outputDir),
|
|
116
|
+
basePath: options.basePath,
|
|
117
|
+
});
|
|
118
|
+
return { name: 'clidoc-docusaurus' };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/index.cjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clidoc/docusaurus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Publish OpenCLI Markdown in Docusaurus",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"docs",
|
|
8
|
+
"documentation",
|
|
9
|
+
"docusaurus",
|
|
10
|
+
"markdown",
|
|
11
|
+
"opencli"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://clidoc.dev",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/bhouston/clidoc/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Ben Houston <ben@ben3d.ca> (https://ben3d.ca)",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/bhouston/clidoc.git",
|
|
22
|
+
"directory": "packages/docusaurus"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.cjs",
|
|
26
|
+
"dist/**/*.js",
|
|
27
|
+
"dist/**/*.js.map",
|
|
28
|
+
"dist/**/*.d.ts",
|
|
29
|
+
"dist/**/*.d.ts.map",
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"!src/**/*.test.ts",
|
|
32
|
+
"CHANGELOG.md"
|
|
33
|
+
],
|
|
34
|
+
"type": "module",
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./index.cjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"tsc": "tsc --noEmit"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@clidoc/core": "^0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.12.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { generatePages, parse, type OpenCliDocument } from '@clidoc/core';
|
|
5
|
+
|
|
6
|
+
export interface DocusaurusOptions {
|
|
7
|
+
/** An OpenCLI document or a JSON/YAML filename relative to siteDir. */
|
|
8
|
+
input: OpenCliDocument | string;
|
|
9
|
+
/** Dedicated generated directory consumed by the Docusaurus docs plugin. */
|
|
10
|
+
outputDir: string;
|
|
11
|
+
basePath?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const manifestName = '.clidoc-generated.json';
|
|
15
|
+
const legacyManifestName = '.opencli-generated.json';
|
|
16
|
+
const legacyFilename = /^(?:clidoc|opencli)-[a-f0-9]{20}\.md$/;
|
|
17
|
+
|
|
18
|
+
function filenamePart(value: string): string {
|
|
19
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) && value.length <= 48) return value;
|
|
20
|
+
const readable =
|
|
21
|
+
value
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.normalize('NFKD')
|
|
24
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
25
|
+
.replace(/^-+|-+$/g, '')
|
|
26
|
+
.slice(0, 32) || 'page';
|
|
27
|
+
return `${readable}-${createHash('sha256').update(value).digest('hex').slice(0, 16)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function filename(binary: string, id: string, title: string): string {
|
|
31
|
+
const prefix = filenamePart(binary);
|
|
32
|
+
if (id === 'index') return `${prefix}.md`;
|
|
33
|
+
const command = title.startsWith(`${binary} `) ? title.slice(binary.length + 1) : title;
|
|
34
|
+
const words = command.trim().split(/\s+/);
|
|
35
|
+
const syntax = words.findIndex((word) => /^(?:<|\[|\{|--)/.test(word));
|
|
36
|
+
const staticWords = syntax === -1 ? words : words.slice(0, syntax);
|
|
37
|
+
const staticCommand = staticWords.length ? staticWords.join('-') : command;
|
|
38
|
+
return `${prefix}-${filenamePart(staticCommand)}.md`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function filenames(binary: string, pages: ReturnType<typeof generatePages>): string[] {
|
|
42
|
+
const candidates = pages.map((page) => filename(binary, page.id, page.title));
|
|
43
|
+
const counts = new Map<string, number>();
|
|
44
|
+
for (const candidate of candidates) counts.set(candidate, (counts.get(candidate) ?? 0) + 1);
|
|
45
|
+
return candidates.map((candidate, position) => {
|
|
46
|
+
if (counts.get(candidate) === 1) return candidate;
|
|
47
|
+
const stem = candidate.slice(0, -'.md'.length);
|
|
48
|
+
const digest = createHash('sha256').update(pages[position]!.id).digest('hex').slice(0, 16);
|
|
49
|
+
return `${stem}--${digest}.md`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function previousFiles(outputDir: string, name: string): Promise<string[]> {
|
|
54
|
+
try {
|
|
55
|
+
const value: unknown = JSON.parse(await readFile(join(outputDir, name), 'utf8'));
|
|
56
|
+
// Array manifests were written by older releases. Only accept their opaque generated
|
|
57
|
+
// names so a malformed old manifest cannot claim an unrelated Markdown document.
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return value.filter((entry): entry is string => typeof entry === 'string' && legacyFilename.test(entry));
|
|
60
|
+
if (
|
|
61
|
+
typeof value !== 'object' ||
|
|
62
|
+
value === null ||
|
|
63
|
+
!('version' in value) ||
|
|
64
|
+
value.version !== 1 ||
|
|
65
|
+
!('files' in value) ||
|
|
66
|
+
!Array.isArray(value.files) ||
|
|
67
|
+
!value.files.every(
|
|
68
|
+
(entry) => typeof entry === 'string' && /^[a-z0-9][a-z0-9-]*\.md$/.test(entry) && entry.length <= 120,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
throw new Error('Invalid generated file manifest');
|
|
72
|
+
return value.files;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Write CommonMark docs and remove only files recorded as generated by this package. */
|
|
80
|
+
export async function writeDocusaurus(document: OpenCliDocument, options: { outputDir: string; basePath?: string }) {
|
|
81
|
+
const pages = generatePages(document, { basePath: options.basePath });
|
|
82
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
83
|
+
const previous = await previousFiles(options.outputDir, manifestName);
|
|
84
|
+
const legacy = await previousFiles(options.outputDir, legacyManifestName);
|
|
85
|
+
const existing = await readdir(options.outputDir);
|
|
86
|
+
const current = filenames(document.info.binary, pages);
|
|
87
|
+
if (new Set(current).size !== current.length) throw new Error('Generated Docusaurus filenames collide');
|
|
88
|
+
const owned = new Set([...previous, ...legacy]);
|
|
89
|
+
const unownedCollision = current.find((name) => !owned.has(name) && existing.includes(name));
|
|
90
|
+
if (unownedCollision) throw new Error(`Refusing to overwrite unowned file: ${unownedCollision}`);
|
|
91
|
+
// Map each page's route to the generated filename holding it, so the landing page can link by
|
|
92
|
+
// file instead of by route: Docusaurus then resolves the URL itself regardless of routeBasePath.
|
|
93
|
+
const filenameByPath = new Map(pages.map((page, position) => [page.path, current[position]!]));
|
|
94
|
+
for (const [position, page] of pages.entries()) {
|
|
95
|
+
const frontmatter = [
|
|
96
|
+
'---',
|
|
97
|
+
'mdx:',
|
|
98
|
+
' format: md',
|
|
99
|
+
`id: ${JSON.stringify(page.id)}`,
|
|
100
|
+
`title: ${JSON.stringify(page.title)}`,
|
|
101
|
+
`slug: ${JSON.stringify(page.path)}`,
|
|
102
|
+
`sidebar_position: ${position + 1}`,
|
|
103
|
+
'---',
|
|
104
|
+
'',
|
|
105
|
+
].join('\n');
|
|
106
|
+
const content =
|
|
107
|
+
page.id === 'index'
|
|
108
|
+
? pages
|
|
109
|
+
.slice(1)
|
|
110
|
+
.reduce(
|
|
111
|
+
(text, target) => text.replaceAll(`](${target.path})`, `](./${filenameByPath.get(target.path)})`),
|
|
112
|
+
page.content,
|
|
113
|
+
)
|
|
114
|
+
: page.content;
|
|
115
|
+
await writeFile(join(options.outputDir, current[position]!), frontmatter + content);
|
|
116
|
+
}
|
|
117
|
+
for (const name of [...previous, ...legacy])
|
|
118
|
+
if (!current.includes(name)) await rm(join(options.outputDir, name), { force: true });
|
|
119
|
+
await writeFile(
|
|
120
|
+
join(options.outputDir, manifestName),
|
|
121
|
+
JSON.stringify({ version: 1, files: current }, null, 2) + '\n',
|
|
122
|
+
);
|
|
123
|
+
await rm(join(options.outputDir, legacyManifestName), { force: true });
|
|
124
|
+
return pages.map((page) => ({ type: 'doc' as const, id: page.id, label: page.title }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Generate before content loading, so plugin-content-docs can discover the files. */
|
|
128
|
+
export default async function clidocPlugin(context: { siteDir: string }, options: DocusaurusOptions) {
|
|
129
|
+
const document =
|
|
130
|
+
typeof options.input === 'string'
|
|
131
|
+
? parse(await readFile(resolve(context.siteDir, options.input), 'utf8'))
|
|
132
|
+
: options.input;
|
|
133
|
+
await writeDocusaurus(document, {
|
|
134
|
+
outputDir: resolve(context.siteDir, options.outputDir),
|
|
135
|
+
basePath: options.basePath,
|
|
136
|
+
});
|
|
137
|
+
return { name: 'clidoc-docusaurus' };
|
|
138
|
+
}
|