Skip to content

Commit 1cd2eba

Browse files
committed
feat: internal documentation generation
1 parent 11c1a3a commit 1cd2eba

File tree

10 files changed

+633
-2
lines changed

10 files changed

+633
-2
lines changed

.changeset/upset-teeth-fetch.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@calycode/core': minor
3+
'@calycode/cli': minor
4+
---
5+
6+
feat: new command `generate-internal-docs` that creates a directory of docsfiy powered browseable documentation for your workspace rendered from markdown on client (Docsify)
7+
chore: update docs to include the new command

.changeset/wise-yaks-scream.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@calycode/core": patch
3+
"@calycode/cli": patch
4+
---
5+
6+
fix: fixing multiple issues with the generated markdown of the workspace, that was causing broken links
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { mkdir, access, readdir, lstat, rm, unlink } from 'node:fs/promises';
2+
import { log, intro, outro } from '@clack/prompts';
3+
import { load } from 'js-yaml';
4+
import { joinPath, dirname, replacePlaceholders, fetchAndExtractYaml } from '@repo/utils';
5+
import {
6+
addFullContextOptions,
7+
addPrintOutputFlag,
8+
attachCliEventHandlers,
9+
findProjectRoot,
10+
printOutputDir,
11+
resolveConfigs,
12+
withErrorHandler,
13+
} from '../utils/index';
14+
15+
/**
16+
* Recursively removes all files and subdirectories in a directory.
17+
* @param {string} directory - The directory to clear.
18+
*/
19+
async function clearDirectory(directory: string): Promise<void> {
20+
try {
21+
await access(directory);
22+
} catch {
23+
// Directory does not exist; nothing to clear
24+
return;
25+
}
26+
27+
const files = await readdir(directory);
28+
await Promise.all(
29+
files.map(async (file) => {
30+
const curPath = joinPath(directory, file);
31+
const stat = await lstat(curPath);
32+
if (stat.isDirectory()) {
33+
await clearDirectory(curPath);
34+
await rm(curPath, { recursive: true, force: true }); // removes the (now-empty) dir
35+
} else {
36+
await unlink(curPath);
37+
}
38+
})
39+
);
40+
}
41+
42+
async function generateInternalDocs({
43+
instance,
44+
workspace,
45+
branch,
46+
input,
47+
output,
48+
fetch = false,
49+
printOutput = false,
50+
core,
51+
}) {
52+
attachCliEventHandlers('generate-internal-docs', core, {
53+
instance,
54+
workspace,
55+
branch,
56+
input,
57+
output,
58+
fetch,
59+
printOutput,
60+
});
61+
62+
//const resolvedContext = await resolveEffectiveContext({ instance, workspace, branch }, core);
63+
const { instanceConfig, workspaceConfig, branchConfig } = await resolveConfigs({
64+
cliContext: { instance, workspace, branch },
65+
core,
66+
});
67+
68+
// Resolve output dir
69+
const outputDir = output
70+
? output
71+
: replacePlaceholders(instanceConfig.internalDocs.output, {
72+
'@': await findProjectRoot(),
73+
instance: instanceConfig.name,
74+
workspace: workspaceConfig.name,
75+
branch: branchConfig.label,
76+
});
77+
78+
clearDirectory(outputDir);
79+
await mkdir(outputDir, { recursive: true });
80+
81+
// Ensure we have the input file, default to local, but override if --fetch
82+
let inputFile = input;
83+
if (fetch) {
84+
inputFile = await fetchAndExtractYaml({
85+
baseUrl: instanceConfig.url,
86+
token: await core.loadToken(instanceConfig.name),
87+
workspaceId: workspaceConfig.id,
88+
branchLabel: branchConfig.label,
89+
outDir: outputDir,
90+
core,
91+
});
92+
}
93+
94+
intro('Building directory structure...');
95+
96+
if (!inputFile) throw new Error('Input YAML file is required');
97+
if (!outputDir) throw new Error('Output directory is required');
98+
99+
log.step(`Reading and parsing YAML file -> ${inputFile}`);
100+
const fileContents = await core.storage.readFile(inputFile, 'utf8');
101+
const jsonData = load(fileContents);
102+
103+
const plannedWrites: { path: string; content: string }[] = await core.generateInternalDocs({
104+
jsonData,
105+
instance: instanceConfig.name,
106+
workspace: workspaceConfig.name,
107+
branch: branchConfig.label,
108+
});
109+
log.step(`Writing Documentation to the output directory -> ${outputDir}`);
110+
await Promise.all(
111+
plannedWrites.map(async ({ path, content }) => {
112+
const outputPath = joinPath(outputDir, path);
113+
const writeDir = dirname(outputPath);
114+
if (!(await core.storage.exists(writeDir))) {
115+
await core.storage.mkdir(writeDir, { recursive: true });
116+
}
117+
await core.storage.writeFile(outputPath, content);
118+
})
119+
);
120+
121+
printOutputDir(printOutput, outputDir);
122+
outro('Documentation built successfully!');
123+
}
124+
125+
function registergenerateInternalDocsCommand(program, core) {
126+
const cmd = program
127+
.command('generate-internal-docs')
128+
.description(
129+
'Collect all descriptions, and internal documentation from a Xano instance and combine it into a nice documentation suite that can be hosted on a static hosting.'
130+
)
131+
.option('-I, --input <file>', 'Workspace yaml file from a local source, if present.')
132+
.option(
133+
'-O, --output <dir>',
134+
'Output directory (overrides default config), useful when ran from a CI/CD pipeline and want to ensure consistent output location.'
135+
);
136+
137+
addFullContextOptions(cmd);
138+
addPrintOutputFlag(cmd);
139+
140+
cmd.option(
141+
'-F, --fetch',
142+
'Forces fetching the workspace schema from the Xano instance via metadata API.'
143+
).action(
144+
withErrorHandler(async (opts) => {
145+
await generateInternalDocs({
146+
instance: opts.instance,
147+
workspace: opts.workspace,
148+
branch: opts.branch,
149+
input: opts.input,
150+
output: opts.output,
151+
fetch: opts.fetch,
152+
printOutput: opts.printOutputDir,
153+
core: core,
154+
});
155+
})
156+
);
157+
}
158+
159+
export { registergenerateInternalDocsCommand };

packages/cli/src/program.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { registerBuildXanoscriptRepoCommand } from './commands/generate-xanoscri
1616
import { Caly } from '@calycode/core';
1717
import { InitializedPostHog } from './utils/posthog/init';
1818
import { nodeConfigStorage } from './node-config-storage';
19+
import { registergenerateInternalDocsCommand } from './commands/generate-internal-docs';
1920

2021
const commandStartTimes = new WeakMap<Command, number>();
2122

@@ -94,6 +95,7 @@ registerGenerateOasCommand(program, core);
9495
registerOasServeCommand(program, core);
9596
registerGenerateCodeCommand(program, core);
9697
registerGenerateRepoCommand(program, core);
98+
registergenerateInternalDocsCommand(program, core);
9799
registerBuildXanoscriptRepoCommand(program, core);
98100
registerRegistryAddCommand(program, core);
99101
registerRegistryScaffoldCommand(program, core);
@@ -123,7 +125,7 @@ program.configureHelp({
123125
},
124126
{
125127
title: font.combo.boldCyan('Code Generation:'),
126-
commands: ['generate-oas', 'oas-serve', 'generate-code', 'generate-repo', 'generate-xs-repo', 'generate-functions'],
128+
commands: ['generate-oas', 'oas-serve', 'generate-code', 'generate-repo', 'generate-internal-docs'],
127129
},
128130
{
129131
title: font.combo.boldCyan('Registry:'),

0 commit comments

Comments
 (0)