Skip to content

[api-extractor] Missing 'export' keyword for namespace re-exports produces invalid TypeScript #5516

@ItsChrisHickman

Description

@ItsChrisHickman

[api-extractor] Missing export keyword for namespace re-exports produces invalid TypeScript

Summary

When a TypeScript source file contains a namespace with re-export declarations using export { } syntax, api-extractor strips the export keyword from the rollup output, producing syntactically invalid TypeScript.

Repro steps

  1. Create a TypeScript file with a namespace that re-exports types:
export type Color = { r: number; g: number; b: number };
export type Vector3 = { x: number; y: number; z: number };

export declare namespace SDK {
  export { Color, Vector3 };
}
  1. Run tsc to generate .d.ts files (output is correct)
  2. Run api-extractor run --local to generate rollup
  3. Check the output .d.ts rollup

Expected behavior

The rollup should contain:

export declare namespace SDK {
        export { Color, Vector3, };
}

Actual behavior

The rollup contains:

export declare namespace SDK {
        { Color, Vector3, };
}

The export keyword is missing, which produces invalid TypeScript:

error TS1109: Expression expected.

Root cause

In DtsRollupGenerator.ts (and ApiReportGenerator.ts), lines ~272-276 unconditionally skip all ExportKeyword tokens:

case ts.SyntaxKind.ExportKeyword:
case ts.SyntaxKind.DefaultKeyword:
case ts.SyntaxKind.DeclareKeyword:
  span.modification.skipAll();
  break;

The export keyword is then re-added for declaration types (interface, class, enum, namespace, function, type) at lines ~279-306. However, ExportDeclaration nodes (like export { Foo }) are not handled, so the export keyword is stripped but never restored.

Proposed fix

Check if the ExportKeyword is part of an ExportDeclaration inside a ModuleBlock (namespace body), and if so, preserve it:

case ts.SyntaxKind.ExportKeyword:
  // Preserve export keyword for ExportDeclarations inside namespaces
  if (span.node.parent && ts.isExportDeclaration(span.node.parent)) {
    const moduleBlock = TypeScriptHelpers.findFirstParent(
      span.node,
      ts.SyntaxKind.ModuleBlock
    );
    if (moduleBlock) {
      break; // Preserve the export keyword
    }
  }
  span.modification.skipAll();
  break;

Environment

  • api-extractor version: 7.55.2
  • TypeScript version: 5.x
  • Node.js version: 20.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions