diff --git a/__e2e__/__snapshots__/config.test.ts.snap b/__e2e__/__snapshots__/config.test.ts.snap index ab8784d6d0..6b34907ff8 100644 --- a/__e2e__/__snapshots__/config.test.ts.snap +++ b/__e2e__/__snapshots__/config.test.ts.snap @@ -4,6 +4,7 @@ exports[`shows up current config without unnecessary output 1`] = ` { "root": "<>/TestProject", "reactNativePath": "<>/TestProject/node_modules/react-native", + "reactNativeVersion": "0.71", "dependencies": {}, "commands": [ { diff --git a/babel.config.js b/babel.config.js index 04db10e3bf..ab7687b102 100644 --- a/babel.config.js +++ b/babel.config.js @@ -18,6 +18,7 @@ module.exports = { ], plugins: [ [require.resolve('@babel/plugin-transform-modules-commonjs'), {lazy: true}], + '@babel/plugin-proposal-export-namespace-from', ], sourceMaps: true, }; diff --git a/package.json b/package.json index 5deaa60a67..6c211ae0f0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@babel/core": "^7.0.0", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-transform-modules-commonjs": "^7.2.0", "@babel/plugin-transform-runtime": "^7.6.2", "@babel/preset-env": "^7.0.0", diff --git a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap index 20364fbac3..72420a42a3 100644 --- a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap +++ b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap @@ -25,6 +25,7 @@ Object { "platforms": Object {}, "project": Object {}, "reactNativePath": "<>", + "reactNativeVersion": "unknown", "root": "<>", } `; diff --git a/packages/cli-config/src/loadConfig.ts b/packages/cli-config/src/loadConfig.ts index 103dc5b033..f009bbf43a 100644 --- a/packages/cli-config/src/loadConfig.ts +++ b/packages/cli-config/src/loadConfig.ts @@ -1,4 +1,6 @@ import path from 'path'; +import fs from 'fs'; +import semver from 'semver'; import { UserDependencyConfig, ProjectConfig, @@ -65,6 +67,7 @@ function loadConfig(projectRoot: string = findProjectRoot()): Config { ? path.resolve(projectRoot, userConfig.reactNativePath) : resolveReactNativePath(projectRoot); }, + reactNativeVersion: 'unknown', dependencies: userConfig.dependencies, commands: userConfig.commands, healthChecks: [], @@ -89,6 +92,25 @@ function loadConfig(projectRoot: string = findProjectRoot()): Config { }, }; + // Try our best to figure out what version of React Native we're running. This is + // currently being used to get our deeplinks working, so it's only worried with + // the major and minor version. + try { + const {version} = JSON.parse( + fs.readFileSync( + path.join(initialConfig.reactNativePath, 'package.json'), + {encoding: 'utf8'}, + ), + ); + const out = semver.parse(version); + if (out) { + // Retain only these version, since they correspond with our documentation. + initialConfig.reactNativeVersion = `${out.major}.${out.minor}`; + } + } catch (_) { + // We don't seem to be in a well formed project, give up quietly. + } + const finalConfig = Array.from( new Set([ ...Object.keys(userConfig.dependencies), diff --git a/packages/cli-doctor/src/tools/healthchecks/androidSDK.ts b/packages/cli-doctor/src/tools/healthchecks/androidSDK.ts index 5e21763410..65cbee776d 100644 --- a/packages/cli-doctor/src/tools/healthchecks/androidSDK.ts +++ b/packages/cli-doctor/src/tools/healthchecks/androidSDK.ts @@ -1,4 +1,4 @@ -import {findProjectRoot, logger} from '@react-native-community/cli-tools'; +import {findProjectRoot, logger, link} from '@react-native-community/cli-tools'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; @@ -182,7 +182,11 @@ export default { return logManualInstallation({ healthcheck: 'Android SDK', - url: 'https://reactnative.dev/docs/environment-setup', + url: link.docs('environment-setup', { + hash: 'android-sdk', + guide: 'native', + platform: 'android', + }), }); }, } as HealthCheckInterface; diff --git a/packages/cli-doctor/src/tools/healthchecks/androidStudio.ts b/packages/cli-doctor/src/tools/healthchecks/androidStudio.ts index 9c194b4fa2..5cb3dadcd3 100644 --- a/packages/cli-doctor/src/tools/healthchecks/androidStudio.ts +++ b/packages/cli-doctor/src/tools/healthchecks/androidStudio.ts @@ -1,5 +1,7 @@ import {join} from 'path'; +import {link} from '@react-native-community/cli-tools'; + import {HealthCheckInterface} from '../../types'; import {downloadAndUnzip} from '../downloadAndUnzip'; @@ -74,7 +76,11 @@ export default { return logManualInstallation({ healthcheck: 'Android Studio', - url: 'https://reactnative.dev/docs/environment-setup', + url: link.docs('environment-setup', { + hash: 'android-studio', + guide: 'native', + platform: 'android', + }), }); }, } as HealthCheckInterface; diff --git a/packages/cli-doctor/src/tools/healthchecks/jdk.ts b/packages/cli-doctor/src/tools/healthchecks/jdk.ts index 5b3fda5bac..f29c16ec05 100644 --- a/packages/cli-doctor/src/tools/healthchecks/jdk.ts +++ b/packages/cli-doctor/src/tools/healthchecks/jdk.ts @@ -1,4 +1,7 @@ import {join} from 'path'; + +import {link} from '@react-native-community/cli-tools'; + import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; import {HealthCheckInterface} from '../../types'; @@ -58,7 +61,11 @@ export default { loader.fail(); logManualInstallation({ healthcheck: 'JDK', - url: 'https://reactnative.dev/docs/environment-setup', + url: link.docs('environment-setup', { + hash: 'jdk-studio', + guide: 'native', + platform: 'android', + }), }); }, } as HealthCheckInterface; diff --git a/packages/cli-doctor/src/tools/healthchecks/ruby.ts b/packages/cli-doctor/src/tools/healthchecks/ruby.ts index e34e599079..d63fb3e99a 100644 --- a/packages/cli-doctor/src/tools/healthchecks/ruby.ts +++ b/packages/cli-doctor/src/tools/healthchecks/ruby.ts @@ -1,7 +1,7 @@ import execa from 'execa'; import chalk from 'chalk'; -import {logger, findProjectRoot} from '@react-native-community/cli-tools'; +import {logger, findProjectRoot, link} from '@react-native-community/cli-tools'; import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; @@ -174,7 +174,7 @@ export default { logManualInstallation({ healthcheck: 'Ruby', - url: 'https://reactnative.dev/docs/environment-setup#ruby', + url: link.docs('environment-setup', 'ruby'), }); }, } as HealthCheckInterface; diff --git a/packages/cli-doctor/src/tools/installPods.ts b/packages/cli-doctor/src/tools/installPods.ts index 86be2c6e87..47274d92a4 100644 --- a/packages/cli-doctor/src/tools/installPods.ts +++ b/packages/cli-doctor/src/tools/installPods.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import execa from 'execa'; import chalk from 'chalk'; -import {logger, NoopLoader} from '@react-native-community/cli-tools'; +import {logger, NoopLoader, link} from '@react-native-community/cli-tools'; import sudo from 'sudo-prompt'; import runBundleInstall from './runBundleInstall'; import {Loader} from '../types'; @@ -38,7 +38,9 @@ async function runPodInstall( logger.error(stderr); throw new Error( - 'Looks like your iOS environment is not properly set. Please go to https://reactnative.dev/docs/next/environment-setup and follow the React Native CLI QuickStart guide for macOS and iOS.', + `Looks like your iOS environment is not properly set. Please go to ${link.docs( + 'environment-setup', + )} and follow the React Native CLI QuickStart guide for macOS and iOS.`, ); } } diff --git a/packages/cli-doctor/src/tools/runBundleInstall.ts b/packages/cli-doctor/src/tools/runBundleInstall.ts index e2fdea6d05..deca93171f 100644 --- a/packages/cli-doctor/src/tools/runBundleInstall.ts +++ b/packages/cli-doctor/src/tools/runBundleInstall.ts @@ -1,5 +1,5 @@ import execa from 'execa'; -import {logger} from '@react-native-community/cli-tools'; +import {logger, link} from '@react-native-community/cli-tools'; import {Loader} from '../types'; @@ -13,7 +13,9 @@ async function runBundleInstall(loader: Loader) { logger.error((error as any).stderr || (error as any).stdout); throw new Error( - 'Looks like your iOS environment is not properly set. Please go to https://reactnative.dev/docs/next/environment-setup and follow the React Native CLI QuickStart guide for macOS and iOS.', + `Looks like your iOS environment is not properly set. Please go to ${link.docs( + 'environment-setup', + )} and follow the React Native CLI QuickStart guide for macOS and iOS.`, ); } diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 8071ab25ac..c45f7909fa 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -13,7 +13,7 @@ import tryRunAdbReverse from './tryRunAdbReverse'; import tryLaunchAppOnDevice from './tryLaunchAppOnDevice'; import tryInstallAppOnDevice from './tryInstallAppOnDevice'; import getAdbPath from './getAdbPath'; -import {logger, CLIError} from '@react-native-community/cli-tools'; +import {logger, CLIError, link} from '@react-native-community/cli-tools'; import {getAndroidProject} from '../../config/getAndroidProject'; import listAndroidDevices from './listAndroidDevices'; import tryLaunchEmulator from './tryLaunchEmulator'; @@ -38,6 +38,12 @@ export type AndroidProject = NonNullable; * Starts the app on a connected Android emulator or device. */ async function runAndroid(_argv: Array, config: Config, args: Flags) { + link.setPlatform('android'); + + if (config.reactNativeVersion !== 'unknown') { + link.setVersion(config.reactNativeVersion); + } + if (args.binaryPath) { if (args.tasks) { throw new CLIError( diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index a238aabcc5..e434e815cb 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -12,7 +12,7 @@ import fs from 'fs'; import chalk from 'chalk'; import {Config, IOSProjectInfo} from '@react-native-community/cli-types'; import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; -import {logger, CLIError} from '@react-native-community/cli-tools'; +import {logger, CLIError, link} from '@react-native-community/cli-tools'; import {BuildFlags, buildProject} from '../buildIOS/buildProject'; import {iosBuildOptions} from '../buildIOS'; import {Device} from '../../types'; @@ -35,6 +35,12 @@ export interface FlagsT extends BuildFlags { } async function runIOS(_: Array, ctx: Config, args: FlagsT) { + link.setPlatform('ios'); + + if (ctx.reactNativeVersion !== 'unknown') { + link.setVersion(ctx.reactNativeVersion); + } + if (!ctx.project.ios) { throw new CLIError( 'iOS project folder not found. Are you sure this is a React Native project?', diff --git a/packages/cli-tools/src/__tests__/doclink.test.ts b/packages/cli-tools/src/__tests__/doclink.test.ts new file mode 100644 index 0000000000..c6d85b6061 --- /dev/null +++ b/packages/cli-tools/src/__tests__/doclink.test.ts @@ -0,0 +1,56 @@ +import * as link from '../doclink'; + +const mockPlatform = jest.fn().mockReturnValue('darwin'); +jest.mock('os', () => ({ + platform: mockPlatform, +})); + +describe('link', () => { + it('builds a link with the platform and os defined', () => { + mockPlatform.mockReturnValueOnce('darwin'); + link.setPlatform('android'); + + const url = new URL(link.docs('environment-setup')).toString(); + expect(url).toMatch(/os=macos/); + expect(url).toMatch(/platform=android/); + expect(url).toEqual( + expect.stringContaining('https://reactnative.dev/docs/environment-setup'), + ); + + // Handles a change of os + mockPlatform.mockReturnValueOnce('win32'); + expect(link.docs('environment-setup')).toMatch(/os=windows/); + + // Handles a change of platform + link.setPlatform('ios'); + expect(link.docs('environment-setup')).toMatch(/platform=ios/); + }); + + it('preserves anchor-links', () => { + expect(link.docs('environment-setup', 'ruby')).toMatch(/#ruby/); + }); + + describe('overrides', () => { + afterAll(() => link.setVersion(null)); + it.each([ + [{hash: 'ruby'}, /#ruby/], + [{hash: 'ruby', os: 'linux'}, /os=linux/], + [{platform: 'ios'}, /platform=ios/], + [{'extra stuff': 'here?ok'}, /extra\+stuff=here%3Fok/], + ])("link.doc('environment-setup, %o) -> %o", (param, re) => { + expect(link.docs('environment-setup', param)).toMatch(re); + }); + }); + + describe('versions', () => { + afterAll(() => link.setVersion(null)); + it('supports linking to a specific version of React Native', () => { + link.setVersion('0.71'); + expect(link.docs('environment-setup', 'ruby')).toEqual( + expect.stringContaining( + 'https://reactnative.dev/docs/0.71/environment-setup', + ), + ); + }); + }); +}); diff --git a/packages/cli-tools/src/doclink.ts b/packages/cli-tools/src/doclink.ts new file mode 100644 index 0000000000..10d447426f --- /dev/null +++ b/packages/cli-tools/src/doclink.ts @@ -0,0 +1,106 @@ +import os from 'os'; +import assert from 'assert'; + +type Platforms = 'android' | 'ios'; + +function getOS(): string { + // Using os.platform instead of process.platform so we can test more easily. Once jest upgrades + // to ^29.4 we could use process.platforms and jest.replaceProperty(process, 'platforms', 'someplatform'); + switch (os.platform()) { + case 'aix': + case 'freebsd': + case 'linux': + case 'openbsd': + case 'sunos': + // King of controversy, right here. + return 'linux'; + case 'darwin': + return 'macos'; + case 'win32': + return 'windows'; + default: + return ''; + } +} + +let _platform: Platforms = 'android'; +let _version: string | undefined; + +interface Overrides { + os?: string; + platform?: string; + hash?: string; + version?: string; +} + +interface Other { + [key: string]: string; +} + +/** + * Create a deeplink to our documentation based on the user's OS and the Platform they're trying to build. + */ +function doclink( + section: string, + path: string, + hashOrOverrides?: string | (Overrides & Other), +): string { + const url = new URL('https://reactnative.dev/'); + + // Overrides + const isObj = typeof hashOrOverrides === 'object'; + + const hash = isObj ? hashOrOverrides.hash : hashOrOverrides; + const version = + isObj && hashOrOverrides.version ? hashOrOverrides.version : _version; + const OS = isObj && hashOrOverrides.os ? hashOrOverrides.os : getOS(); + const platform = + isObj && hashOrOverrides.platform ? hashOrOverrides.platform : _platform; + + url.pathname = _version + ? `${section}/${version}/${path}` + : `${section}/${path}`; + + url.searchParams.set('os', OS); + url.searchParams.set('platform', platform); + + if (isObj) { + const otherKeys = Object.keys(hashOrOverrides).filter( + (key) => !['hash', 'version', 'os', 'platform'].includes(key), + ); + for (let key of otherKeys) { + url.searchParams.set(key, hashOrOverrides[key]); + } + } + + if (hash) { + assert.doesNotMatch( + hash, + /#/, + "Anchor links should be written withou a '#'", + ); + url.hash = hash; + } + + return url.toString(); +} + +export const docs = doclink.bind(null, 'docs'); +export const contributing = doclink.bind(null, 'contributing'); +export const community = doclink.bind(null, 'community'); +export const showcase = doclink.bind(null, 'showcase'); +export const blog = doclink.bind(null, 'blog'); + +/** + * When the user builds, we should define the target platform globally. + */ +export function setPlatform(target: Platforms): void { + _platform = target; +} + +/** + * Can we figure out what version of react native they're using? + */ +export function setVersion(reactNativeVersion: string): void { + _version = reactNativeVersion; +} diff --git a/packages/cli-tools/src/index.ts b/packages/cli-tools/src/index.ts index 47289a9249..0ba14ace87 100644 --- a/packages/cli-tools/src/index.ts +++ b/packages/cli-tools/src/index.ts @@ -11,5 +11,6 @@ export {default as hookStdout} from './hookStdout'; export {getLoader, NoopLoader, Loader} from './loader'; export {default as findProjectRoot} from './findProjectRoot'; export {default as printRunDoctorTip} from './printRunDoctorTip'; +export * as link from './doclink'; export * from './errors'; diff --git a/packages/cli-tools/src/releaseChecker/printNewRelease.ts b/packages/cli-tools/src/releaseChecker/printNewRelease.ts index bd237d7a25..db1a0821ec 100644 --- a/packages/cli-tools/src/releaseChecker/printNewRelease.ts +++ b/packages/cli-tools/src/releaseChecker/printNewRelease.ts @@ -1,4 +1,7 @@ import chalk from 'chalk'; + +import * as link from '../doclink'; + import logger from '../logger'; import {Release} from './getLatestRelease'; import cacheManager from './releaseCacheManager'; @@ -18,7 +21,7 @@ export default function printNewRelease( logger.info(`Diff: ${chalk.dim.underline(latestRelease.diffUrl)}`); logger.info( `For more info, check out "${chalk.dim.underline( - 'https://reactnative.dev/docs/upgrading', + link.docs('upgrading'), )}".`, ); diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index 312cf2727d..b20e4378a4 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -112,6 +112,7 @@ export interface DependencyConfig { export interface Config { root: string; reactNativePath: string; + reactNativeVersion: string; project: ProjectConfig; dependencies: { [key: string]: DependencyConfig; diff --git a/yarn.lock b/yarn.lock index 714b64c159..fb1d48c699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -429,6 +429,14 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-export-default-from" "^7.18.6" +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" @@ -571,6 +579,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.12.1", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6", "@babel/plugin-syntax-flow@^7.8.3": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1"