diff --git a/src/compiler.ts b/src/compiler.ts index e85793ad..22757b66 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -41,6 +41,12 @@ const LOG = log4js.getLogger('jsii/compiler'); export const DIAGNOSTICS = 'diagnostics'; export const JSII_DIAGNOSTICS_CODE = 9999; +export enum TypeScriptConfigValidationRuleSet { + STRICT = 'strict', + GENERATED = 'generated', + OFF = 'off', +} + export interface CompilerOptions { /** The information about the project to be built */ projectInfo: ProjectInfo; @@ -57,10 +63,23 @@ export interface CompilerOptions { /** Whether to add warnings for deprecated elements */ addDeprecationWarnings?: boolean; /** - * The name of the tsconfig file to generate + * The name of the tsconfig file to generate. + * Cannot be used at the same time as `typeScriptConfig`. * @default "tsconfig.json" */ generateTypeScriptConfig?: string; + /** + * The name of the tsconfig file to use. + * Cannot be used at the same time as `generateTypeScriptConfig`. + * @default - generate the tsconfig file + */ + typeScriptConfig?: string; + /** + * The ruleset to validate the provided tsconfig file against. + * Can only be used when `typeScriptConfig` is provided. + * @default TypeScriptConfigValidationRuleSet.STRICT - if `typeScriptConfig` is provided + */ + validateTypeScriptConfig?: TypeScriptConfigValidationRuleSet; /** * Whether to compress the assembly * @default false @@ -78,12 +97,21 @@ export interface TypescriptConfig { export class Compiler implements Emitter { private readonly system: ts.System; private readonly compilerHost: ts.CompilerHost; + private readonly userProvidedTypeScriptConfig: boolean; private typescriptConfig?: TypescriptConfig; private rootFiles: string[] = []; private readonly configPath: string; private readonly projectReferences: boolean; public constructor(private readonly options: CompilerOptions) { + if (options.generateTypeScriptConfig != null && options.typeScriptConfig != null) { + throw new Error( + 'Cannot use `generateTypeScriptConfig` and `typeScriptConfig` together. Provide only one of them.', + ); + } + + this.userProvidedTypeScriptConfig = Boolean(options.typeScriptConfig); + const rootDir = this.options.projectInfo.projectRoot; this.system = { ...ts.sys, @@ -102,7 +130,7 @@ export class Compiler implements Emitter { }; this.compilerHost = ts.createIncrementalCompilerHost(BASE_COMPILER_OPTIONS, this.system); - const configFileName = options.generateTypeScriptConfig ?? 'tsconfig.json'; + const configFileName = options.typeScriptConfig ?? options.generateTypeScriptConfig ?? 'tsconfig.json'; this.configPath = path.join(this.options.projectInfo.projectRoot, configFileName); @@ -192,11 +220,23 @@ export class Compiler implements Emitter { * @param files the files that were specified as input in the CLI invocation. */ private _prepareForBuild(...files: string[]) { - this.buildTypeScriptConfig(); - this.writeTypeScriptConfig(); + if (this.userProvidedTypeScriptConfig) { + this.loadTypeScriptConfig(); + this.validateTypeScriptConfig(); + } else { + this.buildTypeScriptConfig(); + this.validateTypeScriptConfig(); + this.writeTypeScriptConfig(); + } + this.rootFiles = this.determineSources(files); } + /** + * Validate the computed tsconfig against the determined ruleset + */ + validateTypeScriptConfig() {} + /** * Do a single build */ @@ -337,6 +377,13 @@ export class Compiler implements Emitter { }; } + /** + * Load the TypeScript config object from a provided file + */ + private loadTypeScriptConfig() { + this.typescriptConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf-8')); + } + /** * Creates a `tsconfig.json` file to improve the IDE experience. * diff --git a/src/jsii-diagnostic.ts b/src/jsii-diagnostic.ts index 393f776d..8601c6ea 100644 --- a/src/jsii-diagnostic.ts +++ b/src/jsii-diagnostic.ts @@ -427,7 +427,14 @@ export class JsiiDiagnostic implements ts.Diagnostic { }); ////////////////////////////////////////////////////////////////////////////// - // 4000 => 4999 -- RESERVED + // 4000 => 4999 -- TYPESCRIPT & JSII CONFIG ERRORS + + public static readonly JSII_4999_DISABLED_TSCONFIG_VALIDATION = Code.warning({ + code: 4999, + formatter: (config) => + `Validation of typescript config "${config}" is disabled. This is intended for experimental setups only. Compilation might fail or produce incompatible artifacts.`, + name: 'typescript-config/disabled-tsconfig-validation', + }); ////////////////////////////////////////////////////////////////////////////// // 5000 => 5999 -- LANGUAGE COMPATIBILITY ERRORS diff --git a/src/main.ts b/src/main.ts index 0714f49e..2e958de9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import * as log4js from 'log4js'; import { version as tsVersion } from 'typescript/package.json'; import * as yargs from 'yargs'; -import { Compiler } from './compiler'; +import { Compiler, TypeScriptConfigValidationRuleSet } from './compiler'; import { configureCategories } from './jsii-diagnostic'; import { loadProjectInfo } from './project-info'; import { emitSupportPolicyInformation } from './support'; @@ -16,6 +16,24 @@ import { enabledWarnings } from './warnings'; const warningTypes = Object.keys(enabledWarnings); +function choiceWithDesc( + choices: { [choice: string]: string }, + desc: string, +): { + choices: string[]; + desc: string; +} { + return { + choices: Object.keys(choices), + desc: [desc, ...Object.entries(choices).map(([choice, docs]) => `${choice}: ${docs}`)].join('\n'), + }; +} + +enum OPTION_GROUP { + JSII = 'jsii compiler options:', + TS = 'TypeScript config options:', +} + (async () => { await emitSupportPolicyInformation(); @@ -38,9 +56,10 @@ const warningTypes = Object.keys(enabledWarnings); desc: 'Watch for file changes and recompile automatically', }) .option('project-references', { + group: OPTION_GROUP.JSII, alias: 'r', type: 'boolean', - desc: 'Generate TypeScript project references (also [package.json].jsii.projectReferences)', + desc: 'Generate TypeScript project references (also [package.json].jsii.projectReferences)\nHas no effect if --tsconfig is provided', }) .option('fix-peer-dependencies', { type: 'boolean', @@ -49,30 +68,58 @@ const warningTypes = Object.keys(enabledWarnings); hidden: true, }) .options('fail-on-warnings', { + group: OPTION_GROUP.JSII, alias: 'Werr', type: 'boolean', desc: 'Treat warnings as errors', }) .option('silence-warnings', { + group: OPTION_GROUP.JSII, type: 'array', default: [], desc: `List of warnings to silence (warnings: ${warningTypes.join(',')})`, }) .option('strip-deprecated', { + group: OPTION_GROUP.JSII, type: 'string', desc: '[EXPERIMENTAL] Hides all @deprecated members from the API (implementations remain). If an optional file name is given, only FQNs present in the file will be stripped.', }) .option('add-deprecation-warnings', { + group: OPTION_GROUP.JSII, type: 'boolean', default: false, desc: '[EXPERIMENTAL] Injects warning statements for all deprecated elements, to be printed at runtime', }) .option('generate-tsconfig', { + group: OPTION_GROUP.TS, type: 'string', - default: 'tsconfig.json', + defaultDescription: 'tsconfig.json', desc: 'Name of the typescript configuration file to generate with compiler settings', }) + .option('tsconfig', { + group: OPTION_GROUP.TS, + alias: 'c', + type: 'string', + desc: '[EXPERIMENTAL] Use this typescript configuration file to compile the jsii project.', + }) + .conflicts('tsconfig', ['generate-tsconfig', 'project-references']) + .option('validate-tsconfig', { + group: OPTION_GROUP.TS, + ...choiceWithDesc( + { + [TypeScriptConfigValidationRuleSet.STRICT]: + 'Validates the provided config against the strict ruleset designed for maximum backwards-compatibility.', + [TypeScriptConfigValidationRuleSet.GENERATED]: + 'Enforces the same settings as used by --generate-tsconfig. Use this to stay compatible with the generated config, but have full ownership over the file.', + [TypeScriptConfigValidationRuleSet.OFF]: + 'Disables all config validation. Intended for experimental setups only. Use at your own risk.', + }, + '[EXPERIMENTAL] Validate the provided typescript configuration file against a set of rules.', + ), + default: TypeScriptConfigValidationRuleSet.STRICT, + }) .option('compress-assembly', { + group: OPTION_GROUP.JSII, type: 'boolean', default: false, desc: 'Emit a compressed version of the assembly', @@ -86,6 +133,10 @@ const warningTypes = Object.keys(enabledWarnings); async (argv) => { _configureLog4js(argv.verbose); + if (argv['generate-tsconfig'] != null && argv.tsconfig != null) { + throw new Error('Options --generate-tsconfig and --tsconfig are mutually exclusive'); + } + const projectRoot = path.normalize(path.resolve(process.cwd(), argv.PROJECT_ROOT)); const { projectInfo, diagnostics: projectInfoDiagnostics } = loadProjectInfo(projectRoot); @@ -109,6 +160,10 @@ const warningTypes = Object.keys(enabledWarnings); stripDeprecatedAllowListFile: argv['strip-deprecated'], addDeprecationWarnings: argv['add-deprecation-warnings'], generateTypeScriptConfig: argv['generate-tsconfig'], + typeScriptConfig: argv.tsconfig ?? projectInfo.packageJson.jsii?.tsconfig, + validateTypeScriptConfig: + (argv['validate-tsconfig'] as TypeScriptConfigValidationRuleSet) ?? + projectInfo.packageJson.jsii?.validateTsConfig, compressAssembly: argv['compress-assembly'], }); diff --git a/src/project-info.ts b/src/project-info.ts index 2228feea..00bc633a 100644 --- a/src/project-info.ts +++ b/src/project-info.ts @@ -7,6 +7,7 @@ import * as semver from 'semver'; import * as ts from 'typescript'; import { findDependencyDirectory } from './common/find-utils'; +import { TypeScriptConfigValidationRuleSet } from './compiler'; import { JsiiDiagnostic } from './jsii-diagnostic'; import { parsePerson, parseRepository } from './utils'; @@ -112,15 +113,22 @@ export interface PackageJson { readonly bundledDependencies?: readonly string[]; readonly jsii?: { + // main jsii config readonly diagnostics?: { readonly [id: string]: 'error' | 'warning' | 'suggestion' | 'message' }; readonly metadata?: { readonly [key: string]: unknown }; readonly targets?: { readonly [name: string]: unknown }; readonly versionFormat?: 'short' | 'full'; + // Either BYO config ... + readonly tsconfig?: string; + readonly validateTsConfig?: TypeScriptConfigValidationRuleSet; + + // ... or configure tsc here readonly excludeTypescript?: readonly string[]; readonly projectReferences?: boolean; readonly tsc?: TSCompilerOptions; + // unexpected options readonly [key: string]: unknown; };