in src/main.ts [20:462]
async function main() {
await emitSupportPolicyInformation();
const argv = yargs
.usage('$0 <cmd> [args]')
.option('verbose', {
alias: 'v',
type: 'boolean',
desc: 'Increase logging verbosity',
count: true,
default: 0,
})
.command(
'snippet FILE',
'Translate a single snippet',
(command) =>
command
.positional('FILE', {
type: 'string',
describe: 'The file to translate (leave out for stdin)',
})
.option('language', {
type: 'string',
describe: 'Language ID to transliterate to',
choices: Array.from(new Set(Object.values(TargetLanguage))),
})
.option('python', {
alias: 'p',
boolean: true,
deprecated: true,
description: 'Translate snippets to Python. Use --language python instead.',
}),
wrapHandler(async (args) => {
const result = translateTypeScript(await makeFileSource(args.FILE ?? '-', 'stdin.ts'), makeVisitor(args));
handleSingleResult(result);
}),
)
.command(
'markdown FILE',
'Translate a MarkDown file',
(command) =>
command
.positional('FILE', {
type: 'string',
describe: 'The file to translate (leave out for stdin)',
})
.option('language', {
type: 'string',
describe: 'Language ID to transliterate to',
choices: Array.from(new Set(Object.values(TargetLanguage))),
})
.option('python', {
alias: 'p',
boolean: true,
deprecated: true,
description: 'Translate snippets to Python. Use --language python instead.',
}),
wrapHandler(async (args) => {
const result = translateMarkdown(await makeFileSource(args.FILE ?? '-', 'stdin.md'), makeVisitor(args));
handleSingleResult(result);
}),
)
.command(
'infuse [ASSEMBLY..]',
'(EXPERIMENTAL) mutates one or more assemblies by adding documentation examples to top-level types',
(command) =>
command
.positional('ASSEMBLY', {
type: 'string',
array: true,
default: [],
describe: 'Assembly or directory to mutate',
})
.option('log-file', {
alias: 'l',
type: 'string',
describe: 'Output file to store logging results. Ignored if -log is not true',
default: DEFAULT_INFUSION_RESULTS_NAME,
})
.option('cache-from', {
alias: 'C',
type: 'string',
// eslint-disable-next-line prettier/prettier
describe:
'Reuse translations from the given tablet file if the snippet and type definitions did not change',
requiresArg: true,
default: undefined,
})
.option('cache-to', {
alias: 'o',
type: 'string',
describe: 'Append all translated snippets to the given tablet file',
requiresArg: true,
default: undefined,
})
.option('cache', {
alias: 'k',
type: 'string',
describe: 'Alias for --cache-from and --cache-to together',
requiresArg: true,
default: undefined,
})
.conflicts('cache', 'cache-from')
.conflicts('cache', 'cache-to'),
wrapHandler(async (args) => {
const absAssemblies = (args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.']).map((x) => path.resolve(x));
const absCacheFrom = fmap(args.cache ?? args['cache-from'], path.resolve);
const absCacheTo = fmap(args.cache ?? args['cache-to'], path.resolve);
const result = await infuse(absAssemblies, {
logFile: args['log-file'],
cacheToFile: absCacheTo,
cacheFromFile: absCacheFrom,
});
let totalTypes = 0;
let insertedExamples = 0;
for (const [directory, map] of Object.entries(result.coverageResults)) {
const commonName = directory.split('/').pop()!;
const newCoverage = roundPercentage(map.typesWithInsertedExamples / map.types);
process.stdout.write(
`${commonName}: Added ${map.typesWithInsertedExamples} examples to ${map.types} types.\n`,
);
process.stdout.write(`${commonName}: New coverage: ${newCoverage}%.\n`);
insertedExamples += map.typesWithInsertedExamples;
totalTypes += map.types;
}
const newCoverage = roundPercentage(insertedExamples / totalTypes);
process.stdout.write(`\n\nFinal Stats:\nNew coverage: ${newCoverage}%.\n`);
}),
)
.command(
['extract [ASSEMBLY..]', '$0 [ASSEMBLY..]'],
'Extract code snippets from one or more assemblies into language tablets',
(command) =>
command
.positional('ASSEMBLY', {
type: 'string',
array: true,
default: [],
describe: 'Assembly or directory to extract from',
})
.option('output', {
type: 'string',
describe: 'Additional output file where to store translated samples (deprecated, alias for --cache-to)',
requiresArg: true,
default: undefined,
})
.option('compile', {
alias: 'c',
type: 'boolean',
describe: 'Try compiling (on by default, use --no-compile to switch off)',
default: true,
})
.option('directory', {
alias: 'd',
type: 'string',
describe: 'Working directory (for require() etc)',
})
.option('include', {
alias: 'i',
type: 'string',
array: true,
describe: 'Extract only snippets with given ids',
default: [],
})
.option('infuse', {
type: 'boolean',
describe: 'bundle this command with the infuse command',
default: false,
})
.option('fail', {
alias: 'f',
type: 'boolean',
describe: 'Fail if there are compilation errors',
default: false,
})
.option('validate-assemblies', {
type: 'boolean',
describe: 'Whether to validate loaded assemblies or not (this can be slow)',
default: false,
})
.option('cache-from', {
alias: 'C',
type: 'string',
// eslint-disable-next-line prettier/prettier
describe:
'Reuse translations from the given tablet file if the snippet and type definitions did not change',
requiresArg: true,
default: undefined,
})
.option('cache-to', {
alias: 'o',
type: 'string',
describe: 'Append all translated snippets to the given tablet file',
requiresArg: true,
default: undefined,
})
.conflicts('cache-to', 'output')
.option('cache', {
alias: 'k',
type: 'string',
describe: 'Alias for --cache-from and --cache-to together',
requiresArg: true,
default: undefined,
})
.conflicts('cache', 'cache-from')
.conflicts('cache', 'cache-to')
.option('trim-cache', {
alias: 'T',
type: 'boolean',
describe: 'Remove translations that are not referenced by any of the assemblies anymore from the cache',
})
.option('strict', {
alias: 'S',
type: 'boolean',
describe:
'Require all code samples compile, and fail if one does not. Strict mode always enables --compile and --fail',
default: false,
})
.options('loose', {
alias: 'l',
describe: 'Ignore missing fixtures and literate markdown files instead of failing',
type: 'boolean',
})
.options('compress-tablet', {
alias: 'z',
type: 'boolean',
describe: 'Compress the implicit tablet file',
default: false,
})
.options('compress-cache', {
type: 'boolean',
describe: 'Compress the cache-to file',
default: false,
})
.options('cleanup', {
type: 'boolean',
describe: 'Clean up temporary directories',
default: true,
})
.conflicts('loose', 'strict')
.conflicts('loose', 'fail'),
wrapHandler(async (args) => {
// `--strict` is short for `--compile --fail`, and we'll override those even if they're set to `false`, such as
// using `--no-(compile|fail)`, because yargs does not quite give us a better option that does not hurt CX.
if (args.strict) {
args.compile = args.c = true;
args.fail = args.f = true;
}
const absAssemblies = (args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.']).map((x) => path.resolve(x));
const absCacheFrom = fmap(args.cache ?? args['cache-from'], path.resolve);
const absCacheTo = fmap(args.cache ?? args['cache-to'] ?? args.output, path.resolve);
const extractOptions: ExtractOptions = {
compilationDirectory: args.directory,
includeCompilerDiagnostics: !!args.compile,
validateAssemblies: args['validate-assemblies'],
only: args.include,
cacheFromFile: absCacheFrom,
cacheToFile: absCacheTo,
trimCache: args['trim-cache'],
loose: args.loose,
compressTablet: args['compress-tablet'],
compressCacheToFile: args['compress-cache'],
cleanup: args.cleanup,
};
const result = args.infuse
? await extractAndInfuse(absAssemblies, extractOptions)
: await extractSnippets(absAssemblies, extractOptions);
handleDiagnostics(result.diagnostics, args.fail, result.tablet.count);
}),
)
.command(
'transliterate [ASSEMBLY..]',
'(EXPERIMENTAL) Transliterates the designated assemblies',
(command) =>
command
.positional('ASSEMBLY', {
type: 'string',
array: true,
default: [],
required: true,
describe: 'Assembly to transliterate',
})
.option('language', {
type: 'string',
array: true,
default: [],
describe: 'Language ID to transliterate to',
})
.options('strict', {
alias: 's',
conflicts: 'loose',
describe:
'Fail if an example that needs live transliteration fails to compile (which could cause incorrect transpilation results)',
type: 'boolean',
})
.options('loose', {
alias: 'l',
conflicts: 'strict',
describe: 'Ignore missing fixtures and literate markdown files instead of failing',
type: 'boolean',
})
.option('tablet', {
alias: 't',
type: 'string',
describe:
'Language tablet containing pre-translated code examples to use (these are generated by the `extract` command)',
}),
wrapHandler((args) => {
const assemblies = (args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.']).map((dir) =>
path.resolve(process.cwd(), dir),
);
const languages =
args.language.length > 0
? args.language
.map((lang) => lang.toUpperCase())
.map((lang) => {
const target = Object.entries(TargetLanguage).find(([k]) => k === lang)?.[1];
if (target == null) {
throw new Error(
`Unknown target language: ${lang}. Expected one of ${Object.keys(TargetLanguage).join(', ')}`,
);
}
return target;
})
: Object.values(TargetLanguage);
return transliterateAssembly(assemblies, languages, args);
}),
)
.command(
'trim-cache <TABLET> [ASSEMBLY..]',
'Retain only those snippets in the cache which occur in one of the given assemblies',
(command) =>
command
.positional('TABLET', {
type: 'string',
required: true,
describe: 'Language tablet to trim',
})
.positional('ASSEMBLY', {
type: 'string',
array: true,
default: [],
describe: 'Assembly or directory to search',
})
.demandOption('TABLET'),
wrapHandler(async (args) => {
await trimCache({
cacheFile: args.TABLET,
assemblyLocations: args.ASSEMBLY,
});
}),
)
.command(
'coverage [ASSEMBLY..]',
'Check the translation coverage of implicit tablets for the given assemblies',
(command) =>
command.positional('ASSEMBLY', {
type: 'string',
array: true,
default: ['.'],
describe: 'Assembly or directory to search',
}),
wrapHandler(async (args) => {
const absAssemblies = (args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.']).map((x) => path.resolve(x));
await checkCoverage(absAssemblies);
}),
)
.command(
'read <TABLET> [KEY] [LANGUAGE]',
'Display snippets in a language tablet file',
(command) =>
command
.positional('TABLET', {
type: 'string',
required: true,
describe: 'Language tablet to read',
})
.positional('KEY', {
type: 'string',
describe: 'Snippet key to read',
})
.positional('LANGUAGE', {
type: 'string',
describe: 'Language ID to read',
})
.demandOption('TABLET'),
wrapHandler(async (args) => {
await readTablet(args.TABLET, args.KEY, args.LANGUAGE);
}),
)
.command(
'configure-strict [PACKAGE]',
"Enables strict mode for a package's assembly",
(command) =>
command.positional('PACKAGE', {
type: 'string',
describe: 'The path to the package to configure',
required: false,
default: '.',
normalize: true,
}),
wrapHandler(async (args) => {
const packageJsonPath = (await fs.stat(args.PACKAGE)).isDirectory()
? path.join(args.PACKAGE, 'package.json')
: args.PACKAGE;
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
if (packageJson.jsii == null) {
console.error(
`The package in ${args.PACKAGE} does not have a jsii configuration! You can set it up using jsii-config.`,
);
process.exitCode = 1;
return Promise.resolve();
}
if (packageJson.jsii.metadata?.jsii?.rosetta?.strict) {
// Nothing to do - it's already configured, so we assert idempotent success!
return Promise.resolve();
}
const md = (packageJson.jsii.metadata = packageJson.jsii.metadata ?? {});
const mdJsii = (md.jsii = md.jsii ?? {});
const mdRosetta = (mdJsii.rosetta = mdJsii.rosetta ?? {});
mdRosetta.strict = true;
return fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}),
)
.demandCommand()
.help()
.strict() // Error on wrong command
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
.version(require('../package.json').version)
.showHelpOnFail(false).argv;
// Evaluating .argv triggers the parsing but the command gets implicitly executed,
// so we don't need the output.
Array.isArray(argv);
}