From 361bef35a4724c5c5f6db3a276511ed94bfd006d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 13 Feb 2024 13:49:04 +0100 Subject: [PATCH] fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/renovate-151b8b9.md | 50 ++-- plugins/app-backend/src/lib/config.test.ts | 214 +++++------------- .../fetch/rails/railsNewRunner.test.ts | 87 ++++--- yarn.lock | 13 +- 4 files changed, 142 insertions(+), 222 deletions(-) diff --git a/.changeset/renovate-151b8b9.md b/.changeset/renovate-151b8b9.md index 58cdb0c844..1702b28596 100644 --- a/.changeset/renovate-151b8b9.md +++ b/.changeset/renovate-151b8b9.md @@ -1,36 +1,36 @@ --- -'@backstage/backend-app-api': patch -'@backstage/backend-common': patch -'@backstage/backend-dynamic-feature-service': patch -'@backstage/backend-test-utils': patch -'@backstage/cli-node': patch -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/create-app': patch -'@backstage/e2e-test-utils': patch -'@backstage/repo-tools': patch -'@techdocs/cli': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-app-node': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-catalog-backend-module-gerrit': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-devtools-backend': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-linguist-backend': patch -'@backstage/plugin-rollbar-backend': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch -'@backstage/plugin-scaffolder-backend-module-bitbucket': patch '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch '@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/backend-dynamic-feature-service': patch '@backstage/plugin-scaffolder-backend-module-github': patch '@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-kubernetes-backend': patch '@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-node': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-linguist-backend': patch '@backstage/plugin-techdocs-backend': patch +'@backstage/backend-common': patch +'@backstage/e2e-test-utils': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/config-loader': patch +'@techdocs/cli': patch '@backstage/plugin-techdocs-node': patch +'@backstage/plugin-auth-backend': patch +'@backstage/create-app': patch +'@backstage/repo-tools': patch +'@backstage/plugin-app-backend': patch +'@backstage/cli-node': patch +'@backstage/plugin-app-node': patch +'@backstage/cli': patch --- -Updated dependency `fs-extra` to `11.2.0`. +Updated dependency `fs-extra` to `^11.2.0`. Updated dependency `@types/fs-extra` to `^11.0.0`. diff --git a/plugins/app-backend/src/lib/config.test.ts b/plugins/app-backend/src/lib/config.test.ts index ed2b8b8777..9a6e39da4e 100644 --- a/plugins/app-backend/src/lib/config.test.ts +++ b/plugins/app-backend/src/lib/config.test.ts @@ -14,29 +14,21 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; import { getVoidLogger } from '@backstage/backend-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { injectConfig } from './config'; -jest.mock('fs-extra'); - -const fsMock = fs as jest.Mocked; -const readFileMock = fsMock.readFile as unknown as jest.MockedFunction< - (name: string) => Promise ->; - -const MOCK_DIR = 'mock-dir'; - -const baseOptions = { - appConfigs: [], - staticDir: MOCK_DIR, - logger: getVoidLogger(), -}; - describe('injectConfig', () => { + const mockDir = createMockDirectory(); + + const baseOptions = { + appConfigs: [], + staticDir: mockDir.path, + logger: getVoidLogger(), + }; + beforeEach(() => { - fsMock.readdir.mockResolvedValue(['main.js'] as any); + mockDir.clear(); }); afterEach(() => { @@ -44,185 +36,103 @@ describe('injectConfig', () => { }); it('should inject without config', async () => { - fsMock.readdir.mockResolvedValue(['main.js'] as any); - readFileMock.mockImplementation( - async () => '"__APP_INJECTED_RUNTIME_CONFIG__"', - ); - await injectConfig(baseOptions); - expect(fsMock.readdir).toHaveBeenCalledTimes(1); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - '/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/', - 'utf8', - ); + mockDir.setContent({ + 'main.js': '"__APP_INJECTED_RUNTIME_CONFIG__"', + }); - // eslint-disable-next-line no-eval - expect(JSON.parse(eval(fsMock.writeFile.mock.calls[0][1]))).toEqual([]); + await injectConfig(baseOptions); + + expect(mockDir.content()).toEqual({ + 'main.js': '/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/', + }); }); it('should inject config repeatedly if marker appears multiple times', async () => { - fsMock.readdir.mockResolvedValue(['main.js'] as any); - readFileMock.mockImplementation( - async () => + mockDir.setContent({ + 'main.js': '({a:"__APP_INJECTED_RUNTIME_CONFIG__",b:"__APP_INJECTED_RUNTIME_CONFIG__"})', - ); - await injectConfig(baseOptions); - expect(fsMock.readdir).toHaveBeenCalledTimes(1); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - '({a:/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/,b:/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/})', - 'utf8', - ); + }); - // eslint-disable-next-line no-eval - expect(JSON.parse(eval(fsMock.writeFile.mock.calls[0][1]).a)).toEqual([]); - // eslint-disable-next-line no-eval - expect(JSON.parse(eval(fsMock.writeFile.mock.calls[0][1]).b)).toEqual([]); + await injectConfig(baseOptions); + + expect(mockDir.content()).toEqual({ + 'main.js': + '({a:/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/,b:/*__APP_INJECTED_CONFIG_MARKER__*/"[]"/*__INJECTED_END__*/})', + }); }); it('should find the correct file to inject', async () => { - fsMock.readdir.mockResolvedValue([ - 'before.js', - 'not-js.txt', - 'main.js', - 'after.js', - ] as any); - readFileMock.mockImplementation(async (file: string) => { - if (file.endsWith('main.js')) { - return '"__APP_INJECTED_RUNTIME_CONFIG__"'; - } - return 'NO_PLACEHOLDER_HERE'; + mockDir.setContent({ + 'before.js': 'NO_PLACEHOLDER_HERE', + 'not-js.txt': 'NO_PLACEHOLDER_HERE', + 'main.js': '"__APP_INJECTED_RUNTIME_CONFIG__"', + 'after.js': 'NO_PLACEHOLDER_HERE', }); await injectConfig({ ...baseOptions, appConfigs: [{ data: { x: 0 }, context: 'test' }], }); - expect(fsMock.readFile).toHaveBeenCalledTimes(2); - expect(fsMock.readFile).toHaveBeenNthCalledWith( - 1, - resolvePath(MOCK_DIR, 'before.js'), - 'utf8', - ); - expect(fsMock.readFile).toHaveBeenNthCalledWith( - 2, - resolvePath(MOCK_DIR, 'main.js'), - 'utf8', - ); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - '/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/', - 'utf8', - ); - - // eslint-disable-next-line no-eval - expect(JSON.parse(eval(fsMock.writeFile.mock.calls[0][1]))).toEqual([ - { - data: { - x: 0, - }, - context: 'test', - }, - ]); + expect(mockDir.content()).toEqual({ + 'before.js': 'NO_PLACEHOLDER_HERE', + 'not-js.txt': 'NO_PLACEHOLDER_HERE', + 'main.js': + '/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/', + 'after.js': 'NO_PLACEHOLDER_HERE', + }); }); it('should re-inject config', async () => { - fsMock.readdir.mockResolvedValue(['main.js'] as any); - readFileMock.mockResolvedValue( - 'JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__")', - ); + mockDir.setContent({ + 'main.js': 'JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__")', + }); await injectConfig({ ...baseOptions, appConfigs: [{ data: { x: 0 }, context: 'test' }], }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - 'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/)', - 'utf8', - ); - - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[0][1])).toEqual([ - { data: { x: 0 }, context: 'test' }, - ]); - - readFileMock.mockResolvedValue(fsMock.writeFile.mock.calls[0][1]); + expect(mockDir.content()).toEqual({ + 'main.js': + 'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/)', + }); await injectConfig({ ...baseOptions, appConfigs: [{ data: { x: 1, y: 2 }, context: 'test' }], }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(2); - expect(fsMock.writeFile).toHaveBeenLastCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - 'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/)', - 'utf8', - ); - - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[1][1])).toEqual([ - { data: { x: 1, y: 2 }, context: 'test' }, - ]); + expect(mockDir.content()).toEqual({ + 'main.js': + 'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/)', + }); }); it('should re-inject config repeatedly if needed', async () => { - fsMock.readdir.mockResolvedValue(['main.js'] as any); - readFileMock.mockResolvedValue( - '({ a: JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__"), b: JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__") })', - ); + mockDir.setContent({ + 'main.js': + '({ a: JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__"), b: JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__") })', + }); await injectConfig({ ...baseOptions, appConfigs: [{ data: { x: 0 }, context: 'test' }], }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - '({ a: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/), b: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/) })', - 'utf8', - ); - - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[0][1]).a).toEqual([ - { data: { x: 0 }, context: 'test' }, - ]); - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[0][1]).b).toEqual([ - { data: { x: 0 }, context: 'test' }, - ]); - - readFileMock.mockResolvedValue(fsMock.writeFile.mock.calls[0][1]); + expect(mockDir.content()).toEqual({ + 'main.js': + '({ a: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/), b: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":0},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/) })', + }); await injectConfig({ ...baseOptions, appConfigs: [{ data: { x: 1, y: 2 }, context: 'test' }], }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(2); - expect(fsMock.writeFile).toHaveBeenLastCalledWith( - resolvePath(MOCK_DIR, 'main.js'), - '({ a: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/), b: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/) })', - 'utf8', - ); - - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[1][1]).a).toEqual([ - { data: { x: 1, y: 2 }, context: 'test' }, - ]); - // eslint-disable-next-line no-eval - expect(eval(fsMock.writeFile.mock.calls[1][1]).b).toEqual([ - { data: { x: 1, y: 2 }, context: 'test' }, - ]); + expect(mockDir.content()).toEqual({ + 'main.js': + '({ a: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/), b: JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"[{\\"data\\":{\\"x\\":1,\\"y\\":2},\\"context\\":\\"test\\"}]"/*__INJECTED_END__*/) })', + }); }); }); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts index d7e7b12158..47104c66e1 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.test.ts @@ -26,21 +26,23 @@ jest.mock( (...args: any[]) => commandExists(...args), ); -jest.mock('fs-extra'); import { ContainerRunner } from '@backstage/backend-common'; -import fs from 'fs-extra'; import path from 'path'; import { PassThrough } from 'stream'; import { RailsNewRunner } from './railsNewRunner'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('Rails Templater', () => { const containerRunner: jest.Mocked = { runContainer: jest.fn(), }; + const mockDir = createMockDirectory(); + beforeEach(() => { jest.clearAllMocks(); + mockDir.clear(); }); describe('when running on docker', () => { @@ -53,14 +55,15 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - jest - .spyOn(fs, 'realpath') - .mockImplementation(x => Promise.resolve(x.toString())); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream, }); @@ -71,8 +74,8 @@ describe('Rails Templater', () => { args: ['new', '/output/rails-project'], envVars: { HOME: '/tmp' }, mountDirs: { - ['tempdir']: '/input', - [path.join('tempdir', 'intermediate')]: '/output', + [mockDir.path]: '/input', + [path.join(mockDir.path, 'intermediate')]: '/output', }, workingDir: '/input', logStream: logStream, @@ -88,11 +91,15 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream, }); @@ -114,11 +121,15 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream: stream, }); @@ -129,8 +140,8 @@ describe('Rails Templater', () => { args: ['new', '/output/rails-project'], envVars: { HOME: '/tmp' }, mountDirs: { - ['tempdir']: '/input', - [path.join('tempdir', 'intermediate')]: '/output', + [mockDir.path]: '/input', + [path.join(mockDir.path, 'intermediate')]: '/output', }, workingDir: '/input', logStream: stream, @@ -147,14 +158,15 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); - jest - .spyOn(fs, 'realpath') - .mockImplementation(x => Promise.resolve(x.toString())); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream, }); @@ -170,8 +182,8 @@ describe('Rails Templater', () => { ], envVars: { HOME: '/tmp' }, mountDirs: { - ['tempdir']: '/input', - [path.join('tempdir', 'intermediate')]: '/output', + [mockDir.path]: '/input', + [path.join(mockDir.path, 'intermediate')]: '/output', }, workingDir: '/input', logStream: logStream, @@ -190,12 +202,16 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); commandExists.mockImplementationOnce(() => () => true); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream: stream, }); @@ -204,11 +220,12 @@ describe('Rails Templater', () => { command: 'rails', args: expect.arrayContaining([ 'new', - path.join('tempdir', 'intermediate', 'rails-project'), + path.join(mockDir.path, 'intermediate', 'rails-project'), ]), logStream: stream, }); }); + it('update the template path to correct location', async () => { const stream = new PassThrough(); @@ -220,12 +237,16 @@ describe('Rails Templater', () => { imageName: 'foo/rails-custom-image', }; - jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any); + mockDir.setContent({ + intermediate: { + fakeGeneratedOutput: 'a', + }, + }); commandExists.mockImplementationOnce(() => () => true); const templater = new RailsNewRunner({ containerRunner }); await templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values, logStream: stream, }); @@ -234,9 +255,9 @@ describe('Rails Templater', () => { command: 'rails', args: expect.arrayContaining([ 'new', - path.join('tempdir', 'intermediate', 'rails-project'), + path.join(mockDir.path, 'intermediate', 'rails-project'), '--template', - path.join('tempdir', './something.rb'), + path.join(mockDir.path, './something.rb'), ]), logStream: stream, }); @@ -247,14 +268,14 @@ describe('Rails Templater', () => { it('throws an error', async () => { const stream = new PassThrough(); - jest - .spyOn(fs, 'readdir') - .mockImplementationOnce(() => Promise.resolve([])); + mockDir.setContent({ + intermediate: {}, + }); const templater = new RailsNewRunner({ containerRunner }); await expect( templater.run({ - workspacePath: 'tempdir', + workspacePath: mockDir.path, values: { owner: 'angeliski', storePath: 'https://github.com/angeliski/rails-project', diff --git a/yarn.lock b/yarn.lock index 0f470ed500..0b74912d90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28863,7 +28863,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.0.0, fs-extra@npm:^11.2.0": +"fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -28874,17 +28874,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.0": - version: 11.1.1 - resolution: "fs-extra@npm:11.1.1" - dependencies: - graceful-fs: ^4.2.0 - jsonfile: ^6.0.1 - universalify: ^2.0.0 - checksum: fb883c68245b2d777fbc1f2082c9efb084eaa2bbf9fddaa366130d196c03608eebef7fb490541276429ee1ca99f317e2d73e96f5ca0999eefedf5a624ae1edfd - languageName: node - linkType: hard - "fs-extra@npm:^7.0.1, fs-extra@npm:~7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1"