Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live reloading with tsx and tsoa #1667

Closed
2 of 4 tasks
edwinhern opened this issue Aug 27, 2024 · 8 comments
Closed
2 of 4 tasks

Live reloading with tsx and tsoa #1667

edwinhern opened this issue Aug 27, 2024 · 8 comments
Labels

Comments

@edwinhern
Copy link

Sorting

  • I'm submitting a ...

    • bug report
    • feature request
    • support request
  • I confirm that I

    • used the search to make sure that a similar issue hasn't already been submit

Expected Behavior

When I run npm run debug, it should enable hot/live reloading so that whenever I make changes to my code, the spec and routes files are automatically regenerated.

Current Behavior

Currently, when I run npm run debug, the script "debug": "concurrently \"tsoa spec-and-routes\" \"npm:dev\"", only executes tsoa spec-and-routes once on execution. It doesn't provide hot/live reloading functionality, which I need.

Possible Solution

I'm looking for a solution that allows tsoa to watch my files and automatically regenerate the spec and routes files upon changes.

Steps to Reproduce

  1. Git clone https://github.com/edwinhern/express-typescript-2024.git
  2. Git Checkout git checkout -b feat08.24-tsoa
  3. Install Dependencies npm ci
  4. Run npm run debug
  5. Modify any controller file to trigger route changes. [src/api/user/userController.ts]

Context (Environment)

Version of the library: ^6.4.0
Version of NodeJS: 22.7.0

  • Confirm you were using yarn not npm: [npm]

Detailed Description

I'm trying to run the following script:

"debug": "concurrently \"tsoa spec-and-routes\" \"npm:dev\""

However, instead of only running tsoa spec-and-routes on execution, I want to have hot/live reloading where any change in my TypeScript files will automatically regenerate the spec and routes files.

Here’s my tsoa.json file:

{
  "entryFile": "src/index.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/**/*Controller.ts"],
  "spec": {
    "outputDirectory": "src/api/",
    "specVersion": 3
  },
  "routes": {
    "routesDir": "src/api/"
  },
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

I made the outputDirectory inside my src folder so that when I build, it will compile the code rather than keeping it in the ts file for routes.ts.

I'm currently working on the following branch: feat08.24-tsoa

Breaking change?

This is not a breaking change, but I want to ensure that the functionality doesn't disrupt the existing library operations. Any guidance on achieving this would be appreciated.

Copy link

Hello there edwinhern 👋

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

We will try to get back to you as soon as we can.👀

@blipk
Copy link

blipk commented Aug 29, 2024

I had a similar issue and this was my solution:

Switch to the programatic generation rather than CLI commands:
(You will need to change the appRoot path relative to where you store this file. Further changes may be required if you're not using a module)
(edited to updated version that works with tsup and tsConfig.options.verbatimModuleSyntax)

/**
 * @name tsoa-generator.ts
 * This file uses TSOA to generate the OpenAPI/Swagger spec, as well as the TSOA routes from the class controllers.
 */
import fs from "fs"
import path from "path"

import * as ts from "typescript"

import type {
    ExtendedRoutesConfig,
    ExtendedSpecConfig,
    Config
} from "tsoa"
import {
    generateRoutes,
    generateSpec
} from "tsoa"

const dirname = import.meta.dirname

const appRoot = fs.existsSync( path.resolve( dirname, "tsoa.json" ) ) ? dirname : path.resolve( dirname, "../../../" )

// https://github.com/lukeautry/tsoa/issues/868#issuecomment-1445941911
// This probably isn't necessary in this applications case, but may save some trouble in the future
const tsConfigFileName = path.resolve( appRoot, "tsconfig.json" )
const tsConfigFile = ts.readConfigFile( tsConfigFileName, ( path, encoding?: string ) => ts.sys.readFile( path, encoding ) )
const tsConfigContent = ts.parseJsonConfigFileContent( tsConfigFile.config, ts.sys, dirname )
const tsCompilerOptions = tsConfigContent.options

// Import the config from `tsoa.json` - this makes it so options remain the same if we use the CLI or this function
const tsoaConfigFileName = path.resolve( appRoot, "tsoa.json" )
const tsoaConfigFileContent = fs.readFileSync( tsoaConfigFileName, "utf8" )
const tsoaConfig: Config = JSON.parse( tsoaConfigFileContent ) as Config


const specOptions = { ...tsoaConfig.spec, ...tsoaConfig } as ExtendedSpecConfig
const routeOptions = { ...tsoaConfig.routes, ...tsoaConfig } as ExtendedRoutesConfig


const generateTSOA = async (): Promise<void> => {
    /**
     * This function programatically generates the TSOA spec file and routes file.
     * This is so the routes and spec can always be updated before launching the server.
     */
    console.log( "Generating spec..." )
    await generateSpec( specOptions, tsCompilerOptions, tsoaConfig.ignore )

    console.log( "Generating routes..." )
    await generateRoutes( routeOptions, tsCompilerOptions, tsoaConfig.ignore )

    console.log( "Fixing routes imports..." )
    const routesFile = path.resolve( tsoaConfig.routes.routesDir, "routes.ts" )
    if ( tsCompilerOptions.verbatimModuleSyntax && fs.existsSync( routesFile ) ) {
        const routesFileContents = fs.readFileSync( routesFile, "utf8" )
        const lines = routesFileContents.split( "\n" )
        lines.splice( 3, 0, "// @ts-ignore" )
        fs.writeFileSync( routesFile, lines.join( "\n" ) )
    }

    console.log( "TSOA Generation complete." )
}

// Check if the current script is being run directly
const isRunDirectly = import.meta.url.includes( process.argv[ 1 ] ) && import.meta.url.includes( "tsoa-generator.ts" )
if ( isRunDirectly )
    await generateTSOA()


export default generateTSOA

Import the generator in my index.ts

// ...
// Regenerate our routes before importing them
import generateTSOA from "./data/generators/tsoa-generator.ts"
await generateTSOA()
import { RegisterRoutes } from "./tsoa-build/routes.ts"
// ...

Ignore the tsoa-build files in my tsx watch script to avoid recursion:

"dev": "tsx watch --ignore 'src/tsoa-build/**' src/index.ts",

Also can have a script if you want to run it seperately:

"gen-tsoa": "tsx src/data/generators/tsoa-generator.ts",

@edwinhern
Copy link
Author

@blipk Awesome! I'll try it out tn :)

@edwinhern
Copy link
Author

edwinhern commented Aug 29, 2024

Hey @blipk, it worked! I can now auto-generate with code changes. However, I'm stuck migrating from CommonJS to ESM. Do you have any advice? I pushed my latest code to this branch: GitHub Link.

Since I had to use import.meta.url, I switched to ES2022 for module and Node for moduleResolution in tsconfig.json. Everything works during development, and the build runs fine (npm run build). However, when I try to start the app (npm run start), it fails with an error during TSOA generation.

I'm using tsup for the build with this configuration:

"tsup": {
  "entry": ["src", "!src/**/__tests__/**", "!src/**/*.test.*"],
  "sourcemap": false,
  "clean": true,
  "minify": true,
  "format": ["esm"],
  "skipNodeModulesBundle": true
}

I've also tried to set the programmatic version to run only on development

if (env.isDevelopment) await buildApiSpecAndRoutes();

Any tips or code reference would be greatly appreciated!

@blipk
Copy link

blipk commented Aug 29, 2024

@edwinhern

I'm not sure as I'm not using tsup or esbuild.

I just tried to build with tsc and had to solve a couple of issues related to this: #1272 (comment)

Meaning I had to update my tsoa.json config to:

...
    "routes": {
        "esm": true,
        "bodyCoercion": true,
        "basePath": "/",
        "routesDir": "./src/tsoa-build"
    },
...

It builds fine now but not really usable as I only get types, but maybe it could help with your tsup problem

I haven't set up a bundler yet and I'm probably just intending to run it directly with tsx

Let me know if you get it working and I might consider using tsup as well

@blipk
Copy link

blipk commented Aug 29, 2024

@edwinhern What error are you actually running into?

I tried to build with tsup and it builds fine but when I run it I keep running into this:

SyntaxError: Named export 'TsoaResponse' not found. The requested module 'tsoa' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

I can resolve it by changing my import syntax to the commented format here:

import { Get, Route, Tags, Post, Body, Path, Response, SuccessResponse, Controller, Res, TsoaResponse } from "tsoa"
// import * as tsoa from "tsoa"
// const { Get, Route, Tags, Post, Body, Path, Response, SuccessResponse, Controller, Res, TsoaResponse } = tsoa

But I'd rather not do that.

Do you have the same issue?

@blipk
Copy link

blipk commented Aug 29, 2024

After making the import changes I got it to run but had to change these two lines in tsoa-generator.ts

//...
const appRoot = fs.existsSync( path.resolve( dirname, "tsoa.json" ) ) ? dirname : path.resolve( dirname, "../../../" )
//...
const isRunDirectly = import.meta.url.includes( process.argv[ 1 ] ) && import.meta.url.includes( "tsoa-generator.ts" )
//...

As well as copying some files in my build script:

"build": "tsup && cp tsconfig.json dist/tsconfig.json && cp tsoa.json dist/tsoa.json && cp src/tsoa-build/openapi.json dist/tsoa-build/openapi.json",

I also fixed my error by changing my imports to import the TsoaResponse as a type

import { Get, Route, Tags, Post, Body, Path, Response, SuccessResponse, Controller, Res } from "tsoa"
import type { TsoaResponse } from "tsoa"

I have lint rule for this, but I really should pay attention to the caveats: https://typescript-eslint.io/rules/consistent-type-imports/#caveat-decorators--experimentaldecorators-true--emitdecoratormetadata-true

Copy link

github-actions bot commented Oct 1, 2024

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

@github-actions github-actions bot added the Stale label Oct 1, 2024
@github-actions github-actions bot closed this as completed Oct 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants