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
export function listBuiltinSkillNames(): string[] {
export function listBuiltinSkills(): string[] {
return ${JSON.stringify(allSkillNames)}
}

View File

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

View File

@ -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)}`,
)
}

View File

@ -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) {

View File

@ -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<T>(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<typeof JSON.stringify>[1],
space,
return withDisposable(
() =>
JSON.stringify(
value,
replacer as Parameters<typeof JSON.stringify>[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<T>(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<T>(value: T, options?: StructuredSerializeOptions): T {
* const copy = cloneDeep(originalObject)
*/
export function cloneDeep<T>(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})`)
}