Просмотр исходного кода

cofigure biome for pdf-converter

Futa Arai 9 месяцев назад
Родитель
Сommit
4fe407e62b

+ 1 - 0
apps/pdf-converter/.eslintignore

@@ -0,0 +1 @@
+*

+ 0 - 13
apps/pdf-converter/.eslintrc.cjs

@@ -1,13 +0,0 @@
-/**
- * @type {import('eslint').Linter.Config}
- */
-module.exports = {
-  extends: '../../.eslintrc.js',
-  ignorePatterns: [
-    'dist/**',
-  ],
-  rules: {
-    'no-useless-constructor': 'off',
-    '@typescript-eslint/consistent-type-imports': 'off',
-  },
-};

+ 1 - 1
apps/pdf-converter/package.json

@@ -11,7 +11,7 @@
     "dev:pdf-converter": "nodemon -r \"dotenv-flow/config\" src/index.ts",
     "start:prod:ci": "pnpm start:prod --ci",
     "start:prod": "node dist/index.js",
-    "lint": "pnpm eslint **/*.{js,ts}",
+    "lint": "biome check",
     "gen:swagger-spec": "SKIP_PUPPETEER_INIT=true node --import @swc-node/register/esm-register src/bin/index.ts generate-swagger --output ./specs",
     "build": "pnpm tsc -p tsconfig.build.json",
     "version:prerelease": "pnpm version prerelease --preid=RC",

+ 6 - 4
apps/pdf-converter/src/controllers/pdf.spec.ts

@@ -9,7 +9,7 @@ describe('PdfCtrl', () => {
   beforeAll(PlatformTest.bootstrap(Server));
   afterAll(PlatformTest.reset);
 
-  it('should return 500 for invalid appId', async() => {
+  it('should return 500 for invalid appId', async () => {
     const request = SuperTest(PlatformTest.callback());
     await request
       .post('/pdf/sync-job')
@@ -22,7 +22,7 @@ describe('PdfCtrl', () => {
       .expect(500);
   });
 
-  it('should return 400 for invalid jobId', async() => {
+  it('should return 400 for invalid jobId', async () => {
     const request = SuperTest(PlatformTest.callback());
     const res = await request
       .post('/pdf/sync-job')
@@ -34,10 +34,12 @@ describe('PdfCtrl', () => {
       })
       .expect(400);
 
-    expect(res.body.message).toContain('jobId must be a valid MongoDB ObjectId');
+    expect(res.body.message).toContain(
+      'jobId must be a valid MongoDB ObjectId',
+    );
   });
 
-  it('should return 202 and status for valid request', async() => {
+  it('should return 202 and status for valid request', async () => {
     const request = SuperTest(PlatformTest.callback());
     const res = await request
       .post('/pdf/sync-job')

+ 36 - 18
apps/pdf-converter/src/controllers/pdf.ts

@@ -1,26 +1,38 @@
 import { BodyParams } from '@tsed/common';
 import { Controller } from '@tsed/di';
-import { InternalServerError, BadRequest } from '@tsed/exceptions';
-import { Logger } from '@tsed/logger';
+import { BadRequest, InternalServerError } from '@tsed/exceptions';
+import type { Logger } from '@tsed/logger';
 import {
-  Post, Returns, Enum, Description, Required, Integer,
+  Description,
+  Enum,
+  Integer,
+  Post,
+  Required,
+  Returns,
 } from '@tsed/schema';
 
-import PdfConvertService, { JobStatusSharedWithGrowi, JobStatus } from '../service/pdf-convert.js';
+import type PdfConvertService from '../service/pdf-convert.js';
+import { JobStatus, JobStatusSharedWithGrowi } from '../service/pdf-convert.js';
 
 @Controller('/pdf')
 class PdfCtrl {
-
-  constructor(private readonly pdfConvertService: PdfConvertService, private readonly logger: Logger) {}
+  constructor(
+    private readonly pdfConvertService: PdfConvertService,
+    private readonly logger: Logger,
+  ) {}
 
   @Post('/sync-job')
-  @(Returns(202).ContentType('application/json').Schema({
-    type: 'object',
-    properties: {
-      status: { type: 'string', enum: Object.values(JobStatus) },
-    },
-    required: ['status'],
-  }))
+  @(
+    Returns(202)
+      .ContentType('application/json')
+      .Schema({
+        type: 'object',
+        properties: {
+          status: { type: 'string', enum: Object.values(JobStatus) },
+        },
+        required: ['status'],
+      })
+  )
   @Returns(500)
   @Description(`
     Sync job pdf convert status with GROWI.
@@ -30,7 +42,10 @@ class PdfCtrl {
   async syncJobStatus(
     @Required() @BodyParams('jobId') jobId: string,
     @Required() @BodyParams('expirationDate') expirationDateStr: string,
-    @Required() @BodyParams('status') @Enum(Object.values(JobStatusSharedWithGrowi)) growiJobStatus: JobStatusSharedWithGrowi,
+    @Required()
+    @BodyParams('status')
+    @Enum(Object.values(JobStatusSharedWithGrowi))
+    growiJobStatus: JobStatusSharedWithGrowi,
     @Integer() @BodyParams('appId') appId?: number, // prevent path traversal attack
   ): Promise<{ status: JobStatus } | undefined> {
     // prevent path traversal attack
@@ -40,19 +55,22 @@ class PdfCtrl {
 
     const expirationDate = new Date(expirationDateStr);
     try {
-      await this.pdfConvertService.registerOrUpdateJob(jobId, expirationDate, growiJobStatus, appId);
+      await this.pdfConvertService.registerOrUpdateJob(
+        jobId,
+        expirationDate,
+        growiJobStatus,
+        appId,
+      );
       const status = this.pdfConvertService.getJobStatus(jobId); // get status before cleanup
       this.pdfConvertService.cleanUpJobList();
       return { status };
-    }
-    catch (err) {
+    } catch (err) {
       this.logger.error('Failed to register or update job', err);
       if (err instanceof Error) {
         throw new InternalServerError(err.message);
       }
     }
   }
-
 }
 
 export default PdfCtrl;

+ 6 - 5
apps/pdf-converter/src/controllers/terminus.ts

@@ -1,12 +1,14 @@
 import { Injectable } from '@tsed/di';
-import { Logger } from '@tsed/logger';
+import type { Logger } from '@tsed/logger';
 
-import PdfConvertService from '../service/pdf-convert.js';
+import type PdfConvertService from '../service/pdf-convert.js';
 
 @Injectable()
 class TerminusCtrl {
-
-  constructor(private readonly pdfConvertService: PdfConvertService, private readonly logger: Logger) {}
+  constructor(
+    private readonly pdfConvertService: PdfConvertService,
+    private readonly logger: Logger,
+  ) {}
 
   async $onSignal(): Promise<void> {
     this.logger.info('Server is starting cleanup');
@@ -16,7 +18,6 @@ class TerminusCtrl {
   $onShutdown(): void {
     this.logger.info('Cleanup finished, server is shutting down');
   }
-
 }
 
 export default TerminusCtrl;

+ 1 - 2
apps/pdf-converter/src/index.ts

@@ -19,8 +19,7 @@ async function bootstrap() {
       $log.info('"--ci" flag is detected. Exit process.');
       process.exit();
     }
-  }
-  catch (error) {
+  } catch (error) {
     $log.error(error);
   }
 }

+ 2 - 4
apps/pdf-converter/src/server.ts

@@ -1,4 +1,4 @@
-import { PlatformApplication } from '@tsed/common';
+import type { PlatformApplication } from '@tsed/common';
 import { Configuration, Inject } from '@tsed/di';
 import express from 'express';
 import '@tsed/swagger';
@@ -32,10 +32,8 @@ const PORT = Number(process.env.PORT || 3010);
   },
 })
 class Server {
-
   @Inject()
-    app: PlatformApplication | undefined;
-
+  app: PlatformApplication | undefined;
 }
 
 export default Server;

+ 51 - 32
apps/pdf-converter/src/service/pdf-convert.ts

@@ -3,9 +3,9 @@ import path from 'path';
 import { Readable, Writable } from 'stream';
 import { pipeline as pipelinePromise } from 'stream/promises';
 
-import { OnInit } from '@tsed/common';
+import type { OnInit } from '@tsed/common';
 import { Service } from '@tsed/di';
-import { Logger } from '@tsed/logger';
+import type { Logger } from '@tsed/logger';
 import { Cluster } from 'puppeteer-cluster';
 
 interface PageInfo {
@@ -24,8 +24,9 @@ export const JobStatus = {
   PDF_EXPORT_DONE: 'PDF_EXPORT_DONE',
 } as const;
 
-export type JobStatusSharedWithGrowi = typeof JobStatusSharedWithGrowi[keyof typeof JobStatusSharedWithGrowi]
-export type JobStatus = typeof JobStatus[keyof typeof JobStatus]
+export type JobStatusSharedWithGrowi =
+  (typeof JobStatusSharedWithGrowi)[keyof typeof JobStatusSharedWithGrowi];
+export type JobStatus = (typeof JobStatus)[keyof typeof JobStatus];
 
 interface JobInfo {
   expirationDate: Date;
@@ -35,7 +36,6 @@ interface JobInfo {
 
 @Service()
 class PdfConvertService implements OnInit {
-
   private puppeteerCluster: Cluster | undefined;
 
   private maxConcurrency = 1;
@@ -65,17 +65,16 @@ class PdfConvertService implements OnInit {
    * @param appId application ID for GROWI.cloud
    */
   async registerOrUpdateJob(
-      jobId: string,
-      expirationDate: Date,
-      status: JobStatusSharedWithGrowi,
-      appId?: number,
+    jobId: string,
+    expirationDate: Date,
+    status: JobStatusSharedWithGrowi,
+    appId?: number,
   ): Promise<void> {
     const isJobNew = !(jobId in this.jobList);
 
     if (isJobNew) {
       this.jobList[jobId] = { expirationDate, status };
-    }
-    else {
+    } else {
       const jobInfo = this.jobList[jobId];
       jobInfo.expirationDate = expirationDate;
 
@@ -133,20 +132,25 @@ class PdfConvertService implements OnInit {
 
   private isJobCompleted(jobId: string): boolean {
     if (this.jobList[jobId] == null) return true;
-    return this.jobList[jobId].status === JobStatus.PDF_EXPORT_DONE || this.jobList[jobId].status === JobStatus.FAILED;
+    return (
+      this.jobList[jobId].status === JobStatus.PDF_EXPORT_DONE ||
+      this.jobList[jobId].status === JobStatus.FAILED
+    );
   }
 
-
   /**
    * Read html files from shared fs path, convert them to pdf, and save them to shared fs path.
    * Repeat this until all html files are converted to pdf or job fails.
    * @param jobId PageBulkExportJob ID
    * @param appId application ID for GROWI.cloud
    */
-  private async readHtmlAndConvertToPdfUntilFinish(jobId: string, appId?: number): Promise<void> {
+  private async readHtmlAndConvertToPdfUntilFinish(
+    jobId: string,
+    appId?: number,
+  ): Promise<void> {
     while (!this.isJobCompleted(jobId)) {
       // eslint-disable-next-line no-await-in-loop
-      await new Promise(resolve => setTimeout(resolve, 10 * 1000));
+      await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
 
       try {
         if (new Date() > this.jobList[jobId].expirationDate) {
@@ -160,11 +164,12 @@ class PdfConvertService implements OnInit {
         // eslint-disable-next-line no-await-in-loop
         await pipelinePromise(htmlReadable, pdfWritable);
         this.jobList[jobId].currentStream = undefined;
-      }
-      catch (err) {
+      } catch (err) {
         this.logger.error('Failed to convert html to pdf', err);
         this.jobList[jobId].status = JobStatus.FAILED;
-        this.jobList[jobId].currentStream?.destroy(new Error('Failed to convert html to pdf'));
+        this.jobList[jobId].currentStream?.destroy(
+          new Error('Failed to convert html to pdf'),
+        );
         break;
       }
     }
@@ -177,8 +182,14 @@ class PdfConvertService implements OnInit {
    * @returns readable stream
    */
   private getHtmlReadable(jobId: string, appId?: number): Readable {
-    const jobHtmlDir = path.join(this.tmpHtmlDir, appId?.toString() ?? '', jobId);
-    const htmlFileEntries = fs.readdirSync(jobHtmlDir, { recursive: true, withFileTypes: true }).filter(entry => entry.isFile());
+    const jobHtmlDir = path.join(
+      this.tmpHtmlDir,
+      appId?.toString() ?? '',
+      jobId,
+    );
+    const htmlFileEntries = fs
+      .readdirSync(jobHtmlDir, { recursive: true, withFileTypes: true })
+      .filter((entry) => entry.isFile());
     let index = 0;
 
     const jobList = this.jobList;
@@ -187,7 +198,10 @@ class PdfConvertService implements OnInit {
       objectMode: true,
       async read() {
         if (index >= htmlFileEntries.length) {
-          if (jobList[jobId].status === JobStatus.HTML_EXPORT_DONE && htmlFileEntries.length === 0) {
+          if (
+            jobList[jobId].status === JobStatus.HTML_EXPORT_DONE &&
+            htmlFileEntries.length === 0
+          ) {
             jobList[jobId].status = JobStatus.PDF_EXPORT_DONE;
           }
           this.push(null);
@@ -212,8 +226,10 @@ class PdfConvertService implements OnInit {
   private getPdfWritable(): Writable {
     return new Writable({
       objectMode: true,
-      write: async(pageInfo: PageInfo, encoding, callback) => {
-        const fileOutputPath = pageInfo.htmlFilePath.replace(new RegExp(`^${this.tmpHtmlDir}`), this.tmpOutputRootDir).replace(/\.html$/, '.pdf');
+      write: async (pageInfo: PageInfo, encoding, callback) => {
+        const fileOutputPath = pageInfo.htmlFilePath
+          .replace(new RegExp(`^${this.tmpHtmlDir}`), this.tmpOutputRootDir)
+          .replace(/\.html$/, '.pdf');
         const fileOutputParentPath = this.getParentPath(fileOutputPath);
 
         try {
@@ -222,8 +238,7 @@ class PdfConvertService implements OnInit {
           await fs.promises.writeFile(fileOutputPath, pdfBody);
 
           await fs.promises.rm(pageInfo.htmlFilePath, { force: true });
-        }
-        catch (err) {
+        } catch (err) {
           if (err instanceof Error) {
             callback(err);
           }
@@ -240,13 +255,15 @@ class PdfConvertService implements OnInit {
    * @returns converted pdf
    */
   private async convertHtmlToPdf(htmlString: string): Promise<Buffer> {
-    const executeConvert = async(retries: number): Promise<Buffer> => {
+    const executeConvert = async (retries: number): Promise<Buffer> => {
       try {
         return this.puppeteerCluster?.execute(htmlString);
-      }
-      catch (err) {
+      } catch (err) {
         if (retries > 0) {
-          this.logger.error('Failed to convert markdown to pdf. Retrying...', err);
+          this.logger.error(
+            'Failed to convert markdown to pdf. Retrying...',
+            err,
+          );
           return executeConvert(retries - 1);
         }
         throw err;
@@ -270,7 +287,7 @@ class PdfConvertService implements OnInit {
       workerCreationDelay: 10000,
     });
 
-    await this.puppeteerCluster.task(async({ page, data: htmlString }) => {
+    await this.puppeteerCluster.task(async ({ page, data: htmlString }) => {
       await page.setContent(htmlString, { waitUntil: 'domcontentloaded' });
       await page.addStyleTag({
         content: `
@@ -282,7 +299,10 @@ class PdfConvertService implements OnInit {
       await page.emulateMediaType('screen');
       const pdfResult = await page.pdf({
         margin: {
-          top: '100px', right: '50px', bottom: '100px', left: '50px',
+          top: '100px',
+          right: '50px',
+          bottom: '100px',
+          left: '50px',
         },
         printBackground: true,
         format: 'A4',
@@ -303,7 +323,6 @@ class PdfConvertService implements OnInit {
     }
     return parentPath;
   }
-
 }
 
 export default PdfConvertService;

+ 1 - 3
apps/pdf-converter/vitest.config.ts

@@ -6,7 +6,5 @@ export default defineConfig({
     globals: true,
     root: './',
   },
-  plugins: [
-    swc.vite(),
-  ],
+  plugins: [swc.vite()],
 });

+ 18 - 2
biome.json

@@ -18,7 +18,8 @@
       ".eslintrc.js",
       ".stylelintrc.json",
       "package.json",
-      "./apps/**",
+      "./apps/app/**",
+      "./apps/slackbot-proxy/**",
       "./packages/editor/**",
       "./packages/pdf-converter-client/src/index.ts"
     ]
@@ -39,6 +40,21 @@
   "javascript": {
     "formatter": {
       "quoteStyle": "single"
+    },
+    "parser": {
+      "unsafeParameterDecoratorsEnabled": true
     }
-  }
+  },
+  "overrides": [
+    {
+      "include": ["./apps/slackbot-proxy/**"],
+      "linter": {
+        "rules": {
+          "style": {
+            "useImportType": "error"
+          }
+        }
+      }
+    }
+  ]
 }