I am encountering an issue with debugging JavaScript and TypeScript files in a project utilizing an Express server and Swagger API (Swagger).
The problem arises when attempting to set breakpoints in the controller files. These breakpoints are not being set, and I receive the error: “Unbound breakpoint”. However, I am able to debug initialization files, such as index.ts
and expressServer.js
, without any issues.
Steps to reproduce:
-
Add breakpoint in index.ts
-
Add breakpoint in expressServer.js
-
Add breakpoint in some of the controller files.
-
Launch the debugger (The server is stopped)
-
The debugger breaks in index.ts file
-
Upon continue(F5) breaks in expressServer.js
-
Take a look at the breakpoint in some of the controller files and verify it is grayed out with message “Unbound breakpoint”
-
Request the server via Postman
-
Unable to send a request (loading infinitely) since the server is not started
Note: I am testing it on virtual machine where it has NodeJS v20.17.0 and TypeScript 5.7.2
I suspect that the problem lies with the Swagger API configuration. The configuration file openapi.yaml
used by Swagger appears to be distributing the requests to the controllers and their endpoints, making these files inaccessible to the debugger. If this assumption is incorrect, I would appreciate any clarification or guidance on resolving this issue.
Thank you for your assistance.
Server initialization files:
expressServer.js
// const { Middleware } = require('swagger-express-middleware')
const http = require('http')
const https = require('https')
const fs = require('fs')
const path = require('path')
const swaggerUI = require('swagger-ui-express')
const jsYaml = require('js-yaml')
const express = require('express')
const cors = require('cors')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const OpenApiValidator = require('express-openapi-validator')
const logger = require('./logger')
const config = require('./config')
const { getBasePath } = require('./utils/swaggerUtils')
const { updateAPISpecification } = require('./utils/fixes')
const { entrypoint } = require('./utils/entrypoint')
const cleanPath = (path) => path.replace(////,'/')
class ExpressServer {
constructor(port, openApiYaml) {
this.port = port;
this.app = express();
this.openApiPath = openApiYaml;
try {
this.schema = jsYaml.safeLoad(fs.readFileSync(openApiYaml))
updateAPISpecification(this.schema, openApiYaml)
} catch (e) {
logger.error('failed to start Express Server', e.message)
}
this.setupMiddleware()
}
setupMiddleware() {
const basePath = getBasePath().replace(//$/,'')
// this.setupAllowedMedia();
this.app.use(cors())
// this.app.use(bodyParser.json({ limit: '14MB' }))
this.app.use(express.json())
this.app.use(express.text())
this.app.use(express.urlencoded({ extended: false }))
this.app.use(cookieParser())
//Simple test to see that the server is up and responding
this.app.get('/hello', (req, res) => res.send(`Hello World. path: ${this.openApiPath}`))
const openapi=config.OPENAPI || '/openapi'
//Send the openapi document *AS GENERATED BY THE GENERATOR*
this.app.get(cleanPath(`${basePath}${openapi}`), (req, res) => res.sendFile((path.join(__dirname, 'api', 'openapi.yaml'))))
//View the openapi document in a visual interface. Should be able to test from this page
this.app.use(cleanPath(`${basePath}/api-docs`), swaggerUI.serve, swaggerUI.setup(this.schema))
this.app.get('/login-redirect', (req, res) => {
res.status(200)
res.json(req.query)
})
this.app.get('/oauth2-redirect.html', (req, res) => {
res.status(200)
res.json(req.query)
})
this.app.get(`${openapi}`, function(req, res) {
res.redirect(cleanPath(`${basePath}/${openapi}`))
})
this.app.get('/api-docs', function(req, res) {
res.redirect(cleanPath(`${basePath}/api-docs`))
})
this.app.use(cleanPath(`${basePath}/entrypoint`), entrypoint)
this.app.get('/', function(req, res) {
res.redirect(cleanPath(`${basePath}/entrypoint`))
})
this.app.get(cleanPath(`${basePath}/`), function(req, res) {
res.redirect(cleanPath(`${basePath}/entrypoint`))
})
const apiTimeout = 5 * 1000
if(false) this.app.use((req, res, next) => {
// Set the timeout for all responses
res.setTimeout(apiTimeout, () => {
let err = new Error('Service Unavailable')
err.status = 503
next(err)
})
})
}
launch() {
try {
this.app.use( OpenApiValidator.middleware({
apiSpec: this.openApiPath,
operationHandlers: path.join(__dirname, 'built'),
validateRequests: true, // (default)
validateResponses: true, // false by default
unknownFormats: ['base64']
}))
// this.app.use(validator)
logger.info(`validator installed`)
this.app.use((err, req, res, next) => {
// format errors
res.status(err.status || 500).json({
message: err.message || err,
errors: err.errors || '',
})
})
let server;
const fullchainPath = '/certificates/fullchain.pem';
const privkeyPath = '/certificates/privkey.pem';
if (fs.existsSync(fullchainPath) && fs.existsSync(privkeyPath)) {
logger.info('Running with SSL');
const sslCert = {
cert: fs.readFileSync('/certificates/fullchain.pem'),
key: fs.readFileSync('/certificates/privkey.pem')
};
server = https.createServer(sslCert, this.app)
} else {
logger.info('Running without SSL');
server = http.createServer(this.app)
}
server.listen(this.port)
logger.info(`Listening on port ${this.port}`)
} catch(e) {
logger.error(`launch error: ` + e)
}
}
async close() {
if (this.server !== undefined) {
await this.server.close();
logger.info(`Server on port ${this.port} shut down`)
}
}
}
module.exports = ExpressServer;
index.ts
import config from './config';
import logger from './logger';
import ExpressServer from './expressServer';
import Service from './services/Service';
import NotificationHandler from './services/NotificationHandler';
import plugins from './plugins/plugins';
Service.setDB(plugins.db);
Service.setNotifier(NotificationHandler);
NotificationHandler.setDB(plugins.db);
NotificationHandler.setQueue(plugins.queue);
class ServerLauncher {
private expressServer: ExpressServer | undefined;
public async launchServer(): Promise<void> {
try {
this.expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
await this.expressServer.launch();
logger.info('Express server running');
} catch (error) {
logger.error(error);
await this.close();
}
}
private async close(): Promise<void> {
if (this.expressServer) {
await this.expressServer.close();
}
}
}
const serverLauncher = new ServerLauncher();
serverLauncher.launchServer().catch((error) => logger.error(error));
This is the configuration i use:
launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/built/index.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/built/**/*.js"],
"port": 9229
}
]
}
tsconfig.json
{
"compilerOptions": {
"target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./", /* Specify the root folder within your source files. */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"outDir": "./built", /* Specify an output folder for all emitted files. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["/**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}