yarn-plugin: memoize loading of backstage.json

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-12-13 17:32:07 +00:00
parent ac91864de3
commit 7d83df0ad6
7 changed files with 225 additions and 71 deletions
@@ -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(
@@ -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<typeof memoize> & {
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<typeof memoize> & {
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);
});
});
@@ -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;
});
@@ -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,
+17
View File
@@ -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';
@@ -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);
});
});
+29
View File
@@ -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 = <T>(fn: () => T): typeof fn => {
let invoked = false;
let result: ReturnType<typeof fn>;
return () => {
if (!invoked) {
result = fn();
invoked = true;
}
return result;
};
};