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(plugin-nm): support configuring link mode #4981

Merged
merged 12 commits into from
Oct 25, 2022
Merged
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
23 changes: 23 additions & 0 deletions .yarn/versions/40c30377.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
releases:
"@yarnpkg/cli": minor
"@yarnpkg/plugin-nm": 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-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
107 changes: 107 additions & 0 deletions packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +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},
@@ -1827,4 +1831,107 @@ describe(`Node_Modules`, () => {
]);
}),
);

testIf(() => process.platform === `win32`,
`'nmFolderLinkMode: symlinks' on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `symlinks`,
},
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 "<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();
},
),
);

testIf(() => process.platform === `win32`,
`'nmFolderLinkMode: classic' on Windows should use junctions in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `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();
},
),
);

testIf(() => process.platform !== `win32`,
`'nmFolderLinkMode: classic' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `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);

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

testIf(() => process.platform !== `win32`,
`'nmFolderLinkMode: symlinks' not-on Windows should use symlinks in node_modules directories`,
makeTemporaryEnv(
{
workspaces: [`ws1`],
},
{
nodeLinker: `node-modules`,
nmFolderLinkMode: `symlinks`,
},
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);

expect(ppath.isAbsolute(await xfs.readlinkPromise(ws1Path))).toBeFalsy();
expect(ws1Stats.isSymbolicLink()).toBeTruthy();
},
),
);
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Protocols git: it should resolve a git dependency (git+ssh://git@github.com/yarnpkg/util-deprecate.git#v1.0.1) 1`] = `"util-deprecate@git+ssh://git@github.com/yarnpkg/util-deprecate.git#commit=6e923f7d98a0afbe5b9c7db9d0f0029c1936746c"`;

exports[`Protocols git: it should resolve a git dependency (https://github.com/yarnpkg/util-deprecate.git#b3562c2798507869edb767da869cd7b85487726d) 1`] = `"util-deprecate@https://github.com/yarnpkg/util-deprecate.git#commit=b3562c2798507869edb767da869cd7b85487726d"`;

exports[`Protocols git: it should resolve a git dependency (https://github.com/yarnpkg/util-deprecate.git#master) 1`] = `"util-deprecate@https://github.com/yarnpkg/util-deprecate.git#commit=4bcc600d20e3a53ea27fa52c4d1fc49cc2d0eabb"`;
7 changes: 7 additions & 0 deletions packages/gatsby/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
@@ -402,6 +402,13 @@
"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).",
50 changes: 30 additions & 20 deletions packages/plugin-nm/sources/NodeModulesLinker.ts
Original file line number Diff line number Diff line change
@@ -32,6 +32,12 @@ export enum NodeModulesMode {
HARDLINKS_GLOBAL = `hardlinks-global`,
}

export enum NodeModulesFolderLinkMode {
CLASSIC = `classic`,
SYMLINKS = `symlinks`,
}


export class NodeModulesLinker implements Linker {
private installStateCache: Map<string, Promise<InstallState | null>> = new Map();

@@ -664,21 +670,24 @@ const buildLocationTree = (locatorMap: NodeModulesLocatorMap | null, {skipPrefix
return locationTree;
};

const symlinkPromise = async (srcPath: PortablePath, dstPath: PortablePath) => {
let stats;

try {
if (process.platform === `win32`) {
const symlinkPromise = async (srcPath: PortablePath, dstPath: PortablePath, nmFolderLinkMode: NodeModulesFolderLinkMode) => {
// use junctions on windows if in classic mode
if (process.platform === `win32` && nmFolderLinkMode === NodeModulesFolderLinkMode.CLASSIC) {
let stats;
try {
stats = await xfs.lstatPromise(srcPath);
} catch (e) {
}
} catch (e) {
}

if (process.platform == `win32` && (!stats || stats.isDirectory())) {
await xfs.symlinkPromise(srcPath, dstPath, `junction`);
} else {
await xfs.symlinkPromise(ppath.relative(ppath.dirname(dstPath), srcPath), dstPath);
if (!stats || stats.isDirectory()) {
await xfs.symlinkPromise(srcPath, dstPath, `junction`);
return;
}
// fall through to symlink
}

// use symlink if tests for junction case fail
await xfs.symlinkPromise(ppath.relative(ppath.dirname(dstPath), srcPath), dstPath);
};

async function atomicFileWrite(tmpDir: PortablePath, dstPath: PortablePath, content: Buffer) {
@@ -774,7 +783,7 @@ type DirEntry = {
symlinkTo: PortablePath;
};

const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs, globalHardlinksStore, nmMode, packageChecksum}: {baseFs: FakeFS<PortablePath>, globalHardlinksStore: PortablePath | null, nmMode: {value: NodeModulesMode}, packageChecksum: string | null}) => {
const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs, globalHardlinksStore, nmMode, nmFolderLinkMode, packageChecksum}: {baseFs: FakeFS<PortablePath>, globalHardlinksStore: PortablePath | null, nmMode: {value: NodeModulesMode}, nmFolderLinkMode: NodeModulesFolderLinkMode, packageChecksum: string | null}) => {
await xfs.mkdirPromise(dstDir, {recursive: true});

const getEntriesRecursive = async (relativePath: PortablePath = PortablePath.dot): Promise<Map<PortablePath, DirEntry>> => {
@@ -837,7 +846,7 @@ const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs,
mtimesChanged = true;
}
} else if (entry.kind === DirEntryKind.SYMLINK) {
await symlinkPromise(ppath.resolve(ppath.dirname(dstPath), entry.symlinkTo), dstPath);
await symlinkPromise(ppath.resolve(ppath.dirname(dstPath), entry.symlinkTo), dstPath, nmFolderLinkMode);
}
}

@@ -1052,14 +1061,14 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
const locationTree = buildLocationTree(installState, {skipPrefix: project.cwd});

const addQueue: Array<Promise<void>> = [];
const addModule = async ({srcDir, dstDir, linkType, globalHardlinksStore, nmMode, packageChecksum}: {srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType, globalHardlinksStore: PortablePath | null, nmMode: {value: NodeModulesMode}, packageChecksum: string | null}) => {
const addModule = async ({srcDir, dstDir, linkType, globalHardlinksStore, nmMode, nmFolderLinkMode, packageChecksum}: {srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType, globalHardlinksStore: PortablePath | null, nmMode: {value: NodeModulesMode}, nmFolderLinkMode: NodeModulesFolderLinkMode, packageChecksum: string | null}) => {
const promise: Promise<any> = (async () => {
try {
if (linkType === LinkType.SOFT) {
await xfs.mkdirPromise(ppath.dirname(dstDir), {recursive: true});
await symlinkPromise(ppath.resolve(srcDir), dstDir);
await symlinkPromise(ppath.resolve(srcDir), dstDir, nmFolderLinkMode);
} else {
await copyPromise(dstDir, srcDir, {baseFs, globalHardlinksStore, nmMode, packageChecksum});
await copyPromise(dstDir, srcDir, {baseFs, globalHardlinksStore, nmMode, nmFolderLinkMode, packageChecksum});
}
} catch (e) {
e.message = `While persisting ${srcDir} -> ${dstDir} ${e.message}`;
@@ -1270,6 +1279,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
const reportedProgress = report.reportProgress(progress);
const nmModeSetting = project.configuration.get(`nmMode`);
const nmMode = {value: nmModeSetting};
const nmFolderLinkMode = project.configuration.get(`nmFolderLinkMode`);

try {
// For the first pass we'll only want to install a single copy for each
@@ -1288,7 +1298,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
for (const entry of addList) {
if (entry.linkType === LinkType.SOFT || !persistedLocations.has(entry.srcDir)) {
persistedLocations.set(entry.srcDir, entry.dstDir);
await addModule({...entry, globalHardlinksStore, nmMode, packageChecksum: realLocatorChecksums.get(entry.realLocatorHash) || null});
await addModule({...entry, globalHardlinksStore, nmMode, nmFolderLinkMode, packageChecksum: realLocatorChecksums.get(entry.realLocatorHash) || null});
}
}

@@ -1308,7 +1318,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
await xfs.mkdirPromise(rootNmDirPath, {recursive: true});

const binSymlinks = await createBinSymlinkMap(installState, locationTree, project.cwd, {loadManifest});
await persistBinSymlinks(prevBinSymlinks, binSymlinks, project.cwd);
await persistBinSymlinks(prevBinSymlinks, binSymlinks, project.cwd, nmFolderLinkMode);

await writeInstallState(project, installState, binSymlinks, nmMode, {installChangedByUser});

@@ -1320,7 +1330,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
}
}

async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlinks: BinSymlinkMap, projectCwd: PortablePath) {
async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlinks: BinSymlinkMap, projectCwd: PortablePath, nmFolderLinkMode: NodeModulesFolderLinkMode) {
// Delete outdated .bin folders
for (const location of previousBinSymlinks.keys()) {
if (ppath.contains(projectCwd, location) === null)
@@ -1358,7 +1368,7 @@ async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlink
await cmdShim(npath.fromPortablePath(target), npath.fromPortablePath(symlinkPath), {createPwshFile: false});
} else {
await xfs.removePromise(symlinkPath);
await symlinkPromise(target, symlinkPath);
await symlinkPromise(target, symlinkPath, nmFolderLinkMode);
if (ppath.contains(projectCwd, await xfs.realpathPromise(target)) !== null) {
await xfs.chmodPromise(target, 0o755);
}
23 changes: 17 additions & 6 deletions packages/plugin-nm/sources/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {Hooks, Plugin, SettingsType} from '@yarnpkg/core';
import {xfs} from '@yarnpkg/fslib';
import {NodeModulesHoistingLimits} from '@yarnpkg/nm';
import {Hooks, Plugin, SettingsType} from '@yarnpkg/core';
import {xfs} from '@yarnpkg/fslib';
import {NodeModulesHoistingLimits} from '@yarnpkg/nm';

import {NodeModulesLinker, NodeModulesMode} from './NodeModulesLinker';
import {getGlobalHardlinksStore} from './NodeModulesLinker';
import {PnpLooseLinker} from './PnpLooseLinker';
import {NodeModulesLinker, NodeModulesMode, NodeModulesFolderLinkMode} from './NodeModulesLinker';
import {getGlobalHardlinksStore} from './NodeModulesLinker';
import {PnpLooseLinker} from './PnpLooseLinker';

export {NodeModulesLinker};
export {NodeModulesMode};
export {NodeModulesFolderLinkMode};
export {PnpLooseLinker};

declare module '@yarnpkg/core' {
interface ConfigurationValueMap {
nmHoistingLimits: NodeModulesHoistingLimits;
nmMode: NodeModulesMode;
nmFolderLinkMode: NodeModulesFolderLinkMode;
nmSelfReferences: boolean;
}
}
@@ -46,6 +48,15 @@ const plugin: Plugin<Hooks> = {
],
default: NodeModulesMode.HARDLINKS_LOCAL,
},
nmFolderLinkMode: {
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: SettingsType.STRING,
values: [
NodeModulesFolderLinkMode.CLASSIC,
NodeModulesFolderLinkMode.SYMLINKS,
],
default: NodeModulesFolderLinkMode.CLASSIC,
},
nmSelfReferences: {
description: `If set to 'false' the workspace will not be allowed to require itself and corresponding self-referencing symlink will not be created`,
type: SettingsType.BOOLEAN,