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

feat: support configuring link mode across plugins #4990

Merged
merged 14 commits into from
Nov 1, 2022
23 changes: 23 additions & 0 deletions .yarn/versions/cc76e97d.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
releases:
"@yarnpkg/cli": minor
"@yarnpkg/plugin-nm": minor
"@yarnpkg/plugin-pnp": minor
"@yarnpkg/plugin-pnpm": minor

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {PortablePath, npath} from '@yarnpkg/fslib';
import cp from 'child_process';
import {exec} from 'node:child_process';
import {promisify} from 'node:util';

export const execPromise = promisify(exec);

interface Options {
cwd: PortablePath;
Expand Down
29 changes: 29 additions & 0 deletions packages/acceptance-tests/pkg-tests-core/sources/utils/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import tarFs from 'tar-fs';
import zlib from 'zlib';
import {Gzip} from 'zlib';

import {execPromise} from './exec';
import * as miscUtils from './misc';

const IS_WIN32 = process.platform === `win32`;
Expand Down Expand Up @@ -172,3 +173,31 @@ export const makeFakeBinary = async (
await exports.writeFile(realTarget, `${header}printf "%s" "${output}"\nexit ${exitCode}\n`);
await xfs.chmodPromise(realTarget, 0o755);
};

export enum FsLinkType {
SYMBOLIC,
NTFS_JUNCTION,
UNKNOWN,
}

export const determineLinkType = async function(path: PortablePath) {
const stats = await xfs.lstatPromise(path);

if (!stats.isSymbolicLink()) return FsLinkType.UNKNOWN;
if (!IS_WIN32) return FsLinkType.SYMBOLIC;

// Must spawn a process to determine link type on Windows (or include native code)
// `dir` the directory, toss lines that start with whitespace (header/footer), check for type of path passed in
const {stdout: dirOutput} = (await execPromise(`dir /al /l`, {shell: `cmd.exe`, cwd: npath.fromPortablePath(ppath.dirname(path))}));
const linkType = new RegExp(`^\\S.*<(?<linkType>.+)>.*\\s${ppath.basename(path)}(?:\\s|$)`, `gm`).exec(dirOutput)?.groups?.linkType;

switch (linkType) {
case `SYMLINK`:
case `SYMLINKD`:
return FsLinkType.SYMBOLIC;
case `JUNCTION`:
return FsLinkType.NTFS_JUNCTION;
default:
return FsLinkType.UNKNOWN;
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {PortablePath, ppath, xfs} from '@yarnpkg/fslib';
import {PortablePath, ppath, npath, xfs} from '@yarnpkg/fslib';

const {
fs: {FsLinkType, determineLinkType},
tests: {testIf},
} = require(`pkg-tests-core`);

describe(`Features`, () => {
describe(`Pnpm Mode `, () => {
Expand Down Expand Up @@ -32,5 +37,95 @@ describe(`Features`, () => {
await getRecursiveDirectoryListing(path);
}),
);

testIf(() => process.platform === `win32`,
`'nodeLinkerFolderLinkMode: symlinks' on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
dependencies: {
[`no-deps`]: `1.0.0`,
},
},
{
nodeLinker: `pnpm`,
nodeLinkerFolderLinkMode: `symlinks`,
},
async ({path, run}) => {
await run(`install`);

const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.SYMBOLIC);
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(packageLinkPath)))).toBeFalsy();
},
),
);

testIf(() => process.platform === `win32`,
`'nodeLinkerFolderLinkMode: classic' on Windows should use junctions in node_modules directories`,
makeTemporaryEnv(
{
dependencies: {
[`no-deps`]: `1.0.0`,
},
},
{
nodeLinker: `pnpm`,
nodeLinkerFolderLinkMode: `classic`,
},
async ({path, run}) => {
await run(`install`);
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.NTFS_JUNCTION);
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeTruthy();
},
),
);

testIf(() => process.platform !== `win32`,
`'nodeLinkerFolderLinkMode: classic' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
dependencies: {
[`no-deps`]: `1.0.0`,
},
},
{
nodeLinker: `pnpm`,
nodeLinkerFolderLinkMode: `classic`,
},
async ({path, run}) => {
await run(`install`);
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
const packageLinkStat = await xfs.lstatPromise(packageLinkPath);

expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
expect(packageLinkStat.isSymbolicLink()).toBeTruthy();
},
),
);

testIf(() => process.platform !== `win32`,
`'nodeLinkerFolderLinkMode: symlinks' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
dependencies: {
[`no-deps`]: `1.0.0`,
},
},
{
nodeLinker: `pnpm`,
nodeLinkerFolderLinkMode: `symlinks`,
},
async ({path, run}) => {
await run(`install`);

const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
const packageLinkStat = await xfs.lstatPromise(packageLinkPath);

expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
expect(packageLinkStat.isSymbolicLink()).toBeTruthy();
},
),
);
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import {xfs, npath, PortablePath, ppath, Filename} from '@yarnpkg/fslib';
import {exec} from 'node:child_process';
import {promisify} from 'node:util';

const execPromise = promisify(exec);

const {
fs: {writeFile, writeJson},
fs: {writeFile, writeJson, FsLinkType, determineLinkType},
tests: {testIf},
} = require(`pkg-tests-core`);

Expand Down Expand Up @@ -1833,14 +1830,14 @@ describe(`Node_Modules`, () => {
);

testIf(() => process.platform === `win32`,
`'nmFolderLinkMode: symlinks' on Windows should use symlinks in node_modules directories`,
`'nodeLinkerFolderLinkMode: symlinks' on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `symlinks`,
nodeLinkerFolderLinkMode: `symlinks`,
},
async ({path, run}) => {
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
Expand All @@ -1849,75 +1846,70 @@ describe(`Node_Modules`, () => {

await run(`install`);

const {stdout: reparsePoints} = await execPromise(`dir ${npath.fromPortablePath(`${path}/node_modules`)} /al /l | findstr "<SYMLINKD>"`, {shell: `cmd.exe`});

expect(reparsePoints).toMatch(`ws1`);
expect(reparsePoints).toMatch(`<SYMLINKD>`);
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(`${path}/node_modules/ws1`)))).toBeFalsy();
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.SYMBOLIC);
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
},
),
);

testIf(() => process.platform === `win32`,
`'nmFolderLinkMode: classic' on Windows should use junctions in node_modules directories`,
`'nodeLinkerFolderLinkMode: classic' on Windows should use junctions in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `classic`,
nodeLinkerFolderLinkMode: `classic`,
},
async ({path, run}) => {
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
name: `ws1`,
});

await run(`install`);

const {stdout: reparsePoints} = await execPromise(`dir ${npath.fromPortablePath(`${path}/node_modules`)} /al /l | findstr "<JUNCTION>"`, {shell: `cmd.exe`});

expect(reparsePoints).toMatch(`ws1`);
expect(reparsePoints).toMatch(`<JUNCTION>`);
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(`${path}/node_modules/ws1`)))).toBeTruthy();
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.NTFS_JUNCTION);
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeTruthy();
},
),
);

testIf(() => process.platform !== `win32`,
`'nmFolderLinkMode: classic' not-on Windows should use symlinks in node_modules directories`,
`'nodeLinkerFolderLinkMode: classic' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `classic`,
nodeLinkerFolderLinkMode: `classic`,
},
async ({path, run}) => {
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
name: `ws1`,
});

await run(`install`);
const ws1Path = npath.toPortablePath(`${path}/node_modules/ws1`);
const ws1Stats = await xfs.lstatPromise(ws1Path);
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
const ws1Stats = await xfs.lstatPromise(packageLinkPath);

expect(ppath.isAbsolute(await xfs.readlinkPromise(ws1Path))).toBeFalsy();
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
expect(ws1Stats.isSymbolicLink()).toBeTruthy();
},
),
);

testIf(() => process.platform !== `win32`,
`'nmFolderLinkMode: symlinks' not-on Windows should use symlinks in node_modules directories`,
`'nodeLinkerFolderLinkMode: symlinks' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `symlinks`,
nodeLinkerFolderLinkMode: `symlinks`,
},
async ({path, run}) => {
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
Expand All @@ -1926,10 +1918,10 @@ describe(`Node_Modules`, () => {

await run(`install`);

const ws1Path = npath.toPortablePath(`${path}/node_modules/ws1`);
const ws1Stats = await xfs.lstatPromise(ws1Path);
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
const ws1Stats = await xfs.lstatPromise(packageLinkPath);

expect(ppath.isAbsolute(await xfs.readlinkPromise(ws1Path))).toBeFalsy();
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
expect(ws1Stats.isSymbolicLink()).toBeTruthy();
},
),
Expand Down
14 changes: 7 additions & 7 deletions packages/gatsby/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -402,13 +402,6 @@
"enum": ["workspaces", "dependencies", "none"],
"default": "none"
},
"nmFolderLinkMode": {
"_package": "@yarnpkg/plugin-nm",
"description": "If set to `classic` Yarn will use symlinks on Linux and MacOS and Windows `junctions` on Windows when linking workspaces into `node_modules` directories. This can result in inconsistent behavior on Windows because `junctions` are always absolute paths while `symlinks` may be relative. Set to `symlinks`, Yarn will utilize symlinks on all platforms which enables links with relative paths paths on Windows.",
"type": "string",
"enum": ["classic", "symlinks"],
"default": "classic"
},
"nmSelfReferences": {
"_package": "@yarnpkg/plugin-nm",
"description": "Defines whether workspaces are allowed to require themselves - results in creation of self-referencing symlinks. This setting can be overriden per-workspace through the [`installConfig.selfReferences` field](/configuration/manifest#installConfig.selfReferences).",
Expand All @@ -432,6 +425,13 @@
"type": "string",
"default": "pnp"
},
"nodeLinkerFolderLinkMode": {
"_package": "@yarnpkg/plugin-pnp",
"description": "If set to `classic` Yarn will use symlinks on Linux and MacOS and Windows `junctions` on Windows when linking workspaces into `node_modules` directories. This can result in inconsistent behavior on Windows because `junctions` are always absolute paths while `symlinks` may be relative. Set to `symlinks`, Yarn will utilize symlinks on all platforms which enables links with relative paths paths on Windows.",
"type": "string",
"enum": ["classic", "symlinks"],
"default": "classic"
},
"npmAlwaysAuth": {
"_package": "@yarnpkg/plugin-npm",
"description": "If true, Yarn will always send the authentication credentials when making a request to the registries. This typically shouldn't be needed.",
Expand Down
Loading