diff --git a/.cspell.json b/.cspell.json index f30a45a..ef8be66 100644 --- a/.cspell.json +++ b/.cspell.json @@ -16,6 +16,7 @@ "finalhandler", "fullhash", "hono", + "jsonjoy", "leadinghash", "memfs", "middie", diff --git a/package.json b/package.json index ad4a329..2eb3f09 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@hapi/hapi": "^21.4.10", "@hono/node-server": "^2.1.1", + "@jsonjoy.com/fs-core": "4.79.0", "@rspack/core": "2.2.2", "@types/express": "^5.0.6", "@types/node": "^24.13.3", @@ -47,7 +48,7 @@ "finalhandler": "^2.1.1", "hono": "^4.13.5", "koa": "^3.2.1", - "memfs": "^4.68.2", + "memfs": "4.79.0", "mrmime": "^2.0.1", "on-finished": "^2.4.1", "range-parser": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f507f50..0697239 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@hono/node-server': specifier: ^2.1.1 version: 2.1.1(hono@4.13.5) + '@jsonjoy.com/fs-core': + specifier: 4.79.0 + version: 4.79.0(tslib@2.8.1) '@rspack/core': specifier: 2.2.2 version: 2.2.2(@swc/helpers@0.5.23) @@ -54,8 +57,8 @@ importers: specifier: ^3.2.1 version: 3.2.1 memfs: - specifier: ^4.68.2 - version: 4.68.2 + specifier: 4.79.0 + version: 4.79.0 mrmime: specifier: ^2.0.1 version: 2.0.1 @@ -518,50 +521,50 @@ packages: peerDependencies: tslib: '2' - '@jsonjoy.com/fs-core@4.68.2': - resolution: {integrity: sha512-PoBeUNEbjyLKKwCap2z8LkkqdhdfttS4rTHCALVuP65BdF+sAoyBqHo1m+uGTRBiQWv3H7MGfr8f7lY4pDwanw==} + '@jsonjoy.com/fs-core@4.79.0': + resolution: {integrity: sha512-bxSwMrDY71OjEdj2mAYBxSe4BnHtAOjOxqUoFJc0WsllcGgA8iYl6wRF+4aVdFUjjEyHfBwxVv0QqW4AweIUZw==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-fsa@4.68.2': - resolution: {integrity: sha512-h6eGXlLGGMyPfNliDQrbuHKTB9Z29ksCLjreAmdBqrDOBdfrTrHssi+b9WLfrF+J2KzAbIt9OMjJu6AEpTnYkg==} + '@jsonjoy.com/fs-fsa@4.79.0': + resolution: {integrity: sha512-EdadmStAvZw4jYg4iW3mDxBmzMFSI8jcHQkSl+rIQIO7dWbPdXaB83PBQhNCCtMBeX7zwcOFjpp0ecXqbYbjUA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-builtins@4.68.2': - resolution: {integrity: sha512-V8WzQsW2YIrH3RxBGY6HZisxn+dDUltHgksVRuCdPEOXEkAfXuhL/01eHFrabNu84Dn13XuLqvcQUKOYVKAUOA==} + '@jsonjoy.com/fs-node-builtins@4.79.0': + resolution: {integrity: sha512-LoBHewVHp/bZQ8Om1Iivl6NEHg7Mmby9kXJZysfnHYW4256rzXFSEcEpto1qgSAka+sq2v4N/Cnl562Z15MByA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-to-fsa@4.68.2': - resolution: {integrity: sha512-4O1K4w5G4oJIpKpoa3WSLG83AsQYVnGv+aUSBGOvIUph9Axm6bB1mPlvldkNg2tBx/4dkiTlZnqNrK5SQ21Wtg==} + '@jsonjoy.com/fs-node-to-fsa@4.79.0': + resolution: {integrity: sha512-xFza/t81sVL4rik7h3SlcymC6X7pvNllI88LW1GESEANDXinf5lZoqju59i3AsGLyy4i0eBQvPiizWSMwCunQA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-utils@4.68.2': - resolution: {integrity: sha512-CxFwyG9fJr7dAKAd1uanNRNrRMtDDqbYJA8do53+M4LY7SxcBEEmMjC1zi9MOsBlVLHTXvA7OP9+n639UqtIcw==} + '@jsonjoy.com/fs-node-utils@4.79.0': + resolution: {integrity: sha512-Rj/WevHB8DV8VyEvFGSvDD3m3JHOVk3fO1biH26iworL9/rqdUMKQQeyiKTLbdf3Z9s9vVZnn8ZC0FWZgtO2mg==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node@4.68.2': - resolution: {integrity: sha512-Kpj519Qk4OXG4+mZ8vTf9BEflliJrPueIDEVcbx8tAOufMJJ8IxR0WwdbcEvJol2KQog3PJjtALXVclorE+xbQ==} + '@jsonjoy.com/fs-node@4.79.0': + resolution: {integrity: sha512-/k18aT1X+A2yLdlTe3WKpL5y2qrvbcUY1BXA4A+s8fBfTeXfi+xdvEB85JVXVf2i9kzASQZFx8dHw9OchgMUhQ==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-print@4.68.2': - resolution: {integrity: sha512-cBABQmZJXig6bahwNka3+yPNGUF1GeMWbR76ZM1N2ajgCd+S+twZigCoe9+0ueZmkhM9xd2a7688Gy/ch7T+2w==} + '@jsonjoy.com/fs-print@4.79.0': + resolution: {integrity: sha512-Bp5Q+OV1Dnl8oKf8hVhmboeI+NWbWRk1GxtJzuRvopDeNBDBh0RLevEsfnOVtwBwgRZaptSXn3e/mm3kbOzfDw==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-snapshot@4.68.2': - resolution: {integrity: sha512-Aix7+NM38LzvewM0T3PICSgFdF5BVCtVgsjNsrlDPGc9hiMgJzReTDDl2/elgl0A9USMl3QP27x5WwxZSOtrfw==} + '@jsonjoy.com/fs-snapshot@4.79.0': + resolution: {integrity: sha512-/GY+eMvOl4ZQHUnrKYjC/t3N0BImBg7zv/cAGm3kiwUfqSQuo87jtbjmD3mzgQMv16K8zttkHE2wT9GaVsehvA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -1421,8 +1424,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - glob-to-regex.js@1.2.0: - resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + glob-to-regex.js@1.3.1: + resolution: {integrity: sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -1534,8 +1537,8 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memfs@4.68.2: - resolution: {integrity: sha512-Un1ElEBoIdPI9kg0sm3LebVEuEViodfIvlaG8Z1MFXa7ZnR9vWuPKUo3dt/vuWY2ivc05l76B5QOjdgCzAzmGw==} + memfs@4.79.0: + resolution: {integrity: sha512-vDFqA9WSa//xa3XIKwMmVqxgWWft4PMKW0k69gBM2rYOzZ3VqL9BrK3Kyl45jSxS7qImDgwejZzEb/xDWhP37g==} merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} @@ -2308,59 +2311,59 @@ snapshots: dependencies: tslib: 2.8.1 - '@jsonjoy.com/fs-core@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-core@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) thingies: 2.5.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-fsa@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-fsa@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) thingies: 2.5.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node-builtins@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-node-builtins@4.79.0(tslib@2.8.1)': dependencies: tslib: 2.8.1 - '@jsonjoy.com/fs-node-to-fsa@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-node-to-fsa@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-fsa': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node-utils@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-node-utils@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-node@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.68.2(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.79.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) thingies: 2.5.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-print@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-print@4.79.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-snapshot@4.68.2(tslib@2.8.1)': + '@jsonjoy.com/fs-snapshot@4.79.0(tslib@2.8.1)': dependencies: '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) tslib: 2.8.1 @@ -3118,7 +3121,7 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - glob-to-regex.js@1.2.0(tslib@2.8.1): + glob-to-regex.js@1.3.1(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -3222,19 +3225,19 @@ snapshots: media-typer@1.1.0: {} - memfs@4.68.2: + memfs@4.79.0: dependencies: - '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-fsa': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-to-fsa': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.68.2(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.79.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.79.0(tslib@2.8.1) '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) thingies: 2.5.0(tslib@2.8.1) tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 diff --git a/src/utils/memoryFileSystem.js b/src/utils/memoryFileSystem.js new file mode 100644 index 0000000..8d374aa --- /dev/null +++ b/src/utils/memoryFileSystem.js @@ -0,0 +1,53 @@ +import { Node, Superblock } from "@jsonjoy.com/fs-core"; +import { Volume, createFsFromVolume } from "memfs"; + +/** + * Rspack emits complete buffers and truncates files before replacing them. + * Retain those buffers to avoid copying and unused growth capacity. Callers + * must not mutate the input buffer or its aliases after a complete write. + * + * @this {Node} + * @param {Buffer} buffer input buffer + * @param {number} [offset] source offset + * @param {number} [length] number of bytes + * @param {number} [position] file offset + * @returns {number} written bytes + */ +function write(buffer, offset = 0, length = buffer.length, position = 0) { + if ( + this.getSize() === 0 && + position === 0 && + offset === 0 && + length === buffer.length && + length > 0 + ) { + // The pinned fs-core's private setter preserves metadata and change events. + /** @type {{ _setBuf(buffer: Buffer): void }} */ ( + /** @type {unknown} */ (this) + )._setBuf(buffer); + return length; + } + + return Node.prototype.write.call(this, buffer, offset, length, position); +} + +class OutputSuperblock extends Superblock { + /** + * @param {number} mode file mode + * @returns {Node} file node + */ + createNode(mode) { + const node = super.createNode(mode); + node.write = write; + return node; + } +} + +/** + * @returns {import("memfs").IFs} memory file system + */ +function createMemoryFileSystem() { + return createFsFromVolume(new Volume(new OutputSuperblock())); +} + +export default createMemoryFileSystem; diff --git a/src/utils/setupOutputFileSystem.js b/src/utils/setupOutputFileSystem.js index a978d14..05ba40a 100644 --- a/src/utils/setupOutputFileSystem.js +++ b/src/utils/setupOutputFileSystem.js @@ -1,4 +1,4 @@ -import memfs, { Volume, createFsFromVolume } from "memfs"; +import createMemoryFileSystem from "./memoryFileSystem.js"; /** @typedef {import("@rspack/core").MultiCompiler} MultiCompiler */ /** @typedef {import("../index.js").DevServerOption} DevServerOption */ @@ -20,21 +20,7 @@ function setupOutputFileSystem(context) { } // Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense. else if (context.options.writeToDisk !== true) { - const candidateOutputFileSystem = memfs.createFsFromVolume(new Volume()); - - if ( - candidateOutputFileSystem && - typeof candidateOutputFileSystem.readFileSync === "function" && - typeof candidateOutputFileSystem.statSync === "function" - ) { - outputFileSystem = candidateOutputFileSystem; - } else { - outputFileSystem = createFsFromVolume(new Volume()); - - if (candidateOutputFileSystem) { - Object.assign(outputFileSystem, candidateOutputFileSystem); - } - } + outputFileSystem = createMemoryFileSystem(); } else { const isMultiCompiler = /** @type {MultiCompiler} */ diff --git a/test/utils/memoryFileSystem.test.js b/test/utils/memoryFileSystem.test.js new file mode 100644 index 0000000..b8bd3ce --- /dev/null +++ b/test/utils/memoryFileSystem.test.js @@ -0,0 +1,23 @@ +import createMemoryFileSystem from "../../src/utils/memoryFileSystem.js"; + +it("should write whole files, including overwrites", () => { + const fs = createMemoryFileSystem(); + for (const content of ["hello world", "hello", ""]) { + const buffer = Buffer.from(content); + fs.writeFileSync("/asset.js", buffer); + + expect(fs.statSync("/asset.js").size).toBe(buffer.length); + expect(fs.readFileSync("/asset.js", "utf8")).toBe(content); + } +}); + +it("should preserve partial writes, existing contents and appends", () => { + const fs = createMemoryFileSystem(); + const fd = fs.openSync("/asset.js", "w+"); + fs.writeSync(fd, Buffer.from("abcd"), 1, 2, 2); + fs.writeSync(fd, Buffer.from("X"), 0, 1, 0); + fs.closeSync(fd); + fs.appendFileSync("/asset.js", "!"); + + expect(fs.readFileSync("/asset.js", "utf8")).toBe("X\0bc!"); +}); diff --git a/types/utils/memoryFileSystem.d.ts b/types/utils/memoryFileSystem.d.ts new file mode 100644 index 0000000..072cd53 --- /dev/null +++ b/types/utils/memoryFileSystem.d.ts @@ -0,0 +1,5 @@ +/** + * @returns {import("memfs").IFs} memory file system + */ +declare function createMemoryFileSystem(): import("memfs").IFs; +export default createMemoryFileSystem;