diff --git a/__tests__/copy-async.spec.ts b/__tests__/copy-async.spec.ts index 2815167..6009862 100644 --- a/__tests__/copy-async.spec.ts +++ b/__tests__/copy-async.spec.ts @@ -28,10 +28,10 @@ describe('#copyAsync()', () => { describe('using append option', () => { beforeEach(() => { sinon.spy(fs, 'append'); - sinon.spy(fs, 'write'); + sinon.spy(fs, '_write'); }); afterEach(() => { - fs.write.restore(); + fs._write.restore(); fs.append.restore(); }); @@ -41,14 +41,14 @@ describe('#copyAsync()', () => { const newPath = '/new/path/file.txt'; await fs.copyAsync(filepath, newPath, { append: true }); - expect(fs.write.callCount).toBe(1); + expect(fs._write.callCount).toBe(1); expect(fs.append.callCount).toBe(0); expect(fs.read(newPath)).toBe(initialContents); expect(fs.store.get(newPath).state).toBe('modified'); await fs.copyAsync(filepath, newPath, { append: true }); - expect(fs.write.callCount).toBe(2); + expect(fs._write.callCount).toBe(2); expect(fs.append.callCount).toBe(1); expect(fs.read(newPath)).toBe(initialContents + initialContents); }); diff --git a/__tests__/copy-tpl-async.spec.ts b/__tests__/copy-tpl-async.spec.ts index e8973c7..54c93d5 100644 --- a/__tests__/copy-tpl-async.spec.ts +++ b/__tests__/copy-tpl-async.spec.ts @@ -1,6 +1,6 @@ import { describe, beforeEach, it, expect } from 'vitest'; import os from 'os'; -import path from 'path'; +import path, { resolve } from 'path'; import { type MemFsEditor, create } from '../src/index.js'; import { create as createMemFs } from 'mem-fs'; import normalize from 'normalize-path'; @@ -112,4 +112,11 @@ describe('#copyTpl()', () => { await fs.copyTplAsync(filepath, newPath); expect(fs.exists(newPath)).toBeTruthy(); }); + + it('keeps template path in file history', async () => { + const filepath = getFixture('file-tpl.txt'); + const newPath = '/new/path/file.txt'; + await fs.copyTplAsync(filepath, newPath, { name: 'new content' }); + expect(fs.store.get(newPath).history).toMatchObject([resolve(filepath), resolve(newPath)]); + }); }); diff --git a/__tests__/copy-tpl.spec.ts b/__tests__/copy-tpl.spec.ts index de29848..3559187 100644 --- a/__tests__/copy-tpl.spec.ts +++ b/__tests__/copy-tpl.spec.ts @@ -1,6 +1,6 @@ import { describe, beforeEach, it, expect } from 'vitest'; import os from 'os'; -import path from 'path'; +import path, { resolve } from 'path'; import { type MemFsEditor, create } from '../src/index.js'; import { create as createMemFs } from 'mem-fs'; import normalize from 'normalize-path'; @@ -114,4 +114,11 @@ describe('#copyTpl()', () => { fs.copyTpl(filepath, newPath); expect(fs.exists(newPath)).toBeTruthy(); }); + + it('keeps template path in file history', () => { + const filepath = getFixture('ejs/file-ejs-extension.txt.ejs'); + const newPath = '/new/path/file-ejs-extension.txt.ejs'; + fs.copyTpl(filepath, newPath); + expect(fs.store.get(newPath).history).toMatchObject([resolve(filepath), resolve(newPath)]); + }); }); diff --git a/__tests__/copy.spec.ts b/__tests__/copy.spec.ts index 169397f..188c0f1 100644 --- a/__tests__/copy.spec.ts +++ b/__tests__/copy.spec.ts @@ -31,10 +31,10 @@ describe('#copy()', () => { describe('using append option', () => { beforeEach(() => { sinon.spy(fs, 'append'); - sinon.spy(fs, 'write'); + sinon.spy(fs, '_write'); }); afterEach(() => { - fs.write.restore(); + fs._write.restore(); fs.append.restore(); }); @@ -44,14 +44,14 @@ describe('#copy()', () => { const newPath = '/new/path/file.txt'; fs.copy(filepath, newPath, { append: true }); - expect(fs.write.callCount).toBe(1); + expect(fs._write.callCount).toBe(1); expect(fs.append.callCount).toBe(0); expect(fs.read(newPath)).toBe(initialContents); expect(fs.store.get(newPath).state).toBe('modified'); fs.copy(filepath, newPath, { append: true }); - expect(fs.write.callCount).toBe(2); + expect(fs._write.callCount).toBe(2); expect(fs.append.callCount).toBe(1); expect(fs.read(newPath)).toBe(initialContents + initialContents); }); diff --git a/src/actions/copy-async.ts b/src/actions/copy-async.ts index 8094273..54d0550 100644 --- a/src/actions/copy-async.ts +++ b/src/actions/copy-async.ts @@ -1,14 +1,15 @@ import assert from 'assert'; import fs from 'fs'; import fsPromises from 'fs/promises'; -import path from 'path'; +import path, { resolve } from 'path'; +import type { Data, Options } from 'ejs'; import { globbySync, isDynamicPattern, type Options as GlobbyOptions } from 'globby'; import multimatch from 'multimatch'; import { render, globify, getCommonPath } from '../util.js'; import normalize from 'normalize-path'; +import File from 'vinyl'; import type { MemFsEditor } from '../index.js'; import { AppendOptions } from './append.js'; -import { Data, Options } from 'ejs'; import { CopySingleOptions } from './copy.js'; async function applyProcessingFileFunc( @@ -129,6 +130,7 @@ export async function _copySingleAsync( if (!options.processFile) { return this._copySingle(from, to, options); } + from = resolve(from); const contents = await applyProcessingFileFunc.call(this, options.processFile, from); @@ -143,6 +145,12 @@ export async function _copySingleAsync( } } - const stat = await fsPromises.stat(from); - this.write(to, contents, stat); + this._write( + new File({ + contents, + stat: await fsPromises.stat(from), + path: to, + history: [from], + }), + ); } diff --git a/src/actions/copy.ts b/src/actions/copy.ts index 4569e60..84d9086 100644 --- a/src/actions/copy.ts +++ b/src/actions/copy.ts @@ -1,10 +1,11 @@ import assert from 'assert'; import fs from 'fs'; -import path from 'path'; +import path, { resolve } from 'path'; import { globbySync, isDynamicPattern, type Options as GlobbyOptions } from 'globby'; import multimatch from 'multimatch'; import { Data, Options } from 'ejs'; import normalize from 'normalize-path'; +import File, { isVinyl } from 'vinyl'; import type { MemFsEditor } from '../index.js'; import { getCommonPath, globify, render } from '../util.js'; @@ -104,6 +105,7 @@ export function _copySingle(this: MemFsEditor, from: string, to: string, options assert(this.exists(from), 'Trying to copy from a source that does not exist: ' + from); const file = this.store.get(from); + to = resolve(to); let { contents } = file; if (!contents) { @@ -124,5 +126,21 @@ export function _copySingle(this: MemFsEditor, from: string, to: string, options } } - this.write(to, contents, file.stat); + if (isVinyl(file)) { + this._write( + Object.assign(file.clone({ contents: false, deep: false }), { + contents, + path: to, + }), + ); + } else { + this._write( + new File({ + contents, + stat: (file.stat as any) ?? fs.statSync(file.path), + path: to, + history: [file.path], + }), + ); + } } diff --git a/src/actions/write.ts b/src/actions/write.ts index abcbb5e..f0cb93c 100644 --- a/src/actions/write.ts +++ b/src/actions/write.ts @@ -1,29 +1,50 @@ import assert from 'assert'; +import { resolve } from 'path'; import { isFileStateModified, setModifiedFileState } from '../state.js'; -import type { MemFsEditor } from '../index.js'; +import type { MemFsEditor, MemFsEditorFile } from '../index.js'; +import File from 'vinyl'; + +type CompareFile = { contents: null | Buffer; stat?: { mode?: number } | null }; + +export const isMemFsEditorFileEqual = (a: CompareFile, b: CompareFile) => { + if (a.stat?.mode !== b.stat?.mode) { + return false; + } + return a.contents === b.contents || a.contents?.equals(b.contents!); +}; + +export function _write(this: MemFsEditor, file: EditorFile) { + if (this.store.existsInMemory(file.path)) { + // Backward compatibility, keep behavior for existing files, custom properties may have been added + const existingFile = this.store.get(file.path); + if (!isFileStateModified(existingFile) || !isMemFsEditorFileEqual(existingFile, file)) { + const { contents, stat } = file; + setModifiedFileState(existingFile); + Object.assign(existingFile, { contents, stat: stat ?? existingFile.stat }); + this.store.add(existingFile); + } + } else { + setModifiedFileState(file); + this.store.add(file); + } +} export default function write( this: MemFsEditor, filepath: string, contents: string | Buffer, - stat?: { mode?: number } | null, + stat: { mode?: number } | null = null, ) { assert(typeof contents === 'string' || Buffer.isBuffer(contents), 'Expected `contents` to be a String or a Buffer'); - const file = this.store.get(filepath); const newContents = Buffer.isBuffer(contents) ? contents : Buffer.from(contents); - if ( - !isFileStateModified(file) || - !Buffer.isBuffer(file.contents) || - !newContents.equals(file.contents) || - (stat && file.stat !== stat) - ) { - setModifiedFileState(file); - file.contents = newContents; - file.stat = stat ?? null; - this.store.add(file); - } - - return file.contents.toString(); + this._write( + new File({ + path: resolve(filepath), + contents: newContents, + stat: stat as any, + }), + ); + return contents.toString(); } diff --git a/src/index.ts b/src/index.ts index bfb6e4b..dc63eba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export type { PipelineOptions, FileTransform } from 'mem-fs'; import read from './actions/read.js'; import readJSON from './actions/read-json.js'; import exists from './actions/exists.js'; -import write from './actions/write.js'; +import write, { _write } from './actions/write.js'; import writeJSON from './actions/write-json.js'; import extendJSON from './actions/extend-json.js'; import append from './actions/append.js'; @@ -52,6 +52,7 @@ export interface MemFsEditor; write: typeof write; writeJSON: typeof writeJSON; extendJSON: typeof extendJSON; @@ -73,6 +74,7 @@ export interface MemFsEditor