Skip to content

Commit f22be6d

Browse files
feat: use Yarn v3 with nodeLinker: node-modules for new projects (#2134)
* feat: use Yarn v3 when creating new project * fix(ci): use `YARN_ENABLE_IMMUTABLE_INSTALLS` when installing packages * feat: add to config `nmHoistingLimits: workspaces` * chore: add more context on why we should specify explicit version * feat: skip bumping yarn version if project is inside git work tree * fix: remove condition for yarn classic when bumping version * fix(e2e): update yarn specific files
1 parent c70db3d commit f22be6d

File tree

7 files changed

+129
-45
lines changed

7 files changed

+129
-45
lines changed

__e2e__/init.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ function createCustomTemplateFiles() {
2222

2323
const customTemplateCopiedFiles = [
2424
'.git',
25+
'.yarn',
26+
'.yarnrc.yml',
2527
'dir',
2628
'file',
2729
'node_modules',
@@ -155,9 +157,10 @@ test('init uses npm as the package manager with --npm', () => {
155157

156158
const initDirPath = path.join(DIR, PROJECT_NAME);
157159

158-
// Remove yarn.lock and node_modules
160+
// Remove yarn specific files and node_modules
159161
const filteredFiles = customTemplateCopiedFiles.filter(
160-
(file) => !['yarn.lock', 'node_modules'].includes(file),
162+
(file) =>
163+
!['yarn.lock', 'node_modules', '.yarnrc.yml', '.yarn'].includes(file),
161164
);
162165

163166
// Add package-lock.json

jest/helpers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export function runCLI(
2828
return spawnScript(process.execPath, [CLI_PATH, ...(args || [])], {
2929
...options,
3030
cwd: dir,
31+
env: {
32+
YARN_ENABLE_IMMUTABLE_INSTALLS: 'false',
33+
},
3134
});
3235
}
3336

@@ -92,6 +95,7 @@ export const getTempDirectory = (name: string) =>
9295

9396
type SpawnOptions = RunOptions & {
9497
cwd: string;
98+
env?: {[key: string]: string | undefined};
9599
};
96100

97101
type SpawnFunction<T> = (
@@ -117,11 +121,12 @@ function getExecaOptions(options: SpawnOptions) {
117121

118122
const cwd = isRelative ? path.resolve(__dirname, options.cwd) : options.cwd;
119123

120-
const env = Object.assign({}, process.env, {FORCE_COLOR: '0'});
124+
let env = Object.assign({}, process.env, {FORCE_COLOR: '0'}, options.env);
121125

122126
if (options.nodeOptions) {
123127
env.NODE_OPTIONS = options.nodeOptions;
124128
}
129+
125130
if (options.nodePath) {
126131
env.NODE_PATH = options.nodePath;
127132
}

packages/cli/src/commands/init/createGitRepository.ts renamed to packages/cli/src/commands/init/git.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,31 @@ import execa from 'execa';
33
import fs from 'fs';
44
import path from 'path';
55

6-
const createGitRepository = async (folder: string) => {
7-
const loader = getLoader();
8-
6+
export const checkGitInstallation = async (): Promise<boolean> => {
97
try {
108
await execa('git', ['--version'], {stdio: 'ignore'});
9+
return true;
1110
} catch {
12-
loader.fail('Unable to initialize Git repo. `git` not in $PATH.');
13-
return;
11+
return false;
1412
}
13+
};
1514

15+
export const checkIfFolderIsGitRepo = async (
16+
folder: string,
17+
): Promise<boolean> => {
1618
try {
1719
await execa('git', ['rev-parse', '--is-inside-work-tree'], {
1820
stdio: 'ignore',
1921
cwd: folder,
2022
});
21-
loader.succeed(
22-
'New project is already inside of a Git repo, skipping git init.',
23-
);
24-
return;
25-
} catch {}
23+
return true;
24+
} catch {
25+
return false;
26+
}
27+
};
28+
29+
export const createGitRepository = async (folder: string) => {
30+
const loader = getLoader();
2631

2732
loader.start('Initializing Git repository');
2833

@@ -63,5 +68,3 @@ const createGitRepository = async (folder: string) => {
6368
);
6469
}
6570
};
66-
67-
export default createGitRepository;

packages/cli/src/commands/init/init.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ import {getBunVersionIfAvailable} from '../../tools/bun';
2828
import {getNpmVersionIfAvailable} from '../../tools/npm';
2929
import {getYarnVersionIfAvailable} from '../../tools/yarn';
3030
import {createHash} from 'crypto';
31-
import createGitRepository from './createGitRepository';
31+
import {
32+
createGitRepository,
33+
checkGitInstallation,
34+
checkIfFolderIsGitRepo,
35+
} from './git';
36+
import semver from 'semver';
37+
import {executeCommand} from '../../tools/executeCommand';
3238

3339
const DEFAULT_VERSION = 'latest';
3440

@@ -49,6 +55,7 @@ type Options = {
4955

5056
interface TemplateOptions {
5157
projectName: string;
58+
shouldBumpYarnVersion: boolean;
5259
templateUri: string;
5360
npm?: boolean;
5461
pm?: PackageManager.PackageManager;
@@ -64,17 +71,38 @@ interface TemplateReturnType {
6471
didInstallPods?: boolean;
6572
}
6673

74+
// Here we are defining explicit version of Yarn to be used in the new project because in some cases providing `3.x` don't work.
75+
const YARN_VERSION = '3.6.4';
76+
77+
const bumpYarnVersion = async (silent: boolean, root: string) => {
78+
try {
79+
let yarnVersion = semver.parse(getYarnVersionIfAvailable());
80+
81+
if (yarnVersion) {
82+
await executeCommand('yarn', ['set', 'version', YARN_VERSION], {
83+
root,
84+
silent,
85+
});
86+
87+
// React Native doesn't support PnP, so we need to set nodeLinker to node-modules. Read more here: https://github.com/react-native-community/cli/issues/27#issuecomment-1772626767
88+
89+
await executeCommand(
90+
'yarn',
91+
['config', 'set', 'nodeLinker', 'node-modules'],
92+
{root, silent},
93+
);
94+
}
95+
} catch (e) {
96+
logger.debug(e as string);
97+
}
98+
};
99+
67100
function doesDirectoryExist(dir: string) {
68101
return fs.existsSync(dir);
69102
}
70103

71104
async function setProjectDirectory(directory: string) {
72-
if (doesDirectoryExist(directory)) {
73-
throw new DirectoryAlreadyExistsError(directory);
74-
}
75-
76105
try {
77-
fs.mkdirSync(directory, {recursive: true});
78106
process.chdir(directory);
79107
} catch (error) {
80108
throw new CLIError(
@@ -108,6 +136,7 @@ function setEmptyHashForCachedDependencies(projectName: string) {
108136

109137
async function createFromTemplate({
110138
projectName,
139+
shouldBumpYarnVersion,
111140
templateUri,
112141
npm,
113142
pm,
@@ -182,6 +211,11 @@ async function createFromTemplate({
182211
packageName,
183212
});
184213

214+
if (packageManager === 'yarn' && shouldBumpYarnVersion) {
215+
await bumpYarnVersion(false, projectDirectory);
216+
}
217+
218+
loader.succeed();
185219
const {postInitScript} = templateConfig;
186220
if (postInitScript) {
187221
loader.info('Executing post init script ');
@@ -310,12 +344,14 @@ async function createProject(
310344
projectName: string,
311345
directory: string,
312346
version: string,
347+
shouldBumpYarnVersion: boolean,
313348
options: Options,
314349
): Promise<TemplateReturnType> {
315350
const templateUri = createTemplateUri(options, version);
316351

317352
return createFromTemplate({
318353
projectName,
354+
shouldBumpYarnVersion,
319355
templateUri,
320356
npm: options.npm,
321357
pm: options.pm,
@@ -361,6 +397,7 @@ export default (async function initialize(
361397
const version = options.version || DEFAULT_VERSION;
362398

363399
const directoryName = path.relative(root, options.directory || projectName);
400+
const projectFolder = path.join(root, directoryName);
364401

365402
if (options.pm && !checkPackageManagerAvailability(options.pm)) {
366403
logger.error(
@@ -369,16 +406,40 @@ export default (async function initialize(
369406
return;
370407
}
371408

409+
if (doesDirectoryExist(projectFolder)) {
410+
throw new DirectoryAlreadyExistsError(directoryName);
411+
} else {
412+
fs.mkdirSync(projectFolder, {recursive: true});
413+
}
414+
415+
let shouldBumpYarnVersion = true;
416+
let shouldCreateGitRepository = false;
417+
418+
const isGitAvailable = await checkGitInstallation();
419+
420+
if (isGitAvailable) {
421+
const isFolderGitRepo = await checkIfFolderIsGitRepo(projectFolder);
422+
423+
if (isFolderGitRepo) {
424+
shouldBumpYarnVersion = false;
425+
} else {
426+
shouldCreateGitRepository = true; // Initialize git repo after creating project
427+
}
428+
} else {
429+
logger.warn(
430+
'Git is not installed on your system. This might cause some features to work incorrectly.',
431+
);
432+
}
433+
372434
const {didInstallPods} = await createProject(
373435
projectName,
374436
directoryName,
375437
version,
438+
shouldBumpYarnVersion,
376439
options,
377440
);
378441

379-
const projectFolder = path.join(root, directoryName);
380-
381-
if (!options.skipGitInit) {
442+
if (shouldCreateGitRepository && !options.skipGitInit) {
382443
await createGitRepository(projectFolder);
383444
}
384445

packages/cli/src/commands/init/template.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import replacePathSepForRegex from '../../tools/replacePathSepForRegex';
77
import fs from 'fs';
88
import chalk from 'chalk';
99
import {getYarnVersionIfAvailable} from '../../tools/yarn';
10-
import {executeCommand} from '../../tools/packageManager';
10+
import {executeCommand} from '../../tools/executeCommand';
1111

1212
export type TemplateConfig = {
1313
placeholderName: string;
@@ -29,13 +29,24 @@ export async function installTemplatePackage(
2929
root,
3030
});
3131

32-
// React Native doesn't support PnP, so we need to set nodeLinker to node-modules. Read more here: https://github.com/react-native-community/cli/issues/27#issuecomment-1772626767
33-
3432
if (packageManager === 'yarn' && getYarnVersionIfAvailable() !== null) {
35-
executeCommand('yarn', ['config', 'set', 'nodeLinker', 'node-modules'], {
33+
const options = {
3634
root,
3735
silent: true,
38-
});
36+
};
37+
38+
// React Native doesn't support PnP, so we need to set nodeLinker to node-modules. Read more here: https://github.com/react-native-community/cli/issues/27#issuecomment-1772626767
39+
executeCommand(
40+
'yarn',
41+
['config', 'set', 'nodeLinker', 'node-modules'],
42+
options,
43+
);
44+
45+
executeCommand(
46+
'yarn',
47+
['config', 'set', 'nmHoistingLimits', 'workspaces'],
48+
options,
49+
);
3950
}
4051

4152
return PackageManager.install([templateName], {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {logger} from '@react-native-community/cli-tools';
2+
import execa from 'execa';
3+
4+
export function executeCommand(
5+
command: string,
6+
args: Array<string>,
7+
options: {
8+
root: string;
9+
silent?: boolean;
10+
},
11+
) {
12+
return execa(command, args, {
13+
stdio: options.silent && !logger.isVerbose() ? 'pipe' : 'inherit',
14+
cwd: options.root,
15+
});
16+
}

packages/cli/src/tools/packageManager.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import execa from 'execa';
2-
import {logger} from '@react-native-community/cli-tools';
31
import {getYarnVersionIfAvailable, isProjectUsingYarn} from './yarn';
42
import {getBunVersionIfAvailable, isProjectUsingBun} from './bun';
53
import {getNpmVersionIfAvailable, isProjectUsingNpm} from './npm';
4+
import {executeCommand} from './executeCommand';
65

76
export type PackageManager = keyof typeof packageManagers;
87

@@ -65,20 +64,6 @@ function configurePackageManager(
6564
return executeCommand(pm, args, options);
6665
}
6766

68-
export function executeCommand(
69-
command: string,
70-
args: Array<string>,
71-
options: {
72-
root: string;
73-
silent?: boolean;
74-
},
75-
) {
76-
return execa(command, args, {
77-
stdio: options.silent && !logger.isVerbose() ? 'pipe' : 'inherit',
78-
cwd: options.root,
79-
});
80-
}
81-
8267
export function shouldUseYarn(options: Options) {
8368
if (options.packageManager === 'yarn') {
8469
return getYarnVersionIfAvailable();

0 commit comments

Comments
 (0)