Skip to content

Commit b71e8cc

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

File tree

4 files changed

+136
-56
lines changed

4 files changed

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

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)