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

@nestjs/terminus cannot be embedded in a webpack bundle #1423

Open
linsolas opened this issue Sep 21, 2021 · 13 comments
Open

@nestjs/terminus cannot be embedded in a webpack bundle #1423

linsolas opened this issue Sep 21, 2021 · 13 comments

Comments

@linsolas
Copy link

I'm submitting a...


[ ] Regression 
[ ] Bug report
[X] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

Hello,

I'm developing several Nestjs applications, and for deployment purposes, I generally bundle the whole application into one single JS file, using webpack (through the command nest build --webpack).

My first problem is due to the require(``${module}``) statement in checkPackage.util.ts. In front of such statement, Webpack tries to embed all resources from the sames directory, which includes .d.ts files (such as node_modules/@nestjs/terminus/dist/lib/utils/checkPackage.d.ts. And then it fails saying:

ERROR in ./node_modules/@nestjs/terminus/dist/utils/checkPackage.util.d.ts 13:7
Module parse failed: Unexpected token (13:7)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| * checkPackages(['process', 'no_package'], 'TEST')
| */
export declare function checkPackages(packageNames: string[], reason: string): any[];
|

It can be avoided by overriding the default webpack.config.js with something like this:

module.exports = function (options) {
  const outputDirname = path.dirname(options.output.filename);
  // Ignore `.d.ts` files...
  options.module.rules.push({
    test: /\.d\.ts$/,
    use: 'null-loader',
  });
  ...

So now I can build my application... But it does not run anymore, because on runtime, @nestjs/terminus tries to load these dependencies from an inexistent node_modules directory.

[Nest] 12465 - 09/21/2021, 11:23:50 AM ERROR [PackageLoader] The "@nestjs/typeorm", "typeorm" packages are missing. Please, make sure to install the libraries ($ npm install @nestjs/typeorm typeorm) to take advantage of TypeOrmHealthIndicator.

Expected behavior

I would like to embed the @nestjs/terminus module in my application built with webpack. I would like to be able to skip these pre-checks by Terminus with an option, so Terminus will not throw this exception and quits.

What is the motivation / use case for changing the behavior?

To use @nestjs/terminus in my Nestjs application built with webpack.

Environment


Nest version: 8.0.6

 
For Tooling issues:
- Node version: v12
- Platform:  Windows / Linux 


Thanks.
@BrunnerLivio
Copy link
Member

Thanks for reporting. I assumed this would be a problem. I assume this is going to be a tricky one to fix and need to investigate what is going to be the best option here!

@JAZZ-FROM-HELL
Copy link

@BrunnerLivio We've encountered a similar issue with esbuild. Are there any updates on it?

@pietrovismara
Copy link

@BrunnerLivio we have the same issue with yarn pnp. The optional check points to a path that will only work when working in a "classic" npm node.js environment.

I believe a solution would be to dismiss the checkPackages function and use the peerDependenciesMeta package.json field instead.

@ghost
Copy link

ghost commented Apr 4, 2022

Same issue. Could not find a proper way around this. Not even a improper one.

@jknight
Copy link

jknight commented Jun 29, 2022

I was able to get this working with the following webpack.config.js setup, which depends on an external node_modules folder and webpack-node-externals:

const nodeExternals = require('webpack-node-externals');   // pnpm/yarn add to package.json 
module.exports = (config, options, targetOptions) => {
  return {
    target: 'node', // https://webpack.js.org/configuration/target/
    externals: [nodeExternals()], // https://github.com/liady/webpack-node-externals
    externalsPresets: { node: true }, // https://github.com/liady/webpack-node-externals
    module: {
      rules: [
        {
          // "Fixes" build issue as suggested by @[linsolas](https://github.com/linsolas)
          //  https://github.com/nestjs/terminus/issues/1423#issue-1002145070
          test: /@nestjs\/terminus\/dist\/utils\/.*\.ts$/,
          loader: 'null-loader'
        }
      ]
    }
  };
};

We're using docker, so we install the node dependencies in the Dockerfile, the same as it's done here.

Our build output (ie in the docker image in our case) looks like this:

/app/dist # ls -1
3rdpartylicenses.txt
assets
generated-assets
main.js
main.js.map
node_modules
package-lock.json
package.json

And running with node main.js, it works:

/app/dist # curl localhost/health
{"status":"ok","info":{"tcp":{"status":"up"}},"error":{},"details":{"tcp":{"status":"up"}}}

Externalizing node_modules like this has implications and I am not sure if this is a solution or a hack.

Given that:

In many real-world scenarios (depending on what libraries are being used), you should not bundle Node.js applications
(not only NestJS applications) with all dependencies (external packages located in the node_modules folder).

is this a viable solution ?

@orlein
Copy link

orlein commented Dec 7, 2022

In many reasons, along with the hugeness of node_modules, sometimes we need a bundled js file without any other dependencies. All nest.js developers can use nest build without webpack configuration, and that is the same as using nodeExternals(). Therefore, externalizing node_modules is not the solution.

I suggest that this issue can be solved by ignoring checkDependantPackages member method https://github.com/nestjs/terminus/blob/master/lib/health-indicator/database/typeorm.health.ts#L54-L56 defined here. Of course there should be a flag to force ignoring the method.

I've tested by editing the js file inside node_modules folder.

@vance-liu
Copy link

Any updates?

@slabkodima90
Copy link

Hi, same issue. Any updates?

@alvincrisuy
Copy link

Hi I am still encountering this issue.

@elderapo
Copy link

The following patch fixes the issue for me.

diff --git a/dist/utils/checkPackage.util.js b/dist/utils/checkPackage.util.js
index 588531e53cc194a91de222ac3a3dac272863933c..a22c060e2069f586491f07186cc56ff440c37d9c 100644
--- a/dist/utils/checkPackage.util.js
+++ b/dist/utils/checkPackage.util.js
@@ -55,9 +55,9 @@ function checkPackages(packageNames, reason) {
         .filter((pkg) => pkg.pkg === null)
         .map((pkg) => packageNames[pkg.index]);
     if (missingDependenciesNames.length) {
-        logger.error(MISSING_REQUIRED_DEPENDENCY(missingDependenciesNames, reason));
-        logger_service_1.Logger.flush();
-        process.exit(1);
+        // logger.error(MISSING_REQUIRED_DEPENDENCY(missingDependenciesNames, reason));
+        // logger_service_1.Logger.flush();
+        // process.exit(1);
     }
     return packages.map((pkg) => pkg.pkg);
 }

@DannyM1chael
Copy link

Still facing the issue as well

@michsien104
Copy link

Are there any updates on this issue?

@DannyM1chael
Copy link

DannyM1chael commented Jun 21, 2024

Are there any updates on this issue?

Terminus is still not working with webpack, so I had to create my own health check module like workaround:

import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller";
import { HealthService } from "./health.service";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [ConfigModule],
  controllers: [HealthController],
  providers: [HealthService]
})
export class HealthModule {}
import { Injectable, Logger } from "@nestjs/common";
import { Client, credentials } from "@grpc/grpc-js";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class HealthService {
  private readonly logger = new Logger(HealthService.name);
  private client: Client;

  constructor(private configService: ConfigService) {
    const grpcUrl = this.configService.get<string>("GRPC_URL") || "127.0.0.1:50051";
    this.client = new Client(grpcUrl, credentials.createInsecure());
  }

  async checkGrpcHealth(timeoutMs: number): Promise<void> {
    return new Promise((resolve, reject) => {
      const deadline = Date.now() + timeoutMs;
      this.client.waitForReady(deadline, (err) => {
        if (err) {
          this.logger.error("gRPC service is not ready.", err.message);
          return reject(new Error("Timed out waiting for gRPC service to become ready."));
        }
        this.logger.log("gRPC service is ready.");
        resolve();
      });
    });
  }

  async isGrpcServiceReady(): Promise<boolean> {
    try {
      await this.checkGrpcHealth(30000);
      return true;
    } catch (error) {
      this.logger.error("gRPC service is not ready.", error);
      return false;
    }
  }
}
import { Controller, Get, HttpException, HttpStatus } from "@nestjs/common";
import { HealthService } from "./health.service";

@Controller("health")
export class HealthController {
  constructor(private readonly healthService: HealthService) {}

  @Get()
  async checkHealth() {
    const isReady = await this.healthService.isGrpcServiceReady();
    if (isReady) {
      return { status: "ok" };
    } else {
      throw new HttpException("Service Unavailable", HttpStatus.SERVICE_UNAVAILABLE);
    }
  }
}
import { NestFactory } from "@nestjs/core";
import { ApiGatewayModule } from "./api-gateway.module";
import { ValidationPipe, Logger } from "@nestjs/common";
import dotenv from "dotenv";
import { HealthService } from "./health/health.service";
import { HttpExceptionFilter } from "./http-exception.filter";
import { WsExceptionFilter } from "./ws-exception.filter";
import { SetHeadersMiddleware } from "./middleware";

dotenv.config();

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(ApiGatewayModule);
  app.enableCors({ origin: true, credentials: true });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
      transformOptions: {
        enableImplicitConversion: true
      }
    })
  );

  app.use(new SetHeadersMiddleware().use);

  app.useGlobalFilters(new HttpExceptionFilter());
  app.useGlobalFilters(new WsExceptionFilter());

  app.setGlobalPrefix("api/v1");

  const healthService = app.get(HealthService);
  const logger = new Logger("Main");
  try {
    await healthService.isGrpcServiceReady();
    logger.log("Starting the application...");
  } catch (error) {
    logger.error("Failed to connect to gRPC service. Exiting...", error.message);
    process.exit(1);
  }

  const port = process.env.PORT || 8080;
  const devMode = process.env.NODE_ENV === "development";
  const host = devMode ? "127.0.0.1" : "0.0.0.0";

  await app.listen(port, host);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}

bootstrap();

I hope it will help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests