Skip to content

Commit

Permalink
feat: add deeplinks to our documentation
Browse files Browse the repository at this point in the history
These changes will only start to work once our documentation is updated
to support this feature:
- facebook/react-native-website#3618
- facebook/react-native-website#3619
- facebook/react-native-website#3620
  • Loading branch information
blakef committed Mar 10, 2023
1 parent 8e8f51a commit 7fd7884
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 12 deletions.
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
],
plugins: [
[require.resolve('@babel/plugin-transform-modules-commonjs'), {lazy: true}],
'@babel/plugin-proposal-export-namespace-from',
],
sourceMaps: true,
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions packages/cli-config/src/loadConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import path from 'path';
import fs from 'fs';
import semver from 'semver';
import {
UserDependencyConfig,
ProjectConfig,
Expand Down Expand Up @@ -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: [],
Expand All @@ -89,6 +92,24 @@ 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'))
.toString(),
);
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.
}

const finalConfig = Array.from(
new Set([
...Object.keys(userConfig.dependencies),
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-doctor/src/tools/healthchecks/androidSDK.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -182,7 +182,7 @@ export default {

return logManualInstallation({
healthcheck: 'Android SDK',
url: 'https://reactnative.dev/docs/environment-setup',
url: link.docs('environment-setup', 'android-sdk'),
});
},
} as HealthCheckInterface;
4 changes: 3 additions & 1 deletion packages/cli-doctor/src/tools/healthchecks/androidStudio.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {join} from 'path';

import {link} from '@react-native-community/cli-tools';

import {HealthCheckInterface} from '../../types';

import {downloadAndUnzip} from '../downloadAndUnzip';
Expand Down Expand Up @@ -74,7 +76,7 @@ export default {

return logManualInstallation({
healthcheck: 'Android Studio',
url: 'https://reactnative.dev/docs/environment-setup',
url: link.docs('environment-setup', 'android-studio'),
});
},
} as HealthCheckInterface;
5 changes: 4 additions & 1 deletion packages/cli-doctor/src/tools/healthchecks/jdk.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -58,7 +61,7 @@ export default {
loader.fail();
logManualInstallation({
healthcheck: 'JDK',
url: 'https://reactnative.dev/docs/environment-setup',
url: link.docs('environment-setup', 'jdk'),
});
},
} as HealthCheckInterface;
4 changes: 3 additions & 1 deletion packages/cli-doctor/src/tools/healthchecks/ruby.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {link} from '@react-native-community/cli-tools';

import versionRanges from '../versionRanges';
import {doesSoftwareNeedToBeFixed} from '../checkInstallation';
import {HealthCheckInterface} from '../../types';
Expand All @@ -19,7 +21,7 @@ export default {

logManualInstallation({
healthcheck: 'Ruby',
url: 'https://reactnative.dev/docs/environment-setup#ruby',
url: link.docs('environment-setup', 'ruby'),
});
},
} as HealthCheckInterface;
6 changes: 4 additions & 2 deletions packages/cli-doctor/src/tools/installPods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'fs';
import execa from 'execa';
import chalk from 'chalk';
import prompts from 'prompts';
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 {brewInstall} from './brewInstall';
Expand Down Expand Up @@ -45,7 +45,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.`,
);
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/cli-doctor/src/tools/runBundleInstall.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,7 +13,9 @@ async function runBundleInstall(loader: Loader) {
logger.error(error.stderr || error.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.`,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,6 +38,12 @@ export type AndroidProject = NonNullable<Config['project']['android']>;
* Starts the app on a connected Android emulator or device.
*/
async function runAndroid(_argv: Array<string>, config: Config, args: Flags) {
link.setPlatform('android');

if (config.reactNativeVersion !== 'unknown') {
link.setVersion(config.reactNativeVersion);
}

if (args.binaryPath) {
if (args.tasks) {
throw new CLIError(
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-platform-ios/src/commands/runIOS/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +35,12 @@ export interface FlagsT extends BuildFlags {
}

async function runIOS(_: Array<string>, 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?',
Expand Down
44 changes: 44 additions & 0 deletions packages/cli-tools/src/__tests__/doclink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as link from '../doclink';

const mockPlatform = jest.fn();
jest.mock('os', () => ({
platform: mockPlatform,
}));

describe('link', () => {
it('builds a link with the platform and os defined', () => {
mockPlatform.mockReturnValue('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.mockReturnValue('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('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',
),
);
});
});
});
70 changes: 70 additions & 0 deletions packages/cli-tools/src/doclink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 | null = null;

/**
* 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, hash?: string): string {
const url = new URL('https://reactnative.dev/');
url.pathname = version
? `${section}/${version}/${path}`
: `${section}/${path}`;
url.searchParams.set('os', getOS());
url.searchParams.set('platform', platform);
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;
}
1 change: 1 addition & 0 deletions packages/cli-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 4 additions & 1 deletion packages/cli-tools/src/releaseChecker/printNewRelease.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'),
)}".`,
);

Expand Down
1 change: 1 addition & 0 deletions packages/cli-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface DependencyConfig {
export interface Config {
root: string;
reactNativePath: string;
reactNativeVersion: string;
project: ProjectConfig;
dependencies: {
[key: string]: DependencyConfig;
Expand Down
28 changes: 28 additions & 0 deletions scripts/__tests__/helpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const stringLength = require('string-length');

const testsFromStringLengthLibrary = [
['', 0],
['\u001B[1m\u001B[22m', 0],
// countAnsiEscapeCodes: true
//['\u001B[1m\u001B[22m', 9],
['𠀔', 1],
['foo𠁐bar𠀃', 8],
['あ', 1],
['谢', 1],
['🐴', 1],
['𝌆', 1],
['\u001B[1mfoo\u001B[22m', 3],
// countAnsiEscapeCodes: true
//['\u001B[1mfoo\u001B[22m', 12],
['❤️', 1],
['👊🏽', 1],
['🏴󠁧󠁢󠁥󠁮󠁧󠁿❤️谢👪', 4],
['\u001B[1m👩‍👧‍👦°✿\u001B[22m', 3],
// countAnsiEscapeCodes: true
//['\u001B[1m👩‍👧‍👦°✿\u001B[22m', 12],
];
describe('stringLength', () => {
it.each(testsFromStringLengthLibrary)("'%s'.length === %i", (str, length) => {
expect(stringLength(str)).toEqual(length);
});
});
Loading

0 comments on commit 7fd7884

Please sign in to comment.