@facetlayer/docs-tool 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/README.md +80 -0
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/browseLocalLibrary.d.ts +12 -0
- package/dist/browseLocalLibrary.d.ts.map +1 -0
- package/dist/browseNpmLibrary.d.ts +39 -0
- package/dist/browseNpmLibrary.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +549 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +471 -0
- package/docs/project-setup.md +188 -0
- package/docs/writing-doc-files.md +156 -0
- package/package.json +44 -0
- package/src/__tests__/cli.test.ts +80 -0
- package/src/__tests__/index.test.ts +333 -0
- package/src/browseLocalLibrary.ts +35 -0
- package/src/browseNpmLibrary.ts +367 -0
- package/src/cli.ts +105 -0
- package/src/index.ts +288 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-doc-files
|
|
3
|
+
description: How to write and add documentation files for CLI tools
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing Doc Files
|
|
7
|
+
|
|
8
|
+
This guide explains how to write documentation files that work with `@facetlayer/docs-tool`.
|
|
9
|
+
|
|
10
|
+
## Doc File Format
|
|
11
|
+
|
|
12
|
+
Doc files are Markdown files with YAML frontmatter. The frontmatter provides metadata that's displayed when listing docs.
|
|
13
|
+
|
|
14
|
+
### Basic Structure
|
|
15
|
+
|
|
16
|
+
```markdown
|
|
17
|
+
---
|
|
18
|
+
name: my-doc-name
|
|
19
|
+
description: A brief description of what this doc covers
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Doc Title
|
|
23
|
+
|
|
24
|
+
Your markdown content here.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Frontmatter Fields
|
|
28
|
+
|
|
29
|
+
| Field | Required | Description |
|
|
30
|
+
|-------|----------|-------------|
|
|
31
|
+
| `name` | Recommended | Short identifier for the doc (used in `get-doc` command) |
|
|
32
|
+
| `description` | Recommended | One-line description shown in `list-docs` output |
|
|
33
|
+
|
|
34
|
+
If `name` is not provided, the filename (without `.md`) is used as the name.
|
|
35
|
+
|
|
36
|
+
### Example Doc File
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
---
|
|
40
|
+
name: configuration
|
|
41
|
+
description: How to configure the application settings
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Configuration Guide
|
|
45
|
+
|
|
46
|
+
## Environment Variables
|
|
47
|
+
|
|
48
|
+
The app reads the following environment variables:
|
|
49
|
+
|
|
50
|
+
- `API_KEY` - Your API key for authentication
|
|
51
|
+
- `DEBUG` - Set to "true" to enable debug logging
|
|
52
|
+
|
|
53
|
+
## Config File
|
|
54
|
+
|
|
55
|
+
Create a `config.json` in your project root:
|
|
56
|
+
|
|
57
|
+
\`\`\`json
|
|
58
|
+
{
|
|
59
|
+
"timeout": 30,
|
|
60
|
+
"retries": 3
|
|
61
|
+
}
|
|
62
|
+
\`\`\`
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Adding Docs to Your CLI
|
|
66
|
+
|
|
67
|
+
To ensure your doc files are available through the CLI, you need to:
|
|
68
|
+
|
|
69
|
+
1. **Create a docs directory** - Typically `./docs` in your project root
|
|
70
|
+
|
|
71
|
+
2. **Configure DocFilesHelper** - In your CLI script, create an instance pointing to your docs:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { DocFilesHelper } from '@facetlayer/docs-tool';
|
|
75
|
+
|
|
76
|
+
const docFiles = new DocFilesHelper({
|
|
77
|
+
dirs: [join(__packageRoot, 'docs')],
|
|
78
|
+
files: [join(__packageRoot, 'README.md')], // Optional
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Verify the configuration** - Check that your docs are included by running:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# List all available docs
|
|
86
|
+
your-cli list-docs
|
|
87
|
+
|
|
88
|
+
# Verify a specific doc works
|
|
89
|
+
your-cli get-doc your-doc-name
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Troubleshooting
|
|
93
|
+
|
|
94
|
+
### Doc not showing in list-docs
|
|
95
|
+
|
|
96
|
+
1. **Check the file location** - Ensure the file is in a directory listed in `dirs` or explicitly in `files`
|
|
97
|
+
|
|
98
|
+
2. **Check the file extension** - Only `.md` files are included from directories
|
|
99
|
+
|
|
100
|
+
3. **Verify DocFilesHelper setup** - Look for where `DocFilesHelper` is instantiated in your CLI code:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Look for something like this in your cli.ts or main script:
|
|
104
|
+
const docFiles = new DocFilesHelper({
|
|
105
|
+
dirs: [...], // Your docs directory should be here
|
|
106
|
+
files: [...], // Or specific files listed here
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
4. **Check path resolution** - Make sure paths are absolute. Use the `__packageRoot` pattern:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { dirname, join } from 'path';
|
|
114
|
+
import { fileURLToPath } from 'url';
|
|
115
|
+
|
|
116
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
117
|
+
const __dirname = dirname(__filename);
|
|
118
|
+
const __packageRoot = join(__dirname, '..');
|
|
119
|
+
|
|
120
|
+
const docFiles = new DocFilesHelper({
|
|
121
|
+
dirs: [join(__packageRoot, 'docs')], // Absolute path
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Relative paths like `'./docs'` will resolve from the current working directory, not your package root, which breaks when the CLI is run from different directories
|
|
126
|
+
|
|
127
|
+
### Doc content not displaying correctly
|
|
128
|
+
|
|
129
|
+
1. **Check frontmatter syntax** - Ensure the `---` delimiters are on their own lines
|
|
130
|
+
2. **Validate YAML** - Make sure frontmatter uses valid YAML (proper spacing, no tabs)
|
|
131
|
+
|
|
132
|
+
## Publishing Docs with Your Package
|
|
133
|
+
|
|
134
|
+
If you publish your CLI to npm, make sure to include the docs directory in your `package.json`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"files": [
|
|
139
|
+
"src",
|
|
140
|
+
"dist",
|
|
141
|
+
"docs",
|
|
142
|
+
"README.md"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Without this, the docs folder won't be included when users install your package, and `list-docs` will show nothing.
|
|
148
|
+
|
|
149
|
+
## Best Practices
|
|
150
|
+
|
|
151
|
+
1. **Use descriptive names** - Choose names that are easy to type and remember
|
|
152
|
+
2. **Write clear descriptions** - The description appears in `list-docs` so make it helpful
|
|
153
|
+
3. **Keep docs focused** - One topic per doc file
|
|
154
|
+
4. **Include examples** - Show concrete usage examples in your docs
|
|
155
|
+
5. **Test your docs** - Run `list-docs` and `get-doc` after adding new files
|
|
156
|
+
6. **Include README.md** - Add your README to the `files` array so it shows in `list-docs`
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@facetlayer/docs-tool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Library and CLI tool for browsing docs in NPM packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"docs": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node build.mts build",
|
|
13
|
+
"prepublishOnly": "node build.mts validate && node build.mts build",
|
|
14
|
+
"typecheck": "tsc -p .",
|
|
15
|
+
"test": "vitest run --testTimeout=30000"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"doc",
|
|
19
|
+
"frontmatter",
|
|
20
|
+
"markdown",
|
|
21
|
+
"documentation"
|
|
22
|
+
],
|
|
23
|
+
"author": "andyfischer",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"packageManager": "pnpm@10.15.1",
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"dist",
|
|
29
|
+
"docs",
|
|
30
|
+
"tsconfig.json",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@facetlayer/subprocess-wrapper": "^1.1.0",
|
|
35
|
+
"yargs": "18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@facetlayer/build-config-nodejs": "0.3.1",
|
|
39
|
+
"@types/node": "^22.10.2",
|
|
40
|
+
"@types/yargs": "17.0.34",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { runShellCommand } from '@facetlayer/subprocess-wrapper';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const projectRoot = join(import.meta.dirname, '../..');
|
|
6
|
+
const cliPath = join(projectRoot, 'src/cli.ts');
|
|
7
|
+
const sampleDir = join(projectRoot, 'sample');
|
|
8
|
+
|
|
9
|
+
async function runCli(...args: string[]) {
|
|
10
|
+
return runShellCommand('npx', ['tsx', cliPath, ...args], {
|
|
11
|
+
cwd: projectRoot,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('CLI', () => {
|
|
16
|
+
describe('list command', () => {
|
|
17
|
+
it('should list all doc files in the sample directory', async () => {
|
|
18
|
+
const result = await runCli('list', sampleDir);
|
|
19
|
+
|
|
20
|
+
expect(result.exitCode).toBe(0);
|
|
21
|
+
|
|
22
|
+
const output = result.stdoutAsString();
|
|
23
|
+
expect(output).toContain('Available doc files:');
|
|
24
|
+
expect(output).toContain('getting-started.md');
|
|
25
|
+
expect(output).toContain('api-reference.md');
|
|
26
|
+
expect(output).toContain('configuration.md');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should show descriptions from frontmatter', async () => {
|
|
30
|
+
const result = await runCli('list', sampleDir);
|
|
31
|
+
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
|
|
34
|
+
const output = result.stdoutAsString();
|
|
35
|
+
expect(output).toContain('Quick start guide for new users');
|
|
36
|
+
expect(output).toContain('Complete API documentation');
|
|
37
|
+
expect(output).toContain('Configuration options and settings');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should fail with non-existent directory', async () => {
|
|
41
|
+
const result = await runCli('list', './nonexistent-dir');
|
|
42
|
+
|
|
43
|
+
expect(result.exitCode).not.toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('help and version', () => {
|
|
48
|
+
it('should show help with --help flag', async () => {
|
|
49
|
+
const result = await runCli('--help');
|
|
50
|
+
|
|
51
|
+
expect(result.exitCode).toBe(0);
|
|
52
|
+
|
|
53
|
+
const output = result.stdoutAsString();
|
|
54
|
+
expect(output).toContain('list');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should show version with --version flag', async () => {
|
|
58
|
+
const result = await runCli('--version');
|
|
59
|
+
|
|
60
|
+
expect(result.exitCode).toBe(0);
|
|
61
|
+
|
|
62
|
+
const output = result.stdoutAsString();
|
|
63
|
+
expect(output).toMatch(/\d+\.\d+\.\d+/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('error handling', () => {
|
|
68
|
+
it('should require a command', async () => {
|
|
69
|
+
const result = await runCli();
|
|
70
|
+
|
|
71
|
+
expect(result.exitCode).not.toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should reject unknown commands', async () => {
|
|
75
|
+
const result = await runCli('unknown-command');
|
|
76
|
+
|
|
77
|
+
expect(result.exitCode).not.toBe(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { parseFrontmatter, DocFilesHelper } from '../index.ts';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
describe('parseFrontmatter', () => {
|
|
7
|
+
it('should parse frontmatter with name and description', () => {
|
|
8
|
+
const input = `---
|
|
9
|
+
name: test-doc
|
|
10
|
+
description: A test document
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Test Content
|
|
14
|
+
|
|
15
|
+
This is the body.`;
|
|
16
|
+
|
|
17
|
+
const result = parseFrontmatter(input);
|
|
18
|
+
|
|
19
|
+
expect(result.frontmatter.name).toBe('test-doc');
|
|
20
|
+
expect(result.frontmatter.description).toBe('A test document');
|
|
21
|
+
expect(result.content).toBe('# Test Content\n\nThis is the body.');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should parse frontmatter with custom fields', () => {
|
|
25
|
+
const input = `---
|
|
26
|
+
name: my-doc
|
|
27
|
+
author: John Doe
|
|
28
|
+
version: 1.0
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Content here`;
|
|
32
|
+
|
|
33
|
+
const result = parseFrontmatter(input);
|
|
34
|
+
|
|
35
|
+
expect(result.frontmatter.name).toBe('my-doc');
|
|
36
|
+
expect(result.frontmatter.author).toBe('John Doe');
|
|
37
|
+
expect(result.frontmatter.version).toBe('1.0');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return empty frontmatter when none exists', () => {
|
|
41
|
+
const input = '# No Frontmatter\n\nJust content.';
|
|
42
|
+
|
|
43
|
+
const result = parseFrontmatter(input);
|
|
44
|
+
|
|
45
|
+
expect(result.frontmatter).toEqual({});
|
|
46
|
+
expect(result.content).toBe(input);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should handle frontmatter with colons in values', () => {
|
|
50
|
+
const input = `---
|
|
51
|
+
name: doc-name
|
|
52
|
+
description: This has: a colon in it
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
Content`;
|
|
56
|
+
|
|
57
|
+
const result = parseFrontmatter(input);
|
|
58
|
+
|
|
59
|
+
expect(result.frontmatter.description).toBe('This has: a colon in it');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle Windows-style line endings', () => {
|
|
63
|
+
const input = '---\r\nname: test\r\ndescription: desc\r\n---\r\n\r\nContent';
|
|
64
|
+
|
|
65
|
+
const result = parseFrontmatter(input);
|
|
66
|
+
|
|
67
|
+
expect(result.frontmatter.name).toBe('test');
|
|
68
|
+
expect(result.frontmatter.description).toBe('desc');
|
|
69
|
+
expect(result.content).toBe('Content');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle empty content after frontmatter', () => {
|
|
73
|
+
const input = `---
|
|
74
|
+
name: empty
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const result = parseFrontmatter(input);
|
|
80
|
+
|
|
81
|
+
expect(result.frontmatter.name).toBe('empty');
|
|
82
|
+
expect(result.content).toBe('');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('DocFilesHelper', () => {
|
|
87
|
+
const testDir = join(import.meta.dirname, '../../test/temp/docs');
|
|
88
|
+
const extraDir = join(import.meta.dirname, '../../test/temp/extra');
|
|
89
|
+
const standaloneFile = join(import.meta.dirname, '../../test/temp/standalone.md');
|
|
90
|
+
|
|
91
|
+
beforeAll(() => {
|
|
92
|
+
mkdirSync(testDir, { recursive: true });
|
|
93
|
+
mkdirSync(extraDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(testDir, 'first-doc.md'),
|
|
97
|
+
`---
|
|
98
|
+
name: first-doc
|
|
99
|
+
description: The first test doc
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# First Doc
|
|
103
|
+
|
|
104
|
+
Content of first doc.`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
writeFileSync(
|
|
108
|
+
join(testDir, 'second-doc.md'),
|
|
109
|
+
`---
|
|
110
|
+
name: second-doc
|
|
111
|
+
description: The second test doc
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
# Second Doc
|
|
115
|
+
|
|
116
|
+
Content of second doc.`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(testDir, 'no-frontmatter.md'),
|
|
121
|
+
`# No Frontmatter
|
|
122
|
+
|
|
123
|
+
Just content without frontmatter.`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Non-md file should be ignored
|
|
127
|
+
writeFileSync(join(testDir, 'ignored.txt'), 'This should be ignored');
|
|
128
|
+
|
|
129
|
+
// File in extra directory
|
|
130
|
+
writeFileSync(
|
|
131
|
+
join(extraDir, 'extra-doc.md'),
|
|
132
|
+
`---
|
|
133
|
+
name: extra-doc
|
|
134
|
+
description: An extra doc file
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# Extra Doc
|
|
138
|
+
|
|
139
|
+
Content of extra doc.`
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Standalone file outside of directories
|
|
143
|
+
writeFileSync(
|
|
144
|
+
standaloneFile,
|
|
145
|
+
`---
|
|
146
|
+
name: standalone
|
|
147
|
+
description: A standalone file
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
# Standalone
|
|
151
|
+
|
|
152
|
+
Standalone content.`
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterAll(() => {
|
|
157
|
+
rmSync(join(import.meta.dirname, '../../test/temp'), { recursive: true, force: true });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('with dirs option', () => {
|
|
161
|
+
let helper: DocFilesHelper;
|
|
162
|
+
|
|
163
|
+
beforeAll(() => {
|
|
164
|
+
helper = new DocFilesHelper({ dirs: [testDir] });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('listDocs', () => {
|
|
168
|
+
it('should list all doc files with their metadata', () => {
|
|
169
|
+
const docs = helper.listDocs();
|
|
170
|
+
|
|
171
|
+
expect(docs).toHaveLength(3);
|
|
172
|
+
|
|
173
|
+
const firstDoc = docs.find((s) => s.name === 'first-doc');
|
|
174
|
+
expect(firstDoc).toBeDefined();
|
|
175
|
+
expect(firstDoc?.description).toBe('The first test doc');
|
|
176
|
+
expect(firstDoc?.filename).toBe('first-doc.md');
|
|
177
|
+
|
|
178
|
+
const secondDoc = docs.find((s) => s.name === 'second-doc');
|
|
179
|
+
expect(secondDoc).toBeDefined();
|
|
180
|
+
expect(secondDoc?.description).toBe('The second test doc');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should use filename as name when frontmatter has no name', () => {
|
|
184
|
+
const docs = helper.listDocs();
|
|
185
|
+
|
|
186
|
+
const noFrontmatter = docs.find((s) => s.filename === 'no-frontmatter.md');
|
|
187
|
+
expect(noFrontmatter?.name).toBe('no-frontmatter');
|
|
188
|
+
expect(noFrontmatter?.description).toBe('');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should ignore non-md files', () => {
|
|
192
|
+
const docs = helper.listDocs();
|
|
193
|
+
|
|
194
|
+
const txtFile = docs.find((s) => s.filename === 'ignored.txt');
|
|
195
|
+
expect(txtFile).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('getDoc', () => {
|
|
200
|
+
it('should return doc content by name', () => {
|
|
201
|
+
const doc = helper.getDoc('first-doc');
|
|
202
|
+
|
|
203
|
+
expect(doc.name).toBe('first-doc');
|
|
204
|
+
expect(doc.description).toBe('The first test doc');
|
|
205
|
+
expect(doc.content).toBe('# First Doc\n\nContent of first doc.');
|
|
206
|
+
expect(doc.rawContent).toContain('---');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should throw error for non-existent doc', () => {
|
|
210
|
+
expect(() => helper.getDoc('nonexistent')).toThrow('Doc file not found: nonexistent');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle doc without frontmatter', () => {
|
|
214
|
+
const doc = helper.getDoc('no-frontmatter');
|
|
215
|
+
|
|
216
|
+
expect(doc.name).toBe('no-frontmatter');
|
|
217
|
+
expect(doc.description).toBe('');
|
|
218
|
+
expect(doc.content).toContain('# No Frontmatter');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle name with .md extension', () => {
|
|
222
|
+
const doc = helper.getDoc('first-doc.md');
|
|
223
|
+
|
|
224
|
+
expect(doc.name).toBe('first-doc');
|
|
225
|
+
expect(doc.filename).toBe('first-doc.md');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should find doc by partial match', () => {
|
|
229
|
+
const doc = helper.getDoc('first');
|
|
230
|
+
|
|
231
|
+
expect(doc.name).toBe('first-doc');
|
|
232
|
+
expect(doc.filename).toBe('first-doc.md');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should throw error when multiple docs match', () => {
|
|
236
|
+
expect(() => helper.getDoc('doc')).toThrow(/Multiple docs match/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('with multiple dirs', () => {
|
|
242
|
+
let helper: DocFilesHelper;
|
|
243
|
+
|
|
244
|
+
beforeAll(() => {
|
|
245
|
+
helper = new DocFilesHelper({ dirs: [testDir, extraDir] });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should list docs from all directories', () => {
|
|
249
|
+
const docs = helper.listDocs();
|
|
250
|
+
|
|
251
|
+
expect(docs).toHaveLength(4);
|
|
252
|
+
expect(docs.find((s) => s.name === 'first-doc')).toBeDefined();
|
|
253
|
+
expect(docs.find((s) => s.name === 'extra-doc')).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should get doc from any directory', () => {
|
|
257
|
+
const doc = helper.getDoc('extra-doc');
|
|
258
|
+
|
|
259
|
+
expect(doc.name).toBe('extra-doc');
|
|
260
|
+
expect(doc.description).toBe('An extra doc file');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('with files option', () => {
|
|
265
|
+
let helper: DocFilesHelper;
|
|
266
|
+
|
|
267
|
+
beforeAll(() => {
|
|
268
|
+
helper = new DocFilesHelper({ files: [standaloneFile] });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should list standalone files', () => {
|
|
272
|
+
const docs = helper.listDocs();
|
|
273
|
+
|
|
274
|
+
expect(docs).toHaveLength(1);
|
|
275
|
+
expect(docs[0].name).toBe('standalone');
|
|
276
|
+
expect(docs[0].filename).toBe('standalone.md');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should get standalone file by base filename', () => {
|
|
280
|
+
const doc = helper.getDoc('standalone');
|
|
281
|
+
|
|
282
|
+
expect(doc.name).toBe('standalone');
|
|
283
|
+
expect(doc.description).toBe('A standalone file');
|
|
284
|
+
expect(doc.content).toBe('# Standalone\n\nStandalone content.');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('with non-existent files', () => {
|
|
289
|
+
it('should silently skip non-existent files in listDocs', () => {
|
|
290
|
+
const helper = new DocFilesHelper({
|
|
291
|
+
files: [
|
|
292
|
+
standaloneFile,
|
|
293
|
+
'/non/existent/file.md',
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const docs = helper.listDocs();
|
|
298
|
+
|
|
299
|
+
// Should only include the existing file, not throw an error
|
|
300
|
+
expect(docs).toHaveLength(1);
|
|
301
|
+
expect(docs[0].name).toBe('standalone');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('with both dirs and files', () => {
|
|
306
|
+
let helper: DocFilesHelper;
|
|
307
|
+
|
|
308
|
+
beforeAll(() => {
|
|
309
|
+
helper = new DocFilesHelper({
|
|
310
|
+
dirs: [testDir],
|
|
311
|
+
files: [standaloneFile],
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should list docs from both dirs and files', () => {
|
|
316
|
+
const docs = helper.listDocs();
|
|
317
|
+
|
|
318
|
+
expect(docs).toHaveLength(4);
|
|
319
|
+
expect(docs.find((s) => s.name === 'first-doc')).toBeDefined();
|
|
320
|
+
expect(docs.find((s) => s.name === 'standalone')).toBeDefined();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should get doc from dirs', () => {
|
|
324
|
+
const doc = helper.getDoc('first-doc');
|
|
325
|
+
expect(doc.name).toBe('first-doc');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should get doc from files', () => {
|
|
329
|
+
const doc = helper.getDoc('standalone');
|
|
330
|
+
expect(doc.name).toBe('standalone');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve, join } from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { DocFilesHelper } from './index.ts';
|
|
4
|
+
|
|
5
|
+
export interface LocalLibraryDocs {
|
|
6
|
+
libraryPath: string;
|
|
7
|
+
helper: DocFilesHelper;
|
|
8
|
+
hasDocsFolder: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a DocFilesHelper for browsing docs in a local directory.
|
|
13
|
+
* Looks for .md files in the target directory and also in a ./docs subdirectory if it exists.
|
|
14
|
+
*/
|
|
15
|
+
export function browseLocalLibrary(targetPath: string): LocalLibraryDocs {
|
|
16
|
+
const resolvedPath = resolve(targetPath);
|
|
17
|
+
const docsPath = join(resolvedPath, 'docs');
|
|
18
|
+
const hasDocsFolder = existsSync(docsPath);
|
|
19
|
+
|
|
20
|
+
const dirs: string[] = [resolvedPath];
|
|
21
|
+
if (hasDocsFolder) {
|
|
22
|
+
dirs.push(docsPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const helper = new DocFilesHelper({
|
|
26
|
+
dirs,
|
|
27
|
+
overrideGetSubcommand: `show ${targetPath}`,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
libraryPath: resolvedPath,
|
|
32
|
+
helper,
|
|
33
|
+
hasDocsFolder,
|
|
34
|
+
};
|
|
35
|
+
}
|