diff --git a/README.md b/README.md index 4aa31bf09..79f8425b0 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,9 @@ Note that the first 2 steps need to be done only once. Alternatively, the app can be started on `iOS` by running `yarn ios`. -If this is the first time running, you will also need to run `pod install --repo-update` from the `react-native/ReactNativeFlipperExample/ios` folder. +If this is the first time running, you will also need to run +`pod install --repo-update` from the +`react-native/ReactNativeFlipperExample/ios` folder. ## JS SDK + Sample React app diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 1a974dca4..64d7c5b68 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -56,6 +56,7 @@ test('Correct top level API exposed', () => { "createState", "createTablePlugin", "getFlipperLib", + "path", "produce", "renderReactRoot", "sleep", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index b95b78ec3..5752dd7f1 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -129,6 +129,8 @@ export { export {createTablePlugin} from './utils/createTablePlugin'; export {textContent} from './utils/textContent'; +import * as path from './utils/path'; +export {path}; // It's not ideal that this exists in flipper-plugin sources directly, // but is the least pain for plugin authors. diff --git a/desktop/flipper-plugin/src/utils/path.ts b/desktop/flipper-plugin/src/utils/path.ts new file mode 100644 index 000000000..e9a35e25b --- /dev/null +++ b/desktop/flipper-plugin/src/utils/path.ts @@ -0,0 +1,285 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// Partial clone of the POSIX part of https://github.com/nodejs/node/blob/master/lib/path.js +// Docs are copied from https://github.com/nodejs/node/blob/master/doc/api/path.md + +const CHAR_DOT = 46; +const CHAR_FORWARD_SLASH = 47; + +function isPosixPathSeparator(code: number) { + return code === CHAR_FORWARD_SLASH; +} + +// Resolves . and .. elements in a path with directory names +function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code: number) => boolean, +): string { + let res = ''; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code = 0; + for (let i = 0; i <= path.length; ++i) { + if (i < path.length) code = path.charCodeAt(i); + else if (isPathSeparator(code)) break; + else code = CHAR_FORWARD_SLASH; + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CHAR_DOT || + res.charCodeAt(res.length - 2) !== CHAR_DOT + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ''; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length !== 0) { + res = ''; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + res += res.length > 0 ? `${separator}..` : '..'; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) + res += `${separator}${path.slice(lastSlash + 1, i)}`; + else res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CHAR_DOT && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + +/** + * The path.join() method joins all given path segments together using the platform-specific separator as a delimiter, then normalizes the resulting path. + * Zero-length path segments are ignored. If the joined path string is a zero-length string then '.' will be returned, representing the current working directory. + * + * @example + * + * path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); + * Returns: '/foo/bar/baz/asdf' + */ +export function join(...args: string[]): string { + if (args.length === 0) return '.'; + let joined; + for (let i = 0; i < args.length; ++i) { + const arg = args[i]; + if (arg.length > 0) { + if (joined === undefined) joined = arg; + else joined += `/${arg}`; + } + } + if (joined === undefined) return '.'; + return normalize(joined); +} + +/** + * The path.normalize() method normalizes the given path, resolving '..' and '.' segments. + * When multiple, sequential path segment separation characters are found (e.g. /), they are replaced by a single instance of /. Trailing separators are preserved. + * If the path is a zero-length string, '.' is returned, representing the current working directory. + * + * @example + * path.normalize('/foo/bar//baz/asdf/quux/..'); + * Returns: '/foo/bar/baz/asdf' + */ +export function normalize(path: string): string { + if (path.length === 0) return '.'; + + const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; + const trailingSeparator = + path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH; + + // Normalize the path + path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator); + + if (path.length === 0) { + if (isAbsolute) return '/'; + return trailingSeparator ? './' : '.'; + } + if (trailingSeparator) path += '/'; + + return isAbsolute ? `/${path}` : path; +} + +/** + * The path.extname() method returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last portion of the path. If there is no . in the last portion of the path, or if there are no . characters other than the first character of the basename of path (see path.basename()) , an empty string is returned. + * + * @example + * path.extname('index.html'); + * Returns: '.html' + * + * path.extname('index.coffee.md'); + * Returns: '.md' + * + * path.extname('index.'); + * Returns: '.' + * + * path.extname('index'); + * Returns: '' + * + * path.extname('.index'); + * Returns: '' + * + * path.extname('.index.md'); + * Returns: '.md' + */ +export function extname(path: string): string { + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ''; + } + return path.slice(startDot, end); +} + +/** + * The path.basename() method returns the last portion of a path, similar to the Unix basename command. Trailing directory separators are ignored. + * + * @example + * path.basename('/foo/bar/baz/asdf/quux.html'); + * Returns: 'quux.html' + * + * path.basename('/foo/bar/baz/asdf/quux.html', '.html'); + * Returns: 'quux' + */ +export function basename(path: string, ext?: string) { + let start = 0; + let end = -1; + let matchedSlash = true; + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext === path) return ''; + let extIdx = ext.length - 1; + let firstNonSlashEnd = -1; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path.length; + return path.slice(start, end); + } + for (let i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) return ''; + return path.slice(start, end); +} diff --git a/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx b/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx index fd3d3aa40..eb67d4985 100644 --- a/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx +++ b/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx @@ -10,7 +10,7 @@ import type {CrashLog} from './index'; import fs from 'fs-extra'; import os from 'os'; -import path from 'path'; +import {path} from 'flipper-plugin'; import {UNKNOWN_CRASH_REASON} from './crash-utils'; export function parseIosCrash(content: string) { diff --git a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx index 53004ae02..15154b4d8 100644 --- a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx +++ b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx @@ -8,7 +8,7 @@ */ import fs from 'fs'; -import path from 'path'; +import {path} from 'flipper-plugin'; import {AppMatchPattern} from '../types'; import {Device, getFlipperLib} from 'flipper-plugin'; diff --git a/desktop/plugins/public/network/__tests__/chunks.node.tsx b/desktop/plugins/public/network/__tests__/chunks.node.tsx index 4c9dde3fa..161d0586e 100644 --- a/desktop/plugins/public/network/__tests__/chunks.node.tsx +++ b/desktop/plugins/public/network/__tests__/chunks.node.tsx @@ -8,10 +8,9 @@ */ import {combineBase64Chunks} from '../chunks'; -import {TestUtils} from 'flipper-plugin'; +import {TestUtils, path} from 'flipper-plugin'; import * as NetworkPlugin from '../index'; import {assembleChunksIfResponseIsComplete} from '../chunks'; -import path from 'path'; import {Base64} from 'js-base64'; import * as fs from 'fs'; import {promisify} from 'util'; diff --git a/desktop/plugins/public/network/__tests__/encoding.node.tsx b/desktop/plugins/public/network/__tests__/encoding.node.tsx index ed8677deb..f9307ecda 100644 --- a/desktop/plugins/public/network/__tests__/encoding.node.tsx +++ b/desktop/plugins/public/network/__tests__/encoding.node.tsx @@ -8,12 +8,11 @@ */ import {readFile} from 'fs'; -import path from 'path'; import {decodeBody, isTextual} from '../utils'; import {ResponseInfo} from '../types'; import {promisify} from 'util'; import {readFileSync} from 'fs'; -import {TestUtils} from 'flipper-plugin'; +import {TestUtils, path} from 'flipper-plugin'; import * as NetworkPlugin from '../index'; async function createMockResponse( diff --git a/desktop/plugins/public/reactdevtools/index.tsx b/desktop/plugins/public/reactdevtools/index.tsx index db1546a51..bd11bdc31 100644 --- a/desktop/plugins/public/reactdevtools/index.tsx +++ b/desktop/plugins/public/reactdevtools/index.tsx @@ -16,13 +16,13 @@ import { useValue, sleep, Toolbar, + path, } from 'flipper-plugin'; import React from 'react'; import getPort from 'get-port'; import {Button, message, Switch, Typography} from 'antd'; import child_process from 'child_process'; import fs from 'fs'; -import path from 'path'; import {DevToolsEmbedder} from './DevToolsEmbedder'; const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node'; diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index c8882a62b..fcd126bad 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -1052,24 +1052,114 @@ renderReactRoot((unmount) => ( )); ``` -## sleep +### sleep Usage: `await sleep(1000)` Creates a promise that automatically resolves after the specified amount of milliseconds. -## timeout +### timeout Usage `await timeout(1000, promise, message?)` -## styled +### styled A convenience re-export of `styled` from [emotion](https://emotion.sh/docs/styled). -## textContent +### textContent Given a string or React element, returns a text representation of that element, that is suitable as plain text. +### path + +A set of utilizities to handle file paths. A subset of Node.js' [path](https://nodejs.org/api/path.html). + + +#### `path.basename(path[, ext])` + +* `path` {string} +* `ext` {string} An optional file extension +* Returns: {string} + +The `path.basename()` method returns the last portion of a `path`, similar to +the Unix `basename` command. Trailing directory separators are ignored. + +```js +path.basename('/foo/bar/baz/asdf/quux.html'); +// Returns: 'quux.html' + +path.basename('/foo/bar/baz/asdf/quux.html', '.html'); +// Returns: 'quux' +``` + +#### `path.extname(path)` + +* `path` {string} +* Returns: {string} + +The `path.extname()` method returns the extension of the `path`, from the last +occurrence of the `.` (period) character to end of string in the last portion of +the `path`. If there is no `.` in the last portion of the `path`, or if +there are no `.` characters other than the first character of +the basename of `path` (see `path.basename()`) , an empty string is returned. + +```js +path.extname('index.html'); +// Returns: '.html' + +path.extname('index.coffee.md'); +// Returns: '.md' + +path.extname('index.'); +// Returns: '.' + +path.extname('index'); +// Returns: '' + +path.extname('.index'); +// Returns: '' + +path.extname('.index.md'); +// Returns: '.md' +``` + +#### `path.join([...paths])` + +* `...paths` {string} A sequence of path segments +* Returns: {string} + +The `path.join()` method joins all given `path` segments together using the +platform-specific separator as a delimiter, then normalizes the resulting path. + +Zero-length `path` segments are ignored. If the joined path string is a +zero-length string then `'.'` will be returned, representing the current +working directory. + +```js +path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); +// Returns: '/foo/bar/baz/asdf' +``` + +#### `path.normalize(path)` + +* `path` {string} +* Returns: {string} + +The `path.normalize()` method normalizes the given `path`, resolving `'..'` and +`'.'` segments. + +When multiple, sequential path segment separation characters are found (e.g. +`/`), they are replaced by a single +instance of `/`. Trailing separators are preserved. + +If the `path` is a zero-length string, `'.'` is returned, representing the +current working directory. + +```js +path.normalize('/foo/bar//baz/asdf/quux/..'); +// Returns: '/foo/bar/baz/asdf' +``` + ## TestUtils The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins.