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 = (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 = (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 { + 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((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