From 6807d43e6c2262cf59902493572547bd4b2e3451 Mon Sep 17 00:00:00 2001
From: miyaji255 <84168445+miyaji255@users.noreply.github.com>
Date: Sun, 12 Jan 2025 11:33:35 +0900
Subject: [PATCH 1/2] perf(build): Use WebWorker when removing private fields

---
 build/build.ts                                | 22 +++--
 ...s => remove-private-fields-worker.test.ts} |  2 +-
 build/remove-private-fields-worker.ts         | 85 +++++++++++++++++++
 build/remove-private-fields.ts                | 81 ++++++++----------
 4 files changed, 134 insertions(+), 56 deletions(-)
 rename build/{remove-private-fields.test.ts => remove-private-fields-worker.test.ts} (90%)
 create mode 100644 build/remove-private-fields-worker.ts

diff --git a/build/build.ts b/build/build.ts
index b2a439c69..f7e1a8aff 100644
--- a/build/build.ts
+++ b/build/build.ts
@@ -14,7 +14,7 @@ import type { Plugin, PluginBuild, BuildOptions } from 'esbuild'
 import * as glob from 'glob'
 import fs from 'fs'
 import path from 'path'
-import { removePrivateFields } from './remove-private-fields'
+import { cleanupWorkers, removePrivateFields } from './remove-private-fields'
 import { validateExports } from './validate-exports'
 
 const args = arg({
@@ -102,14 +102,18 @@ const dtsEntries = glob.globSync('./dist/types/**/*.d.ts')
 const writer = stdout.writer()
 writer.write('\n')
 let lastOutputLength = 0
-for (let i = 0; i < dtsEntries.length; i++) {
-  const entry = dtsEntries[i]
+let removedCount = 0
 
-  const message = `Removing private fields(${i + 1}/${dtsEntries.length}): ${entry}`
-  writer.write(`\r${' '.repeat(lastOutputLength)}`)
-  lastOutputLength = message.length
-  writer.write(`\r${message}`)
+await Promise.all(
+  dtsEntries.map(async (e) => {
+    await fs.promises.writeFile(e, await removePrivateFields(e))
+
+    const message = `Private fields removed(${++removedCount}/${dtsEntries.length}): ${e}`
+    writer.write(`\r${' '.repeat(lastOutputLength)}`)
+    lastOutputLength = message.length
+    writer.write(`\r${message}`)
+  })
+)
 
-  fs.writeFileSync(entry, removePrivateFields(entry))
-}
 writer.write('\n')
+cleanupWorkers()
diff --git a/build/remove-private-fields.test.ts b/build/remove-private-fields-worker.test.ts
similarity index 90%
rename from build/remove-private-fields.test.ts
rename to build/remove-private-fields-worker.test.ts
index ecbb2d638..9b7e42fca 100644
--- a/build/remove-private-fields.test.ts
+++ b/build/remove-private-fields-worker.test.ts
@@ -3,7 +3,7 @@
 import fs from 'node:fs/promises'
 import os from 'node:os'
 import path from 'node:path'
-import { removePrivateFields } from './remove-private-fields'
+import { removePrivateFields } from './remove-private-fields-worker'
 
 describe('removePrivateFields', () => {
   it('Works', async () => {
diff --git a/build/remove-private-fields-worker.ts b/build/remove-private-fields-worker.ts
new file mode 100644
index 000000000..2cb7710d4
--- /dev/null
+++ b/build/remove-private-fields-worker.ts
@@ -0,0 +1,85 @@
+import * as ts from 'typescript'
+
+export type WorkerInput = {
+  file: string
+  taskId: number
+}
+
+export type WorkerOutput =
+  | {
+      type: 'success'
+      value: string
+      taskId: number
+    }
+  | {
+      type: 'error'
+      value: unknown
+      taskId: number
+    }
+
+const removePrivateTransformer = <T extends ts.Node>(ctx: ts.TransformationContext) => {
+  const visit: ts.Visitor = (node) => {
+    if (ts.isClassDeclaration(node)) {
+      const newMembers = node.members.filter((elem) => {
+        if (ts.isPropertyDeclaration(elem) || ts.isMethodDeclaration(elem)) {
+          for (const modifier of elem.modifiers ?? []) {
+            if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
+              return false
+            }
+          }
+        }
+        if (elem.name && ts.isPrivateIdentifier(elem.name)) {
+          return false
+        }
+        return true
+      })
+      return ts.factory.createClassDeclaration(
+        node.modifiers,
+        node.name,
+        node.typeParameters,
+        node.heritageClauses,
+        newMembers
+      )
+    }
+    return ts.visitEachChild(node, visit, ctx)
+  }
+
+  return (node: T) => {
+    const visited = ts.visitNode(node, visit)
+    if (!visited) {
+      throw new Error('The result visited is undefined.')
+    }
+    return visited
+  }
+}
+
+export const removePrivateFields = (tsPath: string) => {
+  const program = ts.createProgram([tsPath], {
+    target: ts.ScriptTarget.ESNext,
+    module: ts.ModuleKind.ESNext,
+  })
+  const file = program.getSourceFile(tsPath)
+
+  const transformed = ts.transform(file!, [removePrivateTransformer])
+  const printer = ts.createPrinter()
+  const transformedSourceFile = transformed.transformed[0] as ts.SourceFile
+  const code = printer.printFile(transformedSourceFile)
+  transformed.dispose()
+  return code
+}
+
+declare const self: Worker
+
+if (globalThis.self) {
+  self.addEventListener('message', function (e) {
+    const { file, taskId } = e.data as WorkerInput
+
+    try {
+      const result = removePrivateFields(file)
+      self.postMessage({ type: 'success', value: result, taskId } satisfies WorkerOutput)
+    } catch (e) {
+      console.error(e)
+      self.postMessage({ type: 'error', value: e, taskId } satisfies WorkerOutput)
+    }
+  })
+}
diff --git a/build/remove-private-fields.ts b/build/remove-private-fields.ts
index 17663f142..356c354ab 100644
--- a/build/remove-private-fields.ts
+++ b/build/remove-private-fields.ts
@@ -1,52 +1,41 @@
-import * as ts from 'typescript'
+import { cpus } from 'node:os'
+import type { WorkerInput, WorkerOutput } from './remove-private-fields-worker'
 
-const removePrivateTransformer = <T extends ts.Node>(ctx: ts.TransformationContext) => {
-  const visit: ts.Visitor = (node) => {
-    if (ts.isClassDeclaration(node)) {
-      const newMembers = node.members.filter((elem) => {
-        if (ts.isPropertyDeclaration(elem) || ts.isMethodDeclaration(elem)) {
-          for (const modifier of elem.modifiers ?? []) {
-            if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
-              return false
-            }
-          }
-        }
-        if (elem.name && ts.isPrivateIdentifier(elem.name)) {
-          return false
-        }
-        return true
-      })
-      return ts.factory.createClassDeclaration(
-        node.modifiers,
-        node.name,
-        node.typeParameters,
-        node.heritageClauses,
-        newMembers
-      )
-    }
-    return ts.visitEachChild(node, visit, ctx)
-  }
+const workers = Array.from({ length: Math.ceil(cpus().length / 2) }).map(
+  () => new Worker(`${import.meta.dirname}/remove-private-fields-worker.ts`),
+  { type: 'module' }
+)
+let workerIndex = 0
+let taskId = 0
 
-  return (node: T) => {
-    const visited = ts.visitNode(node, visit)
-    if (!visited) {
-      throw new Error('The result visited is undefined.')
-    }
-    return visited
-  }
-}
+export async function removePrivateFields(file: string): Promise<string> {
+  const currentTaskId = taskId++
+  const worker = workers[workerIndex]
+  workerIndex = (workerIndex + 1) % workers.length
 
-export const removePrivateFields = (tsPath: string) => {
-  const program = ts.createProgram([tsPath], {
-    target: ts.ScriptTarget.ESNext,
-    module: ts.ModuleKind.ESNext,
+  return new Promise<string>((resolve, reject) => {
+    const abortController = new AbortController()
+    worker.addEventListener(
+      'message',
+      ({ data: { type, value, taskId } }: { data: WorkerOutput }) => {
+        if (taskId === currentTaskId) {
+          if (type === 'success') {
+            resolve(value)
+          } else {
+            reject(value)
+          }
+
+          abortController.abort()
+        }
+      },
+      { signal: abortController.signal }
+    )
+    worker.postMessage({ file, taskId: currentTaskId } satisfies WorkerInput)
   })
-  const file = program.getSourceFile(tsPath)
+}
 
-  const transformed = ts.transform(file!, [removePrivateTransformer])
-  const printer = ts.createPrinter()
-  const transformedSourceFile = transformed.transformed[0] as ts.SourceFile
-  const code = printer.printFile(transformedSourceFile)
-  transformed.dispose()
-  return code
+export function cleanupWorkers() {
+  for (const worker of workers) {
+    worker.terminate()
+  }
 }

From 36f2a727568eaacb0887aa76f79f9e75426411b2 Mon Sep 17 00:00:00 2001
From: miyaji255 <84168445+miyaji255@users.noreply.github.com>
Date: Wed, 15 Jan 2025 19:13:35 +0900
Subject: [PATCH 2/2] Update build/remove-private-fields.ts

Co-authored-by: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com>
---
 build/remove-private-fields.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/build/remove-private-fields.ts b/build/remove-private-fields.ts
index 356c354ab..d7e53fbf3 100644
--- a/build/remove-private-fields.ts
+++ b/build/remove-private-fields.ts
@@ -2,8 +2,7 @@ import { cpus } from 'node:os'
 import type { WorkerInput, WorkerOutput } from './remove-private-fields-worker'
 
 const workers = Array.from({ length: Math.ceil(cpus().length / 2) }).map(
-  () => new Worker(`${import.meta.dirname}/remove-private-fields-worker.ts`),
-  { type: 'module' }
+  () => new Worker(`${import.meta.dirname}/remove-private-fields-worker.ts`, { type: 'module' })
 )
 let workerIndex = 0
 let taskId = 0