fix: warn and skip unsupported versions (#373)

This commit is contained in:
Tommy Nguyen 2021-06-29 13:03:40 +02:00 коммит произвёл GitHub
Родитель d115d0138d
Коммит 9a8486a16e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 205 добавлений и 39 удалений

Просмотреть файл

@ -3615,6 +3615,7 @@ react-native-test-app@../:
chalk "^4.1.0"
prompts "^2.4.0"
rimraf "^3.0.0"
semver "^7.3.5"
yargs "^16.0.0"
react-native-windows@^0.63.32:
@ -3922,7 +3923,7 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.1.3:
semver@^7.1.3, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==

Просмотреть файл

@ -49,6 +49,7 @@
"chalk": "^4.1.0",
"prompts": "^2.4.0",
"rimraf": "^3.0.0",
"semver": "^7.3.5",
"yargs": "^16.0.0"
},
"peerDependencies": {
@ -84,6 +85,7 @@
"@types/node": "^12.0.0",
"@types/prompts": "^2.0.0",
"@types/rimraf": "^3.0.0",
"@types/semver": "^7.3.6",
"eslint": "^7.10.0",
"eslint-plugin-jest": "^24.0.0",
"eslint-plugin-prettier": "^3.1.4",
@ -149,7 +151,9 @@
]
},
"release": {
"branches": ["trunk"],
"branches": [
"trunk"
],
"tagFormat": "${version}"
}
}

Просмотреть файл

@ -70,11 +70,16 @@ module.exports = {
{
name: "init-test-app",
description: "Initializes a new test app project",
func: (argv, config, { destination, name, platform }) => {
require("./scripts/configure").configure({
func: (_argv, _config, { destination, name, platform }) => {
const {
configure,
getTargetReactNativeVersion,
} = require("./scripts/configure");
configure({
name,
packagePath: destination,
testAppPath: __dirname,
targetVersion: getTargetReactNativeVersion(),
platforms: sanitizePlatformChoice(platform),
flatten: true,
force: true,
@ -86,7 +91,7 @@ module.exports = {
name: "--destination [string]",
description:
"Path to the directory where the test app should be created",
default: process.cwd(),
default: require("path").join(process.cwd(), "test-app"),
},
{
name: "--name [string]",

Просмотреть файл

@ -10,6 +10,7 @@
const chalk = require("chalk");
const fs = require("fs");
const path = require("path");
const semver = require("semver");
/**
* @typedef {{ source: string; }} FileCopy;
@ -19,6 +20,7 @@ const path = require("path");
* oldFiles: string[];
* scripts: Record<string, string>;
* dependencies: Record<string, string>;
* getDependencies?: (params: ConfigureParams) => Record<string, string> | undefined;
* }} Configuration;
*
* @typedef {{
@ -35,6 +37,7 @@ const path = require("path");
* name: string;
* packagePath: string;
* testAppPath: string;
* targetVersion: string;
* platforms: Platform[];
* flatten: boolean;
* force: boolean;
@ -177,6 +180,29 @@ function warn(message, tag = "[!]") {
console.warn(chalk.yellow(`${tag} ${message}`));
}
/**
* Returns platform package at target version if it satisfies version range.
* @param {string} packageName
* @param {string} targetVersion
* @param {string} versionRange
* @returns {Record<string, string> | undefined}
*/
function getPlatformPackage(packageName, targetVersion, versionRange) {
const v = semver.coerce(targetVersion);
if (!v) {
throw new Error(`Invalid ${packageName} version: ${targetVersion}`);
}
if (!semver.satisfies(v.version, versionRange)) {
warn(
`${packageName}@${v.major}.${v.minor} cannot be added because it does not exist or is unsupported`
);
return undefined;
}
return { [packageName]: `^${v.major}.${v.minor}.0` };
}
/**
* Returns the appropriate `react-native.config.js` for specified parameters.
* @param {ConfigureParams} params
@ -334,6 +360,7 @@ const getConfig = (() => {
start: "react-native start",
},
dependencies: {},
getDependencies: () => ({}),
},
android: {
files: {
@ -416,6 +443,7 @@ const getConfig = (() => {
"mkdirp dist/res && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist/res",
},
dependencies: {},
getDependencies: () => ({}),
},
ios: {
files: {
@ -445,6 +473,7 @@ const getConfig = (() => {
ios: "react-native run-ios",
},
dependencies: {},
getDependencies: () => ({}),
},
macos: {
files: {
@ -468,8 +497,13 @@ const getConfig = (() => {
"mkdirp dist && react-native bundle --entry-file index.js --platform macos --dev true --bundle-output dist/main.macos.jsbundle --assets-dest dist",
macos: `react-native run-macos --scheme ${name}`,
},
dependencies: {
"react-native-macos": "^0.63.0",
dependencies: {},
getDependencies: ({ targetVersion }) => {
return getPlatformPackage(
"react-native-macos",
targetVersion,
"^0.0.0-0 || >=0.60.0 <0.64"
);
},
},
windows: {
@ -484,8 +518,13 @@ const getConfig = (() => {
"mkdirp dist && react-native bundle --entry-file index.js --platform windows --dev true --bundle-output dist/main.windows.bundle --assets-dest dist",
windows: `react-native run-windows --sln windows/${name}.sln`,
},
dependencies: {
"react-native-windows": "^0.63.0",
dependencies: {},
getDependencies: ({ targetVersion }) => {
return getPlatformPackage(
"react-native-windows",
targetVersion,
"^0.0.0-0 || >=0.62.0 <0.66"
);
},
},
};
@ -502,26 +541,40 @@ const getConfig = (() => {
function gatherConfig(params) {
const { flatten, platforms } = params;
const config = (() => {
if (platforms.length === 1 && flatten) {
return getConfig(params, platforms[0]);
}
const shouldFlatten = platforms.length === 1 && flatten;
return platforms.reduce(
(config, platform) => {
const platformConfig = getConfig(params, platform);
const { getDependencies, ...platformConfig } = getConfig(
params,
platform
);
const dependencies = getDependencies && getDependencies(params);
if (!dependencies) {
return config;
}
return mergeConfig(config, {
...platformConfig,
files: Object.fromEntries(
// Map each file into its platform specific folder, e.g.
// `Podfile` -> `iod/Podfile`
Object.entries(platformConfig.files).map(([filename, content]) => [
path.join(platform, filename),
content,
])
),
oldFiles: platformConfig.oldFiles.map((file) => {
return path.join(platform, file);
}),
dependencies,
files: shouldFlatten
? platformConfig.files
: Object.fromEntries(
// Map each file into its platform specific folder, e.g.
// `Podfile` -> `ios/Podfile`
Object.entries(platformConfig.files).map(
([filename, content]) => [
path.join(platform, filename),
content,
]
)
),
oldFiles: shouldFlatten
? platformConfig.oldFiles
: platformConfig.oldFiles.map((file) => {
return path.join(platform, file);
}),
});
},
/** @type {Configuration} */ ({
@ -532,6 +585,16 @@ function gatherConfig(params) {
})
);
})();
if (
Object.keys(config.scripts).length === 0 &&
Object.keys(config.dependencies).length === 0 &&
Object.keys(config.files).length === 0 &&
config.oldFiles.length === 0
) {
return config;
}
return mergeConfig(config, getConfig(params, "common"));
}
@ -553,6 +616,16 @@ function getAppName(packagePath) {
return "ReactTestApp";
}
/**
* Retrieves the version of React Native to target.
* @returns {string}
*/
function getTargetReactNativeVersion() {
const manifestPath = require.resolve("react-native/package.json");
const { version } = readJSONFile(manifestPath);
return /** @type {string} */ (version);
}
/**
* Returns whether destructive operations will be required.
* @param {string} packagePath
@ -579,11 +652,11 @@ function isDestructive(packagePath, { files, oldFiles }) {
if (modified.length > 0 || removed.length > 0) {
if (modified.length > 0) {
warn("The following files will be overwritten:");
modified.sort().forEach((file) => warn(file, " "));
modified.sort().forEach((file) => warn(file, " "));
}
if (removed.length > 0) {
warn("The following files will be removed:");
removed.sort().forEach((file) => warn(file, " "));
removed.sort().forEach((file) => warn(file, " "));
}
return true;
}
@ -725,6 +798,7 @@ function configure(params) {
if (require.main === module) {
/** @type {Platform[]} */
const platformChoices = ["android", "ios", "macos", "windows"];
const targetVersion = getTargetReactNativeVersion();
require("yargs").usage(
"$0 [options]",
@ -772,6 +846,7 @@ if (require.main === module) {
name: typeof name === "string" && name ? name : getAppName(packagePath),
packagePath,
testAppPath: path.resolve(__dirname, ".."),
targetVersion,
platforms,
flatten,
force,
@ -789,6 +864,8 @@ exports["error"] = error;
exports["gatherConfig"] = gatherConfig;
exports["getAppName"] = getAppName;
exports["getConfig"] = getConfig;
exports["getPlatformPackage"] = getPlatformPackage;
exports["getTargetReactNativeVersion"] = getTargetReactNativeVersion;
exports["isDestructive"] = isDestructive;
exports["isInstalled"] = isInstalled;
exports["join"] = join;

Просмотреть файл

@ -8,6 +8,9 @@
// @ts-check
(async () => {
const { configure, getTargetReactNativeVersion } = require("./configure");
const targetVersion = getTargetReactNativeVersion();
/**
* @type {{
* name?: string;
@ -47,12 +50,12 @@
}
const path = require("path");
const { configure } = require("./configure");
const result = configure({
name,
packagePath,
testAppPath: path.resolve(__dirname, ".."),
targetVersion,
platforms,
flatten: true,
force: true,

Просмотреть файл

@ -10,8 +10,20 @@ describe("getConfig()", () => {
const { mockParams } = require("./mockParams");
const { getConfig } = require("../../scripts/configure");
/**
* Gets the list of dependencies from specified config.
* @param {import("../../scripts/configure").Configuration} config
* @param {import("../../scripts/configure").ConfigureParams} params
* @returns {string[] | undefined}
*/
function getDependencies({ getDependencies }, params) {
const dependencies = getDependencies && getDependencies(params);
return dependencies && Object.keys(dependencies);
}
test("returns common scripts and files", () => {
const config = getConfig(mockParams(), "common");
const params = mockParams();
const config = getConfig(params, "common");
expect(Object.keys(config.files).sort()).toEqual([
".watchmanconfig",
@ -21,11 +33,12 @@ describe("getConfig()", () => {
]);
expect(config.oldFiles).toEqual([]);
expect(Object.keys(config.scripts).sort()).toEqual(["start"]);
expect(Object.keys(config.dependencies)).toEqual([]);
expect(getDependencies(config, params)).toEqual([]);
});
test("returns more common scripts and files when initializing", () => {
const config = getConfig(mockParams({ init: true }), "common");
const params = mockParams({ init: true });
const config = getConfig(params, "common");
expect(Object.keys(config.files).sort()).toEqual([
".watchmanconfig",
@ -39,17 +52,18 @@ describe("getConfig()", () => {
]);
expect(config.oldFiles).toEqual([]);
expect(Object.keys(config.scripts).sort()).toEqual(["start"]);
expect(Object.keys(config.dependencies)).toEqual([]);
expect(getDependencies(config, params)).toEqual([]);
});
test("returns Android specific scripts and additional files", () => {
const config = getConfig(mockParams(), "android");
const params = mockParams();
const config = getConfig(params, "android");
expect(Object.keys(config.scripts).sort()).toEqual([
"android",
"build:android",
]);
expect(Object.keys(config.dependencies)).toEqual([]);
expect(getDependencies(config, params)).toEqual([]);
expect(Object.keys(config.files).sort()).toEqual([
"build.gradle",
"gradle.properties",
@ -63,10 +77,11 @@ describe("getConfig()", () => {
});
test("returns iOS specific scripts and additional files", () => {
const config = getConfig(mockParams(), "ios");
const params = mockParams();
const config = getConfig(params, "ios");
expect(Object.keys(config.scripts).sort()).toEqual(["build:ios", "ios"]);
expect(Object.keys(config.dependencies)).toEqual([]);
expect(getDependencies(config, params)).toEqual([]);
expect(Object.keys(config.files).sort()).toEqual(["Podfile"]);
expect(config.oldFiles.sort()).toEqual([
"Podfile.lock",
@ -77,14 +92,15 @@ describe("getConfig()", () => {
});
test("returns macOS specific scripts and additional files", () => {
const config = getConfig(mockParams(), "macos");
const params = mockParams();
const config = getConfig(params, "macos");
expect(Object.keys(config.scripts).sort()).toEqual([
"build:macos",
"macos",
]);
expect(Object.keys(config.files).sort()).toEqual(["Podfile"]);
expect(Object.keys(config.dependencies)).toEqual(["react-native-macos"]);
expect(getDependencies(config, params)).toEqual(["react-native-macos"]);
expect(config.oldFiles.sort()).toEqual([
"Podfile.lock",
"Pods",
@ -94,13 +110,14 @@ describe("getConfig()", () => {
});
test("returns Windows specific scripts and additional files", () => {
const config = getConfig(mockParams(), "windows");
const params = mockParams();
const config = getConfig(params, "windows");
expect(Object.keys(config.scripts).sort()).toEqual([
"build:windows",
"windows",
]);
expect(Object.keys(config.dependencies)).toEqual(["react-native-windows"]);
expect(getDependencies(config, params)).toEqual(["react-native-windows"]);
expect(Object.keys(config.files).sort()).toEqual([]);
expect(config.oldFiles.sort()).toEqual([
"Test.sln",

Просмотреть файл

@ -0,0 +1,52 @@
//
// Copyright (c) Microsoft Corporation
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//
// @ts-check
describe("getPlatformPackage()", () => {
const { getPlatformPackage } = require("../../scripts/configure");
const consoleWarnSpy = jest.spyOn(global.console, "warn");
const name = "react-native-*";
const versionRange = "^0.0.0-0 || >=0.60.0 <0.64";
afterEach(() => {
consoleWarnSpy.mockReset();
});
afterAll(() => {
jest.clearAllMocks();
});
test("returns dependency when target version is inside range", () => {
["0.0.0-canary", "^0.0.0-canary"].forEach((targetVersion) => {
const pkg = getPlatformPackage(name, targetVersion, versionRange);
expect(pkg).toEqual({ [name]: "^0.0.0" });
expect(consoleWarnSpy).not.toHaveBeenCalled();
});
["0.63", "0.63.4", "^0.63", "^0.63.4"].forEach((targetVersion) => {
const pkg = getPlatformPackage(name, targetVersion, versionRange);
expect(pkg).toEqual({ [name]: "^0.63.0" });
expect(consoleWarnSpy).not.toHaveBeenCalled();
});
});
test("returns `undefined` when target version is outside range", () => {
["0.59", "0.64"].forEach((targetVersion) => {
const pkg = getPlatformPackage(name, targetVersion, versionRange);
expect(pkg).toBeUndefined();
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
consoleWarnSpy.mockReset();
});
});
test("throws if target version is invalid", () => {
expect(() => getPlatformPackage("", "version", "")).toThrow();
});
});

Просмотреть файл

@ -17,6 +17,7 @@ function mockParams(overrides) {
name: "Test",
packagePath: "test",
testAppPath: ".",
targetVersion: "^0.63.4",
platforms: ["android", "ios", "macos", "windows"],
flatten: false,
force: false,

Просмотреть файл

@ -12,6 +12,7 @@
"resolveJsonModule": true
},
"include": [
"react-native.config.js",
"scripts/**/*.js",
"test/**/*.test.js",
"windows/test-app.js"

Просмотреть файл

@ -1782,6 +1782,11 @@
"@types/glob" "*"
"@types/node" "*"
"@types/semver@^7.3.6":
version "7.3.6"
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.6.tgz#e9831776f4512a7ba6da53e71c26e5fb67882d63"
integrity sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"