Skip to content
Open
Show file tree
Hide file tree
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
41 changes: 41 additions & 0 deletions packages/cli/src/commands/init/__tests__/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {createTemplateUri} from '../version';
import type {Options} from '../types';

const mockGetTemplateVersion = jest.fn();

jest.mock('../../../tools/npm', () => ({
__esModule: true,
getTemplateVersion: (...args) => mockGetTemplateVersion(...args),
}));

const nullOptions = {} as Options;

describe('createTemplateUri', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('for < 0.75', () => {
it('use react-native for the template', async () => {
expect(await createTemplateUri(nullOptions, '0.74.1')).toEqual(
'[email protected]',
);
});
it('looks DOES NOT use npm registry data to find the template', () => {
expect(mockGetTemplateVersion).not.toHaveBeenCalled();
});
});
describe('for >= 0.75', () => {
it('use @react-native-community/template for the template', async () => {
// Imagine for React Native 0.75.1, template 1.2.3 was prepared for this version
mockGetTemplateVersion.mockReturnValue('1.2.3');
expect(await createTemplateUri(nullOptions, '0.75.1')).toEqual(
'@react-native-community/[email protected]',
);
});

it('looks at uses npm registry data to find the matching @react-native-community/template', async () => {
await createTemplateUri(nullOptions, '0.75.0');
expect(mockGetTemplateVersion).toHaveBeenCalledWith('0.75.0');
});
});
});
7 changes: 7 additions & 0 deletions packages/cli/src/commands/init/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const TEMPLATE_PACKAGE_COMMUNITY = '@react-native-community/template';
export const TEMPLATE_PACKAGE_LEGACY = 'react-native';
export const TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT =
'react-native-template-typescript';

// This version moved from inlining the template to using @react-native-community/template
export const TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION = '0.75.0';
82 changes: 3 additions & 79 deletions packages/cli/src/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,11 @@ import {
import semver from 'semver';
import {executeCommand} from '../../tools/executeCommand';
import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError';
import {createTemplateUri} from './version';
import {TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION} from './constants';
import type {Options} from './types';

const DEFAULT_VERSION = 'latest';
// This version moved from inlining the template to using @react-native-community/template
const TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION = '0.75.0';
const TEMPLATE_PACKAGE_COMMUNITY = '@react-native-community/template';
const TEMPLATE_PACKAGE_LEGACY = 'react-native';
const TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT = 'react-native-template-typescript';

type Options = {
template?: string;
npm?: boolean;
pm?: PackageManager.PackageManager;
directory?: string;
displayName?: string;
title?: string;
skipInstall?: boolean;
version: string;
packageName?: string;
installPods?: string | boolean;
platformName?: string;
skipGitInit?: boolean;
replaceDirectory?: string | boolean;
yarnConfigOptions?: Record<string, string>;
};

interface TemplateOptions {
projectName: string;
Expand Down Expand Up @@ -397,63 +378,6 @@ function checkPackageManagerAvailability(
return false;
}

async function createTemplateUri(
options: Options,
version: string,
): Promise<string> {
if (options.platformName && options.platformName !== 'react-native') {
logger.debug('User has specified an out-of-tree platform, using it');
return `${options.platformName}@${version}`;
}

if (options.template === TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT) {
logger.warn(
"Ignoring custom template: 'react-native-template-typescript'. Starting from React Native v0.71 TypeScript is used by default.",
);
return TEMPLATE_PACKAGE_LEGACY;
}

if (options.template) {
logger.debug(`Use the user provided --template=${options.template}`);
return options.template;
}

// 0.75.0-nightly-20240618-5df5ed1a8' -> 0.75.0
// 0.75.0-rc.1 -> 0.75.0
const simpleVersion = semver.coerce(version) ?? version;

// Does the react-native@version package *not* have a template embedded. We know that this applies to
// all version before 0.75. The 1st release candidate is the minimal version that has no template.
const useLegacyTemplate = semver.lt(
simpleVersion,
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
);

logger.debug(
`[template]: is '${version} (${simpleVersion})' < '${TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION}' = ` +
(useLegacyTemplate
? 'yes, look for template in react-native'
: 'no, look for template in @react-native-community/template'),
);

if (!useLegacyTemplate) {
if (/nightly/.test(version)) {
logger.debug(
"[template]: you're using a nightly version of react-native",
);
// Template nightly versions and react-native@nightly versions don't match (template releases at a much
// lower cadence). We have to assume the user is running against the latest nightly by pointing to the tag.
return `${TEMPLATE_PACKAGE_COMMUNITY}@nightly`;
}
return `${TEMPLATE_PACKAGE_COMMUNITY}@${version}`;
}

logger.debug(
`Using the legacy template because '${TEMPLATE_PACKAGE_LEGACY}' still contains a template folder`,
);
return `${TEMPLATE_PACKAGE_LEGACY}@${version}`;
}

async function createProject(
projectName: string,
directory: string,
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/commands/init/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {PackageManager} from '../../tools/packageManager';

export type Options = {
template?: string;
npm?: boolean;
pm?: PackageManager;
directory?: string;
displayName?: string;
title?: string;
skipInstall?: boolean;
version: string;
packageName?: string;
installPods?: string | boolean;
platformName?: string;
skipGitInit?: boolean;
replaceDirectory?: string | boolean;
yarnConfigOptions?: Record<string, string>;
};
69 changes: 69 additions & 0 deletions packages/cli/src/commands/init/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {logger} from '@react-native-community/cli-tools';
import {getTemplateVersion} from '../../tools/npm';
import semver from 'semver';

import type {Options} from './types';
import {
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
TEMPLATE_PACKAGE_COMMUNITY,
TEMPLATE_PACKAGE_LEGACY,
TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT,
} from './constants';

export async function createTemplateUri(
options: Options,
version: string,
): Promise<string> {
if (options.platformName && options.platformName !== 'react-native') {
logger.debug('User has specified an out-of-tree platform, using it');
return `${options.platformName}@${version}`;
}

if (options.template === TEMPLATE_PACKAGE_LEGACY_TYPESCRIPT) {
logger.warn(
"Ignoring custom template: 'react-native-template-typescript'. Starting from React Native v0.71 TypeScript is used by default.",
);
return TEMPLATE_PACKAGE_LEGACY;
}

if (options.template) {
logger.debug(`Use the user provided --template=${options.template}`);
return options.template;
}

// 0.75.0-nightly-20240618-5df5ed1a8' -> 0.75.0
// 0.75.0-rc.1 -> 0.75.0
const simpleVersion = semver.coerce(version) ?? version;

// Does the react-native@version package *not* have a template embedded. We know that this applies to
// all version before 0.75. The 1st release candidate is the minimal version that has no template.
const useLegacyTemplate = semver.lt(
simpleVersion,
TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION,
);

logger.debug(
`[template]: is '${version} (${simpleVersion})' < '${TEMPLATE_COMMUNITY_REACT_NATIVE_VERSION}' = ` +
(useLegacyTemplate
? 'yes, look for template in react-native'
: 'no, look for template in @react-native-community/template'),
);

if (!useLegacyTemplate) {
if (/nightly/.test(version)) {
logger.debug(
"[template]: you're using a nightly version of react-native",
);
// Template nightly versions and react-native@nightly versions don't match (template releases at a much
// lower cadence). We have to assume the user is running against the latest nightly by pointing to the tag.
return `${TEMPLATE_PACKAGE_COMMUNITY}@nightly`;
}
const templateVersion = await getTemplateVersion(version);
return `${TEMPLATE_PACKAGE_COMMUNITY}@${templateVersion}`;
}

logger.debug(
`Using the legacy template because '${TEMPLATE_PACKAGE_LEGACY}' still contains a template folder`,
);
return `${TEMPLATE_PACKAGE_LEGACY}@${version}`;
}
126 changes: 126 additions & 0 deletions packages/cli/src/tools/__tests__/npm-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {getTemplateVersion} from '../npm';
import assert from 'assert';

let ref: any;

global.fetch = jest.fn();

function fetchReturn(json: any): void {
assert(global.fetch != null, 'You forgot to backup global.fetch!');
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({json: () => Promise.resolve(json)}),
);
}

describe('getTemplateVersion', () => {
beforeEach(() => {
ref = global.fetch;
});
afterEach(() => {
global.fetch = ref;
});

it('should order matching versions with the most recent first', async () => {
const VERSION = '0.75.1';
fetchReturn({
versions: {
'3.2.1': {scripts: {version: VERSION}},
'1.0.0': {scripts: {version: '0.75.0'}},
'1.2.3': {scripts: {version: VERSION}},
},
time: {
'3.2.1': '2024-08-15T00:00:00.000Z',
'1.0.0': '2024-08-15T10:10:10.000Z',
'1.2.3': '2024-08-16T00:00:00.000Z', // Last published version
},
});

expect(await getTemplateVersion(VERSION)).toEqual('1.2.3');
});

it('should matching latest MAJOR.MINOR if MAJOR.MINOR.PATCH has no match', async () => {
fetchReturn({
versions: {
'3.2.1': {scripts: {version: '0.75.1'}},
'3.2.2': {scripts: {version: '0.75.2'}},
},
time: {
'3.2.1': '2024-08-15T00:00:00.000Z',
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
},
});

expect(await getTemplateVersion('0.75.3')).toEqual('3.2.2');
});

it('should NOT matching when MAJOR.MINOR is not found', async () => {
fetchReturn({
versions: {
'3.2.1': {scripts: {version: '0.75.1'}},
'3.2.2': {scripts: {version: '0.75.2'}},
},
time: {
'3.2.1': '2024-08-15T00:00:00.000Z',
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
},
});

expect(await getTemplateVersion('0.76.0')).toEqual(undefined);
});

it('ignores packages that have weird script version entries', async () => {
fetchReturn({
versions: {
'1': {},
'2': {scripts: {}},
'3': {scripts: {version: 'echo "not a semver entry"'}},
win: {scripts: {version: '0.75.2'}},
},
time: {
'1': '2024-08-14T00:00:00.000Z',
win: '2024-08-15T00:00:00.000Z',
// These would normally both beat '3' on time:
'2': '2024-08-16T00:00:00.000Z',
'3': '2024-08-16T00:00:00.000Z',
},
});

expect(await getTemplateVersion('0.75.2')).toEqual('win');
});

it('support `version` and `reactNativeVersion` entries from npm', async () => {
fetchReturn({
versions: {
'3.2.1': {scripts: {version: '0.75.1'}},
'3.2.2': {scripts: {reactNativeVersion: '0.75.2'}},
},
time: {
'3.2.1': '2024-08-15T00:00:00.000Z',
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
},
});

expect(await getTemplateVersion('0.75.2')).toEqual('3.2.2');
});

it('prefers `reactNativeVersion` over `version` entries from npm', async () => {
fetchReturn({
versions: {
'3.2.1': {scripts: {version: '0.75.1'}},
'3.2.2': {
scripts: {
reactNativeVersion: '0.75.2',
version: 'should prefer the other one',
},
},
},
time: {
'3.2.1': '2024-08-15T00:00:00.000Z',
'3.2.2': '2024-08-16T00:00:00.000Z', // Last published version
},
});

expect(await getTemplateVersion('0.75.2')).toEqual('3.2.2');
});
});
Loading