From f7a405f703197a2d7744a66ec43f1577ab08684d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=87=AF90331?= <90331@sangfor.com> Date: Thu, 14 May 2026 20:48:37 +0800 Subject: [PATCH] fix(utils): replace using syntax with withDisposable for node v20 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node.js v20 does not support the syntax. Replace all patterns with a new helper that wraps the operation in try/finally and explicitly calls Symbol.dispose. Also rename listBuiltinSkillNames to listBuiltinSkills in generate-review-builtin.ts. Signed-off-by: 林凯90331 <90331@sangfor.com> Co-authored-by: CoStrict --- scripts/generate-review-builtin.ts | 2 +- src/utils/execFileNoThrowPortable.ts | 37 +++--- src/utils/execSyncWrapper.ts | 8 +- src/utils/fsOperations.ts | 192 ++++++++++++++++----------- src/utils/slowOperations.ts | 117 +++++++++------- 5 files changed, 207 insertions(+), 149 deletions(-) diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index 1eb9d0d04..976865958 100644 --- a/scripts/generate-review-builtin.ts +++ b/scripts/generate-review-builtin.ts @@ -240,7 +240,7 @@ ${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')} } // List all skill names -export function listBuiltinSkillNames(): string[] { +export function listBuiltinSkills(): string[] { return ${JSON.stringify(allSkillNames)} } diff --git a/src/utils/execFileNoThrowPortable.ts b/src/utils/execFileNoThrowPortable.ts index f67c93ff1..198b11d05 100644 --- a/src/utils/execFileNoThrowPortable.ts +++ b/src/utils/execFileNoThrowPortable.ts @@ -1,6 +1,6 @@ import { type Options as ExecaOptions, execaSync } from 'execa' import { getCwd } from '../utils/cwd.js' -import { slowLogging } from './slowOperations.js' +import { slowLogging, withDisposable } from './slowOperations.js' const MS_IN_SECOND = 1000 const SECONDS_IN_MINUTE = 60 @@ -67,23 +67,24 @@ export function execSyncWithDefaults_DEPRECATED( } = options abortSignal?.throwIfAborted() - using _ = slowLogging`exec: ${command.slice(0, 200)}` - try { - const result = (execaSync as any)(command, { - env: process.env, - maxBuffer: 1_000_000, - timeout: finalTimeout, - cwd: getCwd(), - stdio, - shell: true, // execSync typically runs shell commands - reject: false, // Don't throw on non-zero exit codes - input, - }) - if (!result.stdout) { + return withDisposable(() => { + try { + const result = (execaSync as any)(command, { + env: process.env, + maxBuffer: 1_000_000, + timeout: finalTimeout, + cwd: getCwd(), + stdio, + shell: true, // execSync typically runs shell commands + reject: false, // Don't throw on non-zero exit codes + input, + }) + if (!result.stdout) { + return null + } + return result.stdout.trim() || null + } catch { return null } - return result.stdout.trim() || null - } catch { - return null - } + }, slowLogging`exec: ${command.slice(0, 200)}`) } diff --git a/src/utils/execSyncWrapper.ts b/src/utils/execSyncWrapper.ts index 38f99676c..3fefa3d12 100644 --- a/src/utils/execSyncWrapper.ts +++ b/src/utils/execSyncWrapper.ts @@ -4,7 +4,7 @@ import { type ExecSyncOptionsWithStringEncoding, execSync as nodeExecSync, } from 'child_process' -import { slowLogging } from './slowOperations.js' +import { slowLogging, withDisposable } from './slowOperations.js' /** * @deprecated Use async alternatives when possible. Sync exec calls block the event loop. @@ -33,6 +33,8 @@ export function execSync_DEPRECATED( command: string, options?: ExecSyncOptions, ): Buffer | string { - using _ = slowLogging`execSync: ${command.slice(0, 100)}` - return nodeExecSync(command, options) + return withDisposable( + () => nodeExecSync(command, options), + slowLogging`execSync: ${command.slice(0, 100)}`, + ) } diff --git a/src/utils/fsOperations.ts b/src/utils/fsOperations.ts index cef6200c2..ec016a00a 100644 --- a/src/utils/fsOperations.ts +++ b/src/utils/fsOperations.ts @@ -13,7 +13,7 @@ import { import { homedir } from 'os' import * as nodePath from 'path' import { getErrnoCode } from './errors.js' -import { slowLogging } from './slowOperations.js' +import { slowLogging, withDisposable } from './slowOperations.js' /** * Simplified filesystem operations interface based on Node.js fs module. @@ -387,8 +387,10 @@ export const NodeFsOperations: FsOperations = { }, existsSync(fsPath) { - using _ = slowLogging`fs.existsSync(${fsPath})` - return fs.existsSync(fsPath) + return withDisposable( + () => fs.existsSync(fsPath), + slowLogging`fs.existsSync(${fsPath})`, + ) }, async stat(fsPath) { @@ -433,77 +435,95 @@ export const NodeFsOperations: FsOperations = { }, statSync(fsPath) { - using _ = slowLogging`fs.statSync(${fsPath})` - return fs.statSync(fsPath) + return withDisposable( + () => fs.statSync(fsPath), + slowLogging`fs.statSync(${fsPath})`, + ) }, lstatSync(fsPath) { - using _ = slowLogging`fs.lstatSync(${fsPath})` - return fs.lstatSync(fsPath) + return withDisposable( + () => fs.lstatSync(fsPath), + slowLogging`fs.lstatSync(${fsPath})`, + ) }, readFileSync(fsPath, options) { - using _ = slowLogging`fs.readFileSync(${fsPath})` - return fs.readFileSync(fsPath, { encoding: options.encoding }) + return withDisposable( + () => fs.readFileSync(fsPath, { encoding: options.encoding }), + slowLogging`fs.readFileSync(${fsPath})`, + ) }, readFileBytesSync(fsPath) { - using _ = slowLogging`fs.readFileBytesSync(${fsPath})` - return fs.readFileSync(fsPath) + return withDisposable( + () => fs.readFileSync(fsPath), + slowLogging`fs.readFileBytesSync(${fsPath})`, + ) }, readSync(fsPath, options) { - using _ = slowLogging`fs.readSync(${fsPath}, ${options.length} bytes)` - let fd: number | undefined - try { - fd = fs.openSync(fsPath, 'r') - const buffer = Buffer.alloc(options.length) - const bytesRead = fs.readSync(fd, buffer, 0, options.length, 0) - return { buffer, bytesRead } - } finally { - if (fd) fs.closeSync(fd) - } + return withDisposable(() => { + let fd: number | undefined + try { + fd = fs.openSync(fsPath, 'r') + const buffer = Buffer.alloc(options.length) + const bytesRead = fs.readSync(fd, buffer, 0, options.length, 0) + return { buffer, bytesRead } + } finally { + if (fd) fs.closeSync(fd) + } + }, slowLogging`fs.readSync(${fsPath}, ${options.length} bytes)`) }, appendFileSync(path, data, options) { - using _ = slowLogging`fs.appendFileSync(${path}, ${data.length} chars)` - // For new files with explicit mode, use 'ax' (atomic create-with-mode) to avoid - // TOCTOU race between existence check and open. Fall back to normal append if exists. - if (options?.mode !== undefined) { - try { - const fd = fs.openSync(path, 'ax', options.mode) + withDisposable(() => { + // For new files with explicit mode, use 'ax' (atomic create-with-mode) to avoid + // TOCTOU race between existence check and open. Fall back to normal append if exists. + if (options?.mode !== undefined) { try { - fs.appendFileSync(fd, data) - } finally { - fs.closeSync(fd) + const fd = fs.openSync(path, 'ax', options.mode) + try { + fs.appendFileSync(fd, data) + } finally { + fs.closeSync(fd) + } + return + } catch (e) { + if (getErrnoCode(e) !== 'EEXIST') throw e + // File exists — fall through to normal append } - return - } catch (e) { - if (getErrnoCode(e) !== 'EEXIST') throw e - // File exists — fall through to normal append } - } - fs.appendFileSync(path, data) + fs.appendFileSync(path, data) + }, slowLogging`fs.appendFileSync(${path}, ${data.length} chars)`) }, copyFileSync(src, dest) { - using _ = slowLogging`fs.copyFileSync(${src} → ${dest})` - fs.copyFileSync(src, dest) + withDisposable( + () => fs.copyFileSync(src, dest), + slowLogging`fs.copyFileSync(${src} → ${dest})`, + ) }, unlinkSync(path: string) { - using _ = slowLogging`fs.unlinkSync(${path})` - fs.unlinkSync(path) + withDisposable( + () => fs.unlinkSync(path), + slowLogging`fs.unlinkSync(${path})`, + ) }, renameSync(oldPath: string, newPath: string) { - using _ = slowLogging`fs.renameSync(${oldPath} → ${newPath})` - fs.renameSync(oldPath, newPath) + withDisposable( + () => fs.renameSync(oldPath, newPath), + slowLogging`fs.renameSync(${oldPath} → ${newPath})`, + ) }, linkSync(target: string, path: string) { - using _ = slowLogging`fs.linkSync(${target} → ${path})` - fs.linkSync(target, path) + withDisposable( + () => fs.linkSync(target, path), + slowLogging`fs.linkSync(${target} → ${path})`, + ) }, symlinkSync( @@ -511,64 +531,80 @@ export const NodeFsOperations: FsOperations = { path: string, type?: 'dir' | 'file' | 'junction', ) { - using _ = slowLogging`fs.symlinkSync(${target} → ${path})` - fs.symlinkSync(target, path, type) + withDisposable( + () => fs.symlinkSync(target, path, type), + slowLogging`fs.symlinkSync(${target} → ${path})`, + ) }, readlinkSync(path: string) { - using _ = slowLogging`fs.readlinkSync(${path})` - return fs.readlinkSync(path) + return withDisposable( + () => fs.readlinkSync(path), + slowLogging`fs.readlinkSync(${path})`, + ) }, realpathSync(path: string) { - using _ = slowLogging`fs.realpathSync(${path})` - return fs.realpathSync(path).normalize('NFC') + return withDisposable( + () => fs.realpathSync(path).normalize('NFC'), + slowLogging`fs.realpathSync(${path})`, + ) }, mkdirSync(dirPath, options) { - using _ = slowLogging`fs.mkdirSync(${dirPath})` - const mkdirOptions: { recursive: boolean; mode?: number } = { - recursive: true, - } - if (options?.mode !== undefined) { - mkdirOptions.mode = options.mode - } - try { - fs.mkdirSync(dirPath, mkdirOptions) - } catch (e) { - // Bun/Windows: recursive:true throws EEXIST on directories with the - // FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini). - // Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir - // (bun-internal src/sys.zig existsAtType). The dir exists; ignore. - // https://github.com/anthropics/claude-code/issues/30924 - if (getErrnoCode(e) !== 'EEXIST') throw e - } + withDisposable(() => { + const mkdirOptions: { recursive: boolean; mode?: number } = { + recursive: true, + } + if (options?.mode !== undefined) { + mkdirOptions.mode = options.mode + } + try { + fs.mkdirSync(dirPath, mkdirOptions) + } catch (e) { + // Bun/Windows: recursive:true throws EEXIST on directories with the + // FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini). + // Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir + // (bun-internal src/sys.zig existsAtType). The dir exists; ignore. + // https://github.com/anthropics/claude-code/issues/30924 + if (getErrnoCode(e) !== 'EEXIST') throw e + } + }, slowLogging`fs.mkdirSync(${dirPath})`) }, readdirSync(dirPath) { - using _ = slowLogging`fs.readdirSync(${dirPath})` - return fs.readdirSync(dirPath, { withFileTypes: true }) + return withDisposable( + () => fs.readdirSync(dirPath, { withFileTypes: true }), + slowLogging`fs.readdirSync(${dirPath})`, + ) }, readdirStringSync(dirPath) { - using _ = slowLogging`fs.readdirStringSync(${dirPath})` - return fs.readdirSync(dirPath) + return withDisposable( + () => fs.readdirSync(dirPath), + slowLogging`fs.readdirStringSync(${dirPath})`, + ) }, isDirEmptySync(dirPath) { - using _ = slowLogging`fs.isDirEmptySync(${dirPath})` - const files = this.readdirSync(dirPath) - return files.length === 0 + return withDisposable(() => { + const files = this.readdirSync(dirPath) + return files.length === 0 + }, slowLogging`fs.isDirEmptySync(${dirPath})`) }, rmdirSync(dirPath) { - using _ = slowLogging`fs.rmdirSync(${dirPath})` - fs.rmdirSync(dirPath) + withDisposable( + () => fs.rmdirSync(dirPath), + slowLogging`fs.rmdirSync(${dirPath})`, + ) }, rmSync(path, options) { - using _ = slowLogging`fs.rmSync(${path})` - fs.rmSync(path, options) + withDisposable( + () => fs.rmSync(path, options), + slowLogging`fs.rmSync(${path})`, + ) }, createWriteStream(path: string) { diff --git a/src/utils/slowOperations.ts b/src/utils/slowOperations.ts index 66e917895..0dcf468b0 100644 --- a/src/utils/slowOperations.ts +++ b/src/utils/slowOperations.ts @@ -149,8 +149,7 @@ function slowLoggingExternal(): Disposable { * zero timing. AntSlowLogger and buildDescription are dead-code-eliminated. * * @example - * using _ = slowLogging`structuredClone(${value})` - * const result = structuredClone(value) + * const result = withDisposable(() => structuredClone(value), slowLogging`structuredClone(${value})`) */ export const slowLogging: ( strings: TemplateStringsArray, @@ -159,6 +158,19 @@ export const slowLogging: ( ? slowLoggingAnt : slowLoggingExternal +/** + * Runs a function with a disposable that is disposed in a finally block. + * Use this instead of `using` declarations for Node.js compatibility (v20 + * does not support the `using` syntax). + */ +export function withDisposable(fn: () => T, disposable: Disposable): T { + try { + return fn() + } finally { + disposable[Symbol.dispose]() + } +} + // --- Wrapped operations --- /** @@ -188,11 +200,14 @@ export function jsonStringify( | null, space?: string | number, ): string { - using _ = slowLogging`JSON.stringify(${value})` - return JSON.stringify( - value, - replacer as Parameters[1], - space, + return withDisposable( + () => + JSON.stringify( + value, + replacer as Parameters[1], + space, + ), + slowLogging`JSON.stringify(${value})`, ) } @@ -204,14 +219,14 @@ export function jsonStringify( * import { jsonParse } from './slowOperations.js' * const data = jsonParse(jsonString) */ -export const jsonParse: typeof JSON.parse = (text, reviver) => { - using _ = slowLogging`JSON.parse(${text})` - // V8 de-opts JSON.parse when a second argument is passed, even if undefined. - // Branch explicitly so the common (no-reviver) path stays on the fast path. - return typeof reviver === 'undefined' - ? JSON.parse(text) - : JSON.parse(text, reviver) -} +export const jsonParse: typeof JSON.parse = (text, reviver) => + withDisposable(() => { + // V8 de-opts JSON.parse when a second argument is passed, even if undefined. + // Branch explicitly so the common (no-reviver) path stays on the fast path. + return typeof reviver === 'undefined' + ? JSON.parse(text) + : JSON.parse(text, reviver) + }, slowLogging`JSON.parse(${text})`) /** * Wrapped structuredClone with slow operation logging. @@ -222,8 +237,10 @@ export const jsonParse: typeof JSON.parse = (text, reviver) => { * const copy = clone(originalObject) */ export function clone(value: T, options?: StructuredSerializeOptions): T { - using _ = slowLogging`structuredClone(${value})` - return structuredClone(value, options) + return withDisposable( + () => structuredClone(value, options), + slowLogging`structuredClone(${value})`, + ) } /** @@ -235,8 +252,10 @@ export function clone(value: T, options?: StructuredSerializeOptions): T { * const copy = cloneDeep(originalObject) */ export function cloneDeep(value: T): T { - using _ = slowLogging`cloneDeep(${value})` - return lodashCloneDeep(value) + return withDisposable( + () => lodashCloneDeep(value), + slowLogging`cloneDeep(${value})`, + ) } /** @@ -253,37 +272,37 @@ export function writeFileSync_DEPRECATED( data: string | NodeJS.ArrayBufferView, options?: WriteFileOptionsWithFlush, ): void { - using _ = slowLogging`fs.writeFileSync(${filePath}, ${data})` + withDisposable(() => { + // Check if flush is requested (for object-style options) + const needsFlush = + options !== null && + typeof options === 'object' && + 'flush' in options && + options.flush === true - // Check if flush is requested (for object-style options) - const needsFlush = - options !== null && - typeof options === 'object' && - 'flush' in options && - options.flush === true - - if (needsFlush) { - // Manual flush: open file, write, fsync, close - const encoding = - typeof options === 'object' && 'encoding' in options - ? options.encoding - : undefined - const mode = - typeof options === 'object' && 'mode' in options - ? options.mode - : undefined - let fd: number | undefined - try { - fd = openSync(filePath, 'w', mode) - fsWriteFileSync(fd, data, { encoding: encoding ?? undefined }) - fsyncSync(fd) - } finally { - if (fd !== undefined) { - closeSync(fd) + if (needsFlush) { + // Manual flush: open file, write, fsync, close + const encoding = + typeof options === 'object' && 'encoding' in options + ? options.encoding + : undefined + const mode = + typeof options === 'object' && 'mode' in options + ? options.mode + : undefined + let fd: number | undefined + try { + fd = openSync(filePath, 'w', mode) + fsWriteFileSync(fd, data, { encoding: encoding ?? undefined }) + fsyncSync(fd) + } finally { + if (fd !== undefined) { + closeSync(fd) + } } + } else { + // No flush needed, use standard writeFileSync + fsWriteFileSync(filePath, data, options as WriteFileOptions) } - } else { - // No flush needed, use standard writeFileSync - fsWriteFileSync(filePath, data, options as WriteFileOptions) - } + }, slowLogging`fs.writeFileSync(${filePath}, ${data})`) }