Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,8 @@ const yarnCling = configureProject(
name: '@aws-cdk/yarn-cling',
description: 'Tool for generating npm-shrinkwrap from yarn.lock',
srcdir: 'lib',
deps: ['@yarnpkg/lockfile', 'semver'],
devDeps: ['@types/semver', '@types/yarnpkg__lockfile', 'fast-check'],
deps: ['@yarnpkg/parsers', 'semver'],
devDeps: ['@types/semver', 'fast-check'],
minNodeVersion: '18',
tsconfig: {
compilerOptions: {
Expand All @@ -610,6 +610,7 @@ const yarnCling = configureProject(
}),
);
yarnCling.testTask.prependExec('ln -sf ../../cdk test/test-fixture/jsii/node_modules/');
yarnCling.testTask.prependExec('ln -sf ../../cdk test/test-fixture-berry/jsii/node_modules/');

// #endregion
//////////////////////////////////////////////////////////////////////
Expand Down
6 changes: 1 addition & 5 deletions packages/@aws-cdk/yarn-cling/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/@aws-cdk/yarn-cling/.projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 54 additions & 5 deletions packages/@aws-cdk/yarn-cling/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { promises as fs, exists } from 'fs';
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as lockfile from '@yarnpkg/lockfile';
import { parseSyml } from '@yarnpkg/parsers';
import * as semver from 'semver';
import { hoistDependencies } from './hoisting';
import { isPackage, iterDeps, type PackageJson, type PackageLockFile, type PackageLockPackage, type PackageLockTree, type YarnLock } from './types';
import { isPackage, iterDeps, type PackageJson, type PackageLockFile, type PackageLockPackage, type PackageLockTree, type ResolvedYarnPackage, type YarnLock } from './types';

export interface ShrinkwrapOptions {
/**
Expand Down Expand Up @@ -33,7 +33,7 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise<Pa
const packageJsonDir = path.dirname(packageJsonFile);

const yarnLockLoc = await findYarnLock(packageJsonDir);
const yarnLock: YarnLock = lockfile.parse(await fs.readFile(yarnLockLoc, { encoding: 'utf8' }));
const yarnLock: YarnLock = parseYarnLock(await fs.readFile(yarnLockLoc, { encoding: 'utf8' }));
const pkgJson = await loadPackageJson(packageJsonFile);

let lock = await generateLockFile(pkgJson, yarnLock, packageJsonDir);
Expand All @@ -52,6 +52,55 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise<Pa
return lock;
}

/**
* Parse a yarn.lock file, supporting both classic (v1) and berry (v2+) formats.
*/
export function parseYarnLock(content: string): YarnLock {
const parsed = parseSyml(content);

// Berry lockfiles have __metadata and use different field names
if (parsed.__metadata) {
return convertBerryToClassicLock(parsed);
}

// Classic v1 lockfiles are already in the right shape
return { type: 'success', object: parsed };
}

/**
* Convert a parsed berry (v2+) lockfile into the classic YarnLock format.
*
* Berry keys look like: "pkg@npm:^1.0.0" or "pkg@npm:^1.0.0, pkg@npm:^2.0.0"
* We convert each to the classic format: "pkg@^1.0.0" -> { version, resolved, integrity, dependencies }
*/
function convertBerryToClassicLock(parsed: Record<string, any>): YarnLock {
const object: Record<string, ResolvedYarnPackage> = Object.create(null);

for (const [key, entry] of Object.entries(parsed)) {
if (key === '__metadata' || !entry?.version) continue;

const resolved: ResolvedYarnPackage = {
version: entry.version,
...(entry.resolution && { resolved: entry.resolution }),
...(entry.checksum && { integrity: entry.checksum }),
...(entry.dependencies && {
dependencies: Object.fromEntries(
Object.entries(entry.dependencies).map(([k, v]: [string, any]) => [k, String(v).replace(/^npm:/, '')]),
),
}),
};

// Convert berry descriptors ("pkg@npm:^1.0.0") to classic format ("pkg@^1.0.0")
for (const descriptor of key.split(', ')) {
const npmMatch = descriptor.match(/^(.+)@npm:(.+)$/);
const classicKey = npmMatch ? `${npmMatch[1]}@${npmMatch[2]}` : descriptor;
object[classicKey] = resolved;
}
}

return { type: 'success', object };
}

async function generateLockFile(pkgJson: PackageJson, yarnLock: YarnLock, rootDir: string): Promise<PackageLockFile> {
const builder = new PackageGraphBuilder(yarnLock);
const rootKeys = await builder.buildGraph(pkgJson.dependencies || {}, rootDir);
Expand Down Expand Up @@ -321,7 +370,7 @@ async function findPackageDir(depName: string, rootDir: string) {
let dir = rootDir;
while (dir !== prevDir) {
const candidateDir = path.join(dir, 'node_modules', depName);
if (await new Promise(ok => exists(path.join(candidateDir, 'package.json'), ok))) {
if (await fileExists(path.join(candidateDir, 'package.json'))) {
return candidateDir;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/yarn-cling/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 56 additions & 1 deletion packages/@aws-cdk/yarn-cling/test/cling.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'path';
import { checkRequiredVersions, generateShrinkwrap } from '../lib';
import { checkRequiredVersions, generateShrinkwrap, parseYarnLock } from '../lib';

test('generate lock for fixture directory', async () => {
const lockFile = await generateShrinkwrap({
Expand Down Expand Up @@ -86,3 +86,58 @@ test('fail when requires cannot be satisfied', async () => {

expect(() => checkRequiredVersions(lockFile)).toThrow(/NPM will not respect/);
});

test('generate lock for berry fixture directory', async () => {
const lockFile = await generateShrinkwrap({
packageJsonFile: path.join(__dirname, 'test-fixture-berry', 'jsii', 'package.json'),
hoist: false,
});

expect(lockFile).toEqual({
lockfileVersion: 1,
name: 'jsii',
requires: true,
version: '1.1.1',
dependencies: {
'cdk': {
version: '2.2.2',
},
'aws-cdk': {
integrity: '10/banana',
requires: {
'aws-cdk-lib': '^2.3.4',
},
resolved: 'aws-cdk@npm:1.2.999',
version: '1.2.999',
},
'aws-cdk-lib': {
integrity: '10-pineapple',
resolved: 'aws-cdk-lib@npm:2.3.999',
version: '2.3.999',
},
},
});
});

test('parseBerryLockfile converts berry format to classic YarnLock', () => {
const berryContent = [
'__metadata:',
' version: 8',
'',
'"foo@npm:^1.0.0":',
' version: 1.2.3',
' resolution: "foo@npm:1.2.3"',
' checksum: 10-abc123',
' languageName: node',
' linkType: hard',
'',
].join('\n');

const result = parseYarnLock(berryContent);
expect(result.type).toBe('success');
expect(result.object['foo@^1.0.0']).toEqual({
version: '1.2.3',
resolved: 'foo@npm:1.2.3',
integrity: '10-abc123',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!node_modules
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test fixtures should not be affected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "cdk",
"version": "2.2.2"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "jsii",
"version": "1.1.1",
"dependencies": {
"aws-cdk": "^1.2.3",
"cdk": "2.2.2"
}
}
22 changes: 22 additions & 0 deletions packages/@aws-cdk/yarn-cling/test/test-fixture-berry/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 8
cacheKey: 10

"aws-cdk@npm:^1.2.3":
version: 1.2.999
resolution: "aws-cdk@npm:1.2.999"
dependencies:
aws-cdk-lib: "npm:^2.3.4"
checksum: 10/banana
languageName: node
linkType: hard

"aws-cdk-lib@npm:^2.3.4":
version: 2.3.999
resolution: "aws-cdk-lib@npm:2.3.999"
checksum: 10-pineapple
languageName: node
linkType: hard
3 changes: 3 additions & 0 deletions packages/@aws-cdk/yarn-cling/test/test-fixture/yarn.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new library uses the header comment to detect v1 lockfiles. I think this is a safe change to make in favor of dropping another dependency. Yarn Classic generated lockfiles will have the header.


"aws-cdk@^1.2.3":
version "1.2.999"
resolved "https://registry.bla.com/stuff"
Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/yarn-cling/tsconfig.dev.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/@aws-cdk/yarn-cling/tsconfig.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading