Skip to content

Commit d396ea8

Browse files
authored
Merge branch '9.3' into backport/9.3/pr-258105
2 parents 7488ec7 + be3c07f commit d396ea8

15 files changed

Lines changed: 1490 additions & 40 deletions

File tree

src/dev/kbn_pm/src/commands/bootstrap/bootstrap_command.mjs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,23 +111,14 @@ export const command = {
111111
});
112112

113113
await Promise.all([
114-
time('extract relevant versions for packages', async () => {
115-
log.info('extract relevant versions for packages');
116-
await moonRun(':extract-version-dependencies', {
114+
time('prepare webpack bundles for packages', async () => {
115+
log.info('extract relevant versions for packages and pre-build webpack bundles');
116+
await moonRun([':extract-version-dependencies', ':build-webpack'], {
117117
pipe: !quiet,
118118
quiet,
119119
noCache: forceInstall,
120120
});
121-
log.success('relevant versions extracted for packages');
122-
}),
123-
time('pre-build webpack bundles for packages', async () => {
124-
log.info('pre-build webpack bundles for packages');
125-
await moonRun(':build-webpack', {
126-
pipe: !quiet,
127-
quiet,
128-
noCache: forceInstall,
129-
});
130-
log.success('shared webpack bundles built');
121+
log.success('relevant versions extracted for packages and shared webpack bundles built');
131122
}),
132123
shouldInstall
133124
? time('run install scripts', async () => {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { collectDependencyVersionLines } from './extract_version_dependencies';
11+
12+
describe('collectDependencyVersionLines', () => {
13+
const rootPackageJsonContent = JSON.stringify({
14+
dependencies: {
15+
alpha: '^1.0.0',
16+
gamma: '^3.0.0',
17+
},
18+
});
19+
20+
const yarnLockContent = `
21+
alpha@^1.0.0:
22+
version "1.0.1"
23+
dependencies:
24+
beta "^1.0.0"
25+
optional-child "^1.0.0"
26+
27+
beta@^1.0.0:
28+
version "1.1.0"
29+
dependencies:
30+
shared "^1.0.0"
31+
32+
optional-child@^1.0.0:
33+
version "1.0.2"
34+
optionalDependencies:
35+
shared "^2.0.0"
36+
37+
gamma@^3.0.0:
38+
version "3.0.0"
39+
dependencies:
40+
shared "^2.0.0"
41+
42+
shared@^1.0.0:
43+
version "1.2.0"
44+
45+
shared@^2.0.0:
46+
version "2.3.0"
47+
`;
48+
49+
it('returns direct resolved dependency versions when transitive is false', () => {
50+
expect(
51+
collectDependencyVersionLines({
52+
dependencies: ['alpha', 'gamma'],
53+
rootPackageJsonContent,
54+
transitive: false,
55+
yarnLockContent,
56+
})
57+
).toEqual(['alpha@1.0.1', 'gamma@3.0.0']);
58+
});
59+
60+
it('returns the full transitive closure including optional dependencies', () => {
61+
expect(
62+
collectDependencyVersionLines({
63+
dependencies: ['alpha'],
64+
rootPackageJsonContent,
65+
transitive: true,
66+
yarnLockContent,
67+
})
68+
).toEqual([
69+
'alpha@1.0.1',
70+
'beta@1.1.0',
71+
'optional-child@1.0.2',
72+
'shared@1.2.0',
73+
'shared@2.3.0',
74+
]);
75+
});
76+
77+
it('keeps multiple resolved versions of the same package when they are both in the closure', () => {
78+
expect(
79+
collectDependencyVersionLines({
80+
dependencies: ['alpha', 'gamma'],
81+
rootPackageJsonContent,
82+
transitive: true,
83+
yarnLockContent,
84+
})
85+
).toEqual([
86+
'alpha@1.0.1',
87+
'beta@1.1.0',
88+
'gamma@3.0.0',
89+
'optional-child@1.0.2',
90+
'shared@1.2.0',
91+
'shared@2.3.0',
92+
]);
93+
});
94+
95+
it('throws when a requested root dependency is not declared in package.json', () => {
96+
expect(() =>
97+
collectDependencyVersionLines({
98+
dependencies: ['missing'],
99+
rootPackageJsonContent,
100+
transitive: true,
101+
yarnLockContent,
102+
})
103+
).toThrowErrorMatchingInlineSnapshot(
104+
`"Unable to find missing in the root package.json dependency list"`
105+
);
106+
});
107+
});

src/dev/yarn/extract_version_dependencies.ts

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,57 +13,158 @@ import fs from 'fs/promises';
1313

1414
import { run } from '@kbn/dev-cli-runner';
1515
import { REPO_ROOT } from '@kbn/repo-info';
16-
import { parseYarnLockFile } from './yarn_lock_v1';
16+
import type { PackageInfo } from './yarn_lock_v1';
17+
import { parseYarnLock } from './yarn_lock_v1';
1718

1819
const options: RunOptions = {
1920
description:
2021
'Extracts declared dependency versions from a Yarn v1 lockfile into a versions file.\n' +
2122
'This can be useful to set up Moon task dependencies on package versions used in the repo.',
2223
flags: {
2324
string: ['collect'],
25+
boolean: ['transitive'],
2426
},
25-
usage: `node scripts/extract_version_dependencies <output_file_path> --collect <package_name1,package_name2,...>`,
27+
usage:
28+
`node scripts/extract_version_dependencies <output_file_path> ` +
29+
`--collect <package_name1> [--collect <package_name2> ...] [--transitive]`,
2630
};
2731

2832
export async function runCli() {
2933
return run(async ({ flagsReader }) => {
3034
const outputFilePath = flagsReader.getPositionals()[0];
3135
const dependencies = flagsReader.arrayOfStrings('collect');
36+
const transitive = flagsReader.boolean('transitive');
3237
if (typeof dependencies === 'undefined') {
3338
throw new Error('--collect flag is required and must specify at least one package name.');
3439
}
3540

36-
await collectDependenciesAndWriteFile(dependencies, outputFilePath);
41+
await collectDependenciesAndWriteFile(dependencies, outputFilePath, { transitive });
3742

3843
return;
3944
}, options);
4045
}
4146

42-
async function collectDependenciesAndWriteFile(dependencies: string[], outputFilePath: string) {
43-
const resolvedDependencyMap = new Map<string, string>();
47+
async function collectDependenciesAndWriteFile(
48+
dependencies: string[],
49+
outputFilePath: string,
50+
{ transitive }: { transitive: boolean }
51+
) {
4452
const rootPackageJson = path.join(REPO_ROOT, 'package.json');
4553
const yarnLockPath = path.join(REPO_ROOT, 'yarn.lock');
4654

47-
const pkgJson = await fs.readFile(rootPackageJson, 'utf-8').then((data) => JSON.parse(data));
48-
const yarnLockContent = parseYarnLockFile(yarnLockPath, dependencies);
49-
const yarnLockEntries = Object.values(yarnLockContent);
55+
const [packageJsonContent, yarnLockContent] = await Promise.all([
56+
fs.readFile(rootPackageJson, 'utf-8'),
57+
fs.readFile(yarnLockPath, 'utf-8'),
58+
]);
59+
60+
const outputLines = collectDependencyVersionLines({
61+
dependencies,
62+
rootPackageJsonContent: packageJsonContent,
63+
transitive,
64+
yarnLockContent,
65+
});
66+
67+
await fs.writeFile(outputFilePath, outputLines.join('\n') + '\n', 'utf-8');
68+
}
69+
70+
export const collectDependencyVersionLines = ({
71+
dependencies,
72+
rootPackageJsonContent,
73+
transitive,
74+
yarnLockContent,
75+
}: {
76+
dependencies: string[];
77+
rootPackageJsonContent: string;
78+
transitive: boolean;
79+
yarnLockContent: string;
80+
}) => {
81+
const pkgJson = JSON.parse(rootPackageJsonContent);
82+
const yarnLockEntries = Object.values(parseYarnLock(yarnLockContent));
83+
const requestedVersionIndex = createRequestedVersionIndex(yarnLockEntries);
5084

5185
const allRequestedDependencies = {
5286
...pkgJson.devDependencies,
5387
...pkgJson.dependencies,
5488
};
5589

56-
dependencies.forEach((dep) => {
57-
const declaredVersion = allRequestedDependencies[dep];
58-
const yarnlockEntry = yarnLockEntries.find((entry) => {
59-
return entry.name === dep && entry.requestedVersions.includes(declaredVersion);
60-
});
61-
resolvedDependencyMap.set(dep, yarnlockEntry!.resolvedVersion!);
90+
const rootDependencies = resolveRootDependencies(
91+
dependencies,
92+
requestedVersionIndex,
93+
allRequestedDependencies
94+
);
95+
96+
const resolvedDependencyKeys = transitive
97+
? collectTransitiveDependencies(rootDependencies, requestedVersionIndex)
98+
: new Set(rootDependencies.map((pkg) => `${pkg.name}@${pkg.resolvedVersion}`));
99+
100+
return Array.from(resolvedDependencyKeys).sort((a, b) => a.localeCompare(b));
101+
};
102+
103+
const createRequestedVersionIndex = (packages: PackageInfo[]) => {
104+
const index = new Map<string, PackageInfo>();
105+
106+
for (const pkg of packages) {
107+
for (const requestedVersion of pkg.requestedVersions) {
108+
index.set(`${pkg.name}@${requestedVersion}`, pkg);
109+
}
110+
}
111+
112+
return index;
113+
};
114+
115+
const resolveDependency = (
116+
dependencyName: string,
117+
requestedVersion: string,
118+
requestedVersionIndex: Map<string, PackageInfo>
119+
) => {
120+
const pkg = requestedVersionIndex.get(`${dependencyName}@${requestedVersion}`);
121+
122+
if (!pkg?.resolvedVersion) {
123+
throw new Error(
124+
`Unable to resolve ${dependencyName}@${requestedVersion} from yarn.lock dependency graph`
125+
);
126+
}
127+
128+
return pkg;
129+
};
130+
131+
const resolveRootDependencies = (
132+
dependencies: string[],
133+
requestedVersionIndex: Map<string, PackageInfo>,
134+
allRequestedDependencies: Record<string, string>
135+
) => {
136+
return dependencies.map((dependencyName) => {
137+
const declaredVersion = allRequestedDependencies[dependencyName];
138+
139+
if (!declaredVersion) {
140+
throw new Error(`Unable to find ${dependencyName} in the root package.json dependency list`);
141+
}
142+
143+
return resolveDependency(dependencyName, declaredVersion, requestedVersionIndex);
62144
});
145+
};
63146

64-
const outputLines = Array.from(resolvedDependencyMap.entries())
65-
.sort((a, b) => a[0].localeCompare(b[0]))
66-
.map(([name, version]) => `${name}@${version}`);
147+
const collectTransitiveDependencies = (
148+
rootDependencies: PackageInfo[],
149+
requestedVersionIndex: Map<string, PackageInfo>
150+
) => {
151+
const visited = new Set<string>();
152+
const queue = [...rootDependencies];
67153

68-
await fs.writeFile(outputFilePath, outputLines.join('\n') + '\n', 'utf-8');
69-
}
154+
while (queue.length > 0) {
155+
const pkg = queue.shift()!;
156+
const packageKey = `${pkg.name}@${pkg.resolvedVersion}`;
157+
158+
if (visited.has(packageKey)) {
159+
continue;
160+
}
161+
162+
visited.add(packageKey);
163+
164+
for (const [dependencyName, requestedVersion] of Object.entries(pkg.dependencies ?? {})) {
165+
queue.push(resolveDependency(dependencyName, requestedVersion, requestedVersionIndex));
166+
}
167+
}
168+
169+
return visited;
170+
};

src/dev/yarn/yarn_lock_v1.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { parseYarnLock } from './yarn_lock_v1';
11+
12+
describe('parseYarnLock', () => {
13+
it('parses dependency versions that contain spaces and comparators', () => {
14+
const lockfile = `
15+
stylis-plugin-rtl@>=2.1.0:
16+
version "2.1.1"
17+
dependencies:
18+
cssjanus ">=1.3.2"
19+
stylis "4.x"
20+
21+
cssjanus@>=1.3.2:
22+
version "2.3.0"
23+
24+
stylis@4.x:
25+
version "4.3.6"
26+
`;
27+
28+
expect(parseYarnLock(lockfile)).toMatchObject({
29+
'stylis-plugin-rtl@2.1.1': {
30+
dependencies: {
31+
cssjanus: '>=1.3.2',
32+
stylis: '4.x',
33+
},
34+
},
35+
});
36+
});
37+
38+
it('includes optionalDependencies in the parsed dependency map', () => {
39+
const lockfile = `
40+
wrapper@^1.0.0:
41+
version "1.0.0"
42+
optionalDependencies:
43+
native-thing "^2.0.0"
44+
45+
native-thing@^2.0.0:
46+
version "2.1.0"
47+
`;
48+
49+
expect(parseYarnLock(lockfile)).toMatchObject({
50+
'wrapper@1.0.0': {
51+
dependencies: {
52+
'native-thing': '^2.0.0',
53+
},
54+
},
55+
});
56+
});
57+
58+
it('merges duplicate lock entries for the same resolved package', () => {
59+
const lockfile = `
60+
shared@^1.0.0:
61+
version "1.2.3"
62+
63+
shared@~1.2.0:
64+
version "1.2.3"
65+
`;
66+
67+
expect(parseYarnLock(lockfile)).toEqual({
68+
'shared@1.2.3': {
69+
name: 'shared',
70+
requestedVersions: ['^1.0.0', '~1.2.0'],
71+
resolvedVersion: '1.2.3',
72+
},
73+
});
74+
});
75+
});

0 commit comments

Comments
 (0)