-
Notifications
You must be signed in to change notification settings - Fork 243
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #759 from hellodword/patch-1
Feat: add generate-docs subcommand
- Loading branch information
Showing
8 changed files
with
367 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as jsonc from 'jsonc-parser'; | ||
import { Log, LogLevel } from '../../spec-utils/log'; | ||
|
||
const FEATURES_README_TEMPLATE = ` | ||
# #{Name} | ||
#{Description} | ||
## Example Usage | ||
\`\`\`json | ||
"features": { | ||
"#{Registry}/#{Namespace}/#{Id}:#{Version}": {} | ||
} | ||
\`\`\` | ||
#{OptionsTable} | ||
#{Customizations} | ||
#{Notes} | ||
--- | ||
_Note: This file was auto-generated from the [devcontainer-feature.json](#{RepoUrl}). Add additional notes to a \`NOTES.md\`._ | ||
`; | ||
|
||
const TEMPLATE_README_TEMPLATE = ` | ||
# #{Name} | ||
#{Description} | ||
#{OptionsTable} | ||
#{Notes} | ||
--- | ||
_Note: This file was auto-generated from the [devcontainer-template.json](#{RepoUrl}). Add additional notes to a \`NOTES.md\`._ | ||
`; | ||
|
||
export async function generateFeaturesDocumentation( | ||
basePath: string, | ||
ociRegistry: string, | ||
namespace: string, | ||
gitHubOwner: string, | ||
gitHubRepo: string, | ||
output: Log | ||
) { | ||
await _generateDocumentation(output, basePath, FEATURES_README_TEMPLATE, | ||
'devcontainer-feature.json', ociRegistry, namespace, gitHubOwner, gitHubRepo); | ||
} | ||
|
||
export async function generateTemplatesDocumentation( | ||
basePath: string, | ||
gitHubOwner: string, | ||
gitHubRepo: string, | ||
output: Log | ||
) { | ||
await _generateDocumentation(output, basePath, TEMPLATE_README_TEMPLATE, | ||
'devcontainer-template.json', '', '', gitHubOwner, gitHubRepo); | ||
} | ||
|
||
async function _generateDocumentation( | ||
output: Log, | ||
basePath: string, | ||
readmeTemplate: string, | ||
metadataFile: string, | ||
ociRegistry: string = '', | ||
namespace: string = '', | ||
gitHubOwner: string = '', | ||
gitHubRepo: string = '' | ||
) { | ||
const directories = fs.readdirSync(basePath); | ||
|
||
await Promise.all( | ||
directories.map(async (f: string) => { | ||
if (!f.startsWith('.')) { | ||
const readmePath = path.join(basePath, f, 'README.md'); | ||
output.write(`Generating ${readmePath}...`, LogLevel.Info); | ||
|
||
const jsonPath = path.join(basePath, f, metadataFile); | ||
|
||
if (!fs.existsSync(jsonPath)) { | ||
output.write(`(!) Warning: ${metadataFile} not found at path '${jsonPath}'. Skipping...`, LogLevel.Warning); | ||
return; | ||
} | ||
|
||
let parsedJson: any | undefined = undefined; | ||
try { | ||
parsedJson = jsonc.parse(fs.readFileSync(jsonPath, 'utf8')); | ||
} catch (err) { | ||
output.write(`Failed to parse ${jsonPath}: ${err}`, LogLevel.Error); | ||
return; | ||
} | ||
|
||
if (!parsedJson || !parsedJson?.id) { | ||
output.write(`${metadataFile} for '${f}' does not contain an 'id'`, LogLevel.Error); | ||
return; | ||
} | ||
|
||
// Add version | ||
let version = 'latest'; | ||
const parsedVersion: string = parsedJson?.version; | ||
if (parsedVersion) { | ||
// example - 1.0.0 | ||
const splitVersion = parsedVersion.split('.'); | ||
version = splitVersion[0]; | ||
} | ||
|
||
const generateOptionsMarkdown = () => { | ||
const options = parsedJson?.options; | ||
if (!options) { | ||
return ''; | ||
} | ||
|
||
const keys = Object.keys(options); | ||
const contents = keys | ||
.map(k => { | ||
const val = options[k]; | ||
|
||
const desc = val.description || '-'; | ||
const type = val.type || '-'; | ||
const def = val.default !== '' ? val.default : '-'; | ||
|
||
return `| ${k} | ${desc} | ${type} | ${def} |`; | ||
}) | ||
.join('\n'); | ||
|
||
return '## Options\n\n' + '| Options Id | Description | Type | Default Value |\n' + '|-----|-----|-----|-----|\n' + contents; | ||
}; | ||
|
||
const generateNotesMarkdown = () => { | ||
const notesPath = path.join(basePath, f, 'NOTES.md'); | ||
return fs.existsSync(notesPath) ? fs.readFileSync(path.join(notesPath), 'utf8') : ''; | ||
}; | ||
|
||
let urlToConfig = `${metadataFile}`; | ||
const basePathTrimmed = basePath.startsWith('./') ? basePath.substring(2) : basePath; | ||
if (gitHubOwner !== '' && gitHubRepo !== '') { | ||
urlToConfig = `https://github.com/${gitHubOwner}/${gitHubRepo}/blob/main/${basePathTrimmed}/${f}/${metadataFile}`; | ||
} | ||
|
||
let header; | ||
const isDeprecated = parsedJson?.deprecated; | ||
const hasLegacyIds = parsedJson?.legacyIds && parsedJson?.legacyIds.length > 0; | ||
|
||
if (isDeprecated || hasLegacyIds) { | ||
header = '### **IMPORTANT NOTE**\n'; | ||
|
||
if (isDeprecated) { | ||
header += `- **This Feature is deprecated, and will no longer receive any further updates/support.**\n`; | ||
} | ||
|
||
if (hasLegacyIds) { | ||
const formattedLegacyIds = parsedJson.legacyIds.map((legacyId: string) => `'${legacyId}'`); | ||
header += `- **Ids used to publish this Feature in the past - ${formattedLegacyIds.join(', ')}**\n`; | ||
} | ||
} | ||
|
||
let extensions = ''; | ||
if (parsedJson?.customizations?.vscode?.extensions) { | ||
const extensionsList = parsedJson.customizations.vscode.extensions; | ||
if (extensionsList && extensionsList.length > 0) { | ||
extensions = | ||
'\n## Customizations\n\n### VS Code Extensions\n\n' + extensionsList.map((ext: string) => `- \`${ext}\``).join('\n') + '\n'; | ||
} | ||
} | ||
|
||
let newReadme = readmeTemplate | ||
// Templates & Features | ||
.replace('#{Id}', parsedJson.id) | ||
.replace('#{Name}', parsedJson.name ? `${parsedJson.name} (${parsedJson.id})` : `${parsedJson.id}`) | ||
.replace('#{Description}', parsedJson.description ?? '') | ||
.replace('#{OptionsTable}', generateOptionsMarkdown()) | ||
.replace('#{Notes}', generateNotesMarkdown()) | ||
.replace('#{RepoUrl}', urlToConfig) | ||
// Features Only | ||
.replace('#{Registry}', ociRegistry) | ||
.replace('#{Namespace}', namespace) | ||
.replace('#{Version}', version) | ||
.replace('#{Customizations}', extensions); | ||
|
||
if (header) { | ||
newReadme = header + newReadme; | ||
} | ||
|
||
// Remove previous readme | ||
if (fs.existsSync(readmePath)) { | ||
fs.unlinkSync(readmePath); | ||
} | ||
|
||
// Write new readme | ||
fs.writeFileSync(readmePath, newReadme); | ||
} | ||
}) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Argv } from 'yargs'; | ||
import { UnpackArgv } from '../devContainersSpecCLI'; | ||
import { generateFeaturesDocumentation } from '../collectionCommonUtils/generateDocsCommandImpl'; | ||
import { createLog } from '../devContainers'; | ||
import { mapLogLevel } from '../../spec-utils/log'; | ||
import { getPackageConfig } from '../../spec-utils/product'; | ||
|
||
// -- 'features generate-docs' command | ||
export function featuresGenerateDocsOptions(y: Argv) { | ||
return y | ||
.options({ | ||
'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.' }, | ||
'registry': { type: 'string', alias: 'r', default: 'ghcr.io', description: 'Name of the OCI registry.' }, | ||
'namespace': { type: 'string', alias: 'n', require: true, description: `Unique indentifier for the collection of features. Example: <owner>/<repo>` }, | ||
'github-owner': { type: 'string', default: '', description: `GitHub owner for docs.` }, | ||
'github-repo': { type: 'string', default: '', description: `GitHub repo for docs.` }, | ||
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' } | ||
}) | ||
.check(_argv => { | ||
return true; | ||
}); | ||
} | ||
|
||
export type FeaturesGenerateDocsArgs = UnpackArgv<ReturnType<typeof featuresGenerateDocsOptions>>; | ||
|
||
export function featuresGenerateDocsHandler(args: FeaturesGenerateDocsArgs) { | ||
(async () => await featuresGenerateDocs(args))().catch(console.error); | ||
} | ||
|
||
export async function featuresGenerateDocs({ | ||
'project-folder': collectionFolder, | ||
'registry': registry, | ||
'namespace': namespace, | ||
'github-owner': gitHubOwner, | ||
'github-repo': gitHubRepo, | ||
'log-level': inputLogLevel, | ||
}: FeaturesGenerateDocsArgs) { | ||
const disposables: (() => Promise<unknown> | undefined)[] = []; | ||
const dispose = async () => { | ||
await Promise.all(disposables.map(d => d())); | ||
}; | ||
|
||
const pkg = getPackageConfig(); | ||
|
||
const output = createLog({ | ||
logLevel: mapLogLevel(inputLogLevel), | ||
logFormat: 'text', | ||
log: (str) => process.stderr.write(str), | ||
terminalDimensions: undefined, | ||
}, pkg, new Date(), disposables); | ||
|
||
await generateFeaturesDocumentation(collectionFolder, registry, namespace, gitHubOwner, gitHubRepo, output); | ||
|
||
// Cleanup | ||
await dispose(); | ||
process.exit(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Argv } from 'yargs'; | ||
import { UnpackArgv } from '../devContainersSpecCLI'; | ||
import { generateTemplatesDocumentation } from '../collectionCommonUtils/generateDocsCommandImpl'; | ||
import { createLog } from '../devContainers'; | ||
import { mapLogLevel } from '../../spec-utils/log'; | ||
import { getPackageConfig } from '../../spec-utils/product'; | ||
|
||
// -- 'templates generate-docs' command | ||
export function templatesGenerateDocsOptions(y: Argv) { | ||
return y | ||
.options({ | ||
'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.' }, | ||
'github-owner': { type: 'string', default: '', description: `GitHub owner for docs.` }, | ||
'github-repo': { type: 'string', default: '', description: `GitHub repo for docs.` }, | ||
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' } | ||
}) | ||
.check(_argv => { | ||
return true; | ||
}); | ||
} | ||
|
||
export type TemplatesGenerateDocsArgs = UnpackArgv<ReturnType<typeof templatesGenerateDocsOptions>>; | ||
|
||
export function templatesGenerateDocsHandler(args: TemplatesGenerateDocsArgs) { | ||
(async () => await templatesGenerateDocs(args))().catch(console.error); | ||
} | ||
|
||
export async function templatesGenerateDocs({ | ||
'project-folder': collectionFolder, | ||
'github-owner': gitHubOwner, | ||
'github-repo': gitHubRepo, | ||
'log-level': inputLogLevel, | ||
}: TemplatesGenerateDocsArgs) { | ||
const disposables: (() => Promise<unknown> | undefined)[] = []; | ||
const dispose = async () => { | ||
await Promise.all(disposables.map(d => d())); | ||
}; | ||
|
||
const pkg = getPackageConfig(); | ||
|
||
const output = createLog({ | ||
logLevel: mapLogLevel(inputLogLevel), | ||
logFormat: 'text', | ||
log: (str) => process.stderr.write(str), | ||
terminalDimensions: undefined, | ||
}, pkg, new Date(), disposables); | ||
|
||
await generateTemplatesDocumentation(collectionFolder, gitHubOwner, gitHubRepo, output); | ||
|
||
// Cleanup | ||
await dispose(); | ||
process.exit(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.