Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Correctly parse v3 lockfiles #1793

Merged
merged 4 commits into from
Jun 6, 2022
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: 5 additions & 0 deletions .changeset/kind-llamas-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"modular-scripts": patch
---

Correctly parse v3 lockfiles for `modular analyze`
3 changes: 2 additions & 1 deletion packages/modular-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@svgr/webpack": "5.5.0",
"@types/micromatch": "4.0.2",
"@types/semver-regex": "3.1.0",
"@types/yarnpkg__lockfile": "^1.1.5",
"@yarnpkg/lockfile": "^1.1.0",
"address": "1.2.0",
"babel-jest": "26.6.3",
"babel-preset-react-app": "10.0.1",
Expand Down Expand Up @@ -104,7 +106,6 @@
"semver": "7.3.7",
"semver-regex": "3.1.3",
"shell-quote": "1.7.3",
"snyk-nodejs-lockfile-parser": "^1.38.0",
"source-map-support": "0.5.21",
"strip-ansi": "6.0.0",
"style-loader": "3.3.1",
Expand Down
76 changes: 51 additions & 25 deletions packages/modular-scripts/src/utils/getPackageDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { Project } from 'ts-morph';
import { buildDepTree, LockfileType } from 'snyk-nodejs-lockfile-parser';
import * as lockfile from '@yarnpkg/lockfile';
import * as yaml from 'js-yaml';

import type { CoreProperties, Dependency } from '@schemastore/package';
import getModularRoot from './getModularRoot';
import getLocation from './getLocation';
import getWorkspaceInfo from './getWorkspaceInfo';
import * as logger from './logger';

type DependencyManifest = NonNullable<CoreProperties['dependencies']>;
type LockFileEntries = Record<string, { version: string }>;

const npmPackageMatcher =
/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*/;
Expand Down Expand Up @@ -42,9 +45,8 @@ function getDependenciesFromSource(workspaceLocation: string) {
export async function getPackageDependencies(
target: string,
): Promise<{ manifest: DependencyManifest; resolutions: DependencyManifest }> {
/* This function is based on the assumption that nested package are not supported, so dependencies can be either declared in the
* target's package.json or hoisted up to the workspace root.
*/
// This function is based on the assumption that nested package are not supported, so dependencies can be either declared in the
// target's package.json or hoisted up to the workspace root.
const targetLocation = await getLocation(target);
const workspaceInfo = getWorkspaceInfo();

Expand All @@ -69,26 +71,7 @@ export async function getPackageDependencies(
targetManifest.dependencies,
) as Dependency;

const lockDeps = await buildDepTree(
// Build a dependency tree from the lockfile, using target dependencies and root dependencies in order of specificity
JSON.stringify(
Object.assign(Object.create(null), targetManifest, {
dependencies: Object.assign(
Object.create(null),
rootManifest.dependencies,
targetManifest.dependencies,
) as Dependency,
devDependencies: Object.assign(
Object.create(null),
rootManifest.devDependencies,
targetManifest.devDependencies,
) as Dependency,
}),
),
lockFile,
true,
LockfileType.yarn,
);
const lockDeps = parseYarnLock(lockFile, deps);

/* Get dependencies from package.json (regular), root package.json (hoisted) or pinned version in lockfile (resolution)
* Exclude workspace dependencies. Warn if a dependency is imported in the source code
Expand All @@ -104,7 +87,7 @@ export async function getPackageDependencies(
`Package ${depName} imported in ${target} source but not found in package dependencies or hoisted dependencies - this will prevent you from successfully build, start or move esm-views and will cause an error in the next release of modular`,
);
}
const resolutionVersion = lockDeps.dependencies[depName].version;
const resolutionVersion = lockDeps[depName];
if (!resolutionVersion) {
logger.error(
`Package ${depName} imported in ${target} source but not found in lockfile - this will prevent you from successfully build, start or move esm-views and will cause an error in the next release of modular. Have you installed your dependencies?`,
Expand All @@ -120,3 +103,46 @@ export async function getPackageDependencies(
);
return { manifest, resolutions };
}

function parseYarnLock(lockFile: string, deps: Dependency): Dependency {
return parseYarnLockV1(lockFile, deps) || parseYarnLockV3(lockFile, deps);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we are explicitly catering to yarn 1 and yarn 3, can we add this to isYarnInstalled in startupCheck.ts ?

Copy link
Contributor

Choose a reason for hiding this comment

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

By this, I mean the check for the version of yarn to be explicit with our compatibility

Copy link
Contributor

Choose a reason for hiding this comment

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

Spoke offline and decided to create a separate ticket to resolve yarn version compatibility

}

function parseYarnLockV1(
lockFile: string,
deps: Dependency,
): Dependency | null {
try {
const parsedLockfile = lockfile.parse(lockFile);
return Object.entries(deps).reduce<Record<string, string>>(
(acc, [name, version]) => {
acc[name] = (parsedLockfile.object as LockFileEntries)[
`${name}@${version}`
].version;
return acc;
},
{},
);
} catch (e) {
return null;
}
}

function parseYarnLockV3(lockFile: string, deps: Dependency): Dependency {
const dependencyArray = Object.entries(deps);
// This function loops over all the dependency ranges listed in the lockfile and tries to match the given dependencies with an exact version.
return Object.entries(
yaml.load(lockFile) as LockFileEntries,
).reduce<Dependency>((acc, [name, { version }]) => {
// Yarn v3 lockfile comes with keys like "'yargs@npm:^15.0.2, yargs@npm:^15.1.0, yargs@npm:^15.3.1, yargs@npm:^15.4.1'" - split them
const entryDependencies = name.split(', ');
for (const [dependencyName, dependencyVersion] of dependencyArray) {
if (
entryDependencies.includes(`${dependencyName}@npm:${dependencyVersion}`)
) {
acc[dependencyName] = version;
}
}
return acc;
}, {});
}
Loading