Merge pull request #28130 from backstage/rewrite-yarn-plugin
yarn-plugin: rewrite using `reduceDependency` hook
This commit is contained in:
@@ -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.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
'yarn-plugin-backstage': minor
|
||||
'yarn-plugin-backstage': patch
|
||||
---
|
||||
|
||||
Fixed windows path resolution on windows
|
||||
Fixed path resolution on windows
|
||||
|
||||
@@ -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,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;
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
+5
-42
@@ -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,
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user