From 7d83df0ad6988bd22e6a3e9b562b6133a5fe2521 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 13 Dec 2024 17:32:07 +0000 Subject: [PATCH] yarn-plugin: memoize loading of backstage.json Signed-off-by: MT Lewis --- .../src/handlers/reduceDependency.test.ts | 44 ------- .../util/getCurrentBackstageVersion.test.ts | 107 ++++++++++++++++++ .../src/util/getCurrentBackstageVersion.ts | 39 +++++++ .../{util.ts => util/getPackageVersion.ts} | 29 +---- packages/yarn-plugin/src/util/index.ts | 17 +++ packages/yarn-plugin/src/util/memoize.test.ts | 31 +++++ packages/yarn-plugin/src/util/memoize.ts | 29 +++++ 7 files changed, 225 insertions(+), 71 deletions(-) create mode 100644 packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts create mode 100644 packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts rename packages/yarn-plugin/src/{util.ts => util/getPackageVersion.ts} (80%) create mode 100644 packages/yarn-plugin/src/util/index.ts create mode 100644 packages/yarn-plugin/src/util/memoize.test.ts create mode 100644 packages/yarn-plugin/src/util/memoize.ts diff --git a/packages/yarn-plugin/src/handlers/reduceDependency.test.ts b/packages/yarn-plugin/src/handlers/reduceDependency.test.ts index 568f39653a..dfe29485b6 100644 --- a/packages/yarn-plugin/src/handlers/reduceDependency.test.ts +++ b/packages/yarn-plugin/src/handlers/reduceDependency.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { join as joinPath } from 'path'; -import fs from 'fs-extra'; import { Configuration, Project, structUtils, httpUtils } from '@yarnpkg/core'; import { npath, ppath } from '@yarnpkg/fslib'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -146,48 +144,6 @@ describe('reduceDependency', () => { ); }); - it('throws if backstage.json is missing', async () => { - await fs.remove(joinPath(mockDir.path, 'backstage.json')); - - await expect( - reduceDependency( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'other'), - 'backstage:^', - ), - project, - ), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringContaining( - 'Valid version string not found in backstage.json', - ), - }), - ); - }); - - it(`throws if backstage.json doesn't contain a valid version`, async () => { - mockDir.addContent({ - 'backstage.json': JSON.stringify({}), - }); - - await expect( - reduceDependency( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'other'), - 'backstage:^', - ), - project, - ), - ).rejects.toThrow( - expect.objectContaining({ - message: expect.stringContaining( - 'Valid version string not found in backstage.json', - ), - }), - ); - }); - it(`throws for packages that don't appear in the manifest`, async () => { await expect( reduceDependency( diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts new file mode 100644 index 0000000000..4b1f008fce --- /dev/null +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { npath, ppath, xfs } from '@yarnpkg/fslib'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { memoize } from './memoize'; +import { getCurrentBackstageVersion } from './getCurrentBackstageVersion'; + +/** + * Disable memoization to allow testing behavior under a variety of + * circumstances. + */ +jest.mock('./memoize', () => { + const memoizeMock: jest.MockedFn & { + memoizationEnabled?: boolean; + } = jest.fn(fn => { + const memoized = jest.requireActual('./memoize').memoize(fn); + + return () => { + if (memoizeMock.memoizationEnabled) { + return memoized(); + } + + return fn(); + }; + }); + + return { memoize: memoizeMock }; +}); + +const memoizeMock = memoize as jest.MockedFunction & { + memoizationEnabled?: boolean; +}; + +describe('getCurrentBackstageVersion', () => { + const mockDir = createMockDirectory(); + + beforeEach(() => { + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + mockDir.setContent({ + 'package.json': JSON.stringify({}), + }); + }); + + it('retrieves the version of Backstage from backstage.json', () => { + mockDir.addContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + }); + + expect(getCurrentBackstageVersion()).toEqual('1.23.45'); + }); + + it.each` + description | content + ${'is missing'} | ${{}} + ${'is invalid'} | ${{ 'backstage.json': '}{' }} + ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} + ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} + `('throws if backstage.json $description', ({ content }) => { + mockDir.addContent(content); + + expect(() => getCurrentBackstageVersion()).toThrow( + /valid version string not found/i, + ); + }); + + it('caches repeated calls', () => { + mockDir.addContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + }); + + memoizeMock.memoizationEnabled = true; + + const readJsonSyncSpy = jest.spyOn(xfs, 'readJsonSync'); + + expect(readJsonSyncSpy).toHaveBeenCalledTimes(0); + + getCurrentBackstageVersion(); + getCurrentBackstageVersion(); + + expect(readJsonSyncSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts new file mode 100644 index 0000000000..83233747ce --- /dev/null +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import { valid as semverValid } from 'semver'; +import { npath, ppath, xfs } from '@yarnpkg/fslib'; +import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common'; +import { memoize } from './memoize'; + +export const getCurrentBackstageVersion = memoize(() => { + const workspaceRoot = npath.toPortablePath( + findPaths(npath.fromPortablePath(ppath.cwd())).targetRoot, + ); + const backstageJsonPath = ppath.join(workspaceRoot, BACKSTAGE_JSON); + + let backstageVersion: string | null = null; + try { + backstageVersion = semverValid(xfs.readJsonSync(backstageJsonPath).version); + + assert(backstageVersion !== null); + } catch { + throw new Error('Valid version string not found in backstage.json'); + } + + return backstageVersion; +}); diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util/getPackageVersion.ts similarity index 80% rename from packages/yarn-plugin/src/util.ts rename to packages/yarn-plugin/src/util/getPackageVersion.ts index 939ce807e5..25ff795c06 100644 --- a/packages/yarn-plugin/src/util.ts +++ b/packages/yarn-plugin/src/util/getPackageVersion.ts @@ -20,35 +20,10 @@ import { httpUtils, structUtils, } from '@yarnpkg/core'; -import { ppath, npath, xfs } from '@yarnpkg/fslib'; -import { valid as semverValid } from 'semver'; -import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common'; import { getManifestByVersion } from '@backstage/release-manifests'; -import { PROTOCOL } from './constants'; - -export const getCurrentBackstageVersion = () => { - const workspaceRoot = npath.toPortablePath( - findPaths(npath.fromPortablePath(ppath.cwd())).targetRoot, - ); - const backstageJsonPath = ppath.join(workspaceRoot, BACKSTAGE_JSON); - - if (!xfs.existsSync(backstageJsonPath)) { - throw new Error('Valid version string not found in backstage.json'); - } - - const backstageJson = xfs.readJsonSync( - ppath.join(workspaceRoot, BACKSTAGE_JSON), - ); - - const backstageVersion = semverValid(backstageJson.version); - - if (backstageVersion === null) { - throw new Error('Valid version string not found in backstage.json'); - } - - return backstageVersion; -}; +import { PROTOCOL } from '../constants'; +import { getCurrentBackstageVersion } from './getCurrentBackstageVersion'; export const getPackageVersion = async ( descriptor: Descriptor, diff --git a/packages/yarn-plugin/src/util/index.ts b/packages/yarn-plugin/src/util/index.ts new file mode 100644 index 0000000000..139c033468 --- /dev/null +++ b/packages/yarn-plugin/src/util/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { getPackageVersion } from './getPackageVersion'; diff --git a/packages/yarn-plugin/src/util/memoize.test.ts b/packages/yarn-plugin/src/util/memoize.test.ts new file mode 100644 index 0000000000..4f8c761eea --- /dev/null +++ b/packages/yarn-plugin/src/util/memoize.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { memoize } from './memoize'; + +describe('memoize', () => { + it('memoizes', () => { + const mockFunc = jest.fn().mockReturnValue('abc'); + + const memoized = memoize(mockFunc); + + expect(memoized()).toEqual('abc'); + expect(memoized()).toEqual('abc'); + expect(memoized()).toEqual('abc'); + + expect(mockFunc).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/yarn-plugin/src/util/memoize.ts b/packages/yarn-plugin/src/util/memoize.ts new file mode 100644 index 0000000000..b224f22467 --- /dev/null +++ b/packages/yarn-plugin/src/util/memoize.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const memoize = (fn: () => T): typeof fn => { + let invoked = false; + let result: ReturnType; + + return () => { + if (!invoked) { + result = fn(); + invoked = true; + } + + return result; + }; +};