Repository: lukeautry/tsoa (TypeScript)

Issue:
Support TypeScript 5.7 "rewriteRelativeImportExtensions" option
[TypeScript 5.7](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7-beta/#path-rewriting-for-relative-paths) will support a new compilerOption called "rewriteRelativeImportExtensions", which allows importing `.ts` extensions for relative paths.


## Sorting

- **I'm submitting a ...**

  - [ ] bug report
  - [X] feature request
  - [ ] support request

- I confirm that I
  - [X] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Expected Behavior

Allow for a config option in `tsoa.json` to have tsoa output `.ts` imports in routes.ts.

<!--- If this is related to a TypeScript issue, then include the way that you expected to be able to write the code  -->

```ts
// should create the following import in routes.ts when rewriteRelativeImportExtensions is enabled
import { UserController } from './../src/controllers/userController.ts';
```

## Current Behavior

`npx tsoa spec-and-routes` creates the following `routes.ts`:
```ts
import { UserController } from './../src/controllers/userController.js';
--------------------------------^
// This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "../src/controllers/userController.ts".

```

## Possible Solution

Allow for a `rewriteRelativeImportExtensions`-tsoa-config option, which would not rewrite the `.ts`-extensions to `.js`.

Or read the `tsconfig.json` used in the project and determine the `rewriteRelativeImportExtensions` setting.

## Steps to Reproduce

1. Install TypeScript 5.7 beta
2. enable `rewriteRelativeImportExtensions` in `tsconfig.json` compilerOptions
3. use tsoa

## Context (Environment)

Version of the library: tsoa@6.2.1
Version of NodeJS: 20

- Confirm you were using yarn not npm: [X]

## Detailed Description

Allow for a config option e.g. called `rewriteRelativeImportExtensions`, which supports the new TypeScript 5.7 feature `rewriteRelativeImportExtensions`

## Breaking change?

None



Discussion:
Hello there Azlond 👋

Thank you for opening your very first issue in this project. 

We will try to get back to you as soon as we can.👀
I can't see the comment for which I received an email, so I'll just reply to this issue in general.
There's no advantage over ` esm: true`, as it's a different use case. `rewriteRelativeImportExtensions` allows to import `.ts` files, so that TypeScript-runtimes without build/transpile-step can correctly handle the file resolution.

You can see an example in the beta announcement I linked in my first message.



Patch:
diff --git a/packages/cli/src/routeGeneration/routeGenerator.ts b/packages/cli/src/routeGeneration/routeGenerator.ts
index c19117750..a4f820f1d 100644
--- a/packages/cli/src/routeGeneration/routeGenerator.ts
+++ b/packages/cli/src/routeGeneration/routeGenerator.ts
@@ -151,9 +151,9 @@ export abstract class AbstractRouteGenerator<Config extends ExtendedRoutesConfig
 
   protected getRelativeImportPath(fileLocation: string) {
     const currentExt = path.extname(fileLocation);
-    let newExtension = '';
+    let newExtension = this.options.rewriteRelativeImportExtensions ? currentExt : '';
 
-    if (this.options.esm) {
+    if (this.options.esm && !this.options.rewriteRelativeImportExtensions) {
       switch (currentExt) {
         case '.ts':
         default:
diff --git a/packages/runtime/src/config.ts b/packages/runtime/src/config.ts
index f670a3816..dfbf4bfcd 100644
--- a/packages/runtime/src/config.ts
+++ b/packages/runtime/src/config.ts
@@ -269,4 +269,10 @@ export interface RoutesConfig {
    * @default true
    */
   bodyCoercion?: boolean;
+
+  /**
+   * When enabled, the imports in the routes files will keep having a `.ts` extention to support the TypeScript 5.7 feature rewriteRelativeImportExtensions.
+   * @default false
+   */
+  rewriteRelativeImportExtensions?: boolean;
 }


Test patch:
diff --git a/tests/unit/templating/routeGenerator.spec.ts b/tests/unit/templating/routeGenerator.spec.ts
index 4c6b388ed..66633d11f 100644
--- a/tests/unit/templating/routeGenerator.spec.ts
+++ b/tests/unit/templating/routeGenerator.spec.ts
@@ -179,5 +179,61 @@ describe('RouteGenerator', () => {
 
       expect(models).to.equal('./controller.cjs');
     });
+
+    it('uses ts for routes if esm is true and rewriteRelativeImportExtensions is true', () => {
+      const generator = new DefaultRouteGenerator(
+        {
+          controllers: [
+            {
+              location: 'controller.ts',
+              methods: [],
+              name: '',
+              path: '',
+            },
+          ],
+          referenceTypeMap: {},
+        },
+        {
+          bodyCoercion: true,
+          entryFile: 'mockEntryFile',
+          routesDir: '.',
+          noImplicitAdditionalProperties: 'silently-remove-extras',
+          esm: true,
+          rewriteRelativeImportExtensions: true,
+        },
+      );
+
+      const models = generator.buildContent('{{#each controllers}}{{modulePath}}{{/each}}');
+
+      expect(models).to.equal('./controller.ts');
+    });
+
+    it('uses mts for routes if rewriteRelativeImportExtensions and esm is true and source is mts', () => {
+      const generator = new DefaultRouteGenerator(
+        {
+          controllers: [
+            {
+              location: 'controller.mts',
+              methods: [],
+              name: '',
+              path: '',
+            },
+          ],
+          referenceTypeMap: {},
+        },
+        {
+          bodyCoercion: true,
+          entryFile: 'mockEntryFile',
+          routesDir: '.',
+          noImplicitAdditionalProperties: 'silently-remove-extras',
+          esm: true,
+          rewriteRelativeImportExtensions: true,
+        },
+      );
+
+      const models = generator.buildContent('{{#each controllers}}{{modulePath}}{{/each}}');
+
+      expect(models).to.equal('./controller.mts');
+    });
   });
 });