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:
commit
4703b608a8
|
|
@ -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)}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}`,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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})`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user