Skip to content

Commit e3bc0dd

Browse files
authored
feat: determine react-native → template from npm registry data (#2475)
1 parent 4573eca commit e3bc0dd

File tree

7 files changed

+373
-79
lines changed

7 files changed

+373
-79
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {createTemplateUri} from '../version';
2+
import type {Options} from '../types';
3+
4+
const mockGetTemplateVersion = jest.fn();
5+
6+
jest.mock('../../../tools/npm', () => ({
7+
__esModule: true,
8+
getTemplateVersion: (...args) => mockGetTemplateVersion(...args),
9+
}));
10+
11+
const nullOptions = {} as Options;
12+
13+
describe('createTemplateUri', () => {
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
describe('for < 0.75', () => {
18+
it('use react-native for the template', async () => {
19+
expect(await createTemplateUri(nullOptions, '0.74.1')).toEqual(
20+
21+
);
22+
});
23+
it('looks DOES NOT use npm registry data to find the template', () => {
24+
expect(mockGetTemplateVersion).not.toHaveBeenCalled();
25+
});
26+
});
27+
describe('for >= 0.75', () => {
28+
it('use @react-native-community/template for the template', async () => {
29+
// Imagine for React Native 0.75.1, template 1.2.3 was prepared for this version
30+
mockGetTemplateVersion.mockReturnValue('1.2.3');
31+
expect(await createTemplateUri(nullOptions, '0.75.1')).toEqual(
32+
'@react-native-community/[email protected]',
33+
);
34+
});
35+
36+
it('looks at uses npm registry data to find the matching @react-native-community/template', async () => {
37+
await createTemplateUri(nullOptions, '0.75.0');
38+
expect(mockGetTemplateVersion).toHaveBeenCalledWith('0.75.0');
39+
});
40+
});
41+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const TEMPLATE_PACKAGE_COMMUNITY = '@react-native-community/template';
2+
export const TEMPLATE_PACKAGE_LEGACY = 'react-native';
3+
export const TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT =
4+
'react-native-template-typescript';
5+
6+
// This version moved from inlining the template to using @react-native-community/template
7+
export const TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION = '0.75.0';

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

Lines changed: 3 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -38,30 +38,11 @@ import {
3838
import semver from 'semver';
3939
import {executeCommand} from '../../tools/executeCommand';
4040
import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError';
41+
import {createTemplateUri} from './version';
42+
import {TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION} from './constants';
43+
import type {Options} from './types';
4144

4245
const DEFAULT_VERSION = 'latest';
43-
// This version moved from inlining the template to using @react-native-community/template
44-
const TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION = '0.75.0';
45-
const TEMPLATE_PACKAGE_COMMUNITY = '@react-native-community/template';
46-
const TEMPLATE_PACKAGE_LEGACY = 'react-native';
47-
const TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT = 'react-native-template-typescript';
48-
49-
type Options = {
50-
template?: string;
51-
npm?: boolean;
52-
pm?: PackageManager.PackageManager;
53-
directory?: string;
54-
displayName?: string;
55-
title?: string;
56-
skipInstall?: boolean;
57-
version: string;
58-
packageName?: string;
59-
installPods?: string | boolean;
60-
platformName?: string;
61-
skipGitInit?: boolean;
62-
replaceDirectory?: string | boolean;
63-
yarnConfigOptions?: Record<string, string>;
64-
};
6546

6647
interface TemplateOptions {
6748
projectName: string;
@@ -397,63 +378,6 @@ function checkPackageManagerAvailability(
397378
return false;
398379
}
399380

400-
async function createTemplateUri(
401-
options: Options,
402-
version: string,
403-
): Promise<string> {
404-
if (options.platformName && options.platformName !== 'react-native') {
405-
logger.debug('User has specified an out-of-tree platform, using it');
406-
return `${options.platformName}@${version}`;
407-
}
408-
409-
if (options.template === TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT) {
410-
logger.warn(
411-
"Ignoring custom template: 'react-native-template-typescript'. Starting from React Native v0.71 TypeScript is used by default.",
412-
);
413-
return TEMPLATE_PACKAGE_LEGACY;
414-
}
415-
416-
if (options.template) {
417-
logger.debug(`Use the user provided --template=${options.template}`);
418-
return options.template;
419-
}
420-
421-
// 0.75.0-nightly-20240618-5df5ed1a8' -> 0.75.0
422-
// 0.75.0-rc.1 -> 0.75.0
423-
const simpleVersion = semver.coerce(version) ?? version;
424-
425-
// Does the react-native@version package *not* have a template embedded. We know that this applies to
426-
// all version before 0.75. The 1st release candidate is the minimal version that has no template.
427-
const useLegacyTemplate = semver.lt(
428-
simpleVersion,
429-
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
430-
);
431-
432-
logger.debug(
433-
`[template]: is '${version} (${simpleVersion})' < '${TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION}' = ` +
434-
(useLegacyTemplate
435-
? 'yes, look for template in react-native'
436-
: 'no, look for template in @react-native-community/template'),
437-
);
438-
439-
if (!useLegacyTemplate) {
440-
if (/nightly/.test(version)) {
441-
logger.debug(
442-
"[template]: you're using a nightly version of react-native",
443-
);
444-
// Template nightly versions and react-native@nightly versions don't match (template releases at a much
445-
// lower cadence). We have to assume the user is running against the latest nightly by pointing to the tag.
446-
return `${TEMPLATE_PACKAGE_COMMUNITY}@nightly`;
447-
}
448-
return `${TEMPLATE_PACKAGE_COMMUNITY}@${version}`;
449-
}
450-
451-
logger.debug(
452-
`Using the legacy template because '${TEMPLATE_PACKAGE_LEGACY}' still contains a template folder`,
453-
);
454-
return `${TEMPLATE_PACKAGE_LEGACY}@${version}`;
455-
}
456-
457381
async function createProject(
458382
projectName: string,
459383
directory: string,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type {PackageManager} from '../../tools/packageManager';
2+
3+
export type Options = {
4+
template?: string;
5+
npm?: boolean;
6+
pm?: PackageManager;
7+
directory?: string;
8+
displayName?: string;
9+
title?: string;
10+
skipInstall?: boolean;
11+
version: string;
12+
packageName?: string;
13+
installPods?: string | boolean;
14+
platformName?: string;
15+
skipGitInit?: boolean;
16+
replaceDirectory?: string | boolean;
17+
yarnConfigOptions?: Record<string, string>;
18+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {logger} from '@react-native-community/cli-tools';
2+
import {getTemplateVersion} from '../../tools/npm';
3+
import semver from 'semver';
4+
5+
import type {Options} from './types';
6+
import {
7+
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
8+
TEMPLATE_PACKAGE_COMMUNITY,
9+
TEMPLATE_PACKAGE_LEGACY,
10+
TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT,
11+
} from './constants';
12+
13+
export async function createTemplateUri(
14+
options: Options,
15+
version: string,
16+
): Promise<string> {
17+
if (options.platformName && options.platformName !== 'react-native') {
18+
logger.debug('User has specified an out-of-tree platform, using it');
19+
return `${options.platformName}@${version}`;
20+
}
21+
22+
if (options.template === TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT) {
23+
logger.warn(
24+
"Ignoring custom template: 'react-native-template-typescript'. Starting from React Native v0.71 TypeScript is used by default.",
25+
);
26+
return TEMPLATE_PACKAGE_LEGACY;
27+
}
28+
29+
if (options.template) {
30+
logger.debug(`Use the user provided --template=${options.template}`);
31+
return options.template;
32+
}
33+
34+
// 0.75.0-nightly-20240618-5df5ed1a8' -> 0.75.0
35+
// 0.75.0-rc.1 -> 0.75.0
36+
const simpleVersion = semver.coerce(version) ?? version;
37+
38+
// Does the react-native@version package *not* have a template embedded. We know that this applies to
39+
// all version before 0.75. The 1st release candidate is the minimal version that has no template.
40+
const useLegacyTemplate = semver.lt(
41+
simpleVersion,
42+
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
43+
);
44+
45+
logger.debug(
46+
`[template]: is '${version} (${simpleVersion})' < '${TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION}' = ` +
47+
(useLegacyTemplate
48+
? 'yes, look for template in react-native'
49+
: 'no, look for template in @react-native-community/template'),
50+
);
51+
52+
if (!useLegacyTemplate) {
53+
if (/nightly/.test(version)) {
54+
logger.debug(
55+
"[template]: you're using a nightly version of react-native",
56+
);
57+
// Template nightly versions and react-native@nightly versions don't match (template releases at a much
58+
// lower cadence). We have to assume the user is running against the latest nightly by pointing to the tag.
59+
return `${TEMPLATE_PACKAGE_COMMUNITY}@nightly`;
60+
}
61+
const templateVersion = await getTemplateVersion(version);
62+
return `${TEMPLATE_PACKAGE_COMMUNITY}@${templateVersion}`;
63+
}
64+
65+
logger.debug(
66+
`Using the legacy template because '${TEMPLATE_PACKAGE_LEGACY}' still contains a template folder`,
67+
);
68+
return `${TEMPLATE_PACKAGE_LEGACY}@${version}`;
69+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {getTemplateVersion} from '../npm';
2+
import assert from 'assert';
3+
4+
let ref: any;
5+
6+
global.fetch = jest.fn();
7+
8+
function fetchReturn(json: any): void {
9+
assert(global.fetch != null, 'You forgot to backup global.fetch!');
10+
// @ts-ignore
11+
global.fetch = jest.fn(() =>
12+
Promise.resolve({json: () => Promise.resolve(json)}),
13+
);
14+
}
15+
16+
describe('getTemplateVersion', () => {
17+
beforeEach(() => {
18+
ref = global.fetch;
19+
});
20+
afterEach(() => {
21+
global.fetch = ref;
22+
});
23+
24+
it('should order matching versions with the most recent first', async () => {
25+
const VERSION = '0.75.1';
26+
fetchReturn({
27+
versions: {
28+
'3.2.1': {scripts: {version: VERSION}},
29+
'1.0.0': {scripts: {version: '0.75.0'}},
30+
'1.2.3': {scripts: {version: VERSION}},
31+
},
32+
time: {
33+
'3.2.1': '2024-08-15T00:00:00.000Z',
34+
'1.0.0': '2024-08-15T10:10:10.000Z',
35+
'1.2.3': '2024-08-16T00:00:00.000Z', // Last published version
36+
},
37+
});
38+
39+
expect(await getTemplateVersion(VERSION)).toEqual('1.2.3');
40+
});
41+
42+
it('should matching latest MAJOR.MINOR if MAJOR.MINOR.PATCH has no match', async () => {
43+
fetchReturn({
44+
versions: {
45+
'3.2.1': {scripts: {version: '0.75.1'}},
46+
'3.2.2': {scripts: {version: '0.75.2'}},
47+
},
48+
time: {
49+
'3.2.1': '2024-08-15T00:00:00.000Z',
50+
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
51+
},
52+
});
53+
54+
expect(await getTemplateVersion('0.75.3')).toEqual('3.2.2');
55+
});
56+
57+
it('should NOT matching when MAJOR.MINOR is not found', async () => {
58+
fetchReturn({
59+
versions: {
60+
'3.2.1': {scripts: {version: '0.75.1'}},
61+
'3.2.2': {scripts: {version: '0.75.2'}},
62+
},
63+
time: {
64+
'3.2.1': '2024-08-15T00:00:00.000Z',
65+
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
66+
},
67+
});
68+
69+
expect(await getTemplateVersion('0.76.0')).toEqual(undefined);
70+
});
71+
72+
it('ignores packages that have weird script version entries', async () => {
73+
fetchReturn({
74+
versions: {
75+
'1': {},
76+
'2': {scripts: {}},
77+
'3': {scripts: {version: 'echo "not a semver entry"'}},
78+
win: {scripts: {version: '0.75.2'}},
79+
},
80+
time: {
81+
'1': '2024-08-14T00:00:00.000Z',
82+
win: '2024-08-15T00:00:00.000Z',
83+
// These would normally both beat '3' on time:
84+
'2': '2024-08-16T00:00:00.000Z',
85+
'3': '2024-08-16T00:00:00.000Z',
86+
},
87+
});
88+
89+
expect(await getTemplateVersion('0.75.2')).toEqual('win');
90+
});
91+
92+
it('support `version` and `reactNativeVersion` entries from npm', async () => {
93+
fetchReturn({
94+
versions: {
95+
'3.2.1': {scripts: {version: '0.75.1'}},
96+
'3.2.2': {scripts: {reactNativeVersion: '0.75.2'}},
97+
},
98+
time: {
99+
'3.2.1': '2024-08-15T00:00:00.000Z',
100+
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
101+
},
102+
});
103+
104+
expect(await getTemplateVersion('0.75.2')).toEqual('3.2.2');
105+
});
106+
107+
it('prefers `reactNativeVersion` over `version` entries from npm', async () => {
108+
fetchReturn({
109+
versions: {
110+
'3.2.1': {scripts: {version: '0.75.1'}},
111+
'3.2.2': {
112+
scripts: {
113+
reactNativeVersion: '0.75.2',
114+
version: 'should prefer the other one',
115+
},
116+
},
117+
},
118+
time: {
119+
'3.2.1': '2024-08-15T00:00:00.000Z',
120+
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
121+
},
122+
});
123+
124+
expect(await getTemplateVersion('0.75.2')).toEqual('3.2.2');
125+
});
126+
});

0 commit comments

Comments
 (0)