Skip to content

Commit edfba19

Browse files
committed
Fix and refactor the generate-docs subcommand
1 parent 1da368b commit edfba19

File tree

8 files changed

+262
-86
lines changed

8 files changed

+262
-86
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
"test": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/*.test.ts",
4141
"test-matrix": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit",
4242
"test-container-features": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-features/*.test.ts",
43-
"test-container-features-cli": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-features/featuresCLICommands.test.ts",
4443
"test-container-templates": "env TS_NODE_PROJECT=src/test/tsconfig.json mocha -r ts-node/register --exit src/test/container-templates/*.test.ts"
4544
},
4645
"files": [
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Argv } from 'yargs';
2+
import { CLIHost } from '../../spec-common/cliHost';
3+
import { Log } from '../../spec-utils/log';
4+
5+
const targetPositionalDescription = (collectionType: GenerateDocsCollectionType) => `
6+
Generate docs of ${collectionType}s at provided [target] (default is cwd), where [target] is either:
7+
1. A path to the src folder of the collection with [1..n] ${collectionType}s.
8+
2. A path to a single ${collectionType} that contains a devcontainer-${collectionType}.json.
9+
`;
10+
11+
export function GenerateDocsOptions(y: Argv, collectionType: GenerateDocsCollectionType) {
12+
return y
13+
.options({
14+
'registry': { type: 'string', alias: 'r', default: 'ghcr.io', description: 'Name of the OCI registry.', hidden: collectionType !== 'feature' },
15+
'namespace': { type: 'string', alias: 'n', require: collectionType === 'feature', description: `Unique indentifier for the collection of features. Example: <owner>/<repo>`, hidden: collectionType !== 'feature' },
16+
'github-owner': { type: 'string', default: '', description: `GitHub owner for docs.` },
17+
'github-repo': { type: 'string', default: '', description: `GitHub repo for docs.` },
18+
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }
19+
})
20+
.positional('target', { type: 'string', default: '.', description: targetPositionalDescription(collectionType) })
21+
.check(_argv => {
22+
return true;
23+
});
24+
}
25+
26+
export type GenerateDocsCollectionType = 'feature' | 'template';
27+
28+
export interface GenerateDocsCommandInput {
29+
cliHost: CLIHost;
30+
targetFolder: string;
31+
registry?: string;
32+
namespace?: string;
33+
gitHubOwner: string;
34+
gitHubRepo: string;
35+
output: Log;
36+
disposables: (() => Promise<unknown> | undefined)[];
37+
isSingle?: boolean; // Generating docs for a collection of many features/templates. Should autodetect.
38+
}

src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as jsonc from 'jsonc-parser';
4-
import { Log, LogLevel } from '../../spec-utils/log';
4+
import { LogLevel } from '../../spec-utils/log';
5+
import { GenerateDocsCollectionType, GenerateDocsCommandInput } from './generateDocs';
6+
import { isLocalFile, isLocalFolder, readLocalDir } from '../../spec-utils/pfs';
57

68
const FEATURES_README_TEMPLATE = `
79
# #{Name}
@@ -39,49 +41,42 @@ const TEMPLATE_README_TEMPLATE = `
3941
_Note: This file was auto-generated from the [devcontainer-template.json](#{RepoUrl}). Add additional notes to a \`NOTES.md\`._
4042
`;
4143

42-
export async function generateFeaturesDocumentation(
43-
basePath: string,
44-
ociRegistry: string,
45-
namespace: string,
46-
gitHubOwner: string,
47-
gitHubRepo: string,
48-
output: Log
49-
) {
50-
await _generateDocumentation(output, basePath, FEATURES_README_TEMPLATE,
51-
'devcontainer-feature.json', ociRegistry, namespace, gitHubOwner, gitHubRepo);
52-
}
44+
const README_TEMPLATES = {
45+
'feature': FEATURES_README_TEMPLATE,
46+
'template': TEMPLATE_README_TEMPLATE,
47+
};
48+
49+
export async function generateDocumentation(args: GenerateDocsCommandInput, collectionType: GenerateDocsCollectionType) {
50+
51+
const {
52+
targetFolder: basePath,
53+
registry,
54+
namespace,
55+
gitHubOwner,
56+
gitHubRepo,
57+
output,
58+
isSingle,
59+
} = args;
60+
61+
const readmeTemplate = README_TEMPLATES[collectionType];
62+
63+
const directories = isSingle ? ['.'] : await readLocalDir(basePath);
5364

54-
export async function generateTemplatesDocumentation(
55-
basePath: string,
56-
gitHubOwner: string,
57-
gitHubRepo: string,
58-
output: Log
59-
) {
60-
await _generateDocumentation(output, basePath, TEMPLATE_README_TEMPLATE,
61-
'devcontainer-template.json', '', '', gitHubOwner, gitHubRepo);
62-
}
6365

64-
async function _generateDocumentation(
65-
output: Log,
66-
basePath: string,
67-
readmeTemplate: string,
68-
metadataFile: string,
69-
ociRegistry: string = '',
70-
namespace: string = '',
71-
gitHubOwner: string = '',
72-
gitHubRepo: string = ''
73-
) {
74-
const directories = fs.readdirSync(basePath);
66+
const metadataFile = `devcontainer-${collectionType}.json`;
7567

7668
await Promise.all(
7769
directories.map(async (f: string) => {
78-
if (!f.startsWith('.')) {
79-
const readmePath = path.join(basePath, f, 'README.md');
80-
output.write(`Generating ${readmePath}...`, LogLevel.Info);
70+
if (f.startsWith('..')) {
71+
return;
72+
}
73+
74+
const readmePath = path.join(basePath, f, 'README.md');
75+
output.write(`Generating ${readmePath}...`, LogLevel.Info);
8176

82-
const jsonPath = path.join(basePath, f, metadataFile);
77+
const jsonPath = path.join(basePath, f, metadataFile);
8378

84-
if (!fs.existsSync(jsonPath)) {
79+
if (!(await isLocalFile(jsonPath))) {
8580
output.write(`(!) Warning: ${metadataFile} not found at path '${jsonPath}'. Skipping...`, LogLevel.Warning);
8681
return;
8782
}
@@ -176,8 +171,8 @@ async function _generateDocumentation(
176171
.replace('#{Notes}', generateNotesMarkdown())
177172
.replace('#{RepoUrl}', urlToConfig)
178173
// Features Only
179-
.replace('#{Registry}', ociRegistry)
180-
.replace('#{Namespace}', namespace)
174+
.replace('#{Registry}', registry || '')
175+
.replace('#{Namespace}', namespace || '')
181176
.replace('#{Version}', version)
182177
.replace('#{Customizations}', extensions);
183178

@@ -191,8 +186,36 @@ async function _generateDocumentation(
191186
}
192187

193188
// Write new readme
194-
fs.writeFileSync(readmePath, newReadme);
195-
}
189+
fs.writeFileSync(readmePath, newReadme);
196190
})
197191
);
198192
}
193+
194+
export async function prepGenerateDocsCommand(args: GenerateDocsCommandInput, collectionType: GenerateDocsCollectionType): Promise<GenerateDocsCommandInput> {
195+
const { cliHost, targetFolder, registry, namespace, gitHubOwner, gitHubRepo, output, disposables } = args;
196+
197+
const targetFolderResolved = cliHost.path.resolve(targetFolder);
198+
if (!(await isLocalFolder(targetFolderResolved))) {
199+
throw new Error(`Target folder '${targetFolderResolved}' does not exist`);
200+
}
201+
202+
// Detect if we're dealing with a collection or a single feature/template
203+
const isValidFolder = await isLocalFolder(cliHost.path.join(targetFolderResolved));
204+
const isSingle = await isLocalFile(cliHost.path.join(targetFolderResolved, `devcontainer-${collectionType}.json`));
205+
206+
if (!isValidFolder) {
207+
throw new Error(`Target folder '${targetFolderResolved}' does not exist`);
208+
}
209+
210+
return {
211+
cliHost,
212+
targetFolder: targetFolderResolved,
213+
registry,
214+
namespace,
215+
gitHubOwner,
216+
gitHubRepo,
217+
output,
218+
disposables,
219+
isSingle
220+
};
221+
}

src/spec-node/devContainersSpecCLI.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa
8080
y.command('publish <target>', 'Package and publish Features', featuresPublishOptions, featuresPublishHandler);
8181
y.command('info <mode> <feature>', 'Fetch metadata for a published Feature', featuresInfoOptions, featuresInfoHandler);
8282
y.command('resolve-dependencies', 'Read and resolve dependency graph from a configuration', featuresResolveDependenciesOptions, featuresResolveDependenciesHandler);
83-
y.command('generate-docs', 'Generate documentation', featuresGenerateDocsOptions, featuresGenerateDocsHandler);
83+
y.command('generate-docs <target>', 'Generate documentation', featuresGenerateDocsOptions, featuresGenerateDocsHandler);
8484
});
8585
y.command('templates', 'Templates commands', (y: Argv) => {
8686
y.command('apply', 'Apply a template to the project', templateApplyOptions, templateApplyHandler);
8787
y.command('publish <target>', 'Package and publish templates', templatesPublishOptions, templatesPublishHandler);
88-
y.command('generate-docs', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler);
88+
y.command('generate-docs <target>', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler);
8989
});
9090
y.command(restArgs ? ['exec', '*'] : ['exec <cmd> [args..]'], 'Execute a command on a running dev container', execOptions, execHandler);
9191
y.epilog(`devcontainer@${version} ${packageFolder}`);
Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
import { Argv } from 'yargs';
22
import { UnpackArgv } from '../devContainersSpecCLI';
3-
import { generateFeaturesDocumentation } from '../collectionCommonUtils/generateDocsCommandImpl';
43
import { createLog } from '../devContainers';
54
import { mapLogLevel } from '../../spec-utils/log';
65
import { getPackageConfig } from '../../spec-utils/product';
6+
import { isLocalFolder } from '../../spec-utils/pfs';
7+
import { getCLIHost } from '../../spec-common/cliHost';
8+
import { loadNativeModule } from '../../spec-common/commonUtils';
9+
import { GenerateDocsCommandInput, GenerateDocsOptions } from '../collectionCommonUtils/generateDocs';
10+
import { generateDocumentation, prepGenerateDocsCommand } from '../collectionCommonUtils/generateDocsCommandImpl';
11+
12+
const collectionType = 'feature';
713

8-
// -- 'features generate-docs' command
914
export function featuresGenerateDocsOptions(y: Argv) {
10-
return y
11-
.options({
12-
'project-folder': { type: 'string', alias: 'p', default: '.', description: 'Path to folder containing \'src\' and \'test\' sub-folders. This is likely the git root of the project.' },
13-
'registry': { type: 'string', alias: 'r', default: 'ghcr.io', description: 'Name of the OCI registry.' },
14-
'namespace': { type: 'string', alias: 'n', require: true, description: `Unique indentifier for the collection of features. Example: <owner>/<repo>` },
15-
'github-owner': { type: 'string', default: '', description: `GitHub owner for docs.` },
16-
'github-repo': { type: 'string', default: '', description: `GitHub repo for docs.` },
17-
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }
18-
})
19-
.check(_argv => {
20-
return true;
21-
});
15+
return GenerateDocsOptions(y, collectionType);
2216
}
2317

2418
export type FeaturesGenerateDocsArgs = UnpackArgv<ReturnType<typeof featuresGenerateDocsOptions>>;
@@ -28,7 +22,7 @@ export function featuresGenerateDocsHandler(args: FeaturesGenerateDocsArgs) {
2822
}
2923

3024
export async function featuresGenerateDocs({
31-
'project-folder': collectionFolder,
25+
'target': targetFolder,
3226
'registry': registry,
3327
'namespace': namespace,
3428
'github-owner': gitHubOwner,
@@ -42,16 +36,36 @@ export async function featuresGenerateDocs({
4236

4337
const pkg = getPackageConfig();
4438

39+
const cwd = process.cwd();
40+
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
4541
const output = createLog({
4642
logLevel: mapLogLevel(inputLogLevel),
4743
logFormat: 'text',
4844
log: (str) => process.stderr.write(str),
4945
terminalDimensions: undefined,
5046
}, pkg, new Date(), disposables);
5147

52-
await generateFeaturesDocumentation(collectionFolder, registry, namespace, gitHubOwner, gitHubRepo, output);
48+
const targetFolderResolved = cliHost.path.resolve(targetFolder);
49+
if (!(await isLocalFolder(targetFolderResolved))) {
50+
throw new Error(`Target folder '${targetFolderResolved}' does not exist`);
51+
}
52+
53+
54+
const args: GenerateDocsCommandInput = {
55+
cliHost,
56+
targetFolder,
57+
registry,
58+
namespace,
59+
gitHubOwner,
60+
gitHubRepo,
61+
output,
62+
disposables,
63+
};
64+
65+
const preparedArgs = await prepGenerateDocsCommand(args, collectionType);
66+
67+
await generateDocumentation(preparedArgs, collectionType);
5368

54-
// Cleanup
5569
await dispose();
5670
process.exit();
5771
}
Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import { Argv } from 'yargs';
22
import { UnpackArgv } from '../devContainersSpecCLI';
3-
import { generateTemplatesDocumentation } from '../collectionCommonUtils/generateDocsCommandImpl';
3+
import { generateDocumentation, prepGenerateDocsCommand } from '../collectionCommonUtils/generateDocsCommandImpl';
44
import { createLog } from '../devContainers';
55
import { mapLogLevel } from '../../spec-utils/log';
66
import { getPackageConfig } from '../../spec-utils/product';
7+
import { GenerateDocsCommandInput, GenerateDocsOptions } from '../collectionCommonUtils/generateDocs';
8+
import { getCLIHost } from '../../spec-common/cliHost';
9+
import { loadNativeModule } from '../../spec-common/commonUtils';
10+
import { isLocalFolder } from '../../spec-utils/pfs';
11+
12+
const collectionType = 'template';
713

8-
// -- 'templates generate-docs' command
914
export function templatesGenerateDocsOptions(y: Argv) {
10-
return y
11-
.options({
12-
'project-folder': { type: 'string', alias: 'p', default: '.', description: 'Path to folder containing \'src\' and \'test\' sub-folders. This is likely the git root of the project.' },
13-
'github-owner': { type: 'string', default: '', description: `GitHub owner for docs.` },
14-
'github-repo': { type: 'string', default: '', description: `GitHub repo for docs.` },
15-
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }
16-
})
17-
.check(_argv => {
18-
return true;
19-
});
15+
return GenerateDocsOptions(y, collectionType);
2016
}
2117

2218
export type TemplatesGenerateDocsArgs = UnpackArgv<ReturnType<typeof templatesGenerateDocsOptions>>;
@@ -26,7 +22,7 @@ export function templatesGenerateDocsHandler(args: TemplatesGenerateDocsArgs) {
2622
}
2723

2824
export async function templatesGenerateDocs({
29-
'project-folder': collectionFolder,
25+
'target': targetFolder,
3026
'github-owner': gitHubOwner,
3127
'github-repo': gitHubRepo,
3228
'log-level': inputLogLevel,
@@ -38,16 +34,34 @@ export async function templatesGenerateDocs({
3834

3935
const pkg = getPackageConfig();
4036

37+
const cwd = process.cwd();
38+
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
4139
const output = createLog({
4240
logLevel: mapLogLevel(inputLogLevel),
4341
logFormat: 'text',
4442
log: (str) => process.stderr.write(str),
4543
terminalDimensions: undefined,
4644
}, pkg, new Date(), disposables);
4745

48-
await generateTemplatesDocumentation(collectionFolder, gitHubOwner, gitHubRepo, output);
46+
const targetFolderResolved = cliHost.path.resolve(targetFolder);
47+
if (!(await isLocalFolder(targetFolderResolved))) {
48+
throw new Error(`Target folder '${targetFolderResolved}' does not exist`);
49+
}
50+
51+
52+
const args: GenerateDocsCommandInput = {
53+
cliHost,
54+
targetFolder,
55+
gitHubOwner,
56+
gitHubRepo,
57+
output,
58+
disposables,
59+
};
60+
61+
const preparedArgs = await prepGenerateDocsCommand(args, collectionType);
62+
63+
await generateDocumentation(preparedArgs, collectionType);
4964

50-
// Cleanup
5165
await dispose();
5266
process.exit();
5367
}

0 commit comments

Comments
 (0)