From 59f1aa775c8eb7a3722e825e204b5cc14a5da51d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 22 Nov 2023 16:18:30 -0800 Subject: [PATCH] Rewrite nested option parsing --- src/compiler/commandLineParser.ts | 175 ++++++++++-------- src/compiler/diagnosticMessages.json | 4 + .../transformers/module/esnextAnd2015.ts | 2 +- src/compiler/types.ts | 19 +- src/compiler/utilities.ts | 73 ++++---- src/executeCommandLine/executeCommandLine.ts | 14 +- src/harness/harnessIO.ts | 33 ++-- src/harness/incrementalUtils.ts | 2 +- src/server/editorServices.ts | 6 +- src/testRunner/unittests/config/showConfig.ts | 5 +- src/testRunner/unittests/helpers/baseline.ts | 2 +- .../unittests/reuseProgramStructure.ts | 2 +- tests/baselines/reference/api/typescript.d.ts | 14 +- .../moduleExportNonStructured.errors.txt | 36 ---- .../reference/nodeNextModuleResolution1.js | 3 +- .../nodeNextModuleResolution2.errors.txt | 18 -- .../reference/nodeNextModuleResolution2.js | 3 +- ...ons module-kind is out-of-range.errors.txt | 2 - ...nd is out-of-range.oldTranspile.errors.txt | 2 - ...s target-script is out-of-range.errors.txt | 2 - ...pt is out-of-range.oldTranspile.errors.txt | 2 - .../conformance/module/esbuildInterop.ts | 6 +- 22 files changed, 217 insertions(+), 208 deletions(-) delete mode 100644 tests/baselines/reference/moduleExportNonStructured.errors.txt delete mode 100644 tests/baselines/reference/nodeNextModuleResolution2.errors.txt diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index e4afc56e870..e543b6bfe04 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -53,6 +53,7 @@ import { getFileMatcherPatterns, getLocaleSpecificMessage, getNormalizedAbsolutePath, + getOwnKeys, getRegexFromPattern, getRegularExpressionForWildcard, getRegularExpressionsForWildcards, @@ -85,7 +86,9 @@ import { ModuleFormatDetectionKind, ModuleFormatInteropKind, ModuleKind, + ModuleOptions, ModuleResolutionKind, + NestedCompilerOption, NewLineKind, Node, NodeArray, @@ -566,6 +569,7 @@ const moduleSubOptionDeclarations: readonly CommandLineOption[] = [ category: Diagnostics.Modules, description: Diagnostics.Specify_defaults_for_module_options_suited_for_common_runtimes_and_bundlers, defaultValueDescription: undefined, + getParentOption: getModuleOptionDeclaration, }, { name: "formatDetection", @@ -581,6 +585,7 @@ const moduleSubOptionDeclarations: readonly CommandLineOption[] = [ category: Diagnostics.Modules, description: Diagnostics.Specify_how_files_are_determined_to_be_ECMAScript_modules_or_CommonJS_modules, defaultValueDescription: Diagnostics.node16_when_module_is_node16_nodenext_when_module_is_nodenext_none_otherwise, + getParentOption: getModuleOptionDeclaration, }, { name: "formatInterop", @@ -594,6 +599,7 @@ const moduleSubOptionDeclarations: readonly CommandLineOption[] = [ category: Diagnostics.Modules, description: Diagnostics.Specify_the_target_runtime_s_rules_for_ESM_CommonJS_interoperation, defaultValueDescription: Diagnostics.node16_when_module_is_node16_nodenext_when_module_is_nodenext_babel_otherwise, + getParentOption: getModuleOptionDeclaration, }, { name: "emit", @@ -602,33 +608,17 @@ const moduleSubOptionDeclarations: readonly CommandLineOption[] = [ category: Diagnostics.Modules, description: Diagnostics.Specify_what_module_code_is_generated, defaultValueDescription: undefined, + getParentOption: getModuleOptionDeclaration, }, ]; /** @internal */ -export const moduleOptionDeclaration: CommandLineOptionOfObjectOrShorthandType = { +export const moduleOptionDeclaration = { name: "module", shortName: "m", - // HEAD type: "objectOrShorthand", shorthandType: moduleKindMap, elementOptions: commandLineOptionsToMap(moduleSubOptionDeclarations), - // - type: new Map(Object.entries({ - none: ModuleKind.None, - commonjs: ModuleKind.CommonJS, - amd: ModuleKind.AMD, - system: ModuleKind.System, - umd: ModuleKind.UMD, - es6: ModuleKind.ES2015, - es2015: ModuleKind.ES2015, - es2020: ModuleKind.ES2020, - es2022: ModuleKind.ES2022, - esnext: ModuleKind.ESNext, - node16: ModuleKind.Node16, - nodenext: ModuleKind.NodeNext, - })), - // parent of 19e60f1a19 (Fix showConfig) affectsSourceFile: true, affectsModuleResolution: true, affectsEmit: true, @@ -638,7 +628,11 @@ export const moduleOptionDeclaration: CommandLineOptionOfObjectOrShorthandType = category: Diagnostics.Modules, description: Diagnostics.Specify_what_module_code_is_generated, defaultValueDescription: undefined, -}; +} satisfies CommandLineOptionOfObjectOrShorthandType; + +function getModuleOptionDeclaration() { + return moduleOptionDeclaration; +} const commandOptionsWithoutBuild: CommandLineOption[] = [ // CommandLine only options @@ -1228,31 +1222,6 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [ category: Diagnostics.Modules, description: Diagnostics.Conditions_to_set_in_addition_to_the_resolver_specific_defaults_when_resolving_imports, }, - { - name: "moduleFormatDetection", - type: new Map(Object.entries({ - none: ModuleFormatDetectionKind.None, - bundler: ModuleFormatDetectionKind.Bundler, - node16: ModuleFormatDetectionKind.Node16, - nodenext: ModuleFormatDetectionKind.NodeNext, - })), - affectsModuleResolution: true, - category: Diagnostics.Modules, - description: Diagnostics.Specify_how_files_are_determined_to_be_ECMAScript_modules_or_CommonJS_modules, - defaultValueDescription: Diagnostics.node16_when_module_is_node16_nodenext_when_module_is_nodenext_none_otherwise, - }, - { - name: "moduleFormatInterop", - type: new Map(Object.entries({ - babel: ModuleFormatInteropKind.Babel, - bundlernode: ModuleFormatInteropKind.BundlerNode, - node16: ModuleFormatInteropKind.Node16, - nodenext: ModuleFormatInteropKind.NodeNext, - })), - category: Diagnostics.Modules, - description: Diagnostics.Specify_the_target_runtime_s_rules_for_ESM_CommonJS_interoperation, - defaultValueDescription: Diagnostics.node16_when_module_is_node16_nodenext_when_module_is_nodenext_babel_otherwise, - }, // Source Maps { @@ -1770,10 +1739,16 @@ export function createOptionNameMap(optionDeclarations: readonly CommandLineOpti const optionsNameMap = new Map(); const shortOptionNames = new Map(); forEach(optionDeclarations, option => { - optionsNameMap.set(option.name.toLowerCase(), option); + const lowerCaseName = option.name.toLowerCase(); + optionsNameMap.set(lowerCaseName, option); if (option.shortName) { shortOptionNames.set(option.shortName, option.name); } + if (option.type === "objectOrShorthand") { + forEachEntry(option.elementOptions, (subOption, subOptionName) => { + optionsNameMap.set(`${lowerCaseName}.${subOptionName.toLowerCase()}`, subOption); + }); + } }); return { optionsNameMap, shortOptionNames }; @@ -1806,14 +1781,15 @@ export function createCompilerDiagnosticForInvalidCustomType(opt: CommandLineOpt return createDiagnosticForInvalidCustomType(opt, createCompilerDiagnostic); } -function createDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType, createDiagnostic: (message: DiagnosticMessage, ...args: DiagnosticArguments) => Diagnostic): Diagnostic { - const namesOfType = arrayFrom(opt.type.keys()); +function createDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType | CommandLineOptionOfObjectOrShorthandType & { shorthandType: CommandLineOptionOfCustomType["type"]; }, createDiagnostic: (message: DiagnosticMessage, ...args: DiagnosticArguments) => Diagnostic): Diagnostic { + const type = opt.type === "objectOrShorthand" ? opt.shorthandType : opt.type; + const namesOfType = arrayFrom(type.keys()); const stringNames = (opt.deprecatedKeys ? namesOfType.filter(k => !opt.deprecatedKeys!.has(k)) : namesOfType).map(key => `'${key}'`).join(", "); return createDiagnostic(Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, stringNames); } /** @internal */ -export function parseCustomTypeOption(opt: CommandLineOptionOfCustomType, value: string | undefined, errors: Diagnostic[]) { +export function parseCustomTypeOption(opt: CommandLineOptionOfCustomType | CommandLineOptionOfObjectOrShorthandType & { shorthandType: CommandLineOptionOfCustomType["type"]; }, value: string | undefined, errors: Diagnostic[]) { return convertJsonOptionOfCustomType(opt, (value ?? "").trim(), errors); } @@ -1992,44 +1968,51 @@ function parseOptionValue( errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); } + const parentOption = opt.getParentOption?.(); + const [base = {}, name] = parentOption ? [options[parentOption.name] as NestedCompilerOption, opt.name] : [options, opt.name]; + if (parentOption) { + options[parentOption.name] = base as NestedCompilerOption; + } + if (args[i] !== "null") { - switch (opt.type) { + switch (opt.type === "objectOrShorthand" ? opt.shorthandType : opt.type) { case "number": - options[opt.name] = validateJsonOptionValue(opt, parseInt(args[i]), errors); + base[name] = validateJsonOptionValue(opt, parseInt(args[i]), errors); i++; break; case "boolean": // boolean flag has optional value true, false, others const optValue = args[i]; - options[opt.name] = validateJsonOptionValue(opt, optValue !== "false", errors); + base[name] = validateJsonOptionValue(opt, optValue !== "false", errors); // consume next argument as boolean flag value if (optValue === "false" || optValue === "true") { i++; } break; case "string": - options[opt.name] = validateJsonOptionValue(opt, args[i] || "", errors); + base[name] = validateJsonOptionValue(opt, args[i] || "", errors); i++; break; case "list": - const result = parseListTypeOption(opt, args[i], errors); - options[opt.name] = result || []; + const result = parseListTypeOption(opt as CommandLineOptionOfListType, args[i], errors); + base[name] = result || []; if (result) { i++; } break; + case "object": case "listOrElement": - Debug.fail("listOrElement not supported here"); + Debug.fail("listOrElement, object, and objectOrShorthand not supported here"); break; // If not a primitive, the possible types are specified in what is effectively a map of options. default: - options[opt.name] = parseCustomTypeOption(opt as CommandLineOptionOfCustomType, args[i], errors); + base[name] = parseCustomTypeOption(opt as CommandLineOptionOfCustomType | CommandLineOptionOfObjectOrShorthandType & { shorthandType: CommandLineOptionOfCustomType["type"]; }, args[i], errors); i++; break; } } else { - options[opt.name] = undefined; + base[name] = undefined; i++; } } @@ -2528,18 +2511,25 @@ function getCompilerOptionValueTypeString(option: CommandLineOption): string { function isCompilerOptionsValue(option: CommandLineOption | undefined, value: any): value is CompilerOptionsValue { if (option) { if (isNullOrUndefined(value)) return !option.disallowNullOrUndefined; // All options are undefinable/nullable - if (option.type === "list") { - return isArray(value); - } if (option.type === "listOrElement") { - return isArray(value) || isCompilerOptionsValue(option.element, value); + return isCompilerOptionsValueWorker("list", value) || isCompilerOptionsValue(option.element, value); } - const expectedType = isString(option.type) ? option.type : "string"; - return typeof value === expectedType; + if (option.type === "objectOrShorthand") { + return isCompilerOptionsValueWorker(option.shorthandType, value) || typeof value === "object"; + } + return isCompilerOptionsValueWorker(option.type, value); } return false; } +function isCompilerOptionsValueWorker(type: CommandLineOption["type"], value: any): value is CompilerOptionsValue { + if (type === "list") { + return isArray(value); + } + const expectedType = isString(type) ? type : "string"; + return typeof value === expectedType; +} + /** @internal */ export interface TSConfig { compilerOptions: CompilerOptions; @@ -2648,6 +2638,8 @@ function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption case "list": case "listOrElement": return getCustomTypeMapOfCommandLineOption(optionDefinition.element); + case "objectOrShorthand": + return typeof optionDefinition.shorthandType === "object" ? optionDefinition.shorthandType : undefined; default: return optionDefinition.type; } @@ -3340,6 +3332,7 @@ function parseOwnConfigOfJsonSourceFile( const options = getDefaultCompilerOptions(configFileName); let typeAcquisition: TypeAcquisition | undefined; let watchOptions: WatchOptions | undefined; + let moduleOption: ModuleOptions | undefined; let extendedConfigPath: string | string[] | undefined; let rootCompilerOptions: PropertyName[] | undefined; @@ -3364,7 +3357,7 @@ function parseOwnConfigOfJsonSourceFile( keyText: string, value: any, propertyAssignment: PropertyAssignment, - parentOption: TsConfigOnlyOption | undefined, + parentOption: TsConfigOnlyOption | CommandLineOptionOfObjectOrShorthandType | undefined, option: CommandLineOption | undefined, ) { // Ensure value is verified except for extends which is handled in its own way for error reporting @@ -3375,6 +3368,7 @@ function parseOwnConfigOfJsonSourceFile( if (parentOption === compilerOptionsDeclaration) currentOption = options; else if (parentOption === watchOptionsDeclaration) currentOption = watchOptions ??= {}; else if (parentOption === typeAcquisitionDeclaration) currentOption = typeAcquisition ??= getDefaultTypeAcquisition(configFileName); + else if (parentOption === moduleOptionDeclaration) currentOption = moduleOption ??= {}; else Debug.fail("Unknown option"); currentOption[option.name] = value; } @@ -3587,6 +3581,29 @@ export function convertJsonOption( convertJsonOptionOfListType(opt, value, basePath, errors, propertyAssignment, valueExpression as ArrayLiteralExpression | undefined, sourceFile) : convertJsonOption(opt.element, value, basePath, errors, propertyAssignment, valueExpression, sourceFile); } + else if (optType === "objectOrShorthand") { + if (!value || typeof value !== "object") { + if (!isString(opt.shorthandType)) { + return convertJsonOptionOfCustomType(opt as CommandLineOptionOfObjectOrShorthandType & { shorthandType: CommandLineOptionOfCustomType["type"]; }, value as string, errors, valueExpression, sourceFile); + } + const validatedValue = validateJsonOptionValue(opt, value, errors, valueExpression, sourceFile); + return isNullOrUndefined(validatedValue) ? validatedValue : normalizeNonListOptionValue(opt, basePath, validatedValue); + } + else { + // const result: NestedCompilerOption = {}; + // for (const id in value as NestedCompilerOption) { + // const property = (value as NestedCompilerOption)[id]; + // const subOption = opt.elementOptions.get(id); + // if (subOption) { + // result[opt.name] = convertJsonOption(opt, property, basePath, errors, propertyAssignment, valueExpression, sourceFile); + // } + // else { + // errors.push(createUnknownOptionError(id, opt.extraKeyDiagnostics, /*unknownOptionErrorText*/ undefined, propertyAssignment?.name, sourceFile)); + // } + // } + return {}; + } + } else if (!isString(opt.type)) { return convertJsonOptionOfCustomType(opt as CommandLineOptionOfCustomType, value as string, errors, valueExpression, sourceFile); } @@ -3623,7 +3640,7 @@ function validateJsonOptionValue( } function convertJsonOptionOfCustomType( - opt: CommandLineOptionOfCustomType, + opt: CommandLineOptionOfCustomType | CommandLineOptionOfObjectOrShorthandType & { shorthandType: CommandLineOptionOfCustomType["type"]; }, value: string, errors: Diagnostic[], valueExpression?: Expression, @@ -3631,7 +3648,8 @@ function convertJsonOptionOfCustomType( ) { if (isNullOrUndefined(value)) return undefined; const key = value.toLowerCase(); - const val = opt.type.get(key); + const type = opt.type === "objectOrShorthand" ? opt.shorthandType : opt.type; + const val = type.get(key); if (val !== undefined) { return validateJsonOptionValue(opt, val, errors, valueExpression, sourceFile); } @@ -4026,10 +4044,18 @@ export function convertCompilerOptionsForTelemetry(opts: CompilerOptions): Compi } function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} | undefined { - if (value === undefined) return value; - switch (option.type) { + // eslint-disable-next-line no-null/no-null + if (value === undefined || value === null) return value; + const type = option.type === "objectOrShorthand" && typeof value !== "object" ? option.shorthandType : option.type; + switch (type) { case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "". return ""; + case "objectOrShorthand": + return getOwnKeys(value).reduce((acc: any, key) => { + const subOption = (option as CommandLineOptionOfObjectOrShorthandType).elementOptions.get(key.toLowerCase()); + acc[key] = subOption ? getOptionValueWithEmptyStrings(value[key], subOption) : ""; + return acc; + }, {}); case "string": // Could be any arbitrary string -- use empty string instead. return ""; case "number": // Allow numbers, but be sure to check it's actually a number. @@ -4037,13 +4063,13 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): case "boolean": return typeof value === "boolean" ? value : ""; case "listOrElement": - if (!isArray(value)) return getOptionValueWithEmptyStrings(value, option.element); + if (!isArray(value)) return getOptionValueWithEmptyStrings(value, (option as CommandLineOptionOfListType).element); // fall through to list case "list": - const elementType = option.element; + const elementType = (option as CommandLineOptionOfListType).element; return isArray(value) ? mapDefined(value, v => getOptionValueWithEmptyStrings(v, elementType)) : ""; default: - return forEachEntry(option.type, (optionEnumValue, optionStringValue) => { + return forEachEntry(type, (optionEnumValue, optionStringValue) => { if (optionEnumValue === value) { return optionStringValue; } @@ -4052,7 +4078,8 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): } function getDefaultValueForOption(option: CommandLineOption): {} { - switch (option.type) { + const type = option.type === "objectOrShorthand" ? option.shorthandType : option.type; + switch (type) { case "number": return 1; case "boolean": @@ -4063,11 +4090,11 @@ function getDefaultValueForOption(option: CommandLineOption): {} { case "list": return []; case "listOrElement": - return getDefaultValueForOption(option.element); + return getDefaultValueForOption((option as CommandLineOptionOfListType).element); case "object": return {}; default: - const value = firstOrUndefinedIterator(option.type.keys()); + const value = firstOrUndefinedIterator((option as CommandLineOptionOfCustomType).type.keys()); if (value !== undefined) return value; return Debug.fail("Expected 'option.type' to have entries."); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8dd4e917736..79bdf46ed76 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -6223,6 +6223,10 @@ "category": "Message", "code": 6806 }, + "Specify defaults for module options suited for common runtimes and bundlers.": { + "category": "Message", + "code": 6807 + }, "one of:": { "category": "Message", diff --git a/src/compiler/transformers/module/esnextAnd2015.ts b/src/compiler/transformers/module/esnextAnd2015.ts index b99185231a8..3ff92a21683 100644 --- a/src/compiler/transformers/module/esnextAnd2015.ts +++ b/src/compiler/transformers/module/esnextAnd2015.ts @@ -245,7 +245,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S function visitExportDeclaration(node: ExportDeclaration) { // `export * as ns` only needs to be transformed in ES2015 - if (compilerOptions.module !== undefined && compilerOptions.module > ModuleKind.ES2015) { + if (getEmitModuleKind(compilerOptions) > ModuleKind.ES2015) { return node; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 7d731c5a20f..3fdb74b6b27 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -7090,7 +7090,8 @@ export enum PollingWatchKind { FixedChunkSize, } -export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; +export type NestedCompilerOption = ModuleOptions; +export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | NestedCompilerOption | PluginImport[] | ProjectReference[] | null | undefined; export interface CompilerOptions { /** @internal */ all?: boolean; @@ -7156,9 +7157,7 @@ export interface CompilerOptions { locale?: string; mapRoot?: string; maxNodeModuleJsDepth?: number; - module?: ModuleKind; - moduleFormatDetection?: ModuleFormatDetectionKind; - moduleFormatInterop?: ModuleFormatInteropKind; + module?: ModuleKind | ModuleOptions; moduleResolution?: ModuleResolutionKind; moduleSuffixes?: string[]; moduleDetection?: ModuleDetectionKind; @@ -7297,6 +7296,14 @@ export enum ModuleFormatInteropKind { NodeNext = 199, } +export interface ModuleOptions { + preset?: ModuleKind; + formatDetection?: ModuleFormatDetectionKind; + formatInterop?: ModuleFormatInteropKind; + emit?: ModuleKind; + [option: string]: CompilerOptionsValue | undefined; +} + export const enum JsxEmit { None = 0, Preserve = 1, @@ -7441,6 +7448,7 @@ export interface CommandLineOptionBase { transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid disallowNullOrUndefined?: true; // If set option does not allow setting null + getParentOption?: () => CommandLineOptionOfObjectOrShorthandType; } /** @internal */ @@ -7489,13 +7497,14 @@ export interface TsConfigOnlyOption extends CommandLineOptionBase { extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics; } -/** @interface */ +/** @internal */ export interface CommandLineOptionOfObjectOrShorthandType extends CommandLineOptionBase { type: "objectOrShorthand"; shorthandType: "string" | "number" | "boolean" | Map; defaultValueDescription?: string | number | boolean | DiagnosticMessage; elementOptions: Map; extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics; + deprecatedKeys?: Set; } /** @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 33f0be9dd22..964db9543c5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8560,7 +8560,10 @@ export function getEmitScriptTarget(compilerOptions: { module?: CompilerOptions[ } /** @internal */ -export function getEmitModuleKind(compilerOptions: { module?: CompilerOptions["module"]; target?: CompilerOptions["target"]; }) { +export function getEmitModuleKind(compilerOptions: { module?: CompilerOptions["module"]; target?: CompilerOptions["target"]; }): ModuleKind { + if (typeof compilerOptions.module === "object" && compilerOptions.module.emit !== undefined) { + return compilerOptions.module.emit; + } return typeof compilerOptions.module === "number" ? compilerOptions.module : getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015 ? ModuleKind.ES2015 : ModuleKind.CommonJS; @@ -8571,6 +8574,44 @@ export function emitModuleKindIsNonNodeESM(moduleKind: ModuleKind) { return moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext; } +/** @internal */ +export function getModulePreset(compilerOptions: CompilerOptions): ModuleKind | undefined { + if (typeof compilerOptions.module === "object") { + return compilerOptions.module.preset; + } + return compilerOptions.module; +} + +/** @internal */ +export function getModuleFormatDetectionKind(compilerOptions: CompilerOptions): ModuleFormatDetectionKind { + if (typeof compilerOptions.module === "object" && compilerOptions.module.formatDetection !== undefined) { + return compilerOptions.module.formatDetection; + } + switch (getModulePreset(compilerOptions)) { + case ModuleKind.Node16: + return ModuleFormatDetectionKind.Node16; + case ModuleKind.NodeNext: + return ModuleFormatDetectionKind.NodeNext; + default: + return ModuleFormatDetectionKind.None; + } +} + +/** @internal */ +export function getModuleFormatInteropKind(compilerOptions: CompilerOptions): ModuleFormatInteropKind { + if (typeof compilerOptions.module === "object" && compilerOptions.module.formatInterop !== undefined) { + return compilerOptions.module.formatInterop; + } + switch (getModulePreset(compilerOptions)) { + case ModuleKind.Node16: + return ModuleFormatInteropKind.Node16; + case ModuleKind.NodeNext: + return ModuleFormatInteropKind.NodeNext; + default: + return ModuleFormatInteropKind.Babel; + } +} + /** @internal */ export function getEmitModuleResolutionKind(compilerOptions: CompilerOptions) { let moduleResolution = compilerOptions.moduleResolution; @@ -8593,36 +8634,6 @@ export function getEmitModuleResolutionKind(compilerOptions: CompilerOptions) { return moduleResolution; } -/** @internal */ -export function getModuleFormatDetectionKind(compilerOptions: CompilerOptions): ModuleFormatDetectionKind { - if (compilerOptions.moduleFormatDetection !== undefined) { - return compilerOptions.moduleFormatDetection; - } - switch (getEmitModuleKind(compilerOptions)) { - case ModuleKind.Node16: - return ModuleFormatDetectionKind.Node16; - case ModuleKind.NodeNext: - return ModuleFormatDetectionKind.NodeNext; - default: - return ModuleFormatDetectionKind.None; - } -} - -/** @internal */ -export function getModuleFormatInteropKind(compilerOptions: CompilerOptions): ModuleFormatInteropKind { - if (compilerOptions.moduleFormatInterop !== undefined) { - return compilerOptions.moduleFormatInterop; - } - switch (getEmitModuleKind(compilerOptions)) { - case ModuleKind.Node16: - return ModuleFormatInteropKind.Node16; - case ModuleKind.NodeNext: - return ModuleFormatInteropKind.NodeNext; - default: - return ModuleFormatInteropKind.Babel; - } -} - /** @internal */ export function getEmitModuleDetectionKind(options: CompilerOptions) { return options.moduleDetection || diff --git a/src/executeCommandLine/executeCommandLine.ts b/src/executeCommandLine/executeCommandLine.ts index 94b99fb6316..f5383818f44 100644 --- a/src/executeCommandLine/executeCommandLine.ts +++ b/src/executeCommandLine/executeCommandLine.ts @@ -8,6 +8,9 @@ import { CharacterCodes, combinePaths, CommandLineOption, + CommandLineOptionOfCustomType, + CommandLineOptionOfListType, + CommandLineOptionOfObjectOrShorthandType, compareStringsCaseInsensitive, CompilerOptions, contains, @@ -377,15 +380,16 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign function getPossibleValues(option: CommandLineOption) { let possibleValues: string; - switch (option.type) { + const type = option.type === "objectOrShorthand" ? option.shorthandType : option.type; + switch (type) { case "string": case "number": case "boolean": - possibleValues = option.type; + possibleValues = type; break; case "list": case "listOrElement": - possibleValues = getPossibleValues(option.element); + possibleValues = getPossibleValues((option as CommandLineOptionOfListType).element); break; case "object": possibleValues = ""; @@ -394,8 +398,8 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign // Map // Group synonyms: es6/es2015 const inverted: { [value: string]: string[]; } = {}; - option.type.forEach((value, name) => { - if (!option.deprecatedKeys?.has(name)) { + type.forEach((value, name) => { + if (!(option as CommandLineOptionOfCustomType | CommandLineOptionOfObjectOrShorthandType).deprecatedKeys?.has(name)) { (inverted[value] ||= []).push(name); } }); diff --git a/src/harness/harnessIO.ts b/src/harness/harnessIO.ts index 298d8310161..e01b2dc8e78 100644 --- a/src/harness/harnessIO.ts +++ b/src/harness/harnessIO.ts @@ -332,16 +332,15 @@ export namespace Compiler { { name: "fullEmitPaths", type: "boolean", defaultValueDescription: false }, ]; - let optionsIndex: Map; + let harnessOptionsMap: Map; function getCommandLineOption(name: string): ts.CommandLineOption | undefined { - if (!optionsIndex) { - optionsIndex = new Map(); - const optionDeclarations = harnessOptionDeclarations.concat(ts.optionDeclarations); - for (const option of optionDeclarations) { - optionsIndex.set(option.name.toLowerCase(), option); + if (!harnessOptionsMap) { + harnessOptionsMap = new Map(); + for (const option of harnessOptionDeclarations) { + harnessOptionsMap.set(option.name.toLowerCase(), option); } } - return optionsIndex.get(name.toLowerCase()); + return harnessOptionsMap.get(name.toLowerCase()) ?? ts.getOptionFromName(name); } export function setCompilerOptionsFromHarnessSetting(settings: TestCaseParser.CompilerSettings, options: ts.CompilerOptions & HarnessOptions): void { @@ -357,7 +356,13 @@ export namespace Compiler { const option = getCommandLineOption(name); if (option) { const errors: ts.Diagnostic[] = []; - options[option.name] = optionValue(option, value, errors); + const parentOption = option.getParentOption?.(); + const [base, name] = parentOption ? [(options[parentOption.name] || {}) as ts.NestedCompilerOption, option.name] : [options, option.name]; + if (parentOption) { + options[parentOption.name] = base as ts.NestedCompilerOption; + } + + base[name] = optionValue(option, value, errors); if (errors.length > 0) { throw new Error(`Unknown value '${value}' for compiler option '${name}'.`); } @@ -1109,10 +1114,14 @@ let booleanVaryByStarSettingValues: Map | undefined; function getVaryByStarSettingValues(varyBy: string): ReadonlyMap | undefined { const option = ts.forEach(ts.optionDeclarations, decl => ts.equateStringsCaseInsensitive(decl.name, varyBy) ? decl : undefined); if (option) { - if (typeof option.type === "object") { - return option.type; + return getVaryByValuesByType(option.type === "objectOrShorthand" ? option.shorthandType : option.type); + } + + function getVaryByValuesByType(type: ts.CommandLineOption["type"]) { + if (typeof type === "object") { + return type; } - if (option.type === "boolean") { + if (type === "boolean") { return booleanVaryByStarSettingValues || (booleanVaryByStarSettingValues = new Map(Object.entries({ true: 1, false: 0, @@ -1178,7 +1187,7 @@ export namespace TestCaseParser { } // Regex for parsing options in the format "@Alpha: Value of any sort" - const optionRegex = /^[/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines + const optionRegex = /^[/]{2}\s*@([\w.]+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines const linkRegex = /^[/]{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)/gm; // multiple matches on multiple lines export function parseSymlinkFromTest(line: string, symlinks: vfs.FileSet | undefined, absoluteRootDir?: string) { diff --git a/src/harness/incrementalUtils.ts b/src/harness/incrementalUtils.ts index c35ffdd9a69..090bc66c15f 100644 --- a/src/harness/incrementalUtils.ts +++ b/src/harness/incrementalUtils.ts @@ -128,7 +128,7 @@ function getResolutionCacheDetails> { const map = new Map>(); for (const option of commandLineOptions) { - if (typeof option.type === "object") { - const optionMap = option.type as Map; + const optionMap = (typeof option.type === "object" ? option.type : + option.type === "objectOrShorthand" && typeof option.shorthandType === "object" ? option.shorthandType : + undefined) as Map | undefined; + if (optionMap) { // verify that map contains only numbers optionMap.forEach(value => { Debug.assert(typeof value === "number"); diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index f870707801c..0b8240cb45d 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -128,7 +128,8 @@ describe("unittests:: config:: showConfig", () => { if (option.name === "project") return; let args: string[]; let optionValue: object | undefined; - switch (option.type) { + const type = option.type === "objectOrShorthand" ? option.shorthandType : option.type; + switch (type) { case "boolean": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; @@ -179,7 +180,7 @@ describe("unittests:: config:: showConfig", () => { break; } default: { - const val = ts.firstOrUndefinedIterator(option.type.keys()); + const val = ts.firstOrUndefinedIterator(type.keys()); if (val === undefined) return ts.Debug.fail("Expected 'option.type' to have entries"); if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; diff --git a/src/testRunner/unittests/helpers/baseline.ts b/src/testRunner/unittests/helpers/baseline.ts index dc2e2c5e65f..4ed5a9091b9 100644 --- a/src/testRunner/unittests/helpers/baseline.ts +++ b/src/testRunner/unittests/helpers/baseline.ts @@ -291,7 +291,7 @@ function generateBuildInfoProgramBaseline(sys: ts.System, buildInfoPath: string, return { original: ts.isString(original) ? undefined : original, ...info, - impliedFormat: info.impliedFormat && ts.getNameOfCompilerOptionValue(info.impliedFormat, ts.moduleOptionDeclaration.type), + impliedFormat: info.impliedFormat && ts.getNameOfCompilerOptionValue(info.impliedFormat, ts.moduleOptionDeclaration.shorthandType), }; } diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 00ca7b35369..1e3b034ee42 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -37,7 +37,7 @@ describe("unittests:: Reuse program structure:: General", () => { addedHeader = true; baselines.push(`${cacheType}:`); } - baselines.push(`${key}: ${mode ? ts.getNameOfCompilerOptionValue(mode, ts.moduleOptionDeclaration.type) + ": " : ""}${jsonToReadableText(resolved)}`); + baselines.push(`${key}: ${mode ? ts.getNameOfCompilerOptionValue(mode, ts.moduleOptionDeclaration.shorthandType) + ": " : ""}${jsonToReadableText(resolved)}`); } } function baselineProgram(baselines: string[], program: ts.Program, host?: TestCompilerHost) { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 2b9110dd81f..0ca965f48d5 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -7481,7 +7481,8 @@ declare namespace ts { DynamicPriority = 2, FixedChunkSize = 3, } - type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; + type NestedCompilerOption = ModuleOptions; + type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | NestedCompilerOption | PluginImport[] | ProjectReference[] | null | undefined; interface CompilerOptions { allowImportingTsExtensions?: boolean; allowJs?: boolean; @@ -7521,9 +7522,7 @@ declare namespace ts { locale?: string; mapRoot?: string; maxNodeModuleJsDepth?: number; - module?: ModuleKind; - moduleFormatDetection?: ModuleFormatDetectionKind; - moduleFormatInterop?: ModuleFormatInteropKind; + module?: ModuleKind | ModuleOptions; moduleResolution?: ModuleResolutionKind; moduleSuffixes?: string[]; moduleDetection?: ModuleDetectionKind; @@ -7631,6 +7630,13 @@ declare namespace ts { Node16 = 100, NodeNext = 199, } + interface ModuleOptions { + preset?: ModuleKind; + formatDetection?: ModuleFormatDetectionKind; + formatInterop?: ModuleFormatInteropKind; + emit?: ModuleKind; + [option: string]: CompilerOptionsValue | undefined; + } enum JsxEmit { None = 0, Preserve = 1, diff --git a/tests/baselines/reference/moduleExportNonStructured.errors.txt b/tests/baselines/reference/moduleExportNonStructured.errors.txt deleted file mode 100644 index 958728c943d..00000000000 --- a/tests/baselines/reference/moduleExportNonStructured.errors.txt +++ /dev/null @@ -1,36 +0,0 @@ -error TS5110: Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'. - - -!!! error TS5110: Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'. -==== package.json (0 errors) ==== - { - "name": "test", - "version": "1.0.0", - "description": "", - "type": "module", - "module": "index.mjs" - } - -==== index.mts (0 errors) ==== - import * as exportAny from "./exportAny.cjs"; - import * as exportUnknown from "./exportUnknown.cjs"; - import * as exportSymbol from "./exportSymbol.cjs"; - - import type * as exportAnyType from "./exportAny.cjs"; - import type * as exportUnknownType from "./exportUnknown.cjs"; - import type * as exportSymbolType from "./exportSymbol.cjs"; - -==== exportAny.d.cts (0 errors) ==== - declare const __: any; - export = __; - - -==== exportUnknown.d.cts (0 errors) ==== - declare const __: unknown; - export = __; - - -==== exportSymbol.d.cts (0 errors) ==== - declare const __: symbol; - export = __; - \ No newline at end of file diff --git a/tests/baselines/reference/nodeNextModuleResolution1.js b/tests/baselines/reference/nodeNextModuleResolution1.js index d76bd3acd7f..a006d7840c3 100644 --- a/tests/baselines/reference/nodeNextModuleResolution1.js +++ b/tests/baselines/reference/nodeNextModuleResolution1.js @@ -15,5 +15,4 @@ import {x} from "foo"; //// [app.js] -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +export {}; diff --git a/tests/baselines/reference/nodeNextModuleResolution2.errors.txt b/tests/baselines/reference/nodeNextModuleResolution2.errors.txt deleted file mode 100644 index 0f97e5e2de6..00000000000 --- a/tests/baselines/reference/nodeNextModuleResolution2.errors.txt +++ /dev/null @@ -1,18 +0,0 @@ -error TS5110: Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'. - - -!!! error TS5110: Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'. -==== /a/node_modules/foo/index.d.ts (0 errors) ==== - export declare let x: number -==== /a/node_modules/foo/package.json (0 errors) ==== - { - "name": "foo", - "type": "module", - "exports": { - ".": "./index.d.ts" - } - } - -==== /a/b/c/d/e/app.mts (0 errors) ==== - import {x} from "foo"; - \ No newline at end of file diff --git a/tests/baselines/reference/nodeNextModuleResolution2.js b/tests/baselines/reference/nodeNextModuleResolution2.js index 19475675a56..e57c0f25756 100644 --- a/tests/baselines/reference/nodeNextModuleResolution2.js +++ b/tests/baselines/reference/nodeNextModuleResolution2.js @@ -16,5 +16,4 @@ import {x} from "foo"; //// [app.mjs] -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +export {}; diff --git a/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.errors.txt b/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.errors.txt index fad550d1d91..96c449e86c0 100644 --- a/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.errors.txt +++ b/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.errors.txt @@ -1,8 +1,6 @@ error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. !!! error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -!!! error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. ==== file.ts (0 errors) ==== \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.oldTranspile.errors.txt b/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.oldTranspile.errors.txt index fad550d1d91..96c449e86c0 100644 --- a/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.oldTranspile.errors.txt +++ b/tests/baselines/reference/transpile/Report an error when compiler-options module-kind is out-of-range.oldTranspile.errors.txt @@ -1,8 +1,6 @@ error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. !!! error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -!!! error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. ==== file.ts (0 errors) ==== \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.errors.txt b/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.errors.txt index fad550d1d91..96c449e86c0 100644 --- a/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.errors.txt +++ b/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.errors.txt @@ -1,8 +1,6 @@ error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. !!! error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -!!! error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. ==== file.ts (0 errors) ==== \ No newline at end of file diff --git a/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.oldTranspile.errors.txt b/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.oldTranspile.errors.txt index fad550d1d91..96c449e86c0 100644 --- a/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.oldTranspile.errors.txt +++ b/tests/baselines/reference/transpile/Report an error when compiler-options target-script is out-of-range.oldTranspile.errors.txt @@ -1,8 +1,6 @@ error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. !!! error TS5107: Option 'target=ES3' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. -!!! error TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'. ==== file.ts (0 errors) ==== \ No newline at end of file diff --git a/tests/cases/conformance/module/esbuildInterop.ts b/tests/cases/conformance/module/esbuildInterop.ts index 44b56fb62d1..c896b2dd96f 100644 --- a/tests/cases/conformance/module/esbuildInterop.ts +++ b/tests/cases/conformance/module/esbuildInterop.ts @@ -1,7 +1,7 @@ -// @module: esnext // @moduleResolution: bundler -// @moduleFormatDetection: bundler -// @moduleFormatInterop: bundlernode +// @module.emit: esnext +// @module.formatDetection: bundler +// @module.formatInterop: bundlernode // @Filename: /node_modules/dep/package.json {