Skip to content

Conversation

@weswigham
Copy link
Member

To allow a export = to be typechecked alongside other exports in the module without the .cjsExportMerged layering we had going on. I've also deleted the "export = can't be in the same module as other exports" error, as, well, that was the point, but if we wanted to, we could always keep that error in ts source files... but, IMO, a more limited "value exports should not be lexically before a value export =" is actually the only error you need to prevent runtime problems with the usual cjs/bundler emit, and would be the better error.

This, in turn, should allow a cjs module.exports = to behave identically to a TS exports = (a goal for corsa), as now the later has all the functionality of the former.

I still need to go through the fourslash diffs and see what LS operations need to handle the export= symbol handling logic getting pushed up a layer (some quickfixes, for sure), but baseline-wise, these actually mostly look really good - this change makes it so modules always have their own symbol (and are directly never forwarded to another symbol - they just behave as though they are at times), and so can always be referred to by that symbol.

@weswigham weswigham requested a review from sandersn July 22, 2025 01:21
@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Jul 22, 2025
*/
function mergeSymbol(target: Symbol, source: Symbol, unidirectional = false): Symbol {
if (target === source) {
return source; // target and source already merged (likely same symbol found in multiple export tables)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is only needed because I reused mergeSymbolTables to build the combined symbol table in resolveStructuredTypeMembers. If it wasn't to taste, I could always write a custom table merging function that handles this case outside of the core mergeSymbol.

}
}
}
if (mainModule.exports?.get(InternalSymbolName.ExportEquals) && moduleAugmentation.symbol.exports?.size) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost a carbon copy of the export * logic above - the exception being that a local declaration doesn't implicitly "beat" something added by an export = - they have equal weight and get merged.

}
if (!symbol) {
if (isExportAssignmentAny(namespace)) {
return unknownSymbol; // no error - lookup on psuedo-`any` module
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this was handled one level up the stack because the module itself was the any-typed const. Now it's not any (and can contain other well-typed members, like @typedefs), but it contains an export= that points at any, and we check that there to reenable the error-less lookup it used to have.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return unknownSymbol; // no error - lookup on psuedo-`any` module
return unknownSymbol; // no error - lookup on pseudo-`any` module

}
getSymbolLinks(merged).cjsExportMerged = merged;
return links.cjsExportMerged = merged;
return resolveSymbol(moduleSymbol, dontResolveAlias)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I could probably just delete resolveExternalModuleSymbol now - it's just an alias for resolveSymbol.

function getExportsOfModule(moduleSymbol: Symbol): SymbolTable {
const links = getSymbolLinks(moduleSymbol);
if (!links.resolvedExports) {
links.resolvedExports = emptySymbols
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getExportsOfModuleWorker can now recursively ask for it's own exports in cases like module.exports = exports - this short-circuits that, but still allows other things to merge into the cycle, despite the (sorta erroneous, sorta not) circular export.

extendExportSymbols(symbols, table);
}
}
symbols.delete(InternalSymbolName.ExportEquals)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maintains the "the resolved exports don't contains an export= symbol" expectation most callers have. It's naturally still accessible on the raw, unresolved symbol.exports table for callers that need it, though.

type.constructSignatures = constructSignatures;
}

// And lastly, if the underlying symbol has an export=, we need to copy the type structure from it
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic and the getExportsOfModuleWorker should be identical, except the former is only concerned with copying values for the sake of symbolic traversal, while this copies up all type structure from the export= target - members, signatures, and index infos.

Comment on lines 34802 to 34803
const isPsuedoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPsuedoAnyModule;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const isPsuedoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPsuedoAnyModule;
const isPseudoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPseudoAnyModule;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants