Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(build): Use WebWorker when removing private fields #3821

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
85 changes: 85 additions & 0 deletions build/remove-private-fields-worker.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

Check warning on line 29 in build/remove-private-fields-worker.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields-worker.ts#L26-L29

Added lines #L26 - L29 were not covered by tests
}
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

Check warning on line 75 in build/remove-private-fields-worker.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields-worker.ts#L74-L75

Added lines #L74 - L75 were not covered by tests

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)
}
})
}

Check warning on line 85 in build/remove-private-fields-worker.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields-worker.ts#L77-L85

Added lines #L77 - L85 were not covered by tests
80 changes: 34 additions & 46 deletions build/remove-private-fields.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,40 @@
import * as ts from 'typescript'
import { cpus } from 'node:os'

Check warning on line 1 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L1

Added line #L1 was not covered by tests
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

Check warning on line 8 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L4-L8

Added lines #L4 - L8 were not covered by tests

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

Check warning on line 13 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L10-L13

Added lines #L10 - L13 were not covered by tests

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)
}

Check warning on line 25 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L15-L25

Added lines #L15 - L25 were not covered by tests

abortController.abort()
}
},
{ signal: abortController.signal }
)
worker.postMessage({ file, taskId: currentTaskId } satisfies WorkerInput)

Check warning on line 32 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L27-L32

Added lines #L27 - L32 were not covered by tests
})
const file = program.getSourceFile(tsPath)
}

Check warning on line 34 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L34

Added line #L34 was not covered by tests

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()
}

Check warning on line 39 in build/remove-private-fields.ts

View check run for this annotation

Codecov / codecov/patch

build/remove-private-fields.ts#L36-L39

Added lines #L36 - L39 were not covered by tests
}
Loading