diff --git a/__mocks__/fs/index.ts b/__mocks__/fs/index.ts new file mode 100644 index 0000000..c99c64b --- /dev/null +++ b/__mocks__/fs/index.ts @@ -0,0 +1,3 @@ +import { fs } from 'memfs'; + +module.exports = fs diff --git a/__mocks__/fs/promises.ts b/__mocks__/fs/promises.ts new file mode 100644 index 0000000..c99c64b --- /dev/null +++ b/__mocks__/fs/promises.ts @@ -0,0 +1,3 @@ +import { fs } from 'memfs'; + +module.exports = fs diff --git a/package.json b/package.json index 8aca683..e16a815 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/supports-color": "8.1.1", "@types/yargs": "17.0.3", "changesets-changelog-clean": "^1.1.0", - "mock-fs": "5.2.0", + "memfs": "^4.2.0", "prettier": "3.0.0", "prettier-plugin-organize-imports": "3.2.2", "rome": "^12.1.3", diff --git a/src/__test__/fixtures.ts b/src/__test__/fixtures.ts new file mode 100644 index 0000000..8cb4ddf --- /dev/null +++ b/src/__test__/fixtures.ts @@ -0,0 +1,8 @@ +import { GlobLists } from '../types'; + +export const EMPTY_GLOB_LISTS: GlobLists = { + included: [], + includedDirs: [], + excluded: [], + originalIncluded: [], +}; diff --git a/src/__fixtures__/fixtures.ts b/src/__test__/getMockedFileStructure.ts similarity index 56% rename from src/__fixtures__/fixtures.ts rename to src/__test__/getMockedFileStructure.ts index 494d013..77024cb 100644 --- a/src/__fixtures__/fixtures.ts +++ b/src/__test__/getMockedFileStructure.ts @@ -1,20 +1,14 @@ import fs from 'fs'; +import { vi } from 'vitest'; import { DEFAULT_GLOBS_FILE_PATH } from '../constants'; -import { GlobLists } from '../types'; -export const EMPTY_GLOB_LISTS: GlobLists = { - included: [], - includedDirs: [], - excluded: [], - originalIncluded: [], -}; - -export function getMockedFileStructure(nodeModulesPath: string): Record { - const stringifiedDefaultGlobs = fs.readFileSync(DEFAULT_GLOBS_FILE_PATH).toString(); +export async function getMockedFileStructure(): Promise> { + const actualFs = await vi.importActual('fs/promises'); + const defaultGlobs = (await actualFs.readFile(DEFAULT_GLOBS_FILE_PATH)).toString(); return { - [DEFAULT_GLOBS_FILE_PATH]: stringifiedDefaultGlobs, - [nodeModulesPath]: { + [DEFAULT_GLOBS_FILE_PATH]: defaultGlobs, + node_modules: { dep1: { __tests__: { 'test1.js': '.', diff --git a/src/analyze.test.ts b/src/analyze.test.ts index f2825bb..f0d54d4 100644 --- a/src/analyze.test.ts +++ b/src/analyze.test.ts @@ -1,32 +1,24 @@ -import mockFs from 'mock-fs'; +import { fs, vol } from 'memfs'; import path from 'path'; -import { afterEach, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getMockedFileStructure } from './__test__/getMockedFileStructure'; import { analyzeIncluded } from './analyze'; -import { EMPTY_GLOB_LISTS, getMockedFileStructure } from './__fixtures__/fixtures'; +import { EMPTY_GLOB_LISTS } from './__test__/fixtures'; -const mockCwd = '/'; -const mockNodeModulesPath = mockCwd + 'node_modules'; -const mockedFileStructure = getMockedFileStructure(mockNodeModulesPath); +const nodeModulesPath = 'node_modules'; -let cwdSpy: SpyInstance<[], string>; - -beforeEach(() => { - cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(mockCwd); - mockFs(mockedFileStructure); +beforeEach(async () => { + const fileStructure = await getMockedFileStructure(); + vol.fromNestedJSON(fileStructure); }); afterEach(() => { - cwdSpy.mockRestore(); - mockFs.restore(); + vol.reset(); }); describe('analyzeIncluded', () => { - beforeEach(() => { - mockFs(mockedFileStructure); - }); - it('returns expected result', async () => { - const results = await analyzeIncluded(mockNodeModulesPath, { + const results = await analyzeIncluded(nodeModulesPath, { ...EMPTY_GLOB_LISTS, included: ['**/__tests__/**', '**/dep3/**'], includedDirs: ['**/__tests__', '**/dep3'], @@ -35,29 +27,25 @@ describe('analyzeIncluded', () => { expect(results).toEqual([ { - filePath: mockCwd + path.join('node_modules', 'dep1', '__tests__', 'test1.js'), + filePath: path.join('node_modules', 'dep1', '__tests__', 'test1.js'), includedByDefault: true, - includedByGlobs: [ - { derived: mockCwd + 'node_modules/**/__tests__/**', original: '__tests__' }, - ], + includedByGlobs: [{ derived: 'node_modules/**/__tests__/**', original: '__tests__' }], }, { - filePath: mockCwd + path.join('node_modules', 'dep1', '__tests__', 'test2.js'), + filePath: path.join('node_modules', 'dep1', '__tests__', 'test2.js'), includedByDefault: true, - includedByGlobs: [ - { derived: mockCwd + 'node_modules/**/__tests__/**', original: '__tests__' }, - ], + includedByGlobs: [{ derived: 'node_modules/**/__tests__/**', original: '__tests__' }], }, { - filePath: mockCwd + path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), + filePath: path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), includedByDefault: false, - includedByGlobs: [{ derived: mockCwd + 'node_modules/**/dep3/**', original: 'dep3' }], + includedByGlobs: [{ derived: 'node_modules/**/dep3/**', original: 'dep3' }], }, ]); }); it('says if a file was excluded or not', async () => { - const results = await analyzeIncluded(mockNodeModulesPath, { + const results = await analyzeIncluded(nodeModulesPath, { ...EMPTY_GLOB_LISTS, included: ['**/tsconfig.json', '**/file.js'], originalIncluded: ['tsconfig.json', 'file.js'], @@ -68,19 +56,19 @@ describe('analyzeIncluded', () => { }); it('lists what globs (original and derived version) included the file', async () => { - const results = await analyzeIncluded(mockNodeModulesPath, { + const results = await analyzeIncluded(nodeModulesPath, { ...EMPTY_GLOB_LISTS, included: ['**/*.json', '**/tsconfig.json', '**/file.js'], originalIncluded: ['*.json', 'tsconfig.json', 'file.js'], }); expect(results[0]).toHaveProperty('includedByGlobs', [ - { derived: mockCwd + 'node_modules/**/file.js', original: 'file.js' }, + { derived: 'node_modules/**/file.js', original: 'file.js' }, ]); expect(results[1]).toHaveProperty('includedByGlobs', [ - { derived: mockCwd + 'node_modules/**/*.json', original: '*.json' }, - { derived: mockCwd + 'node_modules/**/tsconfig.json', original: 'tsconfig.json' }, + { derived: 'node_modules/**/*.json', original: '*.json' }, + { derived: 'node_modules/**/tsconfig.json', original: 'tsconfig.json' }, ]); }); }); diff --git a/src/clean.test.ts b/src/clean.test.ts index c4e46d3..522b938 100644 --- a/src/clean.test.ts +++ b/src/clean.test.ts @@ -1,74 +1,62 @@ import fs from 'fs'; -import mockFs from 'mock-fs'; +import { vol } from 'memfs'; import path from 'path'; -import { afterEach, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { EMPTY_GLOB_LISTS } from './__test__/fixtures'; +import { getMockedFileStructure } from './__test__/getMockedFileStructure'; import { findFilesToRemove, removeEmptyDirs, removeFiles } from './clean'; -import { EMPTY_GLOB_LISTS, getMockedFileStructure } from './__fixtures__/fixtures'; -const mockCwd = '/'; -const mockNodeModulesPath = mockCwd + 'node_modules'; -const mockedFileStructure = getMockedFileStructure(mockNodeModulesPath); +const nodeModulesPath = 'node_modules'; -let cwdSpy: SpyInstance<[], string>; - -beforeEach(() => { - cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(mockCwd); - mockFs(mockedFileStructure); +beforeEach(async () => { + const fileStructure = await getMockedFileStructure(); + vol.fromNestedJSON(fileStructure); }); afterEach(() => { - cwdSpy.mockRestore(); - mockFs.restore(); + vol.reset(); }); describe('findFilesToRemove', () => { - beforeEach(() => { - mockFs(mockedFileStructure); - }); - it('includes dirs', async () => { - const result = await findFilesToRemove(mockNodeModulesPath, { + const result = await findFilesToRemove(nodeModulesPath, { ...EMPTY_GLOB_LISTS, includedDirs: ['**/__tests__', '**/dep3'], }); expect(result).toEqual([ - mockCwd + path.join('node_modules', 'dep1', '__tests__', 'test1.js'), - mockCwd + path.join('node_modules', 'dep1', '__tests__', 'test2.js'), - mockCwd + path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), + path.join('node_modules', 'dep1', '__tests__', 'test1.js'), + path.join('node_modules', 'dep1', '__tests__', 'test2.js'), + path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), ]); }); it('includes files', async () => { - const result = await findFilesToRemove(mockNodeModulesPath, { + const result = await findFilesToRemove(nodeModulesPath, { ...EMPTY_GLOB_LISTS, included: ['**/deeply/nested/file.ext', '**/dep4/**'], }); expect(result).toEqual([ - mockCwd + path.join('node_modules', 'dep4', 'nonDefaultFile.ext'), - mockCwd + path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), + path.join('node_modules', 'dep4', 'nonDefaultFile.ext'), + path.join('node_modules', 'dep3', 'deeply', 'nested', 'file.ext'), ]); }); it('can exclude files and dirs by glob patterns', async () => { - const result = await findFilesToRemove(mockNodeModulesPath, { + const result = await findFilesToRemove(nodeModulesPath, { ...EMPTY_GLOB_LISTS, included: ['**/*.js'], excluded: ['**/test*.js'], }); - expect(result).toEqual([mockCwd + path.join('node_modules', 'dep2', 'file.js')]); + expect(result).toEqual([path.join('node_modules', 'dep2', 'file.js')]); }); }); describe('removeFiles', () => { - beforeEach(() => { - mockFs(mockedFileStructure); - }); - it('removes files at provided file paths', async () => { - const filePaths = ['/node_modules/dep1/__tests__/test1.js', '/node_modules/dep1/a-dir/doc.md']; + const filePaths = ['node_modules/dep1/__tests__/test1.js', 'node_modules/dep1/a-dir/doc.md']; // files are initially there expect(fs.existsSync(filePaths[0])).toBe(true); @@ -82,7 +70,7 @@ describe('removeFiles', () => { }); it('does not remove files during dry runs', async () => { - const filePaths = ['/node_modules/dep1/__tests__/test1.js', '/node_modules/dep1/a-dir/doc.md']; + const filePaths = ['node_modules/dep1/__tests__/test1.js', 'node_modules/dep1/a-dir/doc.md']; await removeFiles(filePaths, { dryRun: true }); @@ -97,16 +85,12 @@ describe('removeFiles', () => { }); describe('removeEmptyDirs', () => { - beforeEach(() => { - mockFs(mockedFileStructure); - }); - it('cleans up empty parent dirs for provided files', async () => { const filePaths = [ - '/node_modules/dep1/__tests__/test1.js', - '/node_modules/dep1/a-dir/doc.md', - '/node_modules/dep2/tsconfig.json', - '/node_modules/dep2/file.js', + 'node_modules/dep1/__tests__/test1.js', + 'node_modules/dep1/a-dir/doc.md', + 'node_modules/dep2/tsconfig.json', + 'node_modules/dep2/file.js', ]; // remove files before testing @@ -114,9 +98,9 @@ describe('removeEmptyDirs', () => { await removeEmptyDirs(filePaths); - expect(fs.existsSync('/node_modules/dep1/__tests__')).toBe(true); // not empty and not removed - expect(fs.existsSync('/node_modules/dep1/a-dir')).toBe(false); // empty and removed - expect(fs.existsSync('/node_modules/dep2')).toBe(false); // empty and removed + expect(fs.existsSync('node_modules/dep1/__tests__')).toBe(true); // not empty and not removed + expect(fs.existsSync('node_modules/dep1/a-dir')).toBe(false); // empty and removed + expect(fs.existsSync('node_modules/dep2')).toBe(false); // empty and removed }); it('does not throw if path is invalid', async () => { diff --git a/src/utils/filesystem.test.ts b/src/utils/filesystem.test.ts index 0af937e..0b1f31e 100644 --- a/src/utils/filesystem.test.ts +++ b/src/utils/filesystem.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; -import mockFs from 'mock-fs'; +import { vol } from 'memfs'; import path from 'path'; -import { afterEach, beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; import { crawlDirFast, crawlDirWithChecks, @@ -13,7 +13,7 @@ import { describe('file exists', () => { beforeEach(() => { - mockFs({ + vol.fromNestedJSON({ testdir: { foo: '.', bar: '.', @@ -21,10 +21,6 @@ describe('file exists', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('returns true if the file exists', async () => { const result = await fileExists('testdir/foo'); expect(result).toBe(true); @@ -49,7 +45,7 @@ describe('file exists', () => { describe('forEachDirentAsync', () => { beforeEach(() => { - mockFs({ + vol.fromNestedJSON({ testdir: { foo: { 'foo-bar': '' }, bar: '', @@ -58,10 +54,6 @@ describe('forEachDirentAsync', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('runs action with dirents for each item in a directory', async () => { const action = vi.fn(); await forEachDirentAsync('testdir', action); @@ -89,11 +81,7 @@ describe('forEachDirentAsync', () => { describe('readDirectory', () => { beforeEach(() => { - mockFs({ 'parent/empty': { 'foo.txt': '', 'bar.txt': '' } }); - }); - - afterEach(() => { - mockFs.restore(); + vol.fromNestedJSON({ 'parent/empty': { 'foo.txt': '', 'bar.txt': '' } }); }); it('returns list of files in directory', async () => { @@ -109,7 +97,7 @@ describe('readDirectory', () => { describe('removeEmptyDirsUp', () => { beforeEach(() => { - mockFs({ + vol.fromNestedJSON({ a0: { b0: { c0: { @@ -123,10 +111,6 @@ describe('removeEmptyDirsUp', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('recursively removes empty directories up in the file tree', async () => { const checkedDirs = new Set(); await removeEmptyDirsUp(checkedDirs, 'a0/b0/c0/d0/e0'); @@ -149,7 +133,7 @@ describe('removeEmptyDirsUp', () => { describe('crawlDirFast', () => { beforeEach(() => { - mockFs({ + vol.fromNestedJSON({ a0: { b0: { c0: { @@ -172,10 +156,6 @@ describe('crawlDirFast', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('appends all nested file paths to the provided array', async () => { const filePaths: string[] = []; await crawlDirFast(filePaths, 'a0'); @@ -195,7 +175,7 @@ describe('crawlDirFast', () => { describe('crawlDirWithChecks', () => { beforeEach(() => { - mockFs({ + vol.fromNestedJSON({ a0: { b0: { c0: { @@ -218,10 +198,6 @@ describe('crawlDirWithChecks', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('runs check functions on each nested item', async () => { const filePaths: string[] = []; const checkDir = vi.fn(() => false); diff --git a/src/utils/glob.test.ts b/src/utils/glob.test.ts index fd9f652..fd69220 100644 --- a/src/utils/glob.test.ts +++ b/src/utils/glob.test.ts @@ -1,9 +1,9 @@ -import mockFs from 'mock-fs'; +import { vol } from 'memfs'; import pm from 'picomatch'; -import { afterEach, describe, expect, it, Mock, vi } from 'vitest'; +import { describe, expect, it, Mock, vi } from 'vitest'; +import { EMPTY_GLOB_LISTS } from '../__test__/fixtures'; import { DEFAULT_PICO_OPTIONS } from '../constants'; import { GlobLists } from '../types'; -import { EMPTY_GLOB_LISTS } from '../__fixtures__/fixtures'; import { formatGlob, makeGlobMatcher, @@ -253,10 +253,6 @@ describe('processGlobs', () => { describe('parseGlobsFile', () => { const globFilePath = process.cwd() + '/.cleanmodules'; - afterEach(() => { - mockFs.restore(); - }); - it('loads globs from a glob file', async () => { const globFile = ` # this is a comment @@ -270,7 +266,7 @@ describe('parseGlobsFile', () => { *.ext `; - mockFs({ [globFilePath]: globFile }); + vol.fromNestedJSON({ [globFilePath]: globFile }); const result = await parseGlobsFile(globFilePath); expect(result).toMatchInlineSnapshot(` @@ -306,7 +302,7 @@ describe('parseGlobsFile', () => { path/to/something `; - mockFs({ [globFilePath]: globFile }); + vol.fromNestedJSON({ [globFilePath]: globFile }); const result = await parseGlobsFile(globFilePath); expect(result.included).toEqual(['**/path/to/something']); expect(result.excluded).toEqual([]); @@ -319,7 +315,7 @@ describe('parseGlobsFile', () => { `; - mockFs({ [globFilePath]: globFile }); + vol.fromNestedJSON({ [globFilePath]: globFile }); const result = await parseGlobsFile(globFilePath); expect(result.included).toEqual(['**/path/to/something']); expect(result.excluded).toEqual([]); @@ -330,7 +326,7 @@ describe('parseGlobsFile', () => { !excludeMe `; - mockFs({ [globFilePath]: globFile }); + vol.fromNestedJSON({ [globFilePath]: globFile }); const result = await parseGlobsFile(globFilePath); expect(result.included).toEqual([]); expect(result.excluded).toEqual(['**/excludeMe']); diff --git a/tsconfig.json b/tsconfig.json index c1e767d..df932d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "declaration": true, "removeComments": true }, - "include": ["src"], + "include": ["src", "__mocks__", "src/__test__"], "exclude": [ "**/lib/**", "**/node_modules/**" diff --git a/vitest.setup.ts b/vitest.setup.ts index e710792..48c0517 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,6 +1,9 @@ -import mockFs from 'mock-fs'; -import { afterEach } from 'vitest'; +import { vol } from 'memfs'; +import { afterEach, vi } from 'vitest'; + +vi.mock('fs'); +vi.mock('fs/promises'); afterEach(() => { - mockFs.restore(); + vol.reset(); }); diff --git a/yarn.lock b/yarn.lock index 7769508..0d4176d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -878,7 +878,7 @@ __metadata: languageName: node linkType: hard -"arg@npm:^5.0.1": +"arg@npm:^5.0.1, arg@npm:^5.0.2": version: 5.0.2 resolution: "arg@npm:5.0.2" checksum: 6c69ada1a9943d332d9e5382393e897c500908d91d5cb735a01120d5f71daf1b339b7b8980cbeaba8fd1afc68e658a739746179e4315a26e8a28951ff9930078 @@ -1138,7 +1138,7 @@ __metadata: "@types/yargs": 17.0.3 arg: ^5.0.1 changesets-changelog-clean: ^1.1.0 - mock-fs: 5.2.0 + memfs: ^4.2.0 picomatch: ^2.3.0 prettier: 3.0.0 prettier-plugin-organize-imports: 3.2.2 @@ -2097,6 +2097,13 @@ __metadata: languageName: node linkType: hard +"hyperdyperid@npm:^1.2.0": + version: 1.2.0 + resolution: "hyperdyperid@npm:1.2.0" + checksum: 210029d1c86926f09109f6317d143f8b056fc38e8dd11b0c3e3205fc6c6ff8429fb55b4b9c2bce065462719ed9d34366eced387aaa0035d93eb76b306a8547ef + languageName: node + linkType: hard + "iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -2418,6 +2425,28 @@ __metadata: languageName: node linkType: hard +"json-joy@npm:^9.2.0": + version: 9.3.0 + resolution: "json-joy@npm:9.3.0" + dependencies: + arg: ^5.0.2 + hyperdyperid: ^1.2.0 + peerDependencies: + quill-delta: ^5 + rxjs: 7 + tslib: 2 + bin: + json-pack: bin/json-pack.js + json-pack-test: bin/json-pack-test.js + json-patch: bin/json-patch.js + json-patch-test: bin/json-patch-test.js + json-pointer: bin/json-pointer.js + json-pointer-test: bin/json-pointer-test.js + json-unpack: bin/json-unpack.js + checksum: 65ce4e841d3ed6b3fd49e7a2defaade8199cf6a6e089db67cecfccb5277856af3a234877d2c1e4054f98b6772b6b09917a995e51f753386a0fd6637e61afd179 + languageName: node + linkType: hard + "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -2597,6 +2626,18 @@ __metadata: languageName: node linkType: hard +"memfs@npm:^4.2.0": + version: 4.2.0 + resolution: "memfs@npm:4.2.0" + dependencies: + json-joy: ^9.2.0 + thingies: ^1.11.1 + peerDependencies: + tslib: 2 + checksum: d1d102c890fc4d7999280be5a3b906daec5cb1121e56a75dd96cd30ec75ab9fcf3311affa33146d5749b3ad82ecd8ff8a8f8ba638aaf6fae769ff0083b3d82c7 + languageName: node + linkType: hard + "meow@npm:^6.0.0": version: 6.1.1 resolution: "meow@npm:6.1.1" @@ -2781,13 +2822,6 @@ __metadata: languageName: node linkType: hard -"mock-fs@npm:5.2.0": - version: 5.2.0 - resolution: "mock-fs@npm:5.2.0" - checksum: c25835247bd26fa4e0189addd61f98973f61a72741e4d2a5694b143a2069b84978443a7ac0fdb1a71aead99273ec22ff4e9c968de11bbd076db020264c5b8312 - languageName: node - linkType: hard - "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -3873,6 +3907,15 @@ __metadata: languageName: node linkType: hard +"thingies@npm:^1.11.1": + version: 1.12.0 + resolution: "thingies@npm:1.12.0" + peerDependencies: + tslib: ^2 + checksum: 04b75d264e1880676fb9f400b2c0e069abdd00de1fa006d9daee527ad6d899828169fd46bb17e7cabf2802b7dbd64053106e29e7a7aa271659f5b41b3a11657f + languageName: node + linkType: hard + "tinybench@npm:^2.5.0": version: 2.5.0 resolution: "tinybench@npm:2.5.0"