Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Advanced module options #56681

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ import {
getMembersOfDeclaration,
getModeForUsageLocation,
getModifiers,
getModuleFormatInteropKind,
getModuleInstanceState,
getNameFromIndexInfo,
getNameOfDeclaration,
Expand Down Expand Up @@ -839,6 +840,7 @@ import {
modifierToFlag,
ModuleBlock,
ModuleDeclaration,
ModuleFormatInteropKind,
ModuleInstanceState,
ModuleKind,
ModuleResolutionKind,
Expand Down Expand Up @@ -4042,7 +4044,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function isESMFormatImportImportingCommonjsFormatFile(usageMode: ResolutionMode, targetMode: ResolutionMode) {
return usageMode === ModuleKind.ESNext && targetMode === ModuleKind.CommonJS;
return usageMode === ModuleKind.ESNext && targetMode !== ModuleKind.ESNext;
}

function isOnlyImportedAsDefault(usage: Expression) {
Expand All @@ -4051,13 +4053,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean, usage: Expression) {
const usageMode = file && getUsageModeForExpression(usage);
if (file && usageMode !== undefined) {
const result = isESMFormatImportImportingCommonjsFormatFile(usageMode, file.impliedNodeFormat);
if (usageMode === ModuleKind.ESNext || result) {
return result;
}
// fallthrough on cjs usages so we imply defaults for interop'd imports, too
const usageMode = file && getModuleFormatInteropKind(compilerOptions) !== ModuleFormatInteropKind.Babel && getUsageModeForExpression(usage);
// fallthrough on cjs usages so we imply defaults for interop'd imports, too
if (usageMode === ModuleKind.ESNext) {
return isESMFormatImportImportingCommonjsFormatFile(usageMode, file!.impliedNodeFormat);
}
if (!allowSyntheticDefaultImports) {
return false;
Expand Down Expand Up @@ -4992,6 +4991,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal;
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
const moduleFormatInterop = getModuleFormatInteropKind(compilerOptions);
const resolvedModule = host.getResolvedModule(currentSourceFile, moduleReference, mode)?.resolvedModule;
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule, currentSourceFile);
const sourceFile = resolvedModule
Expand Down Expand Up @@ -5027,7 +5027,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
errorOnImplicitAnyModule(/*isError*/ false, errorNode, currentSourceFile, mode, resolvedModule, moduleReference);
}
if (moduleResolutionKind === ModuleResolutionKind.Node16 || moduleResolutionKind === ModuleResolutionKind.NodeNext) {
if (moduleFormatInterop === ModuleFormatInteropKind.Node16 || moduleFormatInterop === ModuleFormatInteropKind.NodeNext) {
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
const overrideHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
// An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of
Expand Down
223 changes: 176 additions & 47 deletions src/compiler/commandLineParser.ts

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6215,6 +6215,18 @@
"category": "Message",
"code": 6804
},
"Specify how files are determined to be ECMAScript modules or CommonJS modules.": {
"category": "Message",
"code": 6805
},
"Specify the target runtime's rules for ESM-CommonJS interoperation.": {
"category": "Message",
"code": 6806
},
"Specify defaults for module options suited for common runtimes and bundlers.": {
"category": "Message",
"code": 6807
},

"one of:": {
"category": "Message",
Expand Down Expand Up @@ -6344,6 +6356,14 @@
"category": "Error",
"code": 6931
},
"'node16' when 'module' is 'node16'; 'nodenext' when 'module' is 'nodenext'; 'none' otherwise.": {
"category": "Message",
"code": 6932
},
"'node16' when 'module' is 'node16'; 'nodenext' when 'module' is 'nodenext'; 'babel' otherwise.": {
"category": "Message",
"code": 6933
},

"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
Expand Down
38 changes: 32 additions & 6 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import {
getLineStarts,
getMatchedFileSpec,
getMatchedIncludeSpec,
getModuleFormatDetectionKind,
getNewLineCharacter,
getNormalizedAbsolutePath,
getNormalizedAbsolutePathWithoutRoot,
Expand Down Expand Up @@ -223,6 +224,7 @@ import {
ModifierLike,
ModuleBlock,
ModuleDeclaration,
ModuleFormatDetectionKind,
ModuleKind,
ModuleResolutionCache,
ModuleResolutionHost,
Expand Down Expand Up @@ -1325,23 +1327,47 @@ export function getImpliedNodeFormatForFileWorker(
host: ModuleResolutionHost,
options: CompilerOptions,
) {
switch (getEmitModuleResolutionKind(options)) {
case ModuleResolutionKind.Node16:
case ModuleResolutionKind.NodeNext:
const formatDetection = getModuleFormatDetectionKind(options);
switch (formatDetection) {
case ModuleFormatDetectionKind.Bundler:
case ModuleFormatDetectionKind.Node16:
case ModuleFormatDetectionKind.NodeNext:
case ModuleFormatDetectionKind.DefaultModule:
case ModuleFormatDetectionKind.DefaultCommonJS:
const defaultFormat = formatDetection === ModuleFormatDetectionKind.Bundler ? undefined :
formatDetection === ModuleFormatDetectionKind.DefaultModule ? ModuleKind.ESNext :
ModuleKind.CommonJS;
return fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Mjs]) ? ModuleKind.ESNext :
fileExtensionIsOneOf(fileName, [Extension.Dcts, Extension.Cts, Extension.Cjs]) ? ModuleKind.CommonJS :
fileExtensionIsOneOf(fileName, [Extension.Dts, Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx]) ? lookupFromPackageJson() :
fileExtensionIsOneOf(fileName, [Extension.Dts, Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx]) ? lookupFromPackageJson(defaultFormat) :
undefined; // other extensions, like `json` or `tsbuildinfo`, are set as `undefined` here but they should never be fed through the transformer pipeline
case ModuleFormatDetectionKind.LocalModule:
case ModuleFormatDetectionKind.LocalCommonJS:
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Mjs])) {
return ModuleKind.ESNext;
}
if (fileExtensionIsOneOf(fileName, [Extension.Dcts, Extension.Cts, Extension.Cjs])) {
return ModuleKind.CommonJS;
}
if (!isDeclarationFileName(fileName) && fileExtensionIsOneOf(fileName, [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx])) {
// TODO: replace with more reliable check for "is this an emittable source file of this program"
// TODO: what about nested package.json directories? Should special behavior be restricted to the
// one in scope of the tsconfig?
return formatDetection === ModuleFormatDetectionKind.LocalModule ? ModuleKind.ESNext : ModuleKind.CommonJS;
}
return lookupFromPackageJson(/*defaultFormat*/ ModuleKind.CommonJS);
default:
return undefined;
}
function lookupFromPackageJson(): Partial<CreateSourceFileOptions> {
function lookupFromPackageJson(defaultFormat: ResolutionMode): Partial<CreateSourceFileOptions> {
const state = getTemporaryModuleResolutionState(packageJsonInfoCache, host, options);
const packageJsonLocations: string[] = [];
state.failedLookupLocations = packageJsonLocations;
state.affectingLocations = packageJsonLocations;
const packageJsonScope = getPackageScopeForPath(fileName, state);
const impliedNodeFormat = packageJsonScope?.contents.packageJsonContent.type === "module" ? ModuleKind.ESNext : ModuleKind.CommonJS;
const impliedNodeFormat = packageJsonScope?.contents.packageJsonContent.type === "module" ? ModuleKind.ESNext :
packageJsonScope?.contents.packageJsonContent.type === "commonjs" ? ModuleKind.CommonJS :
defaultFormat;
return { impliedNodeFormat, packageJsonLocations, packageJsonScope };
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/transformers/module/esnextAnd2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
54 changes: 50 additions & 4 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7092,7 +7092,8 @@ export enum PollingWatchKind {
FixedChunkSize,
}

export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined;
export type NestedCompilerOption = ModuleOptions;
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | NestedCompilerOption | PluginImport[] | ProjectReference[] | null | undefined;

export interface CompilerOptions {
/** @internal */ all?: boolean;
Expand Down Expand Up @@ -7158,7 +7159,7 @@ export interface CompilerOptions {
locale?: string;
mapRoot?: string;
maxNodeModuleJsDepth?: number;
module?: ModuleKind;
module?: ModuleKind | ModuleOptions;
moduleResolution?: ModuleResolutionKind;
moduleSuffixes?: string[];
moduleDetection?: ModuleDetectionKind;
Expand Down Expand Up @@ -7282,6 +7283,33 @@ export enum ModuleKind {
NodeNext = 199,
}

export enum ModuleFormatDetectionKind {
None = 0,
Bundler = 1,
DefaultModule = 2,
DefaultCommonJS = 3,
LocalModule = 4,
LocalCommonJS = 5,
Node16 = 100,
NodeNext = 199,
}

export enum ModuleFormatInteropKind {
Babel = 1,
BundlerNode = 2,

Node16 = 100,
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,
Expand Down Expand Up @@ -7403,7 +7431,7 @@ export interface CreateProgramOptions {
/** @internal */
export interface CommandLineOptionBase {
name: string;
type: "string" | "number" | "boolean" | "object" | "list" | "listOrElement" | Map<string, number | string>; // a value of a primitive type, or an object literal mapping named values to actual values
type: "string" | "number" | "boolean" | "object" | "objectOrShorthand" | "list" | "listOrElement" | Map<string, number | string>; // a value of a primitive type, or an object literal mapping named values to actual values
isFilePath?: boolean; // True if option value is a path or fileName
shortName?: string; // A short mnemonic for convenience - for instance, 'h' can be used in place of 'help'
description?: DiagnosticMessage; // The message describing what the command line switch does.
Expand All @@ -7426,6 +7454,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 */
Expand Down Expand Up @@ -7474,6 +7503,16 @@ export interface TsConfigOnlyOption extends CommandLineOptionBase {
extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics;
}

/** @internal */
export interface CommandLineOptionOfObjectOrShorthandType extends CommandLineOptionBase {
type: "objectOrShorthand";
shorthandType: "string" | "number" | "boolean" | Map<string, number | string>;
defaultValueDescription?: string | number | boolean | DiagnosticMessage;
elementOptions: Map<string, CommandLineOption>;
extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics;
deprecatedKeys?: Set<string>;
}

/** @internal */
export interface CommandLineOptionOfListType extends CommandLineOptionBase {
type: "list" | "listOrElement";
Expand All @@ -7482,7 +7521,14 @@ export interface CommandLineOptionOfListType extends CommandLineOptionBase {
}

/** @internal */
export type CommandLineOption = CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption | CommandLineOptionOfListType;
export type CommandLineOption =
| CommandLineOptionOfCustomType
| CommandLineOptionOfStringType
| CommandLineOptionOfNumberType
| CommandLineOptionOfBooleanType
| CommandLineOptionOfObjectOrShorthandType
| TsConfigOnlyOption
| CommandLineOptionOfListType;

// dprint-ignore
/** @internal */
Expand Down
48 changes: 47 additions & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ import {
ModuleBlock,
ModuleDeclaration,
ModuleDetectionKind,
ModuleFormatDetectionKind,
ModuleFormatInteropKind,
ModuleKind,
ModuleResolutionKind,
moduleResolutionOptionDeclarations,
Expand Down Expand Up @@ -8583,7 +8585,13 @@ 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;
}
if (typeof compilerOptions.module === "object" && compilerOptions.module.preset !== undefined) {
return compilerOptions.module.preset;
}
return typeof compilerOptions.module === "number" ?
compilerOptions.module :
getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015 ? ModuleKind.ES2015 : ModuleKind.CommonJS;
Expand All @@ -8594,6 +8602,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;
Expand Down
14 changes: 9 additions & 5 deletions src/executeCommandLine/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
CharacterCodes,
combinePaths,
CommandLineOption,
CommandLineOptionOfCustomType,
CommandLineOptionOfListType,
CommandLineOptionOfObjectOrShorthandType,
compareStringsCaseInsensitive,
CompilerOptions,
contains,
Expand Down Expand Up @@ -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 = "";
Expand All @@ -394,8 +398,8 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign
// Map<string, number | string>
// 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);
}
});
Expand Down
Loading
Loading