Skip to content

Commit a5cfebe

Browse files
committed
feat: determine react-native → template from npm registry data
When we trigger a release of the template, we capture the version of react-native it's built with in the scripts.version field. This is accessible for all version of the template. We use this store from the CLI to figure out which template to install from when running `init`. Why? This means that we can effectively decouple our package version from the react-native package version. The 1:1 version mapping was done as a stop-gap in the 0.74 release, because of problems with release candiates and semver ordering. E.g. 0.75.0, 0.75.0-rc1, 0.75.0-rc2, etc... don't order sanely. What does this do? For example, if we discover a bug in the template for 0.75.0, previously we'd have to: - get react-native to run another release (very expensive), or - change the init logic (see 0.75.0-rc1.1 as an example). Now we just release another version of the template with the fix. As long as [email protected] is still set as the version, the cli will pick the latest published version of the template with a matching version. See the commit for more details about exactly how version are picked. [0] https://github.com/react-native-community/template/blob/main/scripts/updateReactNativeVersion.js#L66-L93
1 parent 6a61d5d commit a5cfebe

File tree

7 files changed

+336
-79
lines changed

7 files changed

+336
-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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
});

0 commit comments

Comments
 (0)