|
import * as ts from 'typescript'; |
|
|
|
// Will be same / similar to current implementation |
|
type ProjectReflection = { projectReflection: true }; |
|
export interface ConverterResult { |
|
/** |
|
* An array containing all errors generated by the TypeScript compiler. |
|
*/ |
|
errors: ReadonlyArray<ts.Diagnostic>; |
|
|
|
/** |
|
* The resulting project reflection. |
|
*/ |
|
project: ProjectReflection; |
|
} |
|
type Logger = { |
|
info(message: string): void; |
|
write(message: string): void; |
|
error(message: string | Error | ts.Diagnostic): void |
|
}; |
|
// Same as current interfaces for now. |
|
interface NodeConverter {} |
|
interface TypeConverter {} |
|
|
|
|
|
// New / significantly changed |
|
|
|
// Plugins do `export function load(app)` with this signature. |
|
type PluginLoadFunction = (app: Application) => void; |
|
|
|
// Can be declaration-merged by plugins to add more options |
|
// Supported types: string, string[], number, boolean |
|
interface OptionValueMap { |
|
project: string; |
|
options: string; |
|
plugins: string[]; |
|
help: boolean; |
|
files: string[]; |
|
// more... |
|
} |
|
|
|
enum OptionGroups { |
|
input, |
|
output, |
|
config, |
|
other |
|
} |
|
|
|
interface OptionDeclaration<K extends keyof OptionValueMap = keyof OptionValueMap> { |
|
name: K; |
|
default: OptionValueMap[K]; |
|
help: string; |
|
group?: OptionGroups; |
|
} |
|
|
|
type OptionReadError = string // Probably should be something with more info |
|
|
|
interface OptionReader { |
|
read(declarations: OptionDeclaration[]): { options: Partial<OptionValueMap>, errors: OptionReadError[] }; |
|
} |
|
|
|
class Options { |
|
private optionMap: Partial<OptionValueMap> = {}; |
|
private options: OptionDeclaration[] = [] |
|
private readers: OptionReader[] = [] |
|
|
|
addReader(reader: OptionReader): void { |
|
if (!this.readers.includes(reader)) { |
|
this.readers.push(reader); |
|
} |
|
} |
|
|
|
removeReader(reader: OptionReader): void { |
|
const index = this.readers.indexOf(reader); |
|
if (index !== -1) { |
|
this.readers.splice(index, 1); |
|
} |
|
} |
|
|
|
read(): OptionReadError[] { |
|
const readErrors: OptionReadError[] = [] |
|
for (const reader of this.readers) { |
|
const { options, errors } = reader.read(this.options); |
|
readErrors.push(...errors); |
|
this.optionMap = { ...this.optionMap, ...options }; |
|
} |
|
return readErrors; |
|
} |
|
|
|
declareOption(option: OptionDeclaration): void { |
|
const declaration = this.options.find(d => d.name === option.name); |
|
if (declaration) { |
|
throw new Error(`An option with name '${name}' already exists.`); |
|
} |
|
this.options.push(option); |
|
} |
|
|
|
getOption<K extends keyof OptionValueMap>(option: K): OptionValueMap[K] { |
|
const declaration = this.options.find(d => d.name === option); |
|
if (!declaration) { |
|
throw new Error(`The option '${option}' does not exist.`); |
|
} |
|
return option in this.optionMap ? this.optionMap[option] : declaration.default; |
|
} |
|
|
|
setOption<K extends keyof OptionValueMap>(option: K, value: OptionValueMap[K]): void { |
|
this.optionMap[option] = value; |
|
} |
|
|
|
getHelpOutput(): string { |
|
return [ |
|
'Usage: typedoc --out ./docs ./src', |
|
'', |
|
'Input', |
|
...this.printGroup(OptionGroups.input), |
|
'', |
|
'Output', |
|
...this.printGroup(OptionGroups.output), |
|
'', |
|
'Config', |
|
...this.printGroup(OptionGroups.config), |
|
'', |
|
'Other', |
|
...this.printGroup(OptionGroups.other) |
|
].join('\n') |
|
} |
|
|
|
private printGroup(group: OptionGroups): string[] { |
|
return this.options |
|
.filter(opt => opt.group === group || group === OptionGroups.other) |
|
.map(({ name, help }) => ` --${name} ${help}`); |
|
} |
|
} |
|
|
|
type GeneratorConstructor = new (app: Application) => OutputGenerator |
|
interface OutputGenerator { |
|
/** |
|
* Returns true if [[generate]] should be called. |
|
*/ |
|
enabled(): boolean; |
|
generate(project: ProjectReflection): void; |
|
} |
|
|
|
class Output { |
|
private generators: OutputGenerator[] = []; |
|
|
|
addGenerator(generator: OutputGenerator): void { |
|
if (!this.generators.includes(generator)) { |
|
this.generators.push(generator); |
|
} |
|
} |
|
|
|
removeGenerator(generator: OutputGenerator): void { |
|
const index = this.generators.indexOf(generator); |
|
if (index !== -1) { |
|
this.generators.splice(index, 1); |
|
} |
|
} |
|
|
|
/** |
|
* To keep previous behavior, erroring if no output is produced. |
|
*/ |
|
willCreateOutput(): boolean { |
|
return this.generators.some(gen => gen.enabled()); |
|
} |
|
|
|
generate(project: ProjectReflection) { |
|
for (const gen of this.generators.filter(gen => gen.enabled())) { |
|
gen.generate(project); |
|
} |
|
} |
|
} |
|
|
|
interface Converter { |
|
addNodeConverter(converter: NodeConverter): void; |
|
removeNodeConverter(converter: NodeConverter): void; |
|
addTypeConverter(converter: TypeConverter): void; |
|
removeTypeConverter(converter: NodeConverter): void; |
|
|
|
convert(files: string[], options?: ts.CreateProgramOptions): ConverterResult; |
|
} |
|
|
|
|
|
class Application { |
|
readonly options = new Options(); |
|
readonly output = new Output(); |
|
readonly converter: Converter; |
|
logger: Logger; |
|
|
|
constructor() { |
|
this.options.declareOption({ |
|
name: 'plugins', |
|
help: 'Specify the plugins to load.', |
|
default: [] |
|
}); |
|
this.options.declareOption({ |
|
name: 'files', |
|
help: 'Specify the input files', |
|
default: [], |
|
group: OptionGroups.input |
|
}); |
|
} |
|
|
|
/** |
|
* Loads the plugins currently specified by the plugins option. |
|
*/ |
|
loadPlugins() { |
|
for (const plugin of this.options.getOption('plugins')) { |
|
// Basically what the PluginHost does now. |
|
try { |
|
const instance = require(plugin); |
|
instance.load(this); |
|
this.logger.info(`Loaded plugin ${plugin}`); |
|
} catch (error) { |
|
this.logger.error(`The plugin ${plugin} could not be loaded.`); |
|
this.logger.error(error); |
|
} |
|
} |
|
} |
|
|
|
convert(options?: ts.CompilerOptions): ConverterResult { |
|
return this.converter.convert(this.options.getOption('files')); |
|
} |
|
|
|
generateOutput(project: ProjectReflection) { |
|
this.output.generate(project); |
|
} |
|
} |
|
|
|
// Read cli arguments |
|
class ArgvReader implements OptionReader { |
|
read(declarations: OptionDeclaration[]): { options: Partial<OptionValueMap>; errors: string[]; } { |
|
return { // Todo, pass off to yargs or similar? |
|
options: {}, |
|
errors: [] |
|
} |
|
} |
|
} |
|
|
|
// Read tsconfig.json, pass off to typescript? |
|
class TsConfigReader extends ArgvReader { |
|
constructor(options: Options) { |
|
super(); |
|
options.declareOption({ |
|
name: 'project', |
|
help: 'The tsconfig.json file to read for options', |
|
default: 'tsconfig.json', |
|
group: OptionGroups.config |
|
}); |
|
} |
|
}; |
|
|
|
// Read typedoc.js / typedoc.json |
|
class TypeDocReader extends ArgvReader { |
|
constructor(options: Options) { |
|
super(); |
|
options.declareOption({ |
|
name: 'options', |
|
help: 'Specify a js or json option file that should be loaded.', |
|
default: 'typedoc.js', |
|
group: OptionGroups.config |
|
}); |
|
} |
|
}; |
|
|
|
function runCli(): never { |
|
const app = new Application(); |
|
|
|
const argvReader = new ArgvReader(); |
|
// First, so tsconfig reader and typedoc readers take --project, --config from argv |
|
app.options.addReader(argvReader); |
|
app.options.addReader(new TsConfigReader(app.options)); |
|
app.options.addReader(new TypeDocReader(app.options)); |
|
|
|
// Not included yet. |
|
// app.output.addGenerator(new HtmlGenerator(app)); |
|
// app.output.addGenerator(new JsonGenerator(app)); |
|
|
|
app.options.read(); // Ignore errors until we have loaded plugins. |
|
app.loadPlugins(); |
|
|
|
// Put argv reader last as it should override tsconfig/typedoc options |
|
app.options.removeReader(argvReader); |
|
app.options.addReader(argvReader); |
|
|
|
// Re-read options, report errors this time |
|
const optionErrors = app.options.read(); |
|
optionErrors.forEach(err => app.logger.error(err)); |
|
|
|
if (app.options.getOption('help')) { |
|
app.logger.write(app.options.getHelpOutput()); |
|
return process.exit(0); |
|
} |
|
|
|
|
|
if (!app.output.willCreateOutput()) { |
|
app.logger.error('No output mode was specified.'); |
|
return process.exit(1); |
|
} |
|
|
|
const { errors, project } = app.convert(); |
|
|
|
if (errors.length) { |
|
errors.forEach(err => app.logger.error(err)); |
|
return process.exit(2); |
|
} |
|
|
|
app.generateOutput(project); |
|
|
|
return process.exit(0); |
|
} |