Merge pull request #96 from y574444354/fix/compile-issue-0514

fix(utils): replace using syntax with withDisposable for node v20 compatibility
This commit is contained in:
linkai0924 2026-05-14 20:51:55 +08:00 committed by GitHub
commit 4703b608a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 207 additions and 149 deletions

View File

@ -240,7 +240,7 @@ ${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')}
} }
// List all skill names // List all skill names
export function listBuiltinSkillNames(): string[] { export function listBuiltinSkills(): string[] {
return ${JSON.stringify(allSkillNames)} return ${JSON.stringify(allSkillNames)}
} }

View File

@ -1,6 +1,6 @@
import { type Options as ExecaOptions, execaSync } from 'execa' import { type Options as ExecaOptions, execaSync } from 'execa'
import { getCwd } from '../utils/cwd.js' import { getCwd } from '../utils/cwd.js'
import { slowLogging } from './slowOperations.js' import { slowLogging, withDisposable } from './slowOperations.js'
const MS_IN_SECOND = 1000 const MS_IN_SECOND = 1000
const SECONDS_IN_MINUTE = 60 const SECONDS_IN_MINUTE = 60
@ -67,23 +67,24 @@ export function execSyncWithDefaults_DEPRECATED(
} = options } = options
abortSignal?.throwIfAborted() abortSignal?.throwIfAborted()
using _ = slowLogging`exec: ${command.slice(0, 200)}` return withDisposable(() => {
try { try {
const result = (execaSync as any)(command, { const result = (execaSync as any)(command, {
env: process.env, env: process.env,
maxBuffer: 1_000_000, maxBuffer: 1_000_000,
timeout: finalTimeout, timeout: finalTimeout,
cwd: getCwd(), cwd: getCwd(),
stdio, stdio,
shell: true, // execSync typically runs shell commands shell: true, // execSync typically runs shell commands
reject: false, // Don't throw on non-zero exit codes reject: false, // Don't throw on non-zero exit codes
input, input,
}) })
if (!result.stdout) { if (!result.stdout) {
return null
}
return result.stdout.trim() || null
} catch {
return null return null
} }
return result.stdout.trim() || null }, slowLogging`exec: ${command.slice(0, 200)}`)
} catch {
return null
}
} }

View File

@ -4,7 +4,7 @@ import {
type ExecSyncOptionsWithStringEncoding, type ExecSyncOptionsWithStringEncoding,
execSync as nodeExecSync, execSync as nodeExecSync,
} from 'child_process' } 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. * @deprecated Use async alternatives when possible. Sync exec calls block the event loop.
@ -33,6 +33,8 @@ export function execSync_DEPRECATED(
command: string, command: string,
options?: ExecSyncOptions, options?: ExecSyncOptions,
): Buffer | string { ): Buffer | string {
using _ = slowLogging`execSync: ${command.slice(0, 100)}` return withDisposable(
return nodeExecSync(command, options) () => nodeExecSync(command, options),
slowLogging`execSync: ${command.slice(0, 100)}`,
)
} }

View File

@ -13,7 +13,7 @@ import {
import { homedir } from 'os' import { homedir } from 'os'
import * as nodePath from 'path' import * as nodePath from 'path'
import { getErrnoCode } from './errors.js' 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. * Simplified filesystem operations interface based on Node.js fs module.
@ -387,8 +387,10 @@ export const NodeFsOperations: FsOperations = {
}, },
existsSync(fsPath) { existsSync(fsPath) {
using _ = slowLogging`fs.existsSync(${fsPath})` return withDisposable(
return fs.existsSync(fsPath) () => fs.existsSync(fsPath),
slowLogging`fs.existsSync(${fsPath})`,
)
}, },
async stat(fsPath) { async stat(fsPath) {
@ -433,77 +435,95 @@ export const NodeFsOperations: FsOperations = {
}, },
statSync(fsPath) { statSync(fsPath) {
using _ = slowLogging`fs.statSync(${fsPath})` return withDisposable(
return fs.statSync(fsPath) () => fs.statSync(fsPath),
slowLogging`fs.statSync(${fsPath})`,
)
}, },
lstatSync(fsPath) { lstatSync(fsPath) {
using _ = slowLogging`fs.lstatSync(${fsPath})` return withDisposable(
return fs.lstatSync(fsPath) () => fs.lstatSync(fsPath),
slowLogging`fs.lstatSync(${fsPath})`,
)
}, },
readFileSync(fsPath, options) { readFileSync(fsPath, options) {
using _ = slowLogging`fs.readFileSync(${fsPath})` return withDisposable(
return fs.readFileSync(fsPath, { encoding: options.encoding }) () => fs.readFileSync(fsPath, { encoding: options.encoding }),
slowLogging`fs.readFileSync(${fsPath})`,
)
}, },
readFileBytesSync(fsPath) { readFileBytesSync(fsPath) {
using _ = slowLogging`fs.readFileBytesSync(${fsPath})` return withDisposable(
return fs.readFileSync(fsPath) () => fs.readFileSync(fsPath),
slowLogging`fs.readFileBytesSync(${fsPath})`,
)
}, },
readSync(fsPath, options) { readSync(fsPath, options) {
using _ = slowLogging`fs.readSync(${fsPath}, ${options.length} bytes)` return withDisposable(() => {
let fd: number | undefined let fd: number | undefined
try { try {
fd = fs.openSync(fsPath, 'r') fd = fs.openSync(fsPath, 'r')
const buffer = Buffer.alloc(options.length) const buffer = Buffer.alloc(options.length)
const bytesRead = fs.readSync(fd, buffer, 0, options.length, 0) const bytesRead = fs.readSync(fd, buffer, 0, options.length, 0)
return { buffer, bytesRead } return { buffer, bytesRead }
} finally { } finally {
if (fd) fs.closeSync(fd) if (fd) fs.closeSync(fd)
} }
}, slowLogging`fs.readSync(${fsPath}, ${options.length} bytes)`)
}, },
appendFileSync(path, data, options) { appendFileSync(path, data, options) {
using _ = slowLogging`fs.appendFileSync(${path}, ${data.length} chars)` withDisposable(() => {
// For new files with explicit mode, use 'ax' (atomic create-with-mode) to avoid // 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. // TOCTOU race between existence check and open. Fall back to normal append if exists.
if (options?.mode !== undefined) { if (options?.mode !== undefined) {
try {
const fd = fs.openSync(path, 'ax', options.mode)
try { try {
fs.appendFileSync(fd, data) const fd = fs.openSync(path, 'ax', options.mode)
} finally { try {
fs.closeSync(fd) 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) { copyFileSync(src, dest) {
using _ = slowLogging`fs.copyFileSync(${src}${dest})` withDisposable(
fs.copyFileSync(src, dest) () => fs.copyFileSync(src, dest),
slowLogging`fs.copyFileSync(${src}${dest})`,
)
}, },
unlinkSync(path: string) { unlinkSync(path: string) {
using _ = slowLogging`fs.unlinkSync(${path})` withDisposable(
fs.unlinkSync(path) () => fs.unlinkSync(path),
slowLogging`fs.unlinkSync(${path})`,
)
}, },
renameSync(oldPath: string, newPath: string) { renameSync(oldPath: string, newPath: string) {
using _ = slowLogging`fs.renameSync(${oldPath}${newPath})` withDisposable(
fs.renameSync(oldPath, newPath) () => fs.renameSync(oldPath, newPath),
slowLogging`fs.renameSync(${oldPath}${newPath})`,
)
}, },
linkSync(target: string, path: string) { linkSync(target: string, path: string) {
using _ = slowLogging`fs.linkSync(${target}${path})` withDisposable(
fs.linkSync(target, path) () => fs.linkSync(target, path),
slowLogging`fs.linkSync(${target}${path})`,
)
}, },
symlinkSync( symlinkSync(
@ -511,64 +531,80 @@ export const NodeFsOperations: FsOperations = {
path: string, path: string,
type?: 'dir' | 'file' | 'junction', type?: 'dir' | 'file' | 'junction',
) { ) {
using _ = slowLogging`fs.symlinkSync(${target}${path})` withDisposable(
fs.symlinkSync(target, path, type) () => fs.symlinkSync(target, path, type),
slowLogging`fs.symlinkSync(${target}${path})`,
)
}, },
readlinkSync(path: string) { readlinkSync(path: string) {
using _ = slowLogging`fs.readlinkSync(${path})` return withDisposable(
return fs.readlinkSync(path) () => fs.readlinkSync(path),
slowLogging`fs.readlinkSync(${path})`,
)
}, },
realpathSync(path: string) { realpathSync(path: string) {
using _ = slowLogging`fs.realpathSync(${path})` return withDisposable(
return fs.realpathSync(path).normalize('NFC') () => fs.realpathSync(path).normalize('NFC'),
slowLogging`fs.realpathSync(${path})`,
)
}, },
mkdirSync(dirPath, options) { mkdirSync(dirPath, options) {
using _ = slowLogging`fs.mkdirSync(${dirPath})` withDisposable(() => {
const mkdirOptions: { recursive: boolean; mode?: number } = { const mkdirOptions: { recursive: boolean; mode?: number } = {
recursive: true, recursive: true,
} }
if (options?.mode !== undefined) { if (options?.mode !== undefined) {
mkdirOptions.mode = options.mode mkdirOptions.mode = options.mode
} }
try { try {
fs.mkdirSync(dirPath, mkdirOptions) fs.mkdirSync(dirPath, mkdirOptions)
} catch (e) { } catch (e) {
// Bun/Windows: recursive:true throws EEXIST on directories with the // Bun/Windows: recursive:true throws EEXIST on directories with the
// FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini). // FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini).
// Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir // Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir
// (bun-internal src/sys.zig existsAtType). The dir exists; ignore. // (bun-internal src/sys.zig existsAtType). The dir exists; ignore.
// https://github.com/anthropics/claude-code/issues/30924 // https://github.com/anthropics/claude-code/issues/30924
if (getErrnoCode(e) !== 'EEXIST') throw e if (getErrnoCode(e) !== 'EEXIST') throw e
} }
}, slowLogging`fs.mkdirSync(${dirPath})`)
}, },
readdirSync(dirPath) { readdirSync(dirPath) {
using _ = slowLogging`fs.readdirSync(${dirPath})` return withDisposable(
return fs.readdirSync(dirPath, { withFileTypes: true }) () => fs.readdirSync(dirPath, { withFileTypes: true }),
slowLogging`fs.readdirSync(${dirPath})`,
)
}, },
readdirStringSync(dirPath) { readdirStringSync(dirPath) {
using _ = slowLogging`fs.readdirStringSync(${dirPath})` return withDisposable(
return fs.readdirSync(dirPath) () => fs.readdirSync(dirPath),
slowLogging`fs.readdirStringSync(${dirPath})`,
)
}, },
isDirEmptySync(dirPath) { isDirEmptySync(dirPath) {
using _ = slowLogging`fs.isDirEmptySync(${dirPath})` return withDisposable(() => {
const files = this.readdirSync(dirPath) const files = this.readdirSync(dirPath)
return files.length === 0 return files.length === 0
}, slowLogging`fs.isDirEmptySync(${dirPath})`)
}, },
rmdirSync(dirPath) { rmdirSync(dirPath) {
using _ = slowLogging`fs.rmdirSync(${dirPath})` withDisposable(
fs.rmdirSync(dirPath) () => fs.rmdirSync(dirPath),
slowLogging`fs.rmdirSync(${dirPath})`,
)
}, },
rmSync(path, options) { rmSync(path, options) {
using _ = slowLogging`fs.rmSync(${path})` withDisposable(
fs.rmSync(path, options) () => fs.rmSync(path, options),
slowLogging`fs.rmSync(${path})`,
)
}, },
createWriteStream(path: string) { createWriteStream(path: string) {

View File

@ -149,8 +149,7 @@ function slowLoggingExternal(): Disposable {
* zero timing. AntSlowLogger and buildDescription are dead-code-eliminated. * zero timing. AntSlowLogger and buildDescription are dead-code-eliminated.
* *
* @example * @example
* using _ = slowLogging`structuredClone(${value})` * const result = withDisposable(() => structuredClone(value), slowLogging`structuredClone(${value})`)
* const result = structuredClone(value)
*/ */
export const slowLogging: ( export const slowLogging: (
strings: TemplateStringsArray, strings: TemplateStringsArray,
@ -159,6 +158,19 @@ export const slowLogging: (
? slowLoggingAnt ? slowLoggingAnt
: slowLoggingExternal : 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<T>(fn: () => T, disposable: Disposable): T {
try {
return fn()
} finally {
disposable[Symbol.dispose]()
}
}
// --- Wrapped operations --- // --- Wrapped operations ---
/** /**
@ -188,11 +200,14 @@ export function jsonStringify(
| null, | null,
space?: string | number, space?: string | number,
): string { ): string {
using _ = slowLogging`JSON.stringify(${value})` return withDisposable(
return JSON.stringify( () =>
value, JSON.stringify(
replacer as Parameters<typeof JSON.stringify>[1], value,
space, replacer as Parameters<typeof JSON.stringify>[1],
space,
),
slowLogging`JSON.stringify(${value})`,
) )
} }
@ -204,14 +219,14 @@ export function jsonStringify(
* import { jsonParse } from './slowOperations.js' * import { jsonParse } from './slowOperations.js'
* const data = jsonParse(jsonString) * const data = jsonParse(jsonString)
*/ */
export const jsonParse: typeof JSON.parse = (text, reviver) => { export const jsonParse: typeof JSON.parse = (text, reviver) =>
using _ = slowLogging`JSON.parse(${text})` withDisposable(() => {
// V8 de-opts JSON.parse when a second argument is passed, even if undefined. // 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. // Branch explicitly so the common (no-reviver) path stays on the fast path.
return typeof reviver === 'undefined' return typeof reviver === 'undefined'
? JSON.parse(text) ? JSON.parse(text)
: JSON.parse(text, reviver) : JSON.parse(text, reviver)
} }, slowLogging`JSON.parse(${text})`)
/** /**
* Wrapped structuredClone with slow operation logging. * Wrapped structuredClone with slow operation logging.
@ -222,8 +237,10 @@ export const jsonParse: typeof JSON.parse = (text, reviver) => {
* const copy = clone(originalObject) * const copy = clone(originalObject)
*/ */
export function clone<T>(value: T, options?: StructuredSerializeOptions): T { export function clone<T>(value: T, options?: StructuredSerializeOptions): T {
using _ = slowLogging`structuredClone(${value})` return withDisposable(
return structuredClone(value, options) () => structuredClone(value, options),
slowLogging`structuredClone(${value})`,
)
} }
/** /**
@ -235,8 +252,10 @@ export function clone<T>(value: T, options?: StructuredSerializeOptions): T {
* const copy = cloneDeep(originalObject) * const copy = cloneDeep(originalObject)
*/ */
export function cloneDeep<T>(value: T): T { export function cloneDeep<T>(value: T): T {
using _ = slowLogging`cloneDeep(${value})` return withDisposable(
return lodashCloneDeep(value) () => lodashCloneDeep(value),
slowLogging`cloneDeep(${value})`,
)
} }
/** /**
@ -253,37 +272,37 @@ export function writeFileSync_DEPRECATED(
data: string | NodeJS.ArrayBufferView, data: string | NodeJS.ArrayBufferView,
options?: WriteFileOptionsWithFlush, options?: WriteFileOptionsWithFlush,
): void { ): 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) if (needsFlush) {
const needsFlush = // Manual flush: open file, write, fsync, close
options !== null && const encoding =
typeof options === 'object' && typeof options === 'object' && 'encoding' in options
'flush' in options && ? options.encoding
options.flush === true : undefined
const mode =
if (needsFlush) { typeof options === 'object' && 'mode' in options
// Manual flush: open file, write, fsync, close ? options.mode
const encoding = : undefined
typeof options === 'object' && 'encoding' in options let fd: number | undefined
? options.encoding try {
: undefined fd = openSync(filePath, 'w', mode)
const mode = fsWriteFileSync(fd, data, { encoding: encoding ?? undefined })
typeof options === 'object' && 'mode' in options fsyncSync(fd)
? options.mode } finally {
: undefined if (fd !== undefined) {
let fd: number | undefined closeSync(fd)
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 { }, slowLogging`fs.writeFileSync(${filePath}, ${data})`)
// No flush needed, use standard writeFileSync
fsWriteFileSync(filePath, data, options as WriteFileOptions)
}
} }