Skip to content

Commit 18e1b9c

Browse files
jeff-wishnieJeff Wishniearcanis
authored
feat: support configuring link mode across plugins (#4990)
* move and rename linkerMode config * extract windows link typing * oops, add flags to regex in determineLinkType, and await determineLinkType * add pnpm tests * update pnpm tests to use deps not workspaces * fix regex typo * version * Fix type check error on settings definition. Remove stay console.log Changes to be committed: modified: packages/acceptance-tests/pkg-tests-specs/sources/features/pnpm.test.ts modified: packages/plugin-nm/sources/NodeModulesLinker.ts modified: packages/plugin-pnp/sources/index.ts * Change Windows Link Type setting name * move settings to core, update versions * update tests to enums * more detail in help * Stylistic tweaks Co-authored-by: Jeff Wishnie <[email protected]> Co-authored-by: Maël Nison <[email protected]>
1 parent d6c6f16 commit 18e1b9c

File tree

12 files changed

+262
-101
lines changed

12 files changed

+262
-101
lines changed

.yarn/versions/cc76e97d.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
releases:
2+
"@yarnpkg/core": minor
3+
"@yarnpkg/plugin-nm": minor
4+
"@yarnpkg/plugin-pnpm": minor
5+
6+
declined:
7+
- "@yarnpkg/plugin-compat"
8+
- "@yarnpkg/plugin-constraints"
9+
- "@yarnpkg/plugin-dlx"
10+
- "@yarnpkg/plugin-essentials"
11+
- "@yarnpkg/plugin-exec"
12+
- "@yarnpkg/plugin-file"
13+
- "@yarnpkg/plugin-git"
14+
- "@yarnpkg/plugin-github"
15+
- "@yarnpkg/plugin-http"
16+
- "@yarnpkg/plugin-init"
17+
- "@yarnpkg/plugin-interactive-tools"
18+
- "@yarnpkg/plugin-link"
19+
- "@yarnpkg/plugin-npm"
20+
- "@yarnpkg/plugin-npm-cli"
21+
- "@yarnpkg/plugin-pack"
22+
- "@yarnpkg/plugin-patch"
23+
- "@yarnpkg/plugin-pnp"
24+
- "@yarnpkg/plugin-stage"
25+
- "@yarnpkg/plugin-typescript"
26+
- "@yarnpkg/plugin-version"
27+
- "@yarnpkg/plugin-workspace-tools"
28+
- "@yarnpkg/builder"
29+
- "@yarnpkg/cli"
30+
- "@yarnpkg/doctor"
31+
- "@yarnpkg/extensions"
32+
- "@yarnpkg/nm"
33+
- "@yarnpkg/pnpify"
34+
- "@yarnpkg/sdks"

packages/acceptance-tests/pkg-tests-core/sources/utils/exec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import {PortablePath, npath} from '@yarnpkg/fslib';
22
import cp from 'child_process';
3+
import {exec} from 'node:child_process';
4+
import {promisify} from 'node:util';
5+
6+
export const execPromise = promisify(exec);
37

48
interface Options {
59
cwd: PortablePath;

packages/acceptance-tests/pkg-tests-core/sources/utils/fs.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import tarFs from 'tar-fs';
55
import zlib from 'zlib';
66
import {Gzip} from 'zlib';
77

8+
import {execPromise} from './exec';
89
import * as miscUtils from './misc';
910

1011
const IS_WIN32 = process.platform === `win32`;
@@ -172,3 +173,33 @@ export const makeFakeBinary = async (
172173
await exports.writeFile(realTarget, `${header}printf "%s" "${output}"\nexit ${exitCode}\n`);
173174
await xfs.chmodPromise(realTarget, 0o755);
174175
};
176+
177+
export enum FsLinkType {
178+
SYMBOLIC,
179+
NTFS_JUNCTION,
180+
UNKNOWN,
181+
}
182+
183+
export const determineLinkType = async function(path: PortablePath) {
184+
const stats = await xfs.lstatPromise(path);
185+
186+
if (!stats.isSymbolicLink())
187+
return FsLinkType.UNKNOWN;
188+
if (!IS_WIN32)
189+
return FsLinkType.SYMBOLIC;
190+
191+
// Must spawn a process to determine link type on Windows (or include native code)
192+
// `dir` the directory, toss lines that start with whitespace (header/footer), check for type of path passed in
193+
const {stdout: dirOutput} = (await execPromise(`dir /al /l`, {shell: `cmd.exe`, cwd: npath.fromPortablePath(ppath.dirname(path))}));
194+
const linkType = new RegExp(`^\\S.*<(?<linkType>.+)>.*\\s${ppath.basename(path)}(?:\\s|$)`, `gm`).exec(dirOutput)?.groups?.linkType;
195+
196+
switch (linkType) {
197+
case `SYMLINK`:
198+
case `SYMLINKD`:
199+
return FsLinkType.SYMBOLIC;
200+
case `JUNCTION`:
201+
return FsLinkType.NTFS_JUNCTION;
202+
default:
203+
return FsLinkType.UNKNOWN;
204+
}
205+
};

packages/acceptance-tests/pkg-tests-specs/sources/features/pnpm.test.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import {PortablePath, ppath, xfs} from '@yarnpkg/fslib';
1+
import {WindowsLinkType} from '@yarnpkg/core';
2+
import {PortablePath, ppath, npath, xfs} from '@yarnpkg/fslib';
3+
4+
const {
5+
fs: {FsLinkType, determineLinkType},
6+
tests: {testIf},
7+
} = require(`pkg-tests-core`);
28

39
describe(`Features`, () => {
410
describe(`Pnpm Mode `, () => {
@@ -32,5 +38,95 @@ describe(`Features`, () => {
3238
await getRecursiveDirectoryListing(path);
3339
}),
3440
);
41+
42+
testIf(() => process.platform === `win32`,
43+
`'winLinkType: symlinks' on Windows should use symlinks in node_modules directories`,
44+
makeTemporaryEnv(
45+
{
46+
dependencies: {
47+
[`no-deps`]: `1.0.0`,
48+
},
49+
},
50+
{
51+
nodeLinker: `pnpm`,
52+
winLinkType: WindowsLinkType.SYMLINKS,
53+
},
54+
async ({path, run}) => {
55+
await run(`install`);
56+
57+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
58+
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.SYMBOLIC);
59+
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(packageLinkPath)))).toBeFalsy();
60+
},
61+
),
62+
);
63+
64+
testIf(() => process.platform === `win32`,
65+
`'winLinkType: junctions' on Windows should use junctions in node_modules directories`,
66+
makeTemporaryEnv(
67+
{
68+
dependencies: {
69+
[`no-deps`]: `1.0.0`,
70+
},
71+
},
72+
{
73+
nodeLinker: `pnpm`,
74+
winLinkType: WindowsLinkType.JUNCTIONS,
75+
},
76+
async ({path, run}) => {
77+
await run(`install`);
78+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
79+
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.NTFS_JUNCTION);
80+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeTruthy();
81+
},
82+
),
83+
);
84+
85+
testIf(() => process.platform !== `win32`,
86+
`'winLinkType: junctions' not-on Windows should use symlinks in node_modules directories`,
87+
makeTemporaryEnv(
88+
{
89+
dependencies: {
90+
[`no-deps`]: `1.0.0`,
91+
},
92+
},
93+
{
94+
nodeLinker: `pnpm`,
95+
winLinkType: WindowsLinkType.JUNCTIONS,
96+
},
97+
async ({path, run}) => {
98+
await run(`install`);
99+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
100+
const packageLinkStat = await xfs.lstatPromise(packageLinkPath);
101+
102+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
103+
expect(packageLinkStat.isSymbolicLink()).toBeTruthy();
104+
},
105+
),
106+
);
107+
108+
testIf(() => process.platform !== `win32`,
109+
`'winLinkType: symlinks' not-on Windows should use symlinks in node_modules directories`,
110+
makeTemporaryEnv(
111+
{
112+
dependencies: {
113+
[`no-deps`]: `1.0.0`,
114+
},
115+
},
116+
{
117+
nodeLinker: `pnpm`,
118+
winLinkType: WindowsLinkType.SYMLINKS,
119+
},
120+
async ({path, run}) => {
121+
await run(`install`);
122+
123+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/no-deps`);
124+
const packageLinkStat = await xfs.lstatPromise(packageLinkPath);
125+
126+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
127+
expect(packageLinkStat.isSymbolicLink()).toBeTruthy();
128+
},
129+
),
130+
);
35131
});
36132
});

packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1+
import {WindowsLinkType} from '@yarnpkg/core';
12
import {xfs, npath, PortablePath, ppath, Filename} from '@yarnpkg/fslib';
2-
import {exec} from 'node:child_process';
3-
import {promisify} from 'node:util';
43

5-
const execPromise = promisify(exec);
64

75
const {
8-
fs: {writeFile, writeJson},
6+
fs: {writeFile, writeJson, FsLinkType, determineLinkType},
97
tests: {testIf},
108
} = require(`pkg-tests-core`);
119

@@ -1833,14 +1831,14 @@ describe(`Node_Modules`, () => {
18331831
);
18341832

18351833
testIf(() => process.platform === `win32`,
1836-
`'nmFolderLinkMode: symlinks' on Windows should use symlinks in node_modules directories`,
1834+
`'winLinkType: symlinks' on Windows should use symlinks in node_modules directories`,
18371835
makeTemporaryEnv(
18381836
{
18391837
workspaces: [`ws1`],
18401838
},
18411839
{
18421840
nodeLinker: `node-modules`,
1843-
nmFolderLinkMode: `symlinks`,
1841+
winLinkType: WindowsLinkType.SYMLINKS,
18441842
},
18451843
async ({path, run}) => {
18461844
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
@@ -1849,24 +1847,23 @@ describe(`Node_Modules`, () => {
18491847

18501848
await run(`install`);
18511849

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

1854-
expect(reparsePoints).toMatch(`ws1`);
1855-
expect(reparsePoints).toMatch(`<SYMLINKD>`);
1856-
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(`${path}/node_modules/ws1`)))).toBeFalsy();
1852+
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.SYMBOLIC);
1853+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
18571854
},
18581855
),
18591856
);
18601857

18611858
testIf(() => process.platform === `win32`,
1862-
`'nmFolderLinkMode: classic' on Windows should use junctions in node_modules directories`,
1859+
`'winLinkType: junctions' on Windows should use junctions in node_modules directories`,
18631860
makeTemporaryEnv(
18641861
{
18651862
workspaces: [`ws1`],
18661863
},
18671864
{
18681865
nodeLinker: `node-modules`,
1869-
nmFolderLinkMode: `classic`,
1866+
winLinkType: WindowsLinkType.JUNCTIONS,
18701867
},
18711868
async ({path, run}) => {
18721869
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
@@ -1875,49 +1872,49 @@ describe(`Node_Modules`, () => {
18751872

18761873
await run(`install`);
18771874

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

1880-
expect(reparsePoints).toMatch(`ws1`);
1881-
expect(reparsePoints).toMatch(`<JUNCTION>`);
1882-
expect(ppath.isAbsolute(await xfs.readlinkPromise(npath.toPortablePath(`${path}/node_modules/ws1`)))).toBeTruthy();
1877+
expect(await determineLinkType(packageLinkPath)).toEqual(FsLinkType.NTFS_JUNCTION);
1878+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeTruthy();
18831879
},
18841880
),
18851881
);
18861882

18871883
testIf(() => process.platform !== `win32`,
1888-
`'nmFolderLinkMode: classic' not-on Windows should use symlinks in node_modules directories`,
1884+
`'winLinkType: junctions' not-on Windows should use symlinks in node_modules directories`,
18891885
makeTemporaryEnv(
18901886
{
18911887
workspaces: [`ws1`],
18921888
},
18931889
{
18941890
nodeLinker: `node-modules`,
1895-
nmFolderLinkMode: `classic`,
1891+
winLinkType: WindowsLinkType.JUNCTIONS,
18961892
},
18971893
async ({path, run}) => {
18981894
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
18991895
name: `ws1`,
19001896
});
19011897

19021898
await run(`install`);
1903-
const ws1Path = npath.toPortablePath(`${path}/node_modules/ws1`);
1904-
const ws1Stats = await xfs.lstatPromise(ws1Path);
19051899

1906-
expect(ppath.isAbsolute(await xfs.readlinkPromise(ws1Path))).toBeFalsy();
1900+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
1901+
const ws1Stats = await xfs.lstatPromise(packageLinkPath);
1902+
1903+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
19071904
expect(ws1Stats.isSymbolicLink()).toBeTruthy();
19081905
},
19091906
),
19101907
);
19111908

19121909
testIf(() => process.platform !== `win32`,
1913-
`'nmFolderLinkMode: symlinks' not-on Windows should use symlinks in node_modules directories`,
1910+
`'winLinkType: symlinks' not-on Windows should use symlinks in node_modules directories`,
19141911
makeTemporaryEnv(
19151912
{
19161913
workspaces: [`ws1`],
19171914
},
19181915
{
19191916
nodeLinker: `node-modules`,
1920-
nmFolderLinkMode: `symlinks`,
1917+
winLinkType: WindowsLinkType.SYMLINKS,
19211918
},
19221919
async ({path, run}) => {
19231920
await writeJson(npath.toPortablePath(`${path}/ws1/package.json`), {
@@ -1926,10 +1923,10 @@ describe(`Node_Modules`, () => {
19261923

19271924
await run(`install`);
19281925

1929-
const ws1Path = npath.toPortablePath(`${path}/node_modules/ws1`);
1930-
const ws1Stats = await xfs.lstatPromise(ws1Path);
1926+
const packageLinkPath = npath.toPortablePath(`${path}/node_modules/ws1`);
1927+
const ws1Stats = await xfs.lstatPromise(packageLinkPath);
19311928

1932-
expect(ppath.isAbsolute(await xfs.readlinkPromise(ws1Path))).toBeFalsy();
1929+
expect(ppath.isAbsolute(await xfs.readlinkPromise(packageLinkPath))).toBeFalsy();
19331930
expect(ws1Stats.isSymbolicLink()).toBeTruthy();
19341931
},
19351932
),

packages/gatsby/static/configuration/yarnrc.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,6 @@
402402
"enum": ["workspaces", "dependencies", "none"],
403403
"default": "none"
404404
},
405-
"nmFolderLinkMode": {
406-
"_package": "@yarnpkg/plugin-nm",
407-
"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.",
408-
"type": "string",
409-
"enum": ["classic", "symlinks"],
410-
"default": "classic"
411-
},
412405
"nmSelfReferences": {
413406
"_package": "@yarnpkg/plugin-nm",
414407
"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).",
@@ -432,6 +425,13 @@
432425
"type": "string",
433426
"default": "pnp"
434427
},
428+
"winLinkType": {
429+
"_package": "@yarnpkg/core",
430+
"description": "Applies to Windows only. If set to `junctions` Yarn will use Windows junctions when linking workspaces into `node_modules` directories, which are always absolute paths. If set to `symlinks` Yarn will use symlinks, which will use relative paths, and is consistent with Yarn's behavior on non-Windows platforms. To create symlinks the Windows user running Yarn must have the `create symbolic links` privilege. For this reason Yarn defaults to using junctions.",
431+
"type": "string",
432+
"enum": ["junctions", "symlinks"],
433+
"default": "junctions"
434+
},
435435
"npmAlwaysAuth": {
436436
"_package": "@yarnpkg/plugin-npm",
437437
"description": "If true, Yarn will always send the authentication credentials when making a request to the registries. This typically shouldn't be needed.",

0 commit comments

Comments
 (0)