Skip to content

Commit 6807d43

Browse files
committed
perf(build): Use WebWorker when removing private fields
1 parent 2ead4d8 commit 6807d43

File tree

4 files changed

+134
-56
lines changed

4 files changed

+134
-56
lines changed

build/build.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { Plugin, PluginBuild, BuildOptions } from 'esbuild'
1414
import * as glob from 'glob'
1515
import fs from 'fs'
1616
import path from 'path'
17-
import { removePrivateFields } from './remove-private-fields'
17+
import { cleanupWorkers, removePrivateFields } from './remove-private-fields'
1818
import { validateExports } from './validate-exports'
1919

2020
const args = arg({
@@ -102,14 +102,18 @@ const dtsEntries = glob.globSync('./dist/types/**/*.d.ts')
102102
const writer = stdout.writer()
103103
writer.write('\n')
104104
let lastOutputLength = 0
105-
for (let i = 0; i < dtsEntries.length; i++) {
106-
const entry = dtsEntries[i]
105+
let removedCount = 0
107106

108-
const message = `Removing private fields(${i + 1}/${dtsEntries.length}): ${entry}`
109-
writer.write(`\r${' '.repeat(lastOutputLength)}`)
110-
lastOutputLength = message.length
111-
writer.write(`\r${message}`)
107+
await Promise.all(
108+
dtsEntries.map(async (e) => {
109+
await fs.promises.writeFile(e, await removePrivateFields(e))
110+
111+
const message = `Private fields removed(${++removedCount}/${dtsEntries.length}): ${e}`
112+
writer.write(`\r${' '.repeat(lastOutputLength)}`)
113+
lastOutputLength = message.length
114+
writer.write(`\r${message}`)
115+
})
116+
)
112117

113-
fs.writeFileSync(entry, removePrivateFields(entry))
114-
}
115118
writer.write('\n')
119+
cleanupWorkers()

build/remove-private-fields.test.ts renamed to build/remove-private-fields-worker.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import fs from 'node:fs/promises'
44
import os from 'node:os'
55
import path from 'node:path'
6-
import { removePrivateFields } from './remove-private-fields'
6+
import { removePrivateFields } from './remove-private-fields-worker'
77

88
describe('removePrivateFields', () => {
99
it('Works', async () => {

build/remove-private-fields-worker.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as ts from 'typescript'
2+
3+
export type WorkerInput = {
4+
file: string
5+
taskId: number
6+
}
7+
8+
export type WorkerOutput =
9+
| {
10+
type: 'success'
11+
value: string
12+
taskId: number
13+
}
14+
| {
15+
type: 'error'
16+
value: unknown
17+
taskId: number
18+
}
19+
20+
const removePrivateTransformer = <T extends ts.Node>(ctx: ts.TransformationContext) => {
21+
const visit: ts.Visitor = (node) => {
22+
if (ts.isClassDeclaration(node)) {
23+
const newMembers = node.members.filter((elem) => {
24+
if (ts.isPropertyDeclaration(elem) || ts.isMethodDeclaration(elem)) {
25+
for (const modifier of elem.modifiers ?? []) {
26+
if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
27+
return false
28+
}
29+
}
30+
}
31+
if (elem.name && ts.isPrivateIdentifier(elem.name)) {
32+
return false
33+
}
34+
return true
35+
})
36+
return ts.factory.createClassDeclaration(
37+
node.modifiers,
38+
node.name,
39+
node.typeParameters,
40+
node.heritageClauses,
41+
newMembers
42+
)
43+
}
44+
return ts.visitEachChild(node, visit, ctx)
45+
}
46+
47+
return (node: T) => {
48+
const visited = ts.visitNode(node, visit)
49+
if (!visited) {
50+
throw new Error('The result visited is undefined.')
51+
}
52+
return visited
53+
}
54+
}
55+
56+
export const removePrivateFields = (tsPath: string) => {
57+
const program = ts.createProgram([tsPath], {
58+
target: ts.ScriptTarget.ESNext,
59+
module: ts.ModuleKind.ESNext,
60+
})
61+
const file = program.getSourceFile(tsPath)
62+
63+
const transformed = ts.transform(file!, [removePrivateTransformer])
64+
const printer = ts.createPrinter()
65+
const transformedSourceFile = transformed.transformed[0] as ts.SourceFile
66+
const code = printer.printFile(transformedSourceFile)
67+
transformed.dispose()
68+
return code
69+
}
70+
71+
declare const self: Worker
72+
73+
if (globalThis.self) {
74+
self.addEventListener('message', function (e) {
75+
const { file, taskId } = e.data as WorkerInput
76+
77+
try {
78+
const result = removePrivateFields(file)
79+
self.postMessage({ type: 'success', value: result, taskId } satisfies WorkerOutput)
80+
} catch (e) {
81+
console.error(e)
82+
self.postMessage({ type: 'error', value: e, taskId } satisfies WorkerOutput)
83+
}
84+
})
85+
}

build/remove-private-fields.ts

Lines changed: 35 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,41 @@
1-
import * as ts from 'typescript'
1+
import { cpus } from 'node:os'
2+
import type { WorkerInput, WorkerOutput } from './remove-private-fields-worker'
23

3-
const removePrivateTransformer = <T extends ts.Node>(ctx: ts.TransformationContext) => {
4-
const visit: ts.Visitor = (node) => {
5-
if (ts.isClassDeclaration(node)) {
6-
const newMembers = node.members.filter((elem) => {
7-
if (ts.isPropertyDeclaration(elem) || ts.isMethodDeclaration(elem)) {
8-
for (const modifier of elem.modifiers ?? []) {
9-
if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
10-
return false
11-
}
12-
}
13-
}
14-
if (elem.name && ts.isPrivateIdentifier(elem.name)) {
15-
return false
16-
}
17-
return true
18-
})
19-
return ts.factory.createClassDeclaration(
20-
node.modifiers,
21-
node.name,
22-
node.typeParameters,
23-
node.heritageClauses,
24-
newMembers
25-
)
26-
}
27-
return ts.visitEachChild(node, visit, ctx)
28-
}
4+
const workers = Array.from({ length: Math.ceil(cpus().length / 2) }).map(
5+
() => new Worker(`${import.meta.dirname}/remove-private-fields-worker.ts`),
6+
{ type: 'module' }
7+
)
8+
let workerIndex = 0
9+
let taskId = 0
2910

30-
return (node: T) => {
31-
const visited = ts.visitNode(node, visit)
32-
if (!visited) {
33-
throw new Error('The result visited is undefined.')
34-
}
35-
return visited
36-
}
37-
}
11+
export async function removePrivateFields(file: string): Promise<string> {
12+
const currentTaskId = taskId++
13+
const worker = workers[workerIndex]
14+
workerIndex = (workerIndex + 1) % workers.length
3815

39-
export const removePrivateFields = (tsPath: string) => {
40-
const program = ts.createProgram([tsPath], {
41-
target: ts.ScriptTarget.ESNext,
42-
module: ts.ModuleKind.ESNext,
16+
return new Promise<string>((resolve, reject) => {
17+
const abortController = new AbortController()
18+
worker.addEventListener(
19+
'message',
20+
({ data: { type, value, taskId } }: { data: WorkerOutput }) => {
21+
if (taskId === currentTaskId) {
22+
if (type === 'success') {
23+
resolve(value)
24+
} else {
25+
reject(value)
26+
}
27+
28+
abortController.abort()
29+
}
30+
},
31+
{ signal: abortController.signal }
32+
)
33+
worker.postMessage({ file, taskId: currentTaskId } satisfies WorkerInput)
4334
})
44-
const file = program.getSourceFile(tsPath)
35+
}
4536

46-
const transformed = ts.transform(file!, [removePrivateTransformer])
47-
const printer = ts.createPrinter()
48-
const transformedSourceFile = transformed.transformed[0] as ts.SourceFile
49-
const code = printer.printFile(transformedSourceFile)
50-
transformed.dispose()
51-
return code
37+
export function cleanupWorkers() {
38+
for (const worker of workers) {
39+
worker.terminate()
40+
}
5241
}

0 commit comments

Comments
 (0)