diff --git a/package.json b/package.json index 627ef69..97068b6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "event-emitter": "^0.3.5", "ext": "^1.7.0", "fast-glob": "^3.3.3", - "fs-extra": "^11.3.4", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.1.0", "log": "^6.3.1", diff --git a/src/utils/fs/fileExists.js b/src/utils/fs/fileExists.js index 937fee4..2231e8b 100644 --- a/src/utils/fs/fileExists.js +++ b/src/utils/fs/fileExists.js @@ -1,10 +1,10 @@ 'use strict'; -const fse = require('fs-extra'); +const fs = require('node:fs').promises; const fileExists = async (filePath) => { try { - const stats = await fse.lstat(filePath); + const stats = await fs.lstat(filePath); return stats.isFile(); } catch { return false; diff --git a/src/utils/fs/fileExistsSync.js b/src/utils/fs/fileExistsSync.js index 5724754..f3b32f0 100644 --- a/src/utils/fs/fileExistsSync.js +++ b/src/utils/fs/fileExistsSync.js @@ -1,10 +1,10 @@ 'use strict'; -const fse = require('fs-extra'); +const fs = require('node:fs'); const fileExistsSync = (filePath) => { try { - const stats = fse.lstatSync(filePath); + const stats = fs.lstatSync(filePath); return stats.isFile(); } catch { return false; diff --git a/src/utils/fs/readFile.js b/src/utils/fs/readFile.js index a355042..782b071 100644 --- a/src/utils/fs/readFile.js +++ b/src/utils/fs/readFile.js @@ -1,10 +1,10 @@ 'use strict'; -const fse = require('fs-extra'); +const fs = require('node:fs').promises; const parseFile = require('./parseFile'); const readFile = async (filePath, options = {}) => { - const contents = await fse.readFile(filePath, 'utf8'); + const contents = await fs.readFile(filePath, 'utf8'); return parseFile(filePath, contents, options); }; diff --git a/src/utils/fs/readFileSync.js b/src/utils/fs/readFileSync.js index 336f192..530d888 100644 --- a/src/utils/fs/readFileSync.js +++ b/src/utils/fs/readFileSync.js @@ -1,10 +1,10 @@ 'use strict'; -const fse = require('fs-extra'); +const fs = require('node:fs'); const parseFile = require('./parseFile'); const readFileSync = (filePath, options = {}) => { - const contents = fse.readFileSync(filePath, 'utf8'); + const contents = fs.readFileSync(filePath, 'utf8'); return parseFile(filePath, contents, options); }; diff --git a/src/utils/fs/writeFile.js b/src/utils/fs/writeFile.js index 2ca2af8..d2fb5e9 100644 --- a/src/utils/fs/writeFile.js +++ b/src/utils/fs/writeFile.js @@ -2,7 +2,7 @@ const path = require('path'); const YAML = require('js-yaml'); -const fse = require('fs-extra'); +const fs = require('node:fs').promises; const isJsonPath = require('./isJsonPath'); const isYamlPath = require('./isYamlPath'); @@ -17,8 +17,8 @@ const formatContents = (filePath, contents, options) => { }; const writeFile = async (filePath, contents = '', options = {}) => { - await fse.ensureDir(path.dirname(filePath)); - await fse.writeFile(filePath, formatContents(filePath, contents, options)); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, formatContents(filePath, contents, options)); }; module.exports = writeFile; diff --git a/test/lib/fs.js b/test/lib/fs.js new file mode 100644 index 0000000..26010a1 --- /dev/null +++ b/test/lib/fs.js @@ -0,0 +1,23 @@ +'use strict'; + +const fs = require('node:fs'); +const fsp = fs.promises; +const path = require('node:path'); + +const ensureDir = (dirPath) => fsp.mkdir(dirPath, { recursive: true }); + +const remove = (targetPath) => fsp.rm(targetPath, { recursive: true, force: true }); + +const removeSync = (targetPath) => fs.rmSync(targetPath, { recursive: true, force: true }); + +const outputFile = async (filePath, contents, options) => { + await ensureDir(path.dirname(filePath)); + await fsp.writeFile(filePath, contents, options); +}; + +const outputFileSync = (filePath, contents, options) => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents, options); +}; + +module.exports = { ensureDir, outputFile, outputFileSync, remove, removeSync }; diff --git a/test/lib/process-tmp-dir.js b/test/lib/process-tmp-dir.js index 4574462..614a264 100644 --- a/test/lib/process-tmp-dir.js +++ b/test/lib/process-tmp-dir.js @@ -1,7 +1,6 @@ 'use strict'; -const { mkdirSync, realpathSync } = require('fs'); -const { removeSync } = require('fs-extra'); +const { mkdirSync, realpathSync, rmSync } = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); @@ -28,7 +27,7 @@ module.exports = (function self() { process.on('exit', () => { try { - removeSync(module.exports); + rmSync(module.exports, { recursive: true, force: true }); } catch (error) { if (rmTmpDirIgnorableErrorCodes.has(error.code)) return; throw error; diff --git a/test/lib/skip-on-disabled-symlinks-in-windows.js b/test/lib/skip-on-disabled-symlinks-in-windows.js new file mode 100644 index 0000000..325f9df --- /dev/null +++ b/test/lib/skip-on-disabled-symlinks-in-windows.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = (error, context, afterCallback) => { + if (error.code !== 'EPERM' || process.platform !== 'win32') return; + + if (!context || typeof context.skip !== 'function') { + throw new TypeError('Passed context is not a valid Mocha context'); + } + + if (afterCallback) afterCallback(); + context.skip(); +}; diff --git a/test/unit/components/framework/index.test.js b/test/unit/components/framework/index.test.js index dfc3504..82e11e4 100644 --- a/test/unit/components/framework/index.test.js +++ b/test/unit/components/framework/index.test.js @@ -4,13 +4,13 @@ const fs = require('node:fs').promises; const path = require('path'); const proxyquire = require('proxyquire'); const chai = require('chai'); -const fse = require('fs-extra'); const sinon = require('sinon'); const Context = require('../../../../src/Context'); const ComponentContext = require('../../../../src/ComponentContext'); const { validateComponentInputs } = require('../../../../src/configuration/validate'); const { configSchema } = require('../../../../components/framework/configuration'); const ServerlessFramework = require('../../../../components/framework'); +const { outputFile, remove } = require('../../../lib/fs'); const expect = chai.expect; @@ -836,7 +836,7 @@ describe('test/unit/components/framework/index.test.js', () => { const serviceDir = await fs.mkdtemp(path.join(process.cwd(), 'cache-hash-skip-')); try { - await fse.outputFile(path.join(serviceDir, 'handler.js'), 'module.exports = 1;\n'); + await outputFile(path.join(serviceDir, 'handler.js'), 'module.exports = 1;\n'); const spawnStub = sinon.stub(); const FrameworkComponent = proxyquire('../../../../components/framework/index.js', { @@ -857,7 +857,7 @@ describe('test/unit/components/framework/index.test.js', () => { expect(spawnStub).to.not.have.been.called; } finally { - await fse.remove(serviceDir); + await remove(serviceDir); } }); @@ -866,7 +866,7 @@ describe('test/unit/components/framework/index.test.js', () => { try { const filePath = path.join(serviceDir, 'handler.js'); - await fse.outputFile(filePath, 'module.exports = 1;\n'); + await outputFile(filePath, 'module.exports = 1;\n'); const spawnStub = sinon.stub(); spawnStub.onFirstCall().returns(createSpawnExecution({ stderr: 'deployed' })); @@ -891,14 +891,14 @@ describe('test/unit/components/framework/index.test.js', () => { context.state.inputs = inputs; context.state.cacheHash = await component.calculateCacheHash(); - await fse.outputFile(filePath, 'module.exports = 2;\n'); + await outputFile(filePath, 'module.exports = 2;\n'); await component.deploy(); expect(spawnStub).to.be.calledTwice; expect(context.state.cacheHash).to.equal(await component.calculateCacheHash()); } finally { - await fse.remove(serviceDir); + await remove(serviceDir); } }); @@ -906,7 +906,7 @@ describe('test/unit/components/framework/index.test.js', () => { const serviceDir = await fs.mkdtemp(path.join(process.cwd(), 'cache-hash-dir-')); try { - await fse.outputFile(path.join(serviceDir, 'src', 'handler.js'), 'module.exports = 1;\n'); + await outputFile(path.join(serviceDir, 'src', 'handler.js'), 'module.exports = 1;\n'); const context = await getContext(); const directoryPatternComponent = new ServerlessFramework('id', context, { @@ -922,7 +922,7 @@ describe('test/unit/components/framework/index.test.js', () => { await globPatternComponent.calculateCacheHash() ); } finally { - await fse.remove(serviceDir); + await remove(serviceDir); } }); @@ -931,9 +931,9 @@ describe('test/unit/components/framework/index.test.js', () => { try { await Promise.all([ - fse.outputFile(path.join(serviceDir, 'keep.js'), 'keep\n'), - fse.outputFile(path.join(serviceDir, 'ignored', 'drop.js'), 'drop\n'), - fse.outputFile(path.join(serviceDir, 'ignored', 'reinclude.js'), 'reinclude\n'), + outputFile(path.join(serviceDir, 'keep.js'), 'keep\n'), + outputFile(path.join(serviceDir, 'ignored', 'drop.js'), 'drop\n'), + outputFile(path.join(serviceDir, 'ignored', 'reinclude.js'), 'reinclude\n'), ]); const context = await getContext(); @@ -950,7 +950,7 @@ describe('test/unit/components/framework/index.test.js', () => { await explicitPatternComponent.calculateCacheHash() ); } finally { - await fse.remove(serviceDir); + await remove(serviceDir); } }); }); diff --git a/test/unit/lib/skip-on-disabled-symlinks-in-windows.test.js b/test/unit/lib/skip-on-disabled-symlinks-in-windows.test.js new file mode 100644 index 0000000..6b27dd4 --- /dev/null +++ b/test/unit/lib/skip-on-disabled-symlinks-in-windows.test.js @@ -0,0 +1,71 @@ +'use strict'; + +const sinon = require('sinon'); +const { expect } = require('chai'); + +const skipOnDisabledSymlinksInWindows = require('../../lib/skip-on-disabled-symlinks-in-windows'); + +describe('test/unit/lib/skip-on-disabled-symlinks-in-windows.test.js', () => { + const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); + + const setPlatform = (platform) => { + Object.defineProperty(process, 'platform', { + configurable: true, + value: platform, + }); + }; + + beforeEach(() => { + setPlatform('win32'); + }); + + afterEach(() => { + Object.defineProperty(process, 'platform', originalPlatformDescriptor); + }); + + it('skips Windows EPERM symlink failures and runs the cleanup callback', () => { + const context = { skip: sinon.stub() }; + const afterCallback = sinon.stub(); + + skipOnDisabledSymlinksInWindows({ code: 'EPERM' }, context, afterCallback); + + expect(afterCallback).to.have.been.calledOnce; + expect(context.skip).to.have.been.calledOnce; + }); + + it('skips Windows EPERM symlink failures without a cleanup callback', () => { + const context = { skip: sinon.stub() }; + + skipOnDisabledSymlinksInWindows({ code: 'EPERM' }, context); + + expect(context.skip).to.have.been.calledOnce; + }); + + it('does nothing for non-Windows EPERM errors', () => { + const context = { skip: sinon.stub() }; + const afterCallback = sinon.stub(); + setPlatform('linux'); + + skipOnDisabledSymlinksInWindows({ code: 'EPERM' }, context, afterCallback); + + expect(afterCallback).to.not.have.been.called; + expect(context.skip).to.not.have.been.called; + }); + + it('does nothing for Windows non-EPERM errors', () => { + const context = { skip: sinon.stub() }; + const afterCallback = sinon.stub(); + + skipOnDisabledSymlinksInWindows({ code: 'ENOENT' }, context, afterCallback); + + expect(afterCallback).to.not.have.been.called; + expect(context.skip).to.not.have.been.called; + }); + + it('rejects invalid Mocha contexts for Windows EPERM errors', () => { + expect(() => skipOnDisabledSymlinksInWindows({ code: 'EPERM' }, {})).to.throw( + TypeError, + 'Passed context is not a valid Mocha context' + ); + }); +}); diff --git a/test/unit/src/configuration/read.test.js b/test/unit/src/configuration/read.test.js index a35eb58..847120a 100644 --- a/test/unit/src/configuration/read.test.js +++ b/test/unit/src/configuration/read.test.js @@ -6,8 +6,8 @@ const expect = chai.expect; const path = require('path'); const fsp = require('fs').promises; -const fse = require('fs-extra'); const readConfiguration = require('../../../../src/configuration/read'); +const { ensureDir, remove } = require('../../../lib/fs'); describe('test/unit/src/configuration/read.test.js', () => { let configurationPath; @@ -62,7 +62,7 @@ describe('test/unit/src/configuration/read.test.js', () => { }); it('should read "serverless-compose.ts"', async () => { - await fse.ensureDir('node_modules'); + await ensureDir('node_modules'); try { await fsp.writeFile('node_modules/ts-node.js', 'module.exports.register = () => null;'); configurationPath = 'serverless-compose.ts'; @@ -73,7 +73,7 @@ describe('test/unit/src/configuration/read.test.js', () => { await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`); expect(await readConfiguration(path.resolve(configurationPath))).to.deep.equal(configuration); } finally { - await fse.remove('node_modules'); + await remove('node_modules'); } }); diff --git a/test/unit/src/configuration/resolve-path.test.js b/test/unit/src/configuration/resolve-path.test.js index 267636c..3ddc2c3 100644 --- a/test/unit/src/configuration/resolve-path.test.js +++ b/test/unit/src/configuration/resolve-path.test.js @@ -3,10 +3,10 @@ const fs = require('node:fs').promises; const os = require('node:os'); const path = require('node:path'); -const fse = require('fs-extra'); const { expect } = require('chai'); const resolveConfigurationPath = require('../../../../src/configuration/resolve-path'); +const { ensureDir, outputFile, remove } = require('../../../lib/fs'); describe('test/unit/src/configuration/resolve-path.test.js', () => { let tmpDir; @@ -16,7 +16,7 @@ describe('test/unit/src/configuration/resolve-path.test.js', () => { }); afterEach(async () => { - await fse.remove(tmpDir); + await remove(tmpDir); }); it('resolves the first existing compose config by supported extension order', async () => { @@ -24,18 +24,18 @@ describe('test/unit/src/configuration/resolve-path.test.js', () => { const jsonPath = path.join(tmpDir, 'serverless-compose.json'); const ymlPath = path.join(tmpDir, 'serverless-compose.yml'); - await fse.outputFile(jsonPath, '{}'); - await fse.outputFile(yamlPath, 'services: {}\n'); + await outputFile(jsonPath, '{}'); + await outputFile(yamlPath, 'services: {}\n'); expect(await resolveConfigurationPath(tmpDir)).to.equal(yamlPath); - await fse.outputFile(ymlPath, 'services: {}\n'); + await outputFile(ymlPath, 'services: {}\n'); expect(await resolveConfigurationPath(tmpDir)).to.equal(ymlPath); }); it('ignores directories named like compose config files', async () => { - await fse.ensureDir(path.join(tmpDir, 'serverless-compose.yml')); + await ensureDir(path.join(tmpDir, 'serverless-compose.yml')); await expect(resolveConfigurationPath(tmpDir)).to.eventually.be.rejected.and.have.property( 'code', diff --git a/test/unit/src/state/LocalStateStorage.test.js b/test/unit/src/state/LocalStateStorage.test.js index 4204a19..c01c9c4 100644 --- a/test/unit/src/state/LocalStateStorage.test.js +++ b/test/unit/src/state/LocalStateStorage.test.js @@ -3,22 +3,22 @@ const os = require('os'); const path = require('path'); const fsp = require('fs').promises; -const fse = require('fs-extra'); const expect = require('chai').expect; const LocalStateStorage = require('../../../../src/state/LocalStateStorage'); +const { ensureDir, remove } = require('../../../lib/fs'); describe('test/unit/src/state/LocalStateStorage.test.js', () => { let rootDir; beforeEach(async () => { rootDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'compose-local-state-')); - await fse.ensureDir(path.join(rootDir, '.serverless')); + await ensureDir(path.join(rootDir, '.serverless')); }); afterEach(async () => { if (rootDir) { - await fse.remove(rootDir); + await remove(rootDir); } }); diff --git a/test/unit/src/utils/cache-hash.test.js b/test/unit/src/utils/cache-hash.test.js index 28d1ac3..cef02ef 100644 --- a/test/unit/src/utils/cache-hash.test.js +++ b/test/unit/src/utils/cache-hash.test.js @@ -5,11 +5,11 @@ const crypto = require('node:crypto'); const os = require('node:os'); const path = require('node:path'); const proxyquire = require('proxyquire'); -const fse = require('fs-extra'); const { expect } = require('chai'); const sinon = require('sinon'); const calculateCacheHash = require('../../../../src/utils/cache-hash'); +const { outputFile, remove } = require('../../../lib/fs'); const md5Hex = (input) => crypto.createHash('md5').update(input).digest('hex'); @@ -29,12 +29,12 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { }); afterEach(async () => { - await fse.remove(tmpDir); + await remove(tmpDir); }); it('calculates the md5 cache hash contract for a single file', async () => { const contents = 'module.exports = 1;\n'; - await fse.outputFile(path.join(tmpDir, 'handler.js'), contents); + await outputFile(path.join(tmpDir, 'handler.js'), contents); expect(await calculateCacheHash(['handler.js'], tmpDir)).to.equal( md5Hex(md5Hex(Buffer.from(contents))) @@ -43,9 +43,9 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { it('calculates cache hashes independently of cache pattern order', async () => { await Promise.all([ - fse.outputFile(path.join(tmpDir, 'a.txt'), 'aaa\n'), - fse.outputFile(path.join(tmpDir, 'b.txt'), 'bbb\n'), - fse.outputFile(path.join(tmpDir, 'c.txt'), 'ccc\n'), + outputFile(path.join(tmpDir, 'a.txt'), 'aaa\n'), + outputFile(path.join(tmpDir, 'b.txt'), 'bbb\n'), + outputFile(path.join(tmpDir, 'c.txt'), 'ccc\n'), ]); const expected = expectedCacheHashForContents(['aaa\n', 'bbb\n', 'ccc\n']); @@ -61,8 +61,8 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { const [lowerHashEntry, higherHashEntry] = contentsByHash; await Promise.all([ - fse.outputFile(path.join(tmpDir, 'a.txt'), higherHashEntry.contents), - fse.outputFile(path.join(tmpDir, 'z.txt'), lowerHashEntry.contents), + outputFile(path.join(tmpDir, 'a.txt'), higherHashEntry.contents), + outputFile(path.join(tmpDir, 'z.txt'), lowerHashEntry.contents), ]); const expected = md5Hex([lowerHashEntry.hash, higherHashEntry.hash].join()); @@ -74,8 +74,8 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { it('counts duplicate file contents when calculating cache hashes', async () => { await Promise.all([ - fse.outputFile(path.join(tmpDir, 'one.txt'), 'same\n'), - fse.outputFile(path.join(tmpDir, 'two.txt'), 'same\n'), + outputFile(path.join(tmpDir, 'one.txt'), 'same\n'), + outputFile(path.join(tmpDir, 'two.txt'), 'same\n'), ]); const fileHash = md5Hex(Buffer.from('same\n')); @@ -90,20 +90,20 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { const otherDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compose-cache-hash-other-')); try { - await fse.outputFile(path.join(tmpDir, 'one.txt'), 'content\n'); - await fse.outputFile(path.join(otherDir, 'nested', 'different-name.js'), 'content\n'); + await outputFile(path.join(tmpDir, 'one.txt'), 'content\n'); + await outputFile(path.join(otherDir, 'nested', 'different-name.js'), 'content\n'); expect(await calculateCacheHash(['one.txt'], tmpDir)).to.equal( await calculateCacheHash(['nested/different-name.js'], otherDir) ); } finally { - await fse.remove(otherDir); + await remove(otherDir); } }); it('hashes binary cache pattern files as raw bytes', async () => { const contents = Buffer.from([0x00, 0xff, 0x80, 0x0a]); - await fse.outputFile(path.join(tmpDir, 'binary.bin'), contents); + await outputFile(path.join(tmpDir, 'binary.bin'), contents); expect(await calculateCacheHash(['binary.bin'], tmpDir)).to.equal( expectedCacheHashForContents([contents]) @@ -117,7 +117,7 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { }); it('includes empty files in cache hashes', async () => { - await fse.outputFile(path.join(tmpDir, 'empty.txt'), ''); + await outputFile(path.join(tmpDir, 'empty.txt'), ''); const actual = await calculateCacheHash(['empty.txt'], tmpDir); @@ -127,8 +127,8 @@ describe('test/unit/src/utils/cache-hash.test.js', () => { it('uses comma-separated file hashes for the aggregate cache hash', async () => { await Promise.all([ - fse.outputFile(path.join(tmpDir, 'a.txt'), 'a'), - fse.outputFile(path.join(tmpDir, 'b.txt'), 'b'), + outputFile(path.join(tmpDir, 'a.txt'), 'a'), + outputFile(path.join(tmpDir, 'b.txt'), 'b'), ]); const hashes = [md5Hex(Buffer.from('a')), md5Hex(Buffer.from('b'))].sort(); diff --git a/test/unit/src/utils/fs.test.js b/test/unit/src/utils/fs.test.js index 871fad2..075ed3a 100644 --- a/test/unit/src/utils/fs.test.js +++ b/test/unit/src/utils/fs.test.js @@ -1,24 +1,26 @@ 'use strict'; -const fs = require('node:fs').promises; +const fs = require('node:fs'); +const fsp = fs.promises; const os = require('node:os'); const path = require('node:path'); -const fse = require('fs-extra'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); const { expect } = require('chai'); const utilsFs = require('../../../../src/utils/fs'); +const { ensureDir, outputFile, outputFileSync, remove, removeSync } = require('../../../lib/fs'); +const skipOnDisabledSymlinksInWindows = require('../../../lib/skip-on-disabled-symlinks-in-windows'); describe('test/unit/src/utils/fs.test.js', () => { let tmpDir; beforeEach(async () => { - tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compose-fs-')); + tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'compose-fs-')); }); afterEach(async () => { - await fse.remove(tmpDir); + await remove(tmpDir); }); it('detects JSON paths by exact lowercase suffix', () => { @@ -42,14 +44,43 @@ describe('test/unit/src/utils/fs.test.js', () => { const dirPath = path.join(tmpDir, 'directory'); const missingPath = path.join(tmpDir, 'missing.txt'); - await fse.outputFile(filePath, 'content'); - await fse.ensureDir(dirPath); + await outputFile(filePath, 'content'); + await ensureDir(dirPath); expect(await utilsFs.fileExists(filePath)).to.equal(true); expect(await utilsFs.fileExists(dirPath)).to.equal(false); expect(await utilsFs.fileExists(missingPath)).to.equal(false); }); + it('checks sync existence only for regular files', () => { + const filePath = path.join(tmpDir, 'file.txt'); + const dirPath = path.join(tmpDir, 'directory'); + const missingPath = path.join(tmpDir, 'missing.txt'); + + outputFileSync(filePath, 'content'); + fs.mkdirSync(dirPath); + + expect(utilsFs.fileExistsSync(filePath)).to.equal(true); + expect(utilsFs.fileExistsSync(dirPath)).to.equal(false); + expect(utilsFs.fileExistsSync(missingPath)).to.equal(false); + }); + + it('does not treat symlinks to files as files', async function () { + const filePath = path.join(tmpDir, 'file.txt'); + const linkPath = path.join(tmpDir, 'link.txt'); + + await outputFile(filePath, 'content'); + try { + await fsp.symlink(filePath, linkPath); + } catch (error) { + skipOnDisabledSymlinksInWindows(error, this, () => removeSync(tmpDir)); + throw error; + } + + expect(await utilsFs.fileExists(linkPath)).to.equal(false); + expect(utilsFs.fileExistsSync(linkPath)).to.equal(false); + }); + it('parses JSON, YAML, slsignore, and plain text contents', () => { expect(utilsFs.parseFile('config.json', '{"a":1}')).to.deep.equal({ a: 1 }); expect(utilsFs.parseFile('config.yml', 'a: 1\n')).to.deep.equal({ a: 1 }); @@ -144,7 +175,7 @@ describe('test/unit/src/utils/fs.test.js', () => { const filePath = path.join(tmpDir, 'config.yml'); const options = { custom: true }; - await fse.outputFile(filePath, 'name: test\n'); + await outputFile(filePath, 'name: test\n'); expect(await readFile(filePath, options)).to.deep.equal({ parsed: true }); expect(parseFile).to.have.been.calledOnce; @@ -159,7 +190,7 @@ describe('test/unit/src/utils/fs.test.js', () => { const filePath = path.join(tmpDir, 'config.yml'); const options = { custom: true }; - fse.outputFileSync(filePath, 'name: test\n'); + outputFileSync(filePath, 'name: test\n'); expect(readFileSync(filePath, options)).to.deep.equal({ parsed: true }); expect(parseFile).to.have.been.calledOnce; @@ -175,8 +206,16 @@ describe('test/unit/src/utils/fs.test.js', () => { await utilsFs.writeFile(yamlPath, { a: 1 }); await utilsFs.writeFile(textPath, 'raw text'); - expect(await fs.readFile(jsonPath, 'utf8')).to.equal(JSON.stringify({ a: 1 }, null, 2)); - expect(await fs.readFile(yamlPath, 'utf8')).to.equal('a: 1\n'); - expect(await fs.readFile(textPath, 'utf8')).to.equal('raw text'); + expect(await fsp.readFile(jsonPath, 'utf8')).to.equal(JSON.stringify({ a: 1 }, null, 2)); + expect(await fsp.readFile(yamlPath, 'utf8')).to.equal('a: 1\n'); + expect(await fsp.readFile(textPath, 'utf8')).to.equal('raw text'); + }); + + it('writes files in nested directories', async () => { + const filePath = path.join(tmpDir, 'nested', 'state.json'); + + await utilsFs.writeFile(filePath, { a: 1 }); + + expect(await fsp.readFile(filePath, 'utf8')).to.equal(JSON.stringify({ a: 1 }, null, 2)); }); }); diff --git a/test/unit/src/utils/glob.test.js b/test/unit/src/utils/glob.test.js index dafd1c3..f45b778 100644 --- a/test/unit/src/utils/glob.test.js +++ b/test/unit/src/utils/glob.test.js @@ -3,10 +3,10 @@ const fs = require('node:fs').promises; const os = require('node:os'); const path = require('node:path'); -const fse = require('fs-extra'); const { expect } = require('chai'); const glob = require('../../../../src/utils/glob'); +const { outputFile, remove } = require('../../../lib/fs'); describe('test/unit/src/utils/glob.test.js', () => { let tmpDir; @@ -16,15 +16,15 @@ describe('test/unit/src/utils/glob.test.js', () => { }); afterEach(async () => { - await fse.remove(tmpDir); + await remove(tmpDir); }); it('matches globby order-sensitive leading negation behavior for async and sync calls', async () => { await Promise.all([ - fse.outputFile(path.join(tmpDir, 'keep.js'), 'keep\n'), - fse.outputFile(path.join(tmpDir, 'keep.ts'), 'keep\n'), - fse.outputFile(path.join(tmpDir, 'ignored', 'drop.js'), 'drop\n'), - fse.outputFile(path.join(tmpDir, 'ignored', 'drop.ts'), 'drop\n'), + outputFile(path.join(tmpDir, 'keep.js'), 'keep\n'), + outputFile(path.join(tmpDir, 'keep.ts'), 'keep\n'), + outputFile(path.join(tmpDir, 'ignored', 'drop.js'), 'drop\n'), + outputFile(path.join(tmpDir, 'ignored', 'drop.ts'), 'drop\n'), ]); expect( @@ -37,9 +37,9 @@ describe('test/unit/src/utils/glob.test.js', () => { it('applies later negations to earlier positives and still supports re-inclusion', async () => { await Promise.all([ - fse.outputFile(path.join(tmpDir, 'keep.js'), 'keep\n'), - fse.outputFile(path.join(tmpDir, 'ignored', 'drop.js'), 'drop\n'), - fse.outputFile(path.join(tmpDir, 'ignored', 'reinclude.js'), 'reinclude\n'), + outputFile(path.join(tmpDir, 'keep.js'), 'keep\n'), + outputFile(path.join(tmpDir, 'ignored', 'drop.js'), 'drop\n'), + outputFile(path.join(tmpDir, 'ignored', 'reinclude.js'), 'reinclude\n'), ]); expect(