@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, basename, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// Re-export from submodules
|
|
5
|
+
export { browseLocalLibrary, type LocalLibraryDocs } from './browseLocalLibrary.ts';
|
|
6
|
+
export {
|
|
7
|
+
browseNpmLibrary,
|
|
8
|
+
findLibrary,
|
|
9
|
+
findLibraryInNodeModules,
|
|
10
|
+
getInstallationDirectory,
|
|
11
|
+
getLibraryDocs,
|
|
12
|
+
type LibraryLocation,
|
|
13
|
+
type NpmLibraryDocs,
|
|
14
|
+
} from './browseNpmLibrary.ts';
|
|
15
|
+
|
|
16
|
+
export interface Frontmatter {
|
|
17
|
+
name?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
[key: string]: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ParsedDocument {
|
|
23
|
+
frontmatter: Frontmatter;
|
|
24
|
+
content: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DocInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
filename: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DocContent extends DocInfo {
|
|
34
|
+
content: string;
|
|
35
|
+
rawContent: string;
|
|
36
|
+
fullPath: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DocFilesHelperOptions {
|
|
40
|
+
// List of directories to search for *.md files
|
|
41
|
+
dirs?: string[];
|
|
42
|
+
|
|
43
|
+
// List of specific files to include
|
|
44
|
+
files?: string[];
|
|
45
|
+
|
|
46
|
+
// If provided, this will override the subcommand to get a single doc file.
|
|
47
|
+
// Default: 'get-doc'
|
|
48
|
+
overrideGetSubcommand?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse YAML frontmatter from a markdown document.
|
|
53
|
+
* Frontmatter is delimited by --- at the start of the file.
|
|
54
|
+
*/
|
|
55
|
+
export function parseFrontmatter(text: string): ParsedDocument {
|
|
56
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
57
|
+
const match = text.match(frontmatterRegex);
|
|
58
|
+
|
|
59
|
+
if (!match) {
|
|
60
|
+
return {
|
|
61
|
+
frontmatter: {},
|
|
62
|
+
content: text,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [, frontmatterBlock, content] = match;
|
|
67
|
+
const frontmatter: Frontmatter = {};
|
|
68
|
+
|
|
69
|
+
for (const line of frontmatterBlock.split('\n')) {
|
|
70
|
+
const colonIndex = line.indexOf(':');
|
|
71
|
+
if (colonIndex === -1) continue;
|
|
72
|
+
|
|
73
|
+
const key = line.slice(0, colonIndex).trim();
|
|
74
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
75
|
+
frontmatter[key] = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
frontmatter,
|
|
80
|
+
content: content.trim(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Helper class for working with doc files.
|
|
86
|
+
*/
|
|
87
|
+
export class DocFilesHelper {
|
|
88
|
+
options: DocFilesHelperOptions
|
|
89
|
+
|
|
90
|
+
// Map of base filename (without directory) -> full file path
|
|
91
|
+
private fileMap: Map<string, string>;
|
|
92
|
+
|
|
93
|
+
constructor(options: DocFilesHelperOptions) {
|
|
94
|
+
this.options = options;
|
|
95
|
+
this.fileMap = new Map();
|
|
96
|
+
|
|
97
|
+
// Add files from directories
|
|
98
|
+
if (options.dirs) {
|
|
99
|
+
for (const dir of options.dirs) {
|
|
100
|
+
const files = readdirSync(dir);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (!file.endsWith('.md')) continue;
|
|
103
|
+
this.fileMap.set(file, join(dir, file));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add specific files
|
|
109
|
+
if (options.files) {
|
|
110
|
+
for (const filePath of options.files) {
|
|
111
|
+
const baseFilename = basename(filePath);
|
|
112
|
+
this.fileMap.set(baseFilename, filePath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
formatGetDocCommand(filename: string): string {
|
|
118
|
+
const script = relative(process.cwd(), process.argv[1]);
|
|
119
|
+
const binName = basename(script);
|
|
120
|
+
const subcommand = this.options.overrideGetSubcommand || 'show';
|
|
121
|
+
// Handle cases like 'node .' or 'node dist/cli.js' or 'node src/cli.ts'
|
|
122
|
+
if (binName === '.' || binName.endsWith('.js') || binName.endsWith('.mjs') || binName.endsWith('.ts')) {
|
|
123
|
+
return `node ${script} ${subcommand} ${filename}`;
|
|
124
|
+
}
|
|
125
|
+
return `${binName} ${subcommand} ${filename}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* List all doc files, returning their metadata from frontmatter.
|
|
130
|
+
* Files that don't exist are silently skipped.
|
|
131
|
+
*/
|
|
132
|
+
listDocs(): DocInfo[] {
|
|
133
|
+
const docs: DocInfo[] = [];
|
|
134
|
+
|
|
135
|
+
for (const [baseFilename, fullPath] of this.fileMap) {
|
|
136
|
+
let rawContent: string;
|
|
137
|
+
try {
|
|
138
|
+
rawContent = readFileSync(fullPath, 'utf-8');
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
if (err.code === 'ENOENT') {
|
|
141
|
+
// File doesn't exist, skip it silently
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
const { frontmatter } = parseFrontmatter(rawContent);
|
|
147
|
+
|
|
148
|
+
docs.push({
|
|
149
|
+
name: frontmatter.name || basename(baseFilename, '.md'),
|
|
150
|
+
description: frontmatter.description || '',
|
|
151
|
+
filename: baseFilename,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return docs;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the contents of a specific doc file by name.
|
|
160
|
+
* If the exact filename doesn't exist, looks for a partial match.
|
|
161
|
+
* Throws an error if the doc file is not found or if multiple matches are found.
|
|
162
|
+
*/
|
|
163
|
+
getDoc(name: string): DocContent {
|
|
164
|
+
const baseName = name.endsWith('.md') ? name.slice(0, -3) : name;
|
|
165
|
+
const filename = `${baseName}.md`;
|
|
166
|
+
|
|
167
|
+
// Try exact match first
|
|
168
|
+
const fullPath = this.fileMap.get(filename);
|
|
169
|
+
if (fullPath) {
|
|
170
|
+
const rawContent = readFileSync(fullPath, 'utf-8');
|
|
171
|
+
const { frontmatter, content } = parseFrontmatter(rawContent);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
name: frontmatter.name || baseName,
|
|
175
|
+
description: frontmatter.description || '',
|
|
176
|
+
filename,
|
|
177
|
+
content,
|
|
178
|
+
rawContent,
|
|
179
|
+
fullPath,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fall back to partial matching on filename or frontmatter name
|
|
184
|
+
const docs = this.listDocs();
|
|
185
|
+
const matches = docs.filter(
|
|
186
|
+
(doc) =>
|
|
187
|
+
doc.filename.toLowerCase().includes(baseName.toLowerCase()) ||
|
|
188
|
+
doc.name.toLowerCase().includes(baseName.toLowerCase())
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (matches.length === 0) {
|
|
192
|
+
throw new Error(`Doc file not found: ${baseName}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (matches.length > 1) {
|
|
196
|
+
const matchNames = matches.map((m) => m.filename).join(', ');
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Multiple docs match "${baseName}": ${matchNames}. Please be more specific.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const matchedFilename = matches[0].filename;
|
|
203
|
+
const matchedPath = this.fileMap.get(matchedFilename)!;
|
|
204
|
+
const rawContent = readFileSync(matchedPath, 'utf-8');
|
|
205
|
+
const { frontmatter, content } = parseFrontmatter(rawContent);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
name: frontmatter.name || basename(matchedFilename, '.md'),
|
|
209
|
+
description: frontmatter.description || '',
|
|
210
|
+
filename: matchedFilename,
|
|
211
|
+
content,
|
|
212
|
+
rawContent,
|
|
213
|
+
fullPath: matchedPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Print a formatted list of all doc files to stdout.
|
|
219
|
+
* Used by the 'list-docs' command.
|
|
220
|
+
*/
|
|
221
|
+
printDocFileList(): void {
|
|
222
|
+
const docs = this.listDocs();
|
|
223
|
+
console.log('Available doc files:\n');
|
|
224
|
+
for (const doc of docs) {
|
|
225
|
+
if (doc.description) {
|
|
226
|
+
console.log(` ${doc.name} (${this.formatGetDocCommand(doc.filename)}):`);
|
|
227
|
+
console.log(` ${doc.description}\n`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` ${doc.name} (${this.formatGetDocCommand(doc.filename)})\n`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Print the raw contents of a specific doc file to stdout.
|
|
236
|
+
*
|
|
237
|
+
* Used by the 'get-doc' command.
|
|
238
|
+
*/
|
|
239
|
+
printDocFileContents(name: string): void {
|
|
240
|
+
try {
|
|
241
|
+
const doc = this.getDoc(name);
|
|
242
|
+
console.log(doc.rawContent);
|
|
243
|
+
console.log(`\n(File source: ${doc.fullPath})`);
|
|
244
|
+
} catch {
|
|
245
|
+
console.error(`Doc file not found: ${name}`);
|
|
246
|
+
console.error('Run with "list-docs" or "list" command to see available docs.');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
yargsSetup(yargs: any) {
|
|
252
|
+
yargs
|
|
253
|
+
.command(
|
|
254
|
+
'list-docs',
|
|
255
|
+
'List available documentation files',
|
|
256
|
+
{},
|
|
257
|
+
async () => this.printDocFileList(),
|
|
258
|
+
)
|
|
259
|
+
.command(
|
|
260
|
+
'get-doc <name>',
|
|
261
|
+
'Display the contents of a documentation file',
|
|
262
|
+
(yargs: any) => {
|
|
263
|
+
return yargs.positional('name', {
|
|
264
|
+
type: 'string',
|
|
265
|
+
describe: 'Name of the doc file',
|
|
266
|
+
demandOption: true,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
async (argv: any) => this.printDocFileContents(argv.name as string),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export type ParsedTarget =
|
|
276
|
+
| { type: 'directory'; value: string }
|
|
277
|
+
| { type: 'npm'; value: string };
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Determine if a target string is a directory path or an NPM package name.
|
|
281
|
+
* Returns 'directory' if it starts with '.' or '/', otherwise 'npm'.
|
|
282
|
+
*/
|
|
283
|
+
export function parseTarget(target: string): ParsedTarget {
|
|
284
|
+
if (target.startsWith('.') || target.startsWith('/')) {
|
|
285
|
+
return { type: 'directory', value: target };
|
|
286
|
+
}
|
|
287
|
+
return { type: 'npm', value: target };
|
|
288
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"moduleResolution": "bundler",
|
|
17
|
+
"resolveJsonModule": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|