Repository: babel/babel (TypeScript)

Issue:
[Bug]: `@babel/preset-typescript@7.22.15` fails when exporting namespace
### 💻

- [x] Would you like to work on a fix?

### How are you using Babel?

@rollup/plugin-babel

### Input code

Building the following code with `@babel/preset-typescript` fails:

```js
import type { Brand, NativeOrWebBrand } from './types';

namespace Platform {
  export type Name = 'web';
}

export { Platform };
```

with the following error:

<img width="868" alt="Screenshot 2023-09-07 at 8 36 52 AM" src="https://github.com/babel/babel/assets/46647141/1894730e-4f23-4059-bd52-49e9a0efd825">

However if I move the export to declaration itself, it builds successful:

```ts
import type { Brand, NativeOrWebBrand } from './types';

export namespace Platform {
  export type Name = 'web';
}
``` 

### Configuration file name

.babelrc.js

### Configuration

```js
{
      presets: [
        ['@babel/preset-env', { modules: false }],
        '@babel/preset-typescript',
        ['@babel/preset-react', { runtime: 'automatic' }],
      ],
      plugins: [
        [
          '@babel/plugin-transform-runtime',
          {
            helpers: true,
            regenerator: true,
          },
        ],
        [
          'babel-plugin-styled-components',
          {
            displayName: true,
            pure: true,
            ssr: true,
          },
        ],
      ],
    },
```

### Current and expected behavior

Currently it throws error with v7.22

It should not throw error like in v7.16

### Environment

```
System:
    OS: macOS 13.4.1
  Binaries:
    Node: 20.2.0 - ~/.nvm/versions/node/v20.2.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 9.6.6 - ~/.nvm/versions/node/v20.2.0/bin/npm
  npmPackages:
    @babel/core: 7.22.15 => 7.22.15 
    @babel/plugin-transform-react-jsx: 7.22.15 => 7.22.15 
    @babel/plugin-transform-runtime: 7.22.15 => 7.22.15 
    @babel/preset-env: 7.22.15 => 7.22.15 
    @babel/preset-react: 7.22.15 => 7.22.15 
    @babel/preset-typescript: 7.22.15 => 7.22.15 
    @babel/runtime: 7.22.15 => 7.22.15 
    babel-jest: 29.6.1 => 29.6.1 
    babel-loader: 8.2.3 => 8.2.3 
    babel-plugin-module-resolver: 4.1.0 => 4.1.0 
    babel-plugin-styled-components: 1.12.0 => 1.12.0 
    jest: 29.6.1 => 29.6.1 
    metro-react-native-babel-preset: 0.76.7 => 0.76.7 
    react-native: 0.72.3 => 0.72.3
   ```

### Possible solution

_No response_

### Additional context

_No response_


Hints:
Hey @snitin315! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant [Slack community](https://babeljs.slack.com) that typically always has someone willing to help. You can sign-up [here](https://slack.babeljs.io/) for an invite.
This has been probably caused by https://github.com/babel/babel/pull/15799, but the old output was wrong too.

We might have to add a "is namespace && will be stripped because it only contains types" check in https://github.com/babel/babel/blob/85a35adf1292a2035c23478da82f827968e64c28/packages/babel-plugin-transform-typescript/src/index.ts#L389, or call `registerGlobalType` in https://github.com/babel/babel/blob/85a35adf1292a2035c23478da82f827968e64c28/packages/babel-plugin-transform-typescript/src/namespace.ts#L27.

I see that you marked "Would you like to work on a fix?", let me know if you need further help :)
>We might have to add a "is namespace && will be stripped because it only contains types" check in

@nicolo-ribaudo How do I check it's a namespace, currently `stmt` doesn't have access to the [`kind` property](https://astexplorer.net/#/gist/b8f4a001006f0e98b099c752600c9d22/c008c1cd3397d6aa0e786bc59fb27e5d2caeac22)
You can use `stmt.isTSModuleDeclaration()` for that
@snitin315 Are you still working on this?
@nicolo-ribaudo not as of now. Feel free to  pick this up.
<!-- ALERT!!!!!!!
    Before submitting this comment, please:
      1. Write where to start searching the bug
      2. Write where to add a new test
    See https://github.com/babel/babel/issues/9563#issuecomment-466799807 for an example
-->

For anyone that wants to help :)

If it is the first time that you contribute to Babel, follow these steps: (you need to have `make` and `yarn` available on your machine)
1. **Write a comment there** to let other possible contributors know that you are working on this bug.
1. Fork the repo
1. Run `git clone https://github.com/<YOUR_USERNAME>/babel.git && cd babel`
1. Run `yarn && make bootstrap`
1. Wait :hourglass_flowing_sand: 
1. Run `make watch` (or `make build` whenever you change a file)
1. Add a test in https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-typescript/test/fixtures/namespace (only `input.ts`; `output.js` will be automatically generated)
1. Update the code!
1. `yarn jest transform-typescript` to run the tests
    - If some test outputs don't match but the new results are correct, you can delete the bad `output.js` files and run the tests again
    - If you prefer, you can run `OVERWRITE=true yarn jest transform-typescript` and they will be automatically updated.
1. If it is working, run `make test` to run all the tests
1. Run `git push` and open a PR!
hey there can I pick this up?
Sure! Feel free to write here if you need more help :)
I could try taking a stab at it if niftytyro isn't working on it anymore!
Yes sure please! I'm not working on it right now. Feel free to take it 

Patch:
diff --git a/packages/babel-plugin-transform-typescript/src/global-types.ts b/packages/babel-plugin-transform-typescript/src/global-types.ts
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/src/global-types.ts
@@ -0,0 +1,24 @@
+import type { NodePath, Scope } from "@babel/traverse";
+
+export const GLOBAL_TYPES = new WeakMap<Scope, Set<string>>();
+
+export function isGlobalType({ scope }: NodePath, name: string) {
+  if (scope.hasBinding(name)) return false;
+  if (GLOBAL_TYPES.get(scope).has(name)) return true;
+
+  console.warn(
+    `The exported identifier "${name}" is not declared in Babel's scope tracker\n` +
+      `as a JavaScript value binding, and "@babel/plugin-transform-typescript"\n` +
+      `never encountered it as a TypeScript type declaration.\n` +
+      `It will be treated as a JavaScript value.\n\n` +
+      `This problem is likely caused by another plugin injecting\n` +
+      `"${name}" without registering it in the scope tracker. If you are the author\n` +
+      ` of that plugin, please use "scope.registerDeclaration(declarationPath)".`,
+  );
+
+  return false;
+}
+
+export function registerGlobalType(programScope: Scope, name: string) {
+  GLOBAL_TYPES.get(programScope).add(name);
+}
diff --git a/packages/babel-plugin-transform-typescript/src/index.ts b/packages/babel-plugin-transform-typescript/src/index.ts
--- a/packages/babel-plugin-transform-typescript/src/index.ts
+++ b/packages/babel-plugin-transform-typescript/src/index.ts
@@ -2,12 +2,17 @@ import { declare } from "@babel/helper-plugin-utils";
 import syntaxTypeScript from "@babel/plugin-syntax-typescript";
 import type { PluginPass, types as t } from "@babel/core";
 import { injectInitialization } from "@babel/helper-create-class-features-plugin";
-import type { Binding, NodePath, Scope } from "@babel/traverse";
+import type { Binding, NodePath } from "@babel/traverse";
 import type { Options as SyntaxOptions } from "@babel/plugin-syntax-typescript";
 
 import transpileConstEnum from "./const-enum.ts";
 import type { NodePathConstEnum } from "./const-enum.ts";
 import transpileEnum from "./enum.ts";
+import {
+  GLOBAL_TYPES,
+  isGlobalType,
+  registerGlobalType,
+} from "./global-types.ts";
 import transpileNamespace from "./namespace.ts";
 
 function isInType(path: NodePath) {
@@ -36,34 +41,12 @@ function isInType(path: NodePath) {
   }
 }
 
-const GLOBAL_TYPES = new WeakMap<Scope, Set<string>>();
 // Track programs which contain imports/exports of values, so that we can include
 // empty exports for programs that do not, but were parsed as modules. This allows
 // tools to infer unambiguously that results are ESM.
 const NEEDS_EXPLICIT_ESM = new WeakMap();
 const PARSED_PARAMS = new WeakSet();
 
-function isGlobalType({ scope }: NodePath, name: string) {
-  if (scope.hasBinding(name)) return false;
-  if (GLOBAL_TYPES.get(scope).has(name)) return true;
-
-  console.warn(
-    `The exported identifier "${name}" is not declared in Babel's scope tracker\n` +
-      `as a JavaScript value binding, and "@babel/plugin-transform-typescript"\n` +
-      `never encountered it as a TypeScript type declaration.\n` +
-      `It will be treated as a JavaScript value.\n\n` +
-      `This problem is likely caused by another plugin injecting\n` +
-      `"${name}" without registering it in the scope tracker. If you are the author\n` +
-      ` of that plugin, please use "scope.registerDeclaration(declarationPath)".`,
-  );
-
-  return false;
-}
-
-function registerGlobalType(programScope: Scope, name: string) {
-  GLOBAL_TYPES.get(programScope).add(name);
-}
-
 // A hack to avoid removing the impl Binding when we remove the declare NodePath
 function safeRemove(path: NodePath) {
   const ids = path.getBindingIdentifiers();
diff --git a/packages/babel-plugin-transform-typescript/src/namespace.ts b/packages/babel-plugin-transform-typescript/src/namespace.ts
--- a/packages/babel-plugin-transform-typescript/src/namespace.ts
+++ b/packages/babel-plugin-transform-typescript/src/namespace.ts
@@ -1,6 +1,8 @@
 import { template, types as t } from "@babel/core";
 import type { NodePath } from "@babel/traverse";
 
+import { registerGlobalType } from "./global-types.ts";
+
 export default function transpileNamespace(
   path: NodePath<t.TSModuleDeclaration>,
   allowNamespaces: boolean,
@@ -24,6 +26,11 @@ export default function transpileNamespace(
   const name = path.node.id.name;
   const value = handleNested(path, t.cloneNode(path.node, true));
   if (value === null) {
+    // This means that `path` is a type-only namespace.
+    // We call `registerGlobalType` here to allow it to be stripped.
+    const program = path.findParent(p => p.isProgram());
+    registerGlobalType(program.scope, name);
+
     path.remove();
   } else if (path.scope.hasOwnBinding(name)) {
     path.replaceWith(value);


Test patch:
diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/input.ts
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/input.ts
@@ -0,0 +1,11 @@
+// #15940
+namespace Platform {
+  export type Name = "web";
+
+  export namespace Data {
+    export type Type = string;
+    export declare namespace Empty {}
+  }
+}
+
+export { Platform };
diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/output.mjs
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/test/fixtures/namespace/export-type-only/output.mjs
@@ -0,0 +1 @@
+export {};