Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
23 changes: 23 additions & 0 deletions .yarn/versions/2847fa24.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
releases:
"@yarnpkg/cli": major
"@yarnpkg/plugin-essentials": major

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
155 changes: 90 additions & 65 deletions packages/plugin-essentials/sources/commands/set/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export default class SetVersionCommand extends BaseCommand {
static usage: Usage = Command.Usage({
description: `lock the Yarn version used by the project`,
details: `
This command will download a specific release of Yarn directly from the Yarn GitHub repository, will store it inside your project, and will change the \`yarnPath\` settings from your project \`.yarnrc.yml\` file to point to the new file.
This command will set a specific release of Yarn to be used by Corepack: https://nodejs.org/api/corepack.html.

By default it only will set the \`packageManager\` field at the root of your project, but if the referenced release cannot be represented this way, if you already have \`yarnPath\` configured, or if you set the \`--yarn-path\` command line flag, then the release will also be downloaded from the Yarn GitHub repository, stored inside your project, and referenced via the \`yarnPath\` settings from your project \`.yarnrc.yml\` file.

A very good use case for this command is to enforce the version of Yarn used by the any single member of your team inside a same project - by doing this you ensure that you have control on Yarn upgrades and downgrades (including on your deployment servers), and get rid of most of the headaches related to someone using a slightly different version and getting a different behavior than you.

Expand Down Expand Up @@ -68,6 +70,10 @@ export default class SetVersionCommand extends BaseCommand {
]],
});

useYarnPath = Option.Boolean(`--yarn-path`, false, {
description: `Set the yarnPath setting even if the version can be accessed by Corepack`,
});

onlyIfNeeded = Option.Boolean(`--only-if-needed`, false, {
description: `Only lock the Yarn version if it isn't already locked`,
});
Expand All @@ -86,25 +92,33 @@ export default class SetVersionCommand extends BaseCommand {
return `file://${process.argv[1]}`;
};

let bundleUrl: string;
let bundleRef: {
version: string;
url: string;
};

const getRef = (url: string, version: string) => {
return {version, url: url.replace(/\{\}/g, version)};
};

if (this.version === `self`)
bundleUrl = getBundlePath();
bundleRef = {url: getBundlePath(), version: YarnVersion ?? `self`};
else if (this.version === `latest` || this.version === `berry` || this.version === `stable`)
bundleUrl = `https://repo.yarnpkg.com/${await resolveTag(configuration, `stable`)}/packages/yarnpkg-cli/bin/yarn.js`;
bundleRef = getRef(`https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js`, await resolveTag(configuration, `stable`));
else if (this.version === `canary`)
bundleUrl = `https://repo.yarnpkg.com/${await resolveTag(configuration, `canary`)}/packages/yarnpkg-cli/bin/yarn.js`;
bundleRef = getRef(`https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js`, await resolveTag(configuration, `canary`));
else if (this.version === `classic`)
bundleUrl = `https://nightly.yarnpkg.com/latest.js`;
bundleRef = {url: `https://nightly.yarnpkg.com/latest.js`, version: `classic`};
else if (this.version.match(/^https?:/))
bundleUrl = this.version;
bundleRef = {url: this.version, version: `remote`};
else if (this.version.match(/^\.{0,2}[\\/]/) || npath.isAbsolute(this.version))
bundleUrl = `file://${npath.resolve(this.version)}`;
bundleRef = {url: `file://${npath.resolve(this.version)}`, version: `file`};
else if (semverUtils.satisfiesWithPrereleases(this.version, `>=2.0.0`))
bundleUrl = `https://repo.yarnpkg.com/${this.version}/packages/yarnpkg-cli/bin/yarn.js`;
bundleRef = getRef(`https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js`, this.version);
else if (semverUtils.satisfiesWithPrereleases(this.version, `^0.x || ^1.x`))
bundleUrl = `https://github.com/yarnpkg/yarn/releases/download/v${this.version}/yarn-${this.version}.js`;
bundleRef = getRef(`https://github.com/yarnpkg/yarn/releases/download/v{}/yarn-{}.js`, this.version);
else if (semverUtils.validRange(this.version))
bundleUrl = `https://repo.yarnpkg.com/${await resolveRange(configuration, this.version)}/packages/yarnpkg-cli/bin/yarn.js`;
bundleRef = getRef(`https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js`, await resolveRange(configuration, this.version));
else
throw new UsageError(`Invalid version descriptor "${this.version}"`);

Expand All @@ -113,18 +127,19 @@ export default class SetVersionCommand extends BaseCommand {
stdout: this.context.stdout,
includeLogs: !this.context.quiet,
}, async (report: StreamReport) => {
const filePrefix = `file://`;

let bundleBuffer: Buffer;
if (bundleUrl.startsWith(filePrefix)) {
report.reportInfo(MessageName.UNNAMED, `Downloading ${formatUtils.pretty(configuration, bundleUrl, FormatType.URL)}`);
bundleBuffer = await xfs.readFilePromise(npath.toPortablePath(bundleUrl.slice(filePrefix.length)));
} else {
report.reportInfo(MessageName.UNNAMED, `Retrieving ${formatUtils.pretty(configuration, bundleUrl, FormatType.PATH)}`);
bundleBuffer = await httpUtils.get(bundleUrl, {configuration});
}

await setVersion(configuration, null, bundleBuffer, {report});
const fetchBuffer = async () => {
const filePrefix = `file://`;

if (bundleRef.url.startsWith(filePrefix)) {
report.reportInfo(MessageName.UNNAMED, `Retrieving ${formatUtils.pretty(configuration, bundleRef.url, FormatType.PATH)}`);
return await xfs.readFilePromise(npath.toPortablePath(bundleRef.url.slice(filePrefix.length)));
} else {
report.reportInfo(MessageName.UNNAMED, `Downloading ${formatUtils.pretty(configuration, bundleRef.url, FormatType.URL)}`);
return await httpUtils.get(bundleRef.url, {configuration});
}
};

await setVersion(configuration, bundleRef.version, fetchBuffer, {report, useYarnPath: this.useYarnPath});
});

return report.exitCode();
Expand All @@ -150,64 +165,74 @@ export async function resolveTag(configuration: Configuration, request: `stable`
return data.latest[request];
}

export async function setVersion(configuration: Configuration, bundleVersion: string | null, bundleBuffer: Buffer, {report}: {report: Report}) {
if (bundleVersion === null) {
await xfs.mktempPromise(async tmpDir => {
const temporaryPath = ppath.join(tmpDir, `yarn.cjs` as Filename);
await xfs.writeFilePromise(temporaryPath, bundleBuffer);

const {stdout} = await execUtils.execvp(process.execPath, [npath.fromPortablePath(temporaryPath), `--version`], {
cwd: tmpDir,
env: {...process.env, YARN_IGNORE_PATH: `1`},
});

bundleVersion = stdout.trim();
if (!semver.valid(bundleVersion)) {
throw new Error(`Invalid semver version. ${formatUtils.pretty(configuration, `yarn --version`, formatUtils.Type.CODE)} returned:\n${bundleVersion}`);
}
});
}

export async function setVersion(configuration: Configuration, bundleVersion: string, fetchBuffer: () => Promise<Buffer>, {report, useYarnPath}: {report: Report, useYarnPath?: boolean}) {
const projectCwd = configuration.projectCwd ?? configuration.startingCwd;

const releaseFolder = ppath.resolve(projectCwd, `.yarn/releases` as PortablePath);
const absolutePath = ppath.resolve(releaseFolder, `yarn-${bundleVersion}.cjs` as Filename);

const displayPath = ppath.relative(configuration.startingCwd, absolutePath);
const projectPath = ppath.relative(projectCwd, absolutePath);

const isTaggedYarnVersion = miscUtils.isTaggedYarnVersion(bundleVersion);

const yarnPath = configuration.get(`yarnPath`);
const updateConfig = yarnPath === null || yarnPath.startsWith(`${releaseFolder}/`);
const updateFile = yarnPath || !isTaggedYarnVersion || useYarnPath;

report.reportInfo(MessageName.UNNAMED, `Saving the new release in ${formatUtils.pretty(configuration, displayPath, `magenta`)}`);
if (updateFile) {
const bundleBuffer = await fetchBuffer();

await xfs.removePromise(ppath.dirname(absolutePath));
await xfs.mkdirPromise(ppath.dirname(absolutePath), {recursive: true});
if (bundleVersion === null) {
await xfs.mktempPromise(async tmpDir => {
const temporaryPath = ppath.join(tmpDir, `yarn.cjs` as Filename);
await xfs.writeFilePromise(temporaryPath, bundleBuffer);

await xfs.writeFilePromise(absolutePath, bundleBuffer, {mode: 0o755});
const {stdout} = await execUtils.execvp(process.execPath, [npath.fromPortablePath(temporaryPath), `--version`], {
cwd: tmpDir,
env: {...process.env, YARN_IGNORE_PATH: `1`},
});

if (updateConfig) {
await Configuration.updateConfiguration(projectCwd, {
yarnPath: projectPath,
});
bundleVersion = stdout.trim();
if (!semver.valid(bundleVersion)) {
throw new Error(`Invalid semver version. ${formatUtils.pretty(configuration, `yarn --version`, formatUtils.Type.CODE)} returned:\n${bundleVersion}`);
}
});
}

const manifest = (await Manifest.tryFind(projectCwd)) || new Manifest();
report.reportInfo(MessageName.UNNAMED, `Saving the new release in ${formatUtils.pretty(configuration, displayPath, `magenta`)}`);

manifest.packageManager = `yarn@${
bundleVersion && miscUtils.isTaggedYarnVersion(bundleVersion)
? bundleVersion
// If the version isn't tagged, we use the latest stable version as the wrapper
: await resolveTag(configuration, `stable`)
}`;
await xfs.removePromise(ppath.dirname(absolutePath));
await xfs.mkdirPromise(ppath.dirname(absolutePath), {recursive: true});

const data = {};
manifest.exportTo(data);
await xfs.writeFilePromise(absolutePath, bundleBuffer, {mode: 0o755});

const path = ppath.join(projectCwd, Manifest.fileName);
const content = `${JSON.stringify(data, null, manifest.indent)}\n`;
if (!yarnPath || ppath.contains(releaseFolder, yarnPath)) {
await Configuration.updateConfiguration(projectCwd, {
yarnPath: ppath.relative(projectCwd, absolutePath),
});
}
} else {
await xfs.removePromise(ppath.dirname(absolutePath));

await xfs.changeFilePromise(path, content, {
automaticNewlines: true,
await Configuration.updateConfiguration(projectCwd, {
yarnPath: Configuration.deleteProperty,
});
}

const manifest = (await Manifest.tryFind(projectCwd)) || new Manifest();

manifest.packageManager = `yarn@${
bundleVersion && isTaggedYarnVersion
? bundleVersion
// If the version isn't tagged, we use the latest stable version as the wrapper
: await resolveTag(configuration, `stable`)
}`;

const data = {};
manifest.exportTo(data);

const path = ppath.join(projectCwd, Manifest.fileName);
const content = `${JSON.stringify(data, null, manifest.indent)}\n`;

await xfs.changeFilePromise(path, content, {
automaticNewlines: true,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default class SetVersionSourcesCommand extends BaseCommand {
const bundlePath = ppath.resolve(target, `packages/yarnpkg-cli/bundles/yarn.js` as PortablePath);
const bundleBuffer = await xfs.readFilePromise(bundlePath);

await setVersion(configuration, `sources`, bundleBuffer, {
await setVersion(configuration, `sources`, async () => bundleBuffer, {
report,
});

Expand Down
11 changes: 6 additions & 5 deletions packages/yarnpkg-cli/sources/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function main({binaryVersion, pluginConfiguration}: {binaryVersion:
strict: false,
});

const yarnPath: PortablePath = configuration.get(`yarnPath`);
const yarnPath = configuration.get(`yarnPath`);
const ignorePath = configuration.get(`ignorePath`);
const ignoreCwd = configuration.get(`ignoreCwd`);

Expand All @@ -83,15 +83,16 @@ export async function main({binaryVersion, pluginConfiguration}: {binaryVersion:
return Buffer.of();
});

const isSameBinary = async () =>
yarnPath === selfPath ||
const isDifferentBinary = async () =>
yarnPath &&
yarnPath !== selfPath &&
Buffer.compare(...await Promise.all([
tryRead(yarnPath),
tryRead(selfPath),
])) === 0;
])) !== 0;

// Avoid unnecessary spawn when run directly
if (!ignorePath && !ignoreCwd && await isSameBinary()) {
if (!ignorePath && !ignoreCwd && !await isDifferentBinary()) {
process.env.YARN_IGNORE_PATH = `1`;
process.env.YARN_IGNORE_CWD = `1`;

Expand Down
10 changes: 8 additions & 2 deletions packages/yarnpkg-core/sources/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ export type MapConfigurationValue<T extends object> = miscUtils.ToMapValue<T>;
export interface ConfigurationValueMap {
lastUpdateCheck: string | null;

yarnPath: PortablePath;
yarnPath: PortablePath | null;
ignorePath: boolean;
ignoreCwd: boolean;

Expand Down Expand Up @@ -897,6 +897,8 @@ export type FindProjectOptions = {
};

export class Configuration {
public static deleteProperty = Symbol();

public static telemetry: TelemetryManager | null = null;

public startingCwd: PortablePath;
Expand Down Expand Up @@ -1265,7 +1267,11 @@ export class Configuration {
if (currentValue === nextValue)
continue;

replacement[key] = nextValue;
if (nextValue === Configuration.deleteProperty)
delete replacement[key];
else
replacement[key] = nextValue;

patched = true;
}

Expand Down