Merge pull request #28130 from backstage/rewrite-yarn-plugin

yarn-plugin: rewrite using `reduceDependency` hook
This commit is contained in:
MT Lewis
2024-12-17 09:14:35 +00:00
committed by GitHub
20 changed files with 558 additions and 489 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'yarn-plugin-backstage': patch
---
Switch to using `reduceDependency` hook to replace `backstage:^` versions. This
makes the same yarn.lock file valid whether or not the plugin is installed.
+2 -2
View File
@@ -1,5 +1,5 @@
---
'yarn-plugin-backstage': minor
'yarn-plugin-backstage': patch
---
Fixed windows path resolution on windows
Fixed path resolution on windows
+1
View File
@@ -41,6 +41,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@yarnpkg/builder": "^4.0.0",
"fs-extra": "^11.2.0",
"nodemon": "^3.0.1"
}
}
+3 -2
View File
@@ -3,10 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Hooks } from '@yarnpkg/plugin-pack';
import { Hooks } from '@yarnpkg/core';
import { Hooks as Hooks_2 } from '@yarnpkg/plugin-pack';
import { Plugin as Plugin_2 } from '@yarnpkg/core';
// @public (undocumented)
const plugin: Plugin_2<Hooks>;
const plugin: Plugin_2<Hooks & Hooks_2>;
export default plugin;
```
@@ -15,11 +15,7 @@
*/
import { Descriptor, Workspace, structUtils } from '@yarnpkg/core';
import {
bindBackstageVersion,
getCurrentBackstageVersion,
getPackageVersion,
} from '../util';
import { getPackageVersion } from '../util';
import { PROTOCOL } from '../constants';
const hasBackstageVersion = (range: string) =>
@@ -45,8 +41,6 @@ export const beforeWorkspacePacking = async (
workspace: Workspace,
rawManifest: any,
) => {
const backstageVersion = getCurrentBackstageVersion();
for (const dependencyType of ['dependencies', 'devDependencies'] as const) {
const entries = Array.from(
workspace.manifest.getForScope(dependencyType).values(),
@@ -69,7 +63,7 @@ export const beforeWorkspacePacking = async (
);
rawManifest[finalDependencyType][ident] = `^${await getPackageVersion(
bindBackstageVersion(descriptor, backstageVersion),
descriptor,
workspace.project.configuration,
)}`;
}
@@ -0,0 +1,18 @@
/*
* 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 { beforeWorkspacePacking } from './beforeWorkspacePacking';
export { reduceDependency } from './reduceDependency';
@@ -0,0 +1,165 @@
/*
* 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 { Configuration, Project, structUtils, httpUtils } from '@yarnpkg/core';
import { npath, ppath } from '@yarnpkg/fslib';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { reduceDependency } from './reduceDependency';
describe('reduceDependency', () => {
const mockDir = createMockDirectory();
let project: Project;
let getMock: jest.SpyInstance<
ReturnType<(typeof httpUtils)['get']>,
Parameters<(typeof httpUtils)['get']>
>;
beforeEach(() => {
project = new Project(ppath.cwd(), {
configuration: Configuration.create(ppath.cwd()),
});
jest
.spyOn(ppath, 'cwd')
.mockReturnValue(npath.toPortablePath(mockDir.path));
jest
.spyOn(process, 'cwd')
.mockReturnValue(npath.toPortablePath(mockDir.path));
getMock = jest.spyOn(httpUtils, 'get').mockResolvedValue({
releaseVersion: '1.23.45',
packages: [
{
name: '@backstage/core',
version: '6.7.8',
},
],
});
mockDir.setContent({
'backstage.json': JSON.stringify({
version: '1.23.45',
}),
'package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
packages: {
a: {
'package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': 'backstage:^',
},
}),
},
},
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it.each`
range
${'npm:1.2.3'}
${'link:../foo/bar'}
${'workspace:^'}
`('returns non-backstage range "$range" unchanged', async ({ range }) => {
const descriptor = structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
range,
);
await expect(reduceDependency(descriptor, project)).resolves.toEqual(
descriptor,
);
});
it.each`
selector
${'*'}
${'latest'}
`(
'rejects backstage: ranges with invalid selector "$selector"',
async ({ selector }) => {
await expect(
reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
`backstage:${selector}`,
),
project,
),
).rejects.toThrow(/unexpected version selector/i);
},
);
describe('with range "backstage:^"', () => {
it('loads the manifest for the current Backstage version', async () => {
await reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
project,
);
expect(getMock).toHaveBeenCalledWith(
'https://versions.backstage.io/v1/releases/1.23.45/manifest.json',
expect.anything(),
);
});
it('replaces the range with the corresponding npm package range', async () => {
await expect(
reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
project,
),
).resolves.toEqual(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'npm:^6.7.8',
),
);
});
it(`throws for packages that don't appear in the manifest`, async () => {
await expect(
reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'other'),
'backstage:^',
),
project,
),
).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
'Package @backstage/other not found in manifest for Backstage v1.23.45',
),
}),
);
});
});
});
@@ -0,0 +1,35 @@
/*
* 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 { Descriptor, Project, structUtils } from '@yarnpkg/core';
import { getPackageVersion } from '../util';
import { PROTOCOL } from '../constants';
export const reduceDependency = async (
dependency: Descriptor,
project: Project,
) => {
const range = structUtils.parseRange(dependency.range);
if (range.protocol === PROTOCOL) {
return structUtils.makeDescriptor(
dependency,
`npm:^${await getPackageVersion(dependency, project.configuration)}`,
);
}
return dependency;
};
+5 -6
View File
@@ -21,10 +21,9 @@
* @packageDocumentation
*/
import { Plugin, semverUtils, YarnVersion } from '@yarnpkg/core';
import { Hooks } from '@yarnpkg/plugin-pack';
import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking';
import { BackstageResolver } from './resolver/BackstageResolver';
import { Plugin, Hooks, semverUtils, YarnVersion } from '@yarnpkg/core';
import { Hooks as PackHooks } from '@yarnpkg/plugin-pack';
import { beforeWorkspacePacking, reduceDependency } from './handlers';
// All dependencies of the yarn plugin are bundled during the build. Chalk
// triples the size of the plugin bundle when included, so we avoid the
@@ -44,11 +43,11 @@ if (!semverUtils.satisfiesWithPrereleases(YarnVersion, '^4.1.1')) {
/**
* @public
*/
const plugin: Plugin<Hooks> = {
const plugin: Plugin<Hooks & PackHooks> = {
hooks: {
reduceDependency,
beforeWorkspacePacking,
},
resolvers: [BackstageResolver],
};
export default plugin;
@@ -1,301 +0,0 @@
/*
* 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 {
Configuration,
Project,
ResolveOptions,
structUtils,
ThrowReport,
httpUtils,
} from '@yarnpkg/core';
import { npath, ppath } from '@yarnpkg/fslib';
import { BackstageResolver } from './BackstageResolver';
import { createMockDirectory } from '@backstage/backend-test-utils';
describe('BackstageResolver', () => {
const mockDir = createMockDirectory();
let backstageResolver: BackstageResolver;
let resolveOptions: ResolveOptions;
beforeEach(() => {
jest
.spyOn(ppath, 'cwd')
.mockReturnValue(npath.toPortablePath(mockDir.path));
jest
.spyOn(process, 'cwd')
.mockReturnValue(npath.toPortablePath(mockDir.path));
jest.spyOn(httpUtils, 'get').mockResolvedValue({
releaseVersion: '1.23.45',
packages: [
{
name: '@backstage/core',
version: '6.7.8',
},
],
});
mockDir.setContent({
'backstage.json': JSON.stringify({
version: '1.23.45',
}),
'package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
packages: {
a: {
'package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': 'backstage:^',
},
}),
},
},
});
backstageResolver = new BackstageResolver();
resolveOptions = {
resolver: backstageResolver,
project: new Project(ppath.cwd(), {
configuration: Configuration.create(ppath.cwd()),
}),
report: new ThrowReport(),
};
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('supportsDescriptor', () => {
it.each([
['backstage:^'],
['backstage:1.26.0-next.3'],
['backstage:anything'],
])('returns true for range "%s"', range => {
expect(
backstageResolver.supportsDescriptor(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
range,
),
),
).toEqual(true);
});
});
describe('bindDescriptor', () => {
describe('with range "backstage:^"', () => {
it('returns a descriptor with a version range for the current Backstage version', () => {
expect(
backstageResolver.bindDescriptor(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
),
).toEqual(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
),
);
});
});
describe('with range "backstage:^::v=1.23.45"', () => {
it('returns the correct descriptor', () => {
expect(
backstageResolver.bindDescriptor(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
),
),
).toEqual(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
),
);
});
});
});
describe('getCandidates', () => {
it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => {
const descriptor = structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
);
await expect(
backstageResolver.getCandidates(descriptor, {}, resolveOptions),
).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]);
});
it('rejects descriptors not using the backstage: protocol', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'npm:1.2.3',
),
{},
resolveOptions,
),
).rejects.toThrow(/unsupported version protocol/i);
});
it('rejects backstage: ranges missing a version parameter', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
{},
resolveOptions,
),
).rejects.toThrow(/missing Backstage version/i);
});
it('rejects backstage: ranges with multiple version parameters', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1&v=2',
),
{},
resolveOptions,
),
).rejects.toThrow(/multiple Backstage versions/i);
});
it.each`
selector
${'*'}
${'latest'}
`(
'rejects backstage: ranges with invalid selector "$selector"',
async ({ selector }) => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
`backstage:${selector}`,
),
{},
resolveOptions,
),
).rejects.toThrow(/unexpected version selector/i);
},
);
});
describe('getSatisfying', () => {
it('filters out locators for other packages', async () => {
await expect(
backstageResolver.getSatisfying(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
),
{},
[
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'foo'),
'npm:1.2.3',
),
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'core'),
'npm:6.7.8',
),
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'bar'),
'npm:1.2.3',
),
],
resolveOptions,
),
).resolves.toEqual({
locators: [
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'core'),
'npm:6.7.8',
),
],
sorted: true,
});
});
it('filters out locators for other package versions', async () => {
await expect(
backstageResolver.getSatisfying(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^::v=1.23.45',
),
{},
[
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'core'),
'npm:5.6.7',
),
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'core'),
'npm:6.7.8',
),
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'bar'),
'npm:7.8.9',
),
],
resolveOptions,
),
).resolves.toEqual({
locators: [
structUtils.makeLocator(
structUtils.makeIdent('backstage', 'core'),
'npm:6.7.8',
),
],
sorted: true,
});
});
it('throws for non `backstage:` descriptors', async () => {
await expect(
backstageResolver.getSatisfying(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'npm:1.2.3',
),
{},
[],
resolveOptions,
),
).rejects.toThrow(/unsupported version protocol/i);
});
});
});
@@ -1,128 +0,0 @@
/*
* 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 {
structUtils,
Descriptor,
Locator,
Package,
Resolver,
ResolveOptions,
} from '@yarnpkg/core';
import { PROTOCOL } from '../constants';
import {
bindBackstageVersion,
getCurrentBackstageVersion,
getPackageVersion,
} from '../util';
export class BackstageResolver implements Resolver {
static protocol = PROTOCOL;
supportsDescriptor = (descriptor: Descriptor) =>
descriptor.range.startsWith(BackstageResolver.protocol);
shouldPersistResolution = () => true;
/**
* Called for each dependency present in the dependency list of a package
* definition. If it returns a new descriptor, this new descriptor will be
* used.
*
* In this plugin, we convert the specific range "backstage:^" to
* "backstage:<version from backstage.json>". This new range will be the one
* stored in lockfile entries, ensuring that we re-resolve the package when
* the version in backstage.json changes.
*/
bindDescriptor(descriptor: Descriptor): Descriptor {
if (descriptor.range === 'backstage:^') {
return bindBackstageVersion(descriptor, getCurrentBackstageVersion());
}
return descriptor;
}
/**
* Given a descriptor, return the list of locators that potentially satisfy
* it. The implementation in this plugin converts a `backstage:` range with a
* concrete version into the appropriate concrete npm version for that
* backstage release.
*/
async getCandidates(
descriptor: Descriptor,
_dependencies: Record<string, Package>,
opts: ResolveOptions,
): Promise<Locator[]> {
return [
structUtils.makeLocator(
descriptor,
`npm:${await getPackageVersion(
descriptor,
opts.project.configuration,
)}`,
),
];
}
/**
* Given a descriptor and a list of possible locators, return a filtered list
* containing only locators that satisfy the descriptor. Since each Backstage
* release version corresponds to a single version for each package, we just
* need to filter that array for locators with that exact version.
*/
async getSatisfying(
descriptor: Descriptor,
_dependencies: Record<string, Package>,
locators: Array<Locator>,
opts: ResolveOptions,
): Promise<{ locators: Locator[]; sorted: boolean }> {
const packageVersion = await getPackageVersion(
descriptor,
opts.project.configuration,
);
return {
locators: locators.filter(
locator =>
structUtils.areIdentsEqual(descriptor, locator) &&
locator.reference === `npm:${packageVersion}`,
),
sorted: true,
};
}
/**
* This plugin does not need to support any locators itself, since the
* `getCandidates` method will always convert `backstage:` versions into
* `npm:` versions which can be handled as usual.
*/
supportsLocator = () => false;
/**
* This resolver never transforms the packages that are actually depended on,
* only replaces versions. As such there's never a need to add additional
* dependencies.
*/
getResolutionDependencies = () => ({});
/**
* Once transformed into locators (through getCandidates), the versions are
* resolved by the NpmSemverResolver
*/
async resolve(): Promise<Package> {
throw new Error(`Unreachable`);
}
}
@@ -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,37 @@
/*
* 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 { ppath, xfs } from '@yarnpkg/fslib';
import { BACKSTAGE_JSON } from '@backstage/cli-common';
import { memoize } from './memoize';
import { getWorkspaceRoot } from './getWorkspaceRoot';
export const getCurrentBackstageVersion = memoize(() => {
const backstageJsonPath = ppath.join(getWorkspaceRoot(), 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,37 +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 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;
};
export const bindBackstageVersion = (
descriptor: Descriptor,
backstageVersion: string,
) => {
return structUtils.bindDescriptor(descriptor, { v: backstageVersion });
};
import { PROTOCOL } from '../constants';
import { getCurrentBackstageVersion } from './getCurrentBackstageVersion';
export const getPackageVersion = async (
descriptor: Descriptor,
@@ -71,20 +44,10 @@ export const getPackageVersion = async (
);
}
if (!range.params?.v) {
throw new Error(
`Missing Backstage version parameter in range "${descriptor.range}" for package ${ident}`,
);
}
if (Array.isArray(range.params.v)) {
throw new Error(
`Multiple Backstage versions specified in range "${descriptor.range}" for package ${ident}`,
);
}
const backstageVersion = getCurrentBackstageVersion();
const manifest = await getManifestByVersion({
version: range.params.v,
version: backstageVersion,
// We override the fetch function used inside getManifestByVersion with a
// custom implementation that calls yarn's built-in `httpUtils` method
// instead. This has a couple of benefits:
@@ -123,7 +86,7 @@ export const getPackageVersion = async (
if (!manifestEntry) {
throw new Error(
`Package ${ident} not found in manifest for Backstage v${range.selector}. ` +
`Package ${ident} not found in manifest for Backstage v${backstageVersion}. ` +
`This means the specified package is not included in this Backstage ` +
`release. This may imply the package has been replaced with an alternative - ` +
`please review the documentation for the package. If you need to continue ` +
@@ -0,0 +1,70 @@
/*
* 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 { findPaths, Paths } from '@backstage/cli-common';
const setPlatform = (platform: string) => {
Object.defineProperty(process, `platform`, {
configurable: true,
value: platform,
});
};
describe('getWorkspaceRoot', () => {
/**
* Yarn uses a PortablePath type which uses the posix separator
* regardless of platform and prefixes absolute paths with a separator.
*
* https://yarnpkg.com/api/yarnpkg-fslib#type-safe-paths
*/
describe.each`
platform | native | portable
${'darwin'} | ${'/test/workspace/'} | ${'/test/workspace/'}
${'win32'} | ${'C:\\test\\workspace\\'} | ${'/C:/test/workspace/'}
`('platform: $platform', ({ platform, native, portable }) => {
let realPlatform: string;
let getWorkspaceRoot: () => string;
let mockFindPaths: jest.MockedFunction<typeof findPaths>;
beforeEach(() => {
realPlatform = process.platform;
setPlatform(platform);
jest.resetModules();
mockFindPaths = jest.fn();
jest.doMock('@backstage/cli-common', () => ({
...jest.requireActual('@backstage/cli-common'),
findPaths: mockFindPaths,
}));
getWorkspaceRoot = require('./getWorkspaceRoot').getWorkspaceRoot;
});
afterEach(() => {
setPlatform(realPlatform);
});
it('returns an appropriately-formatted workspace root path', () => {
mockFindPaths.mockReturnValue({
targetRoot: native,
} as Paths);
expect(getWorkspaceRoot()).toEqual(portable);
});
});
});
@@ -0,0 +1,24 @@
/*
* 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 } from '@yarnpkg/fslib';
import { findPaths } from '@backstage/cli-common';
export const getWorkspaceRoot = () => {
return npath.toPortablePath(
findPaths(npath.fromPortablePath(ppath.cwd())).targetRoot,
);
};
+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;
};
};
+1
View File
@@ -48206,6 +48206,7 @@ __metadata:
"@yarnpkg/core": ^4.0.3
"@yarnpkg/fslib": ^3.0.2
"@yarnpkg/plugin-pack": ^4.0.0
fs-extra: ^11.2.0
nodemon: ^3.0.1
semver: ^7.6.0
languageName: unknown