Skip to content

Conversation

a-tarasyuk
Copy link
Contributor

Fixes #1685


This patch addresses the issue of incomplete export assignment validation by aligning the check with Strada logic.

const declaration = getDeclarationOfAliasSymbol(exportEqualsSymbol) || exportEqualsSymbol.valueDeclaration;
if (declaration && !isTopLevelInExternalModuleAugmentation(declaration) && !isInJSFile(declaration)) {
    error(declaration, Diagnostics.An_export_assignment_cannot_be_used_in_a_module_with_other_exported_elements);
}

@jakebailey
Copy link
Member

@sandersn @weswigham Is this the thing we were discussing about relaxing this for even TS code and not erroring at all? I can't remember where we left that.

@weswigham
Copy link
Member

We're at the "just do it like strada" stage because trying to improve it just moved where the jank in our code is - better the jank we know.

Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

Definitely an improvement. I'm sure we'll have to keep adjusting the JS declaration emit for export assignments, though, since they're still only partially supported.



==== out/source.d.ts (2 errors) ====
export = MyClass;
Copy link
Member

Choose a reason for hiding this comment

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

Hm. This is because we don't actually support constructor functions yet, right @sandersn ?

Comment on lines +70 to +71
-export type SomeType = {
+type SomeType = {
Copy link
Member

@jakebailey jakebailey Sep 10, 2025

Choose a reason for hiding this comment

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

This one is regressing; maybe a bad interplay with the new code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is somewhat of a regression, related to how Corsa handles the module.exports = {} export assignment, for instance

/**
 * @typedef {{x: string} | number | LocalThing | ExportedThing} SomeType
 */
/**
 * @param {number} x
 * @returns {SomeType}
 */
function doTheThing(x) {
    return {x: ""+x};
}
class ExportedThing {
    z = "ok"
}
module.exports = {
    doTheThing,
    ExportedThing,
};
class LocalThing {
    y = "ok"
}

Strada

export type SomeType = {
    x: string;
} | number | LocalThing | ExportedThing;
/**
 * @typedef {{x: string} | number | LocalThing | ExportedThing} SomeType
 */
/**
 * @param {number} x
 * @returns {SomeType}
 */
declare function doTheThing(x: number): SomeType;
declare class ExportedThing {
    z: string;
}
declare class LocalThing {
    y: string;
}
export {};

Corsa

//// [mixed.d.ts]
type SomeType = {
    x: string;
} | number | LocalThing | ExportedThing;
/**
 * @typedef {{x: string} | number | LocalThing | ExportedThing} SomeType
 */
/**
 * @param {number} x
 * @returns {SomeType}
 */
declare function doTheThing(x: number): SomeType;
declare class ExportedThing {
    z: string;
}
declare const _default: {
    doTheThing: typeof doTheThing;
    ExportedThing: typeof ExportedThing;
};
export = _default;
declare class LocalThing {
    y: string;
}

newId := tx.Factory().NewUniqueNameEx("_default", printer.AutoGenerateOptions{Flags: printer.GeneratedIdentifierFlagsOptimistic})
tx.state.getSymbolAccessibilityDiagnostic = func(_ printer.SymbolAccessibilityResult) *SymbolAccessibilityDiagnostic {
return &SymbolAccessibilityDiagnostic{
diagnosticMessage: diagnostics.Default_export_of_the_module_has_or_is_using_private_name_0,
errorNode: input,
}
}
tx.tracker.PushErrorFallbackNode(input)
type_ := tx.ensureType(input, false)
varDecl := tx.Factory().NewVariableDeclaration(newId, nil, type_, nil)
tx.tracker.PopErrorFallbackNode()
var modList *ast.ModifierList
if tx.needsDeclare {
modList = tx.Factory().NewModifierList([]*ast.Node{tx.Factory().NewModifier(ast.KindDeclareKeyword)})
} else {
modList = tx.Factory().NewModifierList([]*ast.Node{})
}
statement := tx.Factory().NewVariableStatement(modList, tx.Factory().NewVariableDeclarationList(ast.NodeFlagsConst, tx.Factory().NewNodeList([]*ast.Node{varDecl})))
assignment := tx.Factory().UpdateExportAssignment(input.AsExportAssignment(), input.Modifiers(), input.Type(), newId)
// Remove comments from the export declaration and copy them onto the synthetic _default declaration
tx.preserveJsDoc(statement, input)
tx.removeAllComments(assignment)
return tx.Factory().NewSyntaxList([]*ast.Node{statement, assignment})

Since export = _default; is present, all other exports should be elided to avoid mismatches related to this error handling.

I think fixing this regression requires changing how export assignments are handled, so cases like

module.exports = {
  doTheThing,
  ExportedThing,
};

are transformed without emitting them as a default export. @jakebailey WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

That sounds about right, I think? (Not personally an expert in this crossover)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weswigham @jakebailey I’m wondering if, instead of replicating Strada’s JavaScript declaration emitter, which produces

// @input: a.js
/**
 * @typedef {string | number} T
 */

class A { }
module.exports = {
    A,
}

// @output: a.d.ts
export type T = string | number;
/**
 * @typedef {string | number} T
 */
export class A {
}

it might make more sense to align more closely with what TypeScript’s transformer does, and adjust the output to something like this...

// @output: a.d.ts
declare class A {}

declare const _default: {
  A: typeof A;
};
export = _default;

declare namespace _default {
  type T = string | number;
}

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't the impact of that be that A is inaccessible from type space?

Comment on lines +46 to +47
-export type Conn = import("./conn");
+type Conn = import("./conn");
Copy link
Member

Choose a reason for hiding this comment

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

This one too? (I hate JSDoc typedef rules)

Comment on lines 30 to +33
-export type TaskGroupIds = "parseHTML" | "styleLayout";
+export type TaskGroupIds = 'parseHTML' | 'styleLayout';
export type TaskGroup = {
-export type TaskGroup = {
+type TaskGroupIds = 'parseHTML' | 'styleLayout';
+type TaskGroup = {
Copy link
Member

Choose a reason for hiding this comment

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

This file's also regressing in a similar way, it seems...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ts-check used with @typedef causes an error on module.exports = ...
3 participants