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 16, 2023
1 parent fb78fe8 commit 36d0f5e
Show file tree
Hide file tree
Showing 19 changed files with 254 additions and 13 deletions.
1 change: 1 addition & 0 deletions __e2e__/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`shows up current config without unnecessary output 1`] = `
{
"root": "<<REPLACED_ROOT>>/TestProject",
"reactNativePath": "<<REPLACED_ROOT>>/TestProject/node_modules/react-native",
"reactNativeVersion": "0.71",
"dependencies": {},
"commands": [
{
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Object {
"platforms": Object {},
"project": Object {},
"reactNativePath": "<<REPLACED>>",
"reactNativeVersion": "unknown",
"root": "<<REPLACED>>",
}
`;
Expand Down
22 changes: 22 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,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),
Expand Down
8 changes: 6 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,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;
8 changes: 7 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,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;
9 changes: 8 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,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;
4 changes: 2 additions & 2 deletions packages/cli-doctor/src/tools/healthchecks/ruby.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
6 changes: 4 additions & 2 deletions packages/cli-doctor/src/tools/installPods.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.`,
);
}
}
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 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.`,
);
}

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
56 changes: 56 additions & 0 deletions packages/cli-tools/src/__tests__/doclink.test.ts
Original file line number Diff line number Diff line change
@@ -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',
),
);
});
});
});
106 changes: 106 additions & 0 deletions packages/cli-tools/src/doclink.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 36d0f5e

Please sign in to comment.