From d940e89b55ccd83f6e88870a1fcdce0f2d0b7768 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 28 Aug 2026 08:18:08 +0200 Subject: [PATCH 1/9] fix(cli): skip identical writes and emit atomically Every emit path truncate-wrote in place and rewrote output even when the bytes were identical. A watch consumer that reads emitted files on change could see a partially written file, and a bulk mtime-only touch of the sources triggered a rebuild storm downstream for no content change. Compiled output, sourcemaps, dts files and copied files now skip the write when the destination already matches, and otherwise go through a temp file next to the destination followed by a rename. --- .changeset/thick-rings-punch.md | 5 + .../cli/src/swc/__tests__/atomicWrite.test.ts | 110 ++++++++++++++++++ packages/cli/src/swc/compile.ts | 9 +- packages/cli/src/swc/dir.ts | 6 +- packages/cli/src/swc/util.ts | 71 ++++++++++- 5 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 .changeset/thick-rings-punch.md create mode 100644 packages/cli/src/swc/__tests__/atomicWrite.test.ts diff --git a/.changeset/thick-rings-punch.md b/.changeset/thick-rings-punch.md new file mode 100644 index 0000000..b4cc4df --- /dev/null +++ b/.changeset/thick-rings-punch.md @@ -0,0 +1,5 @@ +--- +"@swc/cli": patch +--- + +Skip the write when emitted output is byte-identical, and write through a temp file plus rename so watch consumers never read a partially written file diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts new file mode 100644 index 0000000..8cc477f --- /dev/null +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -0,0 +1,110 @@ +import { mkdtempSync, promises, readFileSync, rmSync, statSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { + copyFileAtomicallyIfChanged, + writeFileAtomicallyIfChanged, + writeFileAtomicallyIfChangedSync, +} from "../util"; + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "swc-cli-atomic-")); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +function identity(path: string) { + const { mtimeMs, ino } = statSync(path); + return { mtimeMs, ino }; +} + +async function tmpFiles() { + const entries = await promises.readdir(dir); + return entries.filter(entry => entry.endsWith(".tmp")); +} + +describe("writeFileAtomicallyIfChanged", () => { + it("skips the write when the content is identical", async () => { + const dest = join(dir, "out.js"); + await writeFileAtomicallyIfChanged(dest, "content"); + const before = identity(dest); + + await writeFileAtomicallyIfChanged(dest, "content"); + + expect(identity(dest)).toEqual(before); + expect(await tmpFiles()).toEqual([]); + }); + + it("writes when the content changed", async () => { + const dest = join(dir, "out.js"); + await writeFileAtomicallyIfChanged(dest, "old"); + + await writeFileAtomicallyIfChanged(dest, "new"); + + expect(readFileSync(dest, "utf8")).toBe("new"); + expect(await tmpFiles()).toEqual([]); + }); + + it("applies the given mode", async () => { + const dest = join(dir, "out.js"); + await writeFileAtomicallyIfChanged(dest, "content", { mode: 0o640 }); + + expect(statSync(dest).mode & 0o777).toBe(0o640); + }); +}); + +describe("writeFileAtomicallyIfChangedSync", () => { + it("skips the write when the content is identical", async () => { + const dest = join(dir, "out.js"); + writeFileAtomicallyIfChangedSync(dest, "content"); + const before = identity(dest); + + writeFileAtomicallyIfChangedSync(dest, "content"); + + expect(identity(dest)).toEqual(before); + expect(await tmpFiles()).toEqual([]); + }); + + it("writes when the content changed", async () => { + const dest = join(dir, "out.js"); + writeFileAtomicallyIfChangedSync(dest, "old"); + + writeFileAtomicallyIfChangedSync(dest, "new"); + + expect(readFileSync(dest, "utf8")).toBe("new"); + expect(await tmpFiles()).toEqual([]); + }); +}); + +describe("copyFileAtomicallyIfChanged", () => { + it("skips the copy when the files are identical", async () => { + const src = join(dir, "src.bin"); + const dest = join(dir, "dest.bin"); + await promises.writeFile(src, Buffer.from([0, 1, 2, 255])); + await copyFileAtomicallyIfChanged(src, dest); + const before = identity(dest); + + await copyFileAtomicallyIfChanged(src, dest); + + expect(identity(dest)).toEqual(before); + expect(await tmpFiles()).toEqual([]); + }); + + it("copies when the source changed", async () => { + const src = join(dir, "src.bin"); + const dest = join(dir, "dest.bin"); + await promises.writeFile(src, Buffer.from([0, 1, 2, 255])); + await copyFileAtomicallyIfChanged(src, dest); + + const changed = Buffer.from([3, 4, 5, 254]); + await promises.writeFile(src, changed); + await copyFileAtomicallyIfChanged(src, dest); + + expect(await promises.readFile(dest)).toEqual(changed); + expect(await tmpFiles()).toEqual([]); + }); +}); diff --git a/packages/cli/src/swc/compile.ts b/packages/cli/src/swc/compile.ts index 7441e14..e638910 100644 --- a/packages/cli/src/swc/compile.ts +++ b/packages/cli/src/swc/compile.ts @@ -2,9 +2,10 @@ import slash from "slash"; import { promises } from "fs"; import { dirname, relative } from "path"; import { transformFile, transformFileSync } from "@swc/core"; +import { writeFileAtomicallyIfChanged } from "./util"; import type { Options, Output } from "@swc/core"; -const { mkdir, stat, writeFile } = promises; +const { mkdir, stat } = promises; function withSourceMap( output: Output, @@ -78,14 +79,14 @@ export async function outputResult({ const { mode } = await stat(sourceFile); const dtsPromise = dts - ? writeFile(destDtsFile, dts, { mode }) + ? writeFileAtomicallyIfChanged(destDtsFile, dts, { mode }) : Promise.resolve(); const sourceMapPromise = sourceMap - ? writeFile(destSourcemapFile, sourceMap, { mode }) + ? writeFileAtomicallyIfChanged(destSourcemapFile, sourceMap, { mode }) : Promise.resolve(); await Promise.all([ - writeFile(destFile, sourceCode, { mode }), + writeFileAtomicallyIfChanged(destFile, sourceCode, { mode }), dtsPromise, sourceMapPromise, ]); diff --git a/packages/cli/src/swc/dir.ts b/packages/cli/src/swc/dir.ts index 78733ca..9a9e175 100644 --- a/packages/cli/src/swc/dir.ts +++ b/packages/cli/src/swc/dir.ts @@ -5,7 +5,7 @@ import { stderr } from "process"; import { format } from "util"; import { CompileStatus } from "./constants"; import { Callbacks, CliOptions } from "./options"; -import { exists, getDest, mapTsExt } from "./util"; +import { copyFileAtomicallyIfChanged, exists, getDest, mapTsExt } from "./util"; import handleCompile from "./dirWorker"; import { globSources, @@ -26,7 +26,7 @@ declare module "fs" { } } -const { mkdir, rmdir, rm, copyFile, unlink } = promises; +const { mkdir, rmdir, rm, unlink } = promises; const recursive = { recursive: true }; @@ -39,7 +39,7 @@ async function handleCopy( const dir = dirname(dest); await mkdir(dir, recursive); - await copyFile(filename, dest); + await copyFileAtomicallyIfChanged(filename, dest); return CompileStatus.Copied; } diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index e6efca4..c0d9d5b 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -1,6 +1,12 @@ import * as swc from "@swc/core"; import slash from "slash"; -import { mkdirSync, writeFileSync, promises } from "fs"; +import { + mkdirSync, + readFileSync, + renameSync, + writeFileSync, + promises, +} from "fs"; import { dirname, extname, join, relative } from "path"; import { stderr } from "process"; @@ -90,10 +96,69 @@ export function outputFile( const mapLoc = filename + ".map"; code += "\n//# sourceMappingURL=" + slash(relative(fileDirName, mapLoc)); - writeFileSync(mapLoc, output.map); + writeFileAtomicallyIfChangedSync(mapLoc, output.map); } - writeFileSync(filename, code); + writeFileAtomicallyIfChangedSync(filename, code); +} + +// Watch consumers (bundler child compilers) read these files the moment they +// change: never truncate-write in place, and skip identical content so a +// recompile of unchanged sources causes no downstream rebuilds. +export async function writeFileAtomicallyIfChanged( + filename: string, + content: string, + options?: { mode?: number } +): Promise { + try { + if ((await promises.readFile(filename, "utf8")) === content) { + return; + } + } catch {} + + const tmpFile = tmpPathFor(filename); + await promises.writeFile(tmpFile, content, options); + await promises.rename(tmpFile, filename); +} + +export function writeFileAtomicallyIfChangedSync( + filename: string, + content: string +): void { + try { + if (readFileSync(filename, "utf8") === content) { + return; + } + } catch {} + + const tmpFile = tmpPathFor(filename); + writeFileSync(tmpFile, content); + renameSync(tmpFile, filename); +} + +export async function copyFileAtomicallyIfChanged( + src: string, + dest: string +): Promise { + try { + const [source, existing] = await Promise.all([ + promises.readFile(src), + promises.readFile(dest), + ]); + if (source.equals(existing)) { + return; + } + } catch {} + + const tmpDest = tmpPathFor(dest); + await promises.copyFile(src, tmpDest); + await promises.rename(tmpDest, dest); +} + +// The temporary file must live next to the destination: rename is only atomic +// within a single filesystem. +function tmpPathFor(dest: string) { + return `${dest}.${process.pid}.tmp`; } export function assertCompilationResult( From 210f42ace8850fc0e9ab9ae4f1184899b22ad14f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 28 Aug 2026 08:38:03 +0200 Subject: [PATCH 2/9] test(cli): skip the mode assertion on Windows Windows has no POSIX permission bits, so the mode always reads back as 0o666 and the assertion can never hold there. --- packages/cli/src/swc/__tests__/atomicWrite.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index 8cc477f..a1011f8 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -7,6 +7,9 @@ import { writeFileAtomicallyIfChangedSync, } from "../util"; +// Windows has no POSIX permission bits: mode always reads back as 0o666. +const itPosix = process.platform === "win32" ? it.skip : it; + let dir: string; beforeEach(() => { @@ -49,7 +52,7 @@ describe("writeFileAtomicallyIfChanged", () => { expect(await tmpFiles()).toEqual([]); }); - it("applies the given mode", async () => { + itPosix("applies the given mode", async () => { const dest = join(dir, "out.js"); await writeFileAtomicallyIfChanged(dest, "content", { mode: 0o640 }); From 6378bf3a6e3b50835b9f338abdace4ea2ba856f6 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 28 Aug 2026 09:24:53 +0200 Subject: [PATCH 3/9] fix(cli): use write-file-atomic for emitted files Handles unique temp names per write, keeps the destination's mode and symlinks, and removes the temp file when a write fails. Copies stay hand-rolled: they can be larger than the 2 GiB a buffer holds, so they compare size, mode and then chunked contents before going through copyFile. Content comparison is now byte-based, since decoding to UTF-8 makes malformed bytes compare equal to U+FFFD. --- packages/cli/package.json | 6 +- .../cli/src/swc/__tests__/atomicWrite.test.ts | 81 ++++- packages/cli/src/swc/util.ts | 118 ++++-- pnpm-lock.yaml | 335 +++++------------- 4 files changed, 255 insertions(+), 285 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a6e5ff3..86b78b0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,7 +54,8 @@ "semver": "^7.3.8", "slash": "3.0.0", "source-map": "^0.7.3", - "tinyglobby": "^0.2.13" + "tinyglobby": "^0.2.13", + "write-file-atomic": "^7.0.1" }, "devDependencies": { "@swc/cli": "0.8.0", @@ -64,6 +65,7 @@ "@types/jest": "^29.5.0", "@types/node": "^20.11.5", "@types/semver": "^7.3.13", + "@types/write-file-atomic": "^4.0.3", "chokidar": "^5.0.0", "deepmerge": "^4.2.2", "jest": "^29.5.0", @@ -83,4 +85,4 @@ "bin", "lib" ] -} \ No newline at end of file +} diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index a1011f8..cbc2690 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -1,4 +1,15 @@ -import { mkdtempSync, promises, readFileSync, rmSync, statSync } from "fs"; +import { + chmodSync, + lstatSync, + mkdirSync, + mkdtempSync, + promises, + readFileSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { @@ -111,3 +122,71 @@ describe("copyFileAtomicallyIfChanged", () => { expect(await tmpFiles()).toEqual([]); }); }); + +describe("write-file-atomic behaviour", () => { + itPosix("keeps the destination's existing mode", async () => { + const dest = join(dir, "out.js"); + writeFileSync(dest, "old"); + chmodSync(dest, 0o755); + + await writeFileAtomicallyIfChanged(dest, "new"); + + expect(statSync(dest).mode & 0o777).toBe(0o755); + }); + + itPosix("writes through a symlinked destination", async () => { + const target = join(dir, "target.js"); + const link = join(dir, "link.js"); + writeFileSync(target, "old"); + symlinkSync(target, link); + + await writeFileAtomicallyIfChanged(link, "new"); + + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("new"); + }); + + it("leaves no temp file behind when the write fails", async () => { + const dest = join(dir, "adir"); + mkdirSync(dest); + + await expect( + writeFileAtomicallyIfChanged(dest, "content") + ).rejects.toThrow(); + + expect(await tmpFiles()).toEqual([]); + }); + + it("compares bytes rather than decoded text", async () => { + const dest = join(dir, "malformed.js"); + writeFileSync(dest, Buffer.from([0xff])); + + await writeFileAtomicallyIfChanged(dest, "\uFFFD"); + + expect([...readFileSync(dest)]).toEqual([0xef, 0xbf, 0xbd]); + }); + + it("survives concurrent writes to the same destination", async () => { + const dest = join(dir, "out.js"); + + await Promise.all([ + writeFileAtomicallyIfChanged(dest, "a".repeat(100)), + writeFileAtomicallyIfChanged(dest, "b".repeat(100)), + ]); + + expect(readFileSync(dest, "utf8")).toHaveLength(100); + expect(await tmpFiles()).toEqual([]); + }); + + itPosix("copies when only the source mode changed", async () => { + const src = join(dir, "script.sh"); + const dest = join(dir, "out.sh"); + writeFileSync(src, "#!/bin/sh\n"); + await copyFileAtomicallyIfChanged(src, dest); + + chmodSync(src, 0o755); + await copyFileAtomicallyIfChanged(src, dest); + + expect(statSync(dest).mode & 0o777).toBe(0o755); + }); +}); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index c0d9d5b..4617793 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -1,12 +1,7 @@ import * as swc from "@swc/core"; import slash from "slash"; -import { - mkdirSync, - readFileSync, - renameSync, - writeFileSync, - promises, -} from "fs"; +import writeFileAtomic from "write-file-atomic"; +import { mkdirSync, readFileSync, promises } from "fs"; import { dirname, extname, join, relative } from "path"; import { stderr } from "process"; @@ -110,15 +105,11 @@ export async function writeFileAtomicallyIfChanged( content: string, options?: { mode?: number } ): Promise { - try { - if ((await promises.readFile(filename, "utf8")) === content) { - return; - } - } catch {} + if (await hasContent(filename, content)) { + return; + } - const tmpFile = tmpPathFor(filename); - await promises.writeFile(tmpFile, content, options); - await promises.rename(tmpFile, filename); + await writeFileAtomic(filename, content, { ...options, fsync: false }); } export function writeFileAtomicallyIfChangedSync( @@ -126,39 +117,102 @@ export function writeFileAtomicallyIfChangedSync( content: string ): void { try { - if (readFileSync(filename, "utf8") === content) { + if (readFileSync(filename).equals(Buffer.from(content))) { return; } } catch {} - const tmpFile = tmpPathFor(filename); - writeFileSync(tmpFile, content); - renameSync(tmpFile, filename); + writeFileAtomic.sync(filename, content, { fsync: false }); } export async function copyFileAtomicallyIfChanged( src: string, dest: string ): Promise { + if (await isSameFile(src, dest)) { + return; + } + + // Copies can be arbitrarily large binaries, so they go through copyFile + // rather than a buffer. The temporary file must live next to the + // destination: rename is only atomic within a single filesystem. + const tmpDest = `${dest}.${process.pid}.${++copies}.tmp`; + try { + await promises.copyFile(src, tmpDest); + await promises.rename(tmpDest, dest); + } catch (err: any) { + await promises.unlink(tmpDest).catch(() => {}); + throw err; + } +} + +let copies = 0; + +async function hasContent(filename: string, content: string) { + try { + const existing = await promises.readFile(filename); + return existing.equals(Buffer.from(content)); + } catch { + return false; + } +} + +// Compare size and mode before reading anything: a differing size rules out +// identical content, and copyFile applies the source mode, so a chmod-only +// change still has to be copied. +async function isSameFile(src: string, dest: string) { try { - const [source, existing] = await Promise.all([ - promises.readFile(src), - promises.readFile(dest), + const [srcStats, destStats] = await Promise.all([ + promises.stat(src), + promises.stat(dest), ]); - if (source.equals(existing)) { - return; + if ( + srcStats.size !== destStats.size || + srcStats.mode !== destStats.mode + ) { + return false; } - } catch {} - const tmpDest = tmpPathFor(dest); - await promises.copyFile(src, tmpDest); - await promises.rename(tmpDest, dest); + return await hasSameBytes(src, dest); + } catch { + return false; + } } -// The temporary file must live next to the destination: rename is only atomic -// within a single filesystem. -function tmpPathFor(dest: string) { - return `${dest}.${process.pid}.tmp`; +// Read in chunks rather than with readFile: a copied asset can be larger than +// the 2 GiB a single buffer holds. +async function hasSameBytes(src: string, dest: string) { + const chunkSize = 64 * 1024; + const [srcFile, destFile] = await Promise.all([ + promises.open(src, "r"), + promises.open(dest, "r"), + ]); + + try { + const srcChunk = Buffer.allocUnsafe(chunkSize); + const destChunk = Buffer.allocUnsafe(chunkSize); + + for (;;) { + const [srcRead, destRead] = await Promise.all([ + srcFile.read(srcChunk, 0, chunkSize), + destFile.read(destChunk, 0, chunkSize), + ]); + if (srcRead.bytesRead !== destRead.bytesRead) { + return false; + } + if (srcRead.bytesRead === 0) { + return true; + } + const read = srcRead.bytesRead; + if ( + !srcChunk.subarray(0, read).equals(destChunk.subarray(0, read)) + ) { + return false; + } + } + } finally { + await Promise.all([srcFile.close(), destFile.close()]); + } } export function assertCompilationResult( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85be910..391cab7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ importers: version: 13.2.3(enquirer@2.4.1) prettier: specifier: ^2 - version: 2.0.0 + version: 2.8.8 packages/cli: dependencies: @@ -46,56 +46,62 @@ importers: version: 8.3.0 minimatch: specifier: ^9.0.3 - version: 9.0.3 + version: 9.0.5 piscina: specifier: ^4.3.1 - version: 4.3.1 + version: 4.9.2 semver: specifier: ^7.3.8 - version: 7.3.8 + version: 7.7.2 slash: specifier: 3.0.0 version: 3.0.0 source-map: specifier: ^0.7.3 - version: 0.7.3 + version: 0.7.4 tinyglobby: specifier: ^0.2.13 version: 0.2.13 + write-file-atomic: + specifier: ^7.0.1 + version: 7.0.1 devDependencies: '@swc/cli': specifier: 0.8.0 - version: 0.8.0(@swc/core@1.6.4)(chokidar@5.0.0) + version: 0.8.0(@swc/core@1.11.24)(chokidar@5.0.0) '@swc/core': specifier: ^1.6.4 - version: 1.6.4 + version: 1.11.24 '@swc/jest': specifier: workspace:^ version: link:../jest '@swc/types': specifier: ^0.1.9 - version: 0.1.9 + version: 0.1.21 '@types/jest': specifier: ^29.5.0 version: 29.5.0 '@types/node': specifier: ^20.11.5 - version: 20.11.16 + version: 20.17.32 '@types/semver': specifier: ^7.3.13 - version: 7.3.13 + version: 7.7.0 + '@types/write-file-atomic': + specifier: ^4.0.3 + version: 4.0.3 chokidar: specifier: ^5.0.0 version: 5.0.0 deepmerge: specifier: ^4.2.2 - version: 4.2.2 + version: 4.3.1 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@20.11.16) + version: 29.5.0(@types/node@20.17.32) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.27.1)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.5.0(@types/node@20.11.16))(typescript@5.8.3) + version: 29.0.5(@babel/core@7.27.1)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.5.0(@types/node@20.17.32))(typescript@5.8.3) typescript: specifier: ~5.8.3 version: 5.8.3 @@ -140,7 +146,7 @@ importers: version: 30.0.4 '@swc/core': specifier: ^1.6.4 - version: 1.6.4 + version: 1.11.24 '@types/node': specifier: ^16.11.12 version: 16.11.12 @@ -164,13 +170,13 @@ importers: devDependencies: '@swc/core': specifier: ^1.6.4 - version: 1.6.4 + version: 1.11.24 packages/swc-info: devDependencies: '@types/node': specifier: ^20.11.16 - version: 20.11.16 + version: 20.17.32 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -183,10 +189,10 @@ importers: devDependencies: '@swc/core': specifier: ^1.6.4 - version: 1.6.4 + version: 1.11.24 webpack: specifier: ^5.69.1 - version: 5.69.1(@swc/core@1.6.4) + version: 5.69.1(@swc/core@1.11.24) packages: @@ -855,6 +861,7 @@ packages: '@lerna/create@8.0.2': resolution: {integrity: sha512-AueSlfiYXqEmy9/EIc17mjlaHFuv734dfgVBegyoefIA7hdeoExtsXnACWf8Tw5af6gwyTL3KAp6QQyc1sTuZQ==} engines: {node: '>=18.0.0'} + deprecated: This package is an implementation detail of Lerna and is no longer published separately. '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1248,120 +1255,60 @@ packages: cpu: [arm64] os: [darwin] - '@swc/core-darwin-arm64@1.6.4': - resolution: {integrity: sha512-3gyal/qjiGN3ek4kIFCk/XdF/WP1ZkFmU6ork4qz+x/1HEhmU9c8z/MdbaBdLx2wxOOD7ZARj+T2TI92QEcXqQ==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - '@swc/core-darwin-x64@1.11.24': resolution: {integrity: sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-darwin-x64@1.6.4': - resolution: {integrity: sha512-7MRkx09zGmgpknjTEeH7gk7od2WFtaHnwdiw9BCu809/1Py44yC8FEPSU7iquJTGZZcfvhe+aPlKceMO506gBA==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.24': resolution: {integrity: sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm-gnueabihf@1.6.4': - resolution: {integrity: sha512-SC0+zirvX/GMDTNL0J4XrItU66J3f61Pu/GjiiLy1KpIIDzisog27VJkS4noSnwr+6dytTQTHvUIuivlLI4Bgg==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - '@swc/core-linux-arm64-gnu@1.11.24': resolution: {integrity: sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-gnu@1.6.4': - resolution: {integrity: sha512-Z1wlQU9Ncie55rnrq97OOdH7Y540JGFclzfEbqIE0bN68sOxxE1M+sPAet/9LNtwA822BVM+CB1Z53C/osAFtg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - '@swc/core-linux-arm64-musl@1.11.24': resolution: {integrity: sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.6.4': - resolution: {integrity: sha512-xrZFXX4aQk/ENz2szl11pPgPH3QVj3CnuGXKj8exZwGwuz0g5JbUxJgW7lShm6l2hp67fy90yQcP4DCipI3ykw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - '@swc/core-linux-x64-gnu@1.11.24': resolution: {integrity: sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-gnu@1.6.4': - resolution: {integrity: sha512-5hVhC65J9kM3CH5uk90LuRokG7xutO+5t92yNAY4c1raHkh7ZLgDTHeYNTwqdkApGlf7Dj0/2X7vLOy/9qVXLg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - '@swc/core-linux-x64-musl@1.11.24': resolution: {integrity: sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.6.4': - resolution: {integrity: sha512-FPdeyL+4GnEqZ0MSkJuT6E+1PAr0JEAxfOuyHgD96yZDFPVEvs9IvLtF9+mfpCePl+LZATN/xridY97uxc167g==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - '@swc/core-win32-arm64-msvc@1.11.24': resolution: {integrity: sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-arm64-msvc@1.6.4': - resolution: {integrity: sha512-HlPKfrkqS0j6V58/fOwbZOOiW0q+SjviB2FROdply/twwuTRkGxG460nU0bRdtA/S8FH7X+HN7dLTr9ANdPMRw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - '@swc/core-win32-ia32-msvc@1.11.24': resolution: {integrity: sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-ia32-msvc@1.6.4': - resolution: {integrity: sha512-42zn2s15H6mkbcuWATqSl9qC6Iuj7mY0KhkkeuQFcpF8IQFHZJ58Fi83C8ANG0cvlZGyISIwJD4ElIyp3PGZkQ==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - '@swc/core-win32-x64-msvc@1.11.24': resolution: {integrity: sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core-win32-x64-msvc@1.6.4': - resolution: {integrity: sha512-gZlvqabopveGR5WnuiGLKFsBrBGbSTYb87FpO3emL+skcAYyFWOWxq3PhxAsDC+XrtEbhaqAKyVo2gwfznQ+LQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - '@swc/core@1.11.24': resolution: {integrity: sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==} engines: {node: '>=10'} @@ -1371,24 +1318,12 @@ packages: '@swc/helpers': optional: true - '@swc/core@1.6.4': - resolution: {integrity: sha512-DgoGwes0tq5m0xrygccCMs1fQ7OBPDvB2gf7c0v2FlzRC8tSsNs24zIFhLxwLzzf2IRr0M3b5Ex1JXENe+Fs3w==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '*' - peerDependenciesMeta: - '@swc/helpers': - optional: true - '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} '@swc/types@0.1.21': resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==} - '@swc/types@0.1.9': - resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} - '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -1481,24 +1416,21 @@ packages: '@types/node@16.11.12': resolution: {integrity: sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==} - '@types/node@20.11.16': - resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==} - '@types/node@20.17.32': resolution: {integrity: sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/semver@7.3.13': - resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} - '@types/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/write-file-atomic@4.0.3': + resolution: {integrity: sha512-qdo+vZRchyJIHNeuI1nrpsLw+hnkgqP/8mlaN6Wle/NKhydHmUN9l4p3ZE8yP90AJNJW4uB8HQhedb4f1vNayQ==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1555,6 +1487,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.0': resolution: {integrity: sha512-LRw5BW29sYj9NsQC6QoqeLVQhEa+BwVINYyMlcve+6stwdBsSt5UB7zw4UZB4+4PNqIVilHoMaPWCb/KhABHQw==} @@ -2339,6 +2272,7 @@ packages: conventional-changelog-core@5.0.1: resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. conventional-changelog-preset-loader@3.0.0: resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} @@ -2522,10 +2456,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} - engines: {node: '>=0.10.0'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -3084,6 +3014,7 @@ packages: git-raw-commits@3.0.0: resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-remote-origin-url@2.0.0: @@ -3093,6 +3024,7 @@ packages: git-semver-tags@5.0.1: resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-up@7.0.0: @@ -3117,20 +3049,22 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} @@ -4291,13 +4225,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nice-napi@1.0.2: - resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} - os: ['!win32'] - - node-addon-api@3.2.1: - resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -4316,10 +4243,6 @@ packages: encoding: optional: true - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - node-gyp@10.3.1: resolution: {integrity: sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4695,9 +4618,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - piscina@4.3.1: - resolution: {integrity: sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==} - piscina@4.9.2: resolution: {integrity: sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==} @@ -4717,11 +4637,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@2.0.0: - resolution: {integrity: sha512-vI55PC+GFLOVtpwr2di1mYhJF36v+kztJov8sx3AmqbfdA+2Dhozxb+3e1hTgoV9lyhnVJFF3Z8GCVeMBOS1bA==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5022,11 +4937,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -5169,10 +5079,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.3: - resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} - engines: {node: '>= 8'} - source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} @@ -5349,10 +5255,12 @@ packages: tar@6.1.11: resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} engines: {node: '>= 10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@1.0.0: resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} @@ -5570,9 +5478,6 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -5627,6 +5532,7 @@ packages: uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache@2.4.0: @@ -5761,6 +5667,10 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} + engines: {node: ^20.17.0 || >=22.9.0} + write-json-file@3.2.0: resolution: {integrity: sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==} engines: {node: '>=6'} @@ -6569,7 +6479,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -6591,14 +6501,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.16) + jest-config: 29.7.0(@types/node@20.17.32) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6748,7 +6658,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.11.16 + '@types/node': 20.17.32 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -6897,7 +6807,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.11.16 + '@types/node': 20.17.32 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -7396,9 +7306,9 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@swc/cli@0.8.0(@swc/core@1.6.4)(chokidar@5.0.0)': + '@swc/cli@0.8.0(@swc/core@1.11.24)(chokidar@5.0.0)': dependencies: - '@swc/core': 1.6.4 + '@swc/core': 1.11.24 '@swc/counter': 0.1.3 '@xhmikosr/bin-wrapper': 13.0.5 commander: 8.3.0 @@ -7419,10 +7329,11 @@ snapshots: commander: 8.3.0 minimatch: 9.0.5 piscina: 4.9.2 - semver: 7.7.1 + semver: 7.7.2 slash: 3.0.0 source-map: 0.7.4 tinyglobby: 0.2.13 + write-file-atomic: 7.0.1 optionalDependencies: chokidar: 5.0.0 transitivePeerDependencies: @@ -7431,63 +7342,33 @@ snapshots: '@swc/core-darwin-arm64@1.11.24': optional: true - '@swc/core-darwin-arm64@1.6.4': - optional: true - '@swc/core-darwin-x64@1.11.24': optional: true - '@swc/core-darwin-x64@1.6.4': - optional: true - '@swc/core-linux-arm-gnueabihf@1.11.24': optional: true - '@swc/core-linux-arm-gnueabihf@1.6.4': - optional: true - '@swc/core-linux-arm64-gnu@1.11.24': optional: true - '@swc/core-linux-arm64-gnu@1.6.4': - optional: true - '@swc/core-linux-arm64-musl@1.11.24': optional: true - '@swc/core-linux-arm64-musl@1.6.4': - optional: true - '@swc/core-linux-x64-gnu@1.11.24': optional: true - '@swc/core-linux-x64-gnu@1.6.4': - optional: true - '@swc/core-linux-x64-musl@1.11.24': optional: true - '@swc/core-linux-x64-musl@1.6.4': - optional: true - '@swc/core-win32-arm64-msvc@1.11.24': optional: true - '@swc/core-win32-arm64-msvc@1.6.4': - optional: true - '@swc/core-win32-ia32-msvc@1.11.24': optional: true - '@swc/core-win32-ia32-msvc@1.6.4': - optional: true - '@swc/core-win32-x64-msvc@1.11.24': optional: true - '@swc/core-win32-x64-msvc@1.6.4': - optional: true - '@swc/core@1.11.24': dependencies: '@swc/counter': 0.1.3 @@ -7504,32 +7385,12 @@ snapshots: '@swc/core-win32-ia32-msvc': 1.11.24 '@swc/core-win32-x64-msvc': 1.11.24 - '@swc/core@1.6.4': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.9 - optionalDependencies: - '@swc/core-darwin-arm64': 1.6.4 - '@swc/core-darwin-x64': 1.6.4 - '@swc/core-linux-arm-gnueabihf': 1.6.4 - '@swc/core-linux-arm64-gnu': 1.6.4 - '@swc/core-linux-arm64-musl': 1.6.4 - '@swc/core-linux-x64-gnu': 1.6.4 - '@swc/core-linux-x64-musl': 1.6.4 - '@swc/core-win32-arm64-msvc': 1.6.4 - '@swc/core-win32-ia32-msvc': 1.6.4 - '@swc/core-win32-x64-msvc': 1.6.4 - '@swc/counter@0.1.3': {} '@swc/types@0.1.21': dependencies: '@swc/counter': 0.1.3 - '@swc/types@0.1.9': - dependencies: - '@swc/counter': 0.1.3 - '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -7630,22 +7491,20 @@ snapshots: '@types/node@16.11.12': {} - '@types/node@20.11.16': - dependencies: - undici-types: 5.26.5 - '@types/node@20.17.32': dependencies: undici-types: 6.19.8 '@types/normalize-package-data@2.4.4': {} - '@types/semver@7.3.13': {} - '@types/semver@7.7.0': {} '@types/stack-utils@2.0.3': {} + '@types/write-file-atomic@4.0.3': + dependencies: + '@types/node': 20.17.32 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -7662,7 +7521,7 @@ snapshots: functional-red-black-tree: 1.0.1 ignore: 5.3.2 regexpp: 3.2.0 - semver: 7.7.1 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -8711,13 +8570,13 @@ snapshots: optionalDependencies: typescript: 5.8.3 - create-jest@29.7.0(@types/node@20.11.16): + create-jest@29.7.0(@types/node@20.17.32): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.11.16) + jest-config: 29.7.0(@types/node@20.17.32) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -8890,8 +8749,6 @@ snapshots: deep-is@0.1.4: {} - deepmerge@4.2.2: {} - deepmerge@4.3.1: {} defaults@1.0.4: @@ -9146,7 +9003,7 @@ snapshots: optionator: 0.9.4 progress: 2.0.3 regexpp: 3.2.0 - semver: 7.7.1 + semver: 7.7.2 strip-ansi: 6.0.1 strip-json-comments: 3.1.1 text-table: 0.2.0 @@ -10187,16 +10044,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.11.16): + jest-cli@29.7.0(@types/node@20.17.32): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.11.16) + create-jest: 29.7.0(@types/node@20.17.32) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.11.16) + jest-config: 29.7.0(@types/node@20.17.32) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -10225,7 +10082,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.11.16): + jest-config@29.7.0(@types/node@20.17.32): dependencies: '@babel/core': 7.27.1 '@jest/test-sequencer': 29.7.0 @@ -10233,7 +10090,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.27.1) chalk: 4.1.2 ci-info: 3.9.0 - deepmerge: 4.2.2 + deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 jest-circus: 29.7.0 @@ -10250,7 +10107,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.11.16 + '@types/node': 20.17.32 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10382,7 +10239,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.11.16 + '@types/node': 20.17.32 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -10525,7 +10382,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -10580,7 +10437,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -10679,7 +10536,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10716,7 +10573,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.16 + '@types/node': 20.17.32 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -10755,12 +10612,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.5.0(@types/node@20.11.16): + jest@29.5.0(@types/node@20.17.32): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.11.16) + jest-cli: 29.7.0(@types/node@20.17.32) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -11298,15 +11155,6 @@ snapshots: neo-async@2.6.2: {} - nice-napi@1.0.2: - dependencies: - node-addon-api: 3.2.1 - node-gyp-build: 4.8.4 - optional: true - - node-addon-api@3.2.1: - optional: true - node-fetch@2.6.7(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -11319,9 +11167,6 @@ snapshots: optionalDependencies: encoding: 0.1.13 - node-gyp-build@4.8.4: - optional: true - node-gyp@10.3.1: dependencies: env-paths: 2.2.1 @@ -11776,10 +11621,6 @@ snapshots: pirates@4.0.7: {} - piscina@4.3.1: - optionalDependencies: - nice-napi: 1.0.2 - piscina@4.9.2: optionalDependencies: '@napi-rs/nice': 1.0.1 @@ -11799,8 +11640,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier@2.0.0: {} - prettier@2.8.8: {} pretty-format@29.7.0: @@ -12093,10 +11932,6 @@ snapshots: semver@6.3.1: {} - semver@7.3.8: - dependencies: - lru-cache: 6.0.0 - semver@7.7.1: {} semver@7.7.2: {} @@ -12275,8 +12110,6 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.3: {} - source-map@0.7.4: {} spawndamnit@2.0.0: @@ -12494,16 +12327,16 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.14(@swc/core@1.6.4)(webpack@5.69.1(@swc/core@1.6.4)): + terser-webpack-plugin@5.3.14(@swc/core@1.11.24)(webpack@5.69.1(@swc/core@1.11.24)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.39.0 - webpack: 5.69.1(@swc/core@1.6.4) + webpack: 5.69.1(@swc/core@1.11.24) optionalDependencies: - '@swc/core': 1.6.4 + '@swc/core': 1.11.24 terser@5.39.0: dependencies: @@ -12564,16 +12397,16 @@ snapshots: trim-newlines@3.0.1: {} - ts-jest@29.0.5(@babel/core@7.27.1)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.5.0(@types/node@20.11.16))(typescript@5.8.3): + ts-jest@29.0.5(@babel/core@7.27.1)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(jest@29.5.0(@types/node@20.17.32))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@20.11.16) + jest: 29.5.0(@types/node@20.17.32) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.7.2 typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: @@ -12702,8 +12535,6 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@5.26.5: {} - undici-types@6.19.8: {} unicorn-magic@0.3.0: {} @@ -12814,7 +12645,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.69.1(@swc/core@1.6.4): + webpack@5.69.1(@swc/core@1.11.24): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 0.0.51 @@ -12837,7 +12668,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(@swc/core@1.6.4)(webpack@5.69.1(@swc/core@1.6.4)) + terser-webpack-plugin: 5.3.14(@swc/core@1.11.24)(webpack@5.69.1(@swc/core@1.11.24)) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -12961,6 +12792,10 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + write-file-atomic@7.0.1: + dependencies: + signal-exit: 4.1.0 + write-json-file@3.2.0: dependencies: detect-indent: 5.0.0 From ebf88f18ef4e7cc2dde6f8d2eb0807079ffe0190 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 28 Aug 2026 09:52:49 +0200 Subject: [PATCH 4/9] reorder --- packages/cli/src/swc/compile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/swc/compile.ts b/packages/cli/src/swc/compile.ts index e638910..8c71ede 100644 --- a/packages/cli/src/swc/compile.ts +++ b/packages/cli/src/swc/compile.ts @@ -2,8 +2,8 @@ import slash from "slash"; import { promises } from "fs"; import { dirname, relative } from "path"; import { transformFile, transformFileSync } from "@swc/core"; -import { writeFileAtomicallyIfChanged } from "./util"; import type { Options, Output } from "@swc/core"; +import { writeFileAtomicallyIfChanged } from "./util"; const { mkdir, stat } = promises; From 1421d5bfa9ee5dd4a3cfe4fed7bba2d2e55b2378 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 28 Aug 2026 10:13:37 +0200 Subject: [PATCH 5/9] fix(cli): follow symlinked copy destinations and close handles on failure Opening the source and the destination together left the source handle open when the destination open rejected, which Node 26 turns into an uncaught error once the handle is collected. Open them one at a time. Copies also renamed over a symlinked destination rather than following it, unlike copyFile before it and unlike the generated output path. --- .../cli/src/swc/__tests__/atomicWrite.test.ts | 39 +++++++++++++++++++ packages/cli/src/swc/util.ts | 20 +++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index cbc2690..b61d8be 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -3,6 +3,7 @@ import { lstatSync, mkdirSync, mkdtempSync, + readdirSync, promises, readFileSync, rmSync, @@ -190,3 +191,41 @@ describe("write-file-atomic behaviour", () => { expect(statSync(dest).mode & 0o777).toBe(0o755); }); }); + +describe("copy edge cases", () => { + itPosix("copies through a symlinked destination", async () => { + const src = join(dir, "src.txt"); + const target = join(dir, "target.txt"); + const link = join(dir, "link.txt"); + writeFileSync(src, "new"); + writeFileSync(target, "old"); + symlinkSync(target, link); + + await copyFileAtomicallyIfChanged(src, link); + + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("new"); + expect(await tmpFiles()).toEqual([]); + }); + + itPosix( + "closes the source when the destination vanishes mid-compare", + async () => { + const openFds = () => readdirSync("/dev/fd").length; + const before = openFds(); + + for (let i = 0; i < 20; i++) { + const src = join(dir, `src${i}`); + const dest = join(dir, `dest${i}`); + writeFileSync(src, "same"); + writeFileSync(dest, "same"); + + const copy = copyFileAtomicallyIfChanged(src, dest); + rmSync(dest, { force: true }); + await copy.catch(() => {}); + } + + expect(openFds() - before).toBeLessThan(10); + } + ); +}); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index 4617793..613ec7a 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -133,13 +133,17 @@ export async function copyFileAtomicallyIfChanged( return; } + // Follow a symlinked destination rather than replacing the link, which is + // what copyFile did and what write-file-atomic does for generated output. + const target = await promises.realpath(dest).catch(() => dest); + // Copies can be arbitrarily large binaries, so they go through copyFile // rather than a buffer. The temporary file must live next to the // destination: rename is only atomic within a single filesystem. - const tmpDest = `${dest}.${process.pid}.${++copies}.tmp`; + const tmpDest = `${target}.${process.pid}.${++copies}.tmp`; try { await promises.copyFile(src, tmpDest); - await promises.rename(tmpDest, dest); + await promises.rename(tmpDest, target); } catch (err: any) { await promises.unlink(tmpDest).catch(() => {}); throw err; @@ -183,10 +187,14 @@ async function isSameFile(src: string, dest: string) { // the 2 GiB a single buffer holds. async function hasSameBytes(src: string, dest: string) { const chunkSize = 64 * 1024; - const [srcFile, destFile] = await Promise.all([ - promises.open(src, "r"), - promises.open(dest, "r"), - ]); + const srcFile = await promises.open(src, "r"); + let destFile; + try { + destFile = await promises.open(dest, "r"); + } catch (err: any) { + await srcFile.close(); + throw err; + } try { const srcChunk = Buffer.allocUnsafe(chunkSize); From 414d2d185b484b21682bc8f3e5e5e47a807360c4 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 31 Aug 2026 08:59:35 +0200 Subject: [PATCH 6/9] fix(cli): handle dangling links, long names and special files A dangling destination symlink now resolves through its link text, so the copy creates the target instead of replacing the link. Temporary copies use a short name in the destination's directory. The previous name extended the destination's own, which fails when that name already fills a path component. An --out-file target that is not a regular file, such as a FIFO or /dev/null, is written directly: reading it can block, and renaming over it would replace the special file. --- .../cli/src/swc/__tests__/atomicWrite.test.ts | 32 ++++++++++++ packages/cli/src/swc/util.ts | 50 ++++++++++++++++--- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index b61d8be..8c3471c 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -229,3 +229,35 @@ describe("copy edge cases", () => { } ); }); + +describe("unusual destinations", () => { + itPosix("copies through a dangling symlink", async () => { + const src = join(dir, "src.txt"); + const target = join(dir, "not-yet.txt"); + const link = join(dir, "link.txt"); + writeFileSync(src, "payload"); + symlinkSync(target, link); + + await copyFileAtomicallyIfChanged(src, link); + + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("payload"); + }); + + it("copies to a destination whose name fills a path component", async () => { + const src = join(dir, "src.txt"); + const dest = join(dir, `${"z".repeat(250)}.txt`); + writeFileSync(src, "payload"); + + await copyFileAtomicallyIfChanged(src, dest); + + expect(readFileSync(dest, "utf8")).toBe("payload"); + expect(await tmpFiles()).toEqual([]); + }); + + itPosix("writes directly to a non-regular destination", () => { + writeFileAtomicallyIfChangedSync("/dev/null", "payload"); + + expect(statSync("/dev/null").isCharacterDevice()).toBe(true); + }); +}); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index 613ec7a..95a9f8d 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -1,8 +1,8 @@ import * as swc from "@swc/core"; import slash from "slash"; import writeFileAtomic from "write-file-atomic"; -import { mkdirSync, readFileSync, promises } from "fs"; -import { dirname, extname, join, relative } from "path"; +import { mkdirSync, readFileSync, statSync, writeFileSync, promises } from "fs"; +import { dirname, extname, join, relative, resolve } from "path"; import { stderr } from "process"; export async function exists(path: string): Promise { @@ -116,6 +116,13 @@ export function writeFileAtomicallyIfChangedSync( filename: string, content: string ): void { + // A FIFO or device such as /dev/null has to be written directly: reading it + // can block, and renaming over it would replace the special file. + if (!isRegularFile(filename)) { + writeFileSync(filename, content); + return; + } + try { if (readFileSync(filename).equals(Buffer.from(content))) { return; @@ -125,6 +132,15 @@ export function writeFileAtomicallyIfChangedSync( writeFileAtomic.sync(filename, content, { fsync: false }); } +function isRegularFile(filename: string) { + try { + return statSync(filename).isFile(); + } catch { + // A destination that does not exist yet is created as a regular file. + return true; + } +} + export async function copyFileAtomicallyIfChanged( src: string, dest: string @@ -133,14 +149,17 @@ export async function copyFileAtomicallyIfChanged( return; } - // Follow a symlinked destination rather than replacing the link, which is - // what copyFile did and what write-file-atomic does for generated output. - const target = await promises.realpath(dest).catch(() => dest); + const target = await resolveLink(dest); // Copies can be arbitrarily large binaries, so they go through copyFile // rather than a buffer. The temporary file must live next to the - // destination: rename is only atomic within a single filesystem. - const tmpDest = `${target}.${process.pid}.${++copies}.tmp`; + // destination: rename is only atomic within a single filesystem. Its name + // does not extend the destination's, which can already be at the length a + // single path component allows. + const tmpDest = join( + dirname(target), + `.swc-${process.pid}-${++copies}.tmp` + ); try { await promises.copyFile(src, tmpDest); await promises.rename(tmpDest, target); @@ -152,6 +171,23 @@ export async function copyFileAtomicallyIfChanged( let copies = 0; +// Follow a symlinked destination rather than replacing the link, which is what +// copyFile did and what write-file-atomic does for generated output. realpath +// rejects on a link whose target does not exist yet, so fall back to the link +// text, which copyFile would have created. +async function resolveLink(dest: string) { + try { + return await promises.realpath(dest); + } catch {} + + try { + const link = await promises.readlink(dest); + return resolve(dirname(dest), link); + } catch { + return dest; + } +} + async function hasContent(filename: string, content: string) { try { const existing = await promises.readFile(filename); From 789a6155a30d5b21c8c1974acbfd56d8dd950188 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 31 Aug 2026 09:10:40 +0200 Subject: [PATCH 7/9] fix(cli): resolve dangling symlinks before writing output write-file-atomic follows a symlinked destination, but falls back to the link itself when the target does not exist yet, which replaces the link. Resolve the link text first, as the copy path already does, so a dangling link keeps pointing at a file the write creates. --- .../cli/src/swc/__tests__/atomicWrite.test.ts | 22 +++++++++++++ packages/cli/src/swc/util.ts | 31 +++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index 8c3471c..c04d3d0 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -231,6 +231,28 @@ describe("copy edge cases", () => { }); describe("unusual destinations", () => { + itPosix("writes through a dangling symlink", async () => { + const target = join(dir, "not-yet.js"); + const link = join(dir, "link.js"); + symlinkSync(target, link); + + await writeFileAtomicallyIfChanged(link, "content"); + + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("content"); + }); + + itPosix("writes through a dangling symlink synchronously", () => { + const target = join(dir, "not-yet-sync.js"); + const link = join(dir, "link-sync.js"); + symlinkSync(target, link); + + writeFileAtomicallyIfChangedSync(link, "content"); + + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("content"); + }); + itPosix("copies through a dangling symlink", async () => { const src = join(dir, "src.txt"); const target = join(dir, "not-yet.txt"); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index 95a9f8d..3254802 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -1,7 +1,15 @@ import * as swc from "@swc/core"; import slash from "slash"; import writeFileAtomic from "write-file-atomic"; -import { mkdirSync, readFileSync, statSync, writeFileSync, promises } from "fs"; +import { + mkdirSync, + readFileSync, + readlinkSync, + realpathSync, + statSync, + writeFileSync, + promises, +} from "fs"; import { dirname, extname, join, relative, resolve } from "path"; import { stderr } from "process"; @@ -109,7 +117,12 @@ export async function writeFileAtomicallyIfChanged( return; } - await writeFileAtomic(filename, content, { ...options, fsync: false }); + // write-file-atomic follows a symlinked destination, but falls back to the + // link itself when the target does not exist yet. Resolve that case here so + // a dangling link keeps pointing at a file the write creates. + const target = await resolveLink(filename); + + await writeFileAtomic(target, content, { ...options, fsync: false }); } export function writeFileAtomicallyIfChangedSync( @@ -129,7 +142,7 @@ export function writeFileAtomicallyIfChangedSync( } } catch {} - writeFileAtomic.sync(filename, content, { fsync: false }); + writeFileAtomic.sync(resolveLinkSync(filename), content, { fsync: false }); } function isRegularFile(filename: string) { @@ -188,6 +201,18 @@ async function resolveLink(dest: string) { } } +function resolveLinkSync(dest: string) { + try { + return realpathSync(dest); + } catch {} + + try { + return resolve(dirname(dest), readlinkSync(dest)); + } catch { + return dest; + } +} + async function hasContent(filename: string, content: string) { try { const existing = await promises.readFile(filename); From e9d0eb65b6aa435b565a610dbb4c51e36735b67a Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 31 Aug 2026 09:55:29 +0200 Subject: [PATCH 8/9] fix(cli): write directly when a temp file cannot be created An output directory that allows no new files cannot host the temporary file, even when the output itself is writable, so an EACCES from the atomic write falls back to writing the destination directly. Every other error still propagates. --- .../cli/src/swc/__tests__/atomicWrite.test.ts | 45 +++++++++++++++++++ packages/cli/src/swc/util.ts | 22 ++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index c04d3d0..24c8b64 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -21,6 +21,9 @@ import { // Windows has no POSIX permission bits: mode always reads back as 0o666. const itPosix = process.platform === "win32" ? it.skip : it; +// Permission bits do not constrain root, so those cases cannot be exercised. +const itUnprivileged = + process.platform === "win32" || process.getuid?.() === 0 ? it.skip : it; let dir: string; @@ -277,6 +280,48 @@ describe("unusual destinations", () => { expect(await tmpFiles()).toEqual([]); }); + itUnprivileged( + "writes when the directory cannot host a temp file", + async () => { + const locked = join(dir, "locked"); + const dest = join(locked, "out.js"); + mkdirSync(locked); + writeFileSync(dest, "old"); + chmodSync(dest, 0o666); + chmodSync(locked, 0o555); + + try { + await writeFileAtomicallyIfChanged(dest, "async"); + expect(readFileSync(dest, "utf8")).toBe("async"); + + writeFileAtomicallyIfChangedSync(dest, "sync"); + expect(readFileSync(dest, "utf8")).toBe("sync"); + } finally { + chmodSync(locked, 0o755); + } + } + ); + + itUnprivileged( + "still fails when the destination itself is read only", + async () => { + const locked = join(dir, "read-only"); + const dest = join(locked, "out.js"); + mkdirSync(locked); + writeFileSync(dest, "old"); + chmodSync(dest, 0o444); + chmodSync(locked, 0o555); + + try { + await expect( + writeFileAtomicallyIfChanged(dest, "new") + ).rejects.toThrow(); + } finally { + chmodSync(locked, 0o755); + } + } + ); + itPosix("writes directly to a non-regular destination", () => { writeFileAtomicallyIfChangedSync("/dev/null", "payload"); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index 3254802..46944d6 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -122,7 +122,16 @@ export async function writeFileAtomicallyIfChanged( // a dangling link keeps pointing at a file the write creates. const target = await resolveLink(filename); - await writeFileAtomic(target, content, { ...options, fsync: false }); + try { + await writeFileAtomic(target, content, { ...options, fsync: false }); + } catch (err: any) { + // An output directory that does not allow new files cannot host the + // temporary file, but the output itself may still be writable. + if (err.code !== "EACCES") { + throw err; + } + await promises.writeFile(target, content, options); + } } export function writeFileAtomicallyIfChangedSync( @@ -142,7 +151,16 @@ export function writeFileAtomicallyIfChangedSync( } } catch {} - writeFileAtomic.sync(resolveLinkSync(filename), content, { fsync: false }); + const target = resolveLinkSync(filename); + + try { + writeFileAtomic.sync(target, content, { fsync: false }); + } catch (err: any) { + if (err.code !== "EACCES") { + throw err; + } + writeFileSync(target, content); + } } function isRegularFile(filename: string) { From 5237f26629b5b3485beccba715b963c9abf3cfdb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 31 Aug 2026 10:10:48 +0200 Subject: [PATCH 9/9] fix(cli): follow whole symlink chains and guard special files Resolving a dangling destination stopped after one link, so a chain such as out.js -> current.js -> missing.js replaced the intermediate link rather than creating the target. Walk the chain instead, and report ELOOP once it stops ending anywhere, as a plain write did. The asynchronous writer also needs the direct write its synchronous counterpart already does for a FIFO or device, which --out-dir can reach just as easily as --out-file. --- .../cli/src/swc/__tests__/atomicWrite.test.ts | 57 +++++++++++++++++++ packages/cli/src/swc/util.ts | 51 ++++++++++++++--- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/swc/__tests__/atomicWrite.test.ts b/packages/cli/src/swc/__tests__/atomicWrite.test.ts index 24c8b64..2d0ea88 100644 --- a/packages/cli/src/swc/__tests__/atomicWrite.test.ts +++ b/packages/cli/src/swc/__tests__/atomicWrite.test.ts @@ -328,3 +328,60 @@ describe("unusual destinations", () => { expect(statSync("/dev/null").isCharacterDevice()).toBe(true); }); }); + +describe("symlink chains", () => { + const chain = (name: string) => { + const target = join(dir, `${name}-target`); + const middle = join(dir, `${name}-middle`); + const top = join(dir, `${name}-top`); + symlinkSync(target, middle); + symlinkSync(middle, top); + return { target, middle, top }; + }; + + const loop = (name: string) => { + const first = join(dir, `${name}-first`); + const second = join(dir, `${name}-second`); + symlinkSync(second, first); + symlinkSync(first, second); + return first; + }; + + itPosix("writes through a chain of dangling links", async () => { + const { target, middle, top } = chain("write"); + + await writeFileAtomicallyIfChanged(top, "content"); + + expect(lstatSync(middle).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("content"); + }); + + itPosix("writes through a chain synchronously", () => { + const { target, middle, top } = chain("sync"); + + writeFileAtomicallyIfChangedSync(top, "content"); + + expect(lstatSync(middle).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("content"); + }); + + itPosix("copies through a chain of dangling links", async () => { + const src = join(dir, "src.txt"); + writeFileSync(src, "content"); + const { target, middle, top } = chain("copy"); + + await copyFileAtomicallyIfChanged(src, top); + + expect(lstatSync(middle).isSymbolicLink()).toBe(true); + expect(readFileSync(target, "utf8")).toBe("content"); + }); + + itPosix("reports a link loop instead of replacing the link", async () => { + const first = loop("write"); + + await expect( + writeFileAtomicallyIfChanged(first, "content") + ).rejects.toMatchObject({ code: "ELOOP" }); + expect(lstatSync(first).isSymbolicLink()).toBe(true); + }); +}); diff --git a/packages/cli/src/swc/util.ts b/packages/cli/src/swc/util.ts index 46944d6..37bb475 100644 --- a/packages/cli/src/swc/util.ts +++ b/packages/cli/src/swc/util.ts @@ -113,6 +113,13 @@ export async function writeFileAtomicallyIfChanged( content: string, options?: { mode?: number } ): Promise { + // A FIFO or device such as /dev/null has to be written directly: reading it + // can block, and renaming over it would replace the special file. + if (!isRegularFile(filename)) { + await promises.writeFile(filename, content, options); + return; + } + if (await hasContent(filename, content)) { return; } @@ -211,12 +218,20 @@ async function resolveLink(dest: string) { return await promises.realpath(dest); } catch {} - try { - const link = await promises.readlink(dest); - return resolve(dirname(dest), link); - } catch { - return dest; + // realpath only fails once the chain ends somewhere that does not exist. + // Walk the rest of it by hand, as a plain write would have. + let target = dest; + for (let hop = 0; hop < maxLinkDepth; hop++) { + let link; + try { + link = await promises.readlink(target); + } catch { + return target; + } + target = resolve(dirname(target), link); } + + throw tooManyLinks(dest); } function resolveLinkSync(dest: string) { @@ -224,11 +239,29 @@ function resolveLinkSync(dest: string) { return realpathSync(dest); } catch {} - try { - return resolve(dirname(dest), readlinkSync(dest)); - } catch { - return dest; + let target = dest; + for (let hop = 0; hop < maxLinkDepth; hop++) { + let link; + try { + link = readlinkSync(target); + } catch { + return target; + } + target = resolve(dirname(target), link); } + + throw tooManyLinks(dest); +} + +const maxLinkDepth = 40; + +// A chain this long is a loop in practice. A plain write reported ELOOP and +// left the link alone, and renaming over it silently would not. +function tooManyLinks(dest: string) { + return Object.assign( + new Error(`ELOOP: too many symbolic links encountered, open '${dest}'`), + { code: "ELOOP", path: dest } + ); } async function hasContent(filename: string, content: string) {