Create dev-middleware package (#38194)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/38194

## Context

RFC: Decoupling Flipper from React Native core: https://github.com/react-native-community/discussions-and-proposals/pull/641

## Changes

Inits the `react-native/dev-middleware` package. Contains an initial implementation of `/open-debugger`, migrated from 2535dbe234.

## Attribution

This implementation is greatly inspired by `expo/dev-server`: 1120c716f3/packages/%40expo/dev-server/src/JsInspector.ts (L18)

Changelog: [Internal]

Reviewed By: motiz88

Differential Revision: D46283818

fbshipit-source-id: 7b38ad2f6d7346366a7c599d16e289e04b7bd88d
This commit is contained in:
Alex Hunt 2023-07-07 09:22:09 -07:00 committed by Facebook GitHub Bot
parent 8431509407
commit a991ff3837
18 changed files with 740 additions and 1 deletions

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
declare module 'chrome-launcher' {
import typeof fs from 'fs';
import typeof childProcess from 'child_process';
import type {ChildProcess} from 'child_process';
declare export type Options = {
startingUrl?: string,
chromeFlags?: Array<string>,
prefs?: mixed,
port?: number,
handleSIGINT?: boolean,
chromePath?: string,
userDataDir?: string | boolean,
logLevel?: 'verbose' | 'info' | 'error' | 'warn' | 'silent',
ignoreDefaultFlags?: boolean,
connectionPollInterval?: number,
maxConnectionRetries?: number,
envVars?: {[key: string]: ?string},
};
declare export type LaunchedChrome = {
pid: number,
port: number,
process: ChildProcess,
kill: () => void,
};
declare export type ModuleOverrides = {
fs?: fs,
spawn?: childProcess['spawn'],
};
declare class Launcher {
launch(options: Options): Promise<LaunchedChrome>;
}
declare module.exports: Launcher;
}

50
flow-typed/npm/connect_v3.x.x.js vendored Normal file
View File

@ -0,0 +1,50 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
declare module 'connect' {
import type http from 'http';
declare export type ServerHandle = HandleFunction | http.Server;
declare type NextFunction = (err?: mixed) => void;
declare export type NextHandleFunction = (
req: IncomingMessage,
res: http.ServerResponse,
next: NextFunction,
) => void | Promise<void>;
declare export type HandleFunction = NextHandleFunction;
declare export interface IncomingMessage extends http.IncomingMessage {
originalUrl?: http.IncomingMessage['url'];
}
declare export interface Server extends events$EventEmitter {
(req: IncomingMessage, res: http.ServerResponse): void;
use(fn: HandleFunction): Server;
use(route: string, fn: HandleFunction): Server;
listen(
port: number,
hostname?: string,
backlog?: number,
callback?: Function,
): http.Server;
listen(port: number, hostname?: string, callback?: Function): http.Server;
listen(path: string, callback?: Function): http.Server;
listen(handle: any, listeningListener?: Function): http.Server;
}
declare type createServer = () => Server;
declare module.exports: createServer;
}

189
flow-typed/npm/node-fetch_v2.x.x.js vendored Normal file
View File

@ -0,0 +1,189 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
// Modified from flow-typed repo:
// https://github.com/flow-typed/flow-typed/blob/master/definitions/npm/node-fetch_v2.x.x/flow_v0.104.x-/node-fetch_v2.x.x.js
declare module 'node-fetch' {
import type http from 'http';
import type https from 'https';
import type {Readable} from 'stream';
declare type AbortSignal = {
+aborted: boolean,
+onabort: (event?: {...}) => void,
+addEventListener: (name: string, cb: () => mixed) => void,
+removeEventListener: (name: string, cb: () => mixed) => void,
+dispatchEvent: (event: {...}) => void,
...
};
declare class Request mixins Body {
constructor(
input: string | {href: string, ...} | Request,
init?: RequestInit,
): this;
context: RequestContext;
headers: Headers;
method: string;
redirect: RequestRedirect;
referrer: string;
url: string;
// node-fetch extensions
agent: http.Agent | https.Agent;
compress: boolean;
counter: number;
follow: number;
hostname: string;
port: number;
protocol: string;
size: number;
timeout: number;
}
declare type HeaderObject = {[index: string]: string | number, ...};
declare type RequestInit = {|
body?: BodyInit,
headers?: HeaderObject | null,
method?: string,
redirect?: RequestRedirect,
signal?: AbortSignal | null,
// node-fetch extensions
agent?: (URL => http.Agent | https.Agent) | http.Agent | https.Agent | null,
compress?: boolean,
follow?: number,
size?: number,
timeout?: number,
|};
declare interface FetchError extends Error {
// cannot set name due to incompatible extend error
// name: 'FetchError';
type: string;
code: ?number;
errno: ?number;
}
declare interface AbortError extends Error {
// cannot set name due to incompatible extend error
// name: 'AbortError';
type: 'aborted';
}
declare type RequestContext =
| 'audio'
| 'beacon'
| 'cspreport'
| 'download'
| 'embed'
| 'eventsource'
| 'favicon'
| 'fetch'
| 'font'
| 'form'
| 'frame'
| 'hyperlink'
| 'iframe'
| 'image'
| 'imageset'
| 'import'
| 'internal'
| 'location'
| 'manifest'
| 'object'
| 'ping'
| 'plugin'
| 'prefetch'
| 'script'
| 'serviceworker'
| 'sharedworker'
| 'subresource'
| 'style'
| 'track'
| 'video'
| 'worker'
| 'xmlhttprequest'
| 'xslt';
declare type RequestRedirect = 'error' | 'follow' | 'manual';
declare class Headers {
append(name: string, value: string): void;
delete(name: string): void;
forEach(callback: (value: string, name: string) => void): void;
get(name: string): string;
getAll(name: string): Array<string>;
has(name: string): boolean;
raw(): {[k: string]: string[], ...};
set(name: string, value: string): void;
entries(): Iterator<[string, string]>;
keys(): Iterator<string>;
values(): Iterator<string>;
@@iterator(): Iterator<[string, string]>;
}
declare class Body {
buffer(): Promise<Buffer>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
body: stream$Readable;
bodyUsed: boolean;
}
declare class Response mixins Body {
constructor(body?: BodyInit, init?: ResponseInit): this;
clone(): Response;
error(): Response;
redirect(url: string, status: number): Response;
headers: Headers;
ok: boolean;
status: number;
statusText: string;
size: number;
timeout: number;
type: ResponseType;
url: string;
}
declare type ResponseType =
| 'basic'
| 'cors'
| 'default'
| 'error'
| 'opaque'
| 'opaqueredirect';
declare interface ResponseInit {
headers?: HeaderInit;
status: number;
statusText?: string;
}
declare type HeaderInit = Headers | Array<string>;
declare type BodyInit =
| string
| null
| Buffer
| Blob
| Readable
| URLSearchParams;
declare function fetch(
url: string | URL | Request,
init?: RequestInit,
): Promise<Response>;
declare module.exports: typeof fetch;
}

14
flow-typed/npm/temp-dir_2.x.x.js vendored Normal file
View File

@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
declare module 'temp-dir' {
declare module.exports: string;
}

View File

@ -0,0 +1,13 @@
{
"presets": [
"@babel/preset-flow",
[
"@babel/preset-env",
{
"targets": {
"node": "16"
}
}
]
]
}

5
packages/dev-middleware/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Dependencies
/node_modules
# Build output
/dist

View File

@ -0,0 +1,11 @@
# @react-native/dev-middleware
![https://img.shields.io/npm/v/@react-native/dev-middleware?color=brightgreen&label=npm%20package](https://www.npmjs.com/package/@react-native/dev-middleware)
Dev server middleware supporting core React Native development features. This package is preconfigured in all React Native projects.
## Endpoints
### `/open-debugger`
Open the JavaScript debugger for a given CDP target (direct Hermes debugging).

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
export * from './src';

View File

@ -0,0 +1,43 @@
{
"name": "@react-native/dev-middleware",
"version": "0.73.0",
"description": "Dev server middleware for React Native",
"keywords": [
"react-native",
"tools"
],
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/dev-middleware#readme",
"bugs": "https://github.com/facebook/react-native/issues",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react-native.git",
"directory": "packages/dev-middleware"
},
"license": "MIT",
"exports": "./dist/index.js",
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && babel src --out-dir dist",
"dev": "babel src --out-dir dist --source-maps --watch",
"clean": "rimraf dist",
"prepare": "yarn build"
},
"dependencies": {
"chrome-launcher": "^0.15.2",
"connect": "^3.6.5",
"node-fetch": "^2.2.0",
"temp-dir": "^2.0.0"
},
"devDependencies": {
"@babel/cli": "^7.20.0",
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/preset-flow": "^7.20.0",
"rimraf": "^3.0.2"
},
"engines": {
"node": ">=18"
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import type {NextHandleFunction} from 'connect';
import type {Logger} from './types/Logger';
import connect from 'connect';
import openDebuggerMiddleware from './middleware/openDebuggerMiddleware';
type Options = $ReadOnly<{
logger?: Logger,
}>;
export default function createDevMiddleware({logger}: Options = {}): {
middleware: NextHandleFunction,
} {
const middleware = connect().use(
'/open-debugger',
openDebuggerMiddleware({logger}),
);
return {middleware};
}

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
export {default as createDevMiddleware} from './createDevMiddleware';

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import type {LaunchedChrome} from 'chrome-launcher';
import type {NextHandleFunction} from 'connect';
import type {IncomingMessage, ServerResponse} from 'http';
import type {Logger} from '../types/Logger';
import url from 'url';
import getDevServerUrl from '../utils/getDevServerUrl';
import launchChromeDevTools from '../utils/launchChromeDevTools';
import queryInspectorTargets from '../utils/queryInspectorTargets';
const debuggerInstances = new Map<string, LaunchedChrome>();
type Options = $ReadOnly<{
logger?: Logger,
}>;
/**
* Open the JavaScript debugger for a given CDP target (direct Hermes debugging).
*
* Currently supports Hermes targets, opening debugger websocket URL in Chrome
* DevTools.
*
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export default function openDebuggerMiddleware({
logger,
}: Options): NextHandleFunction {
return async (
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void,
) => {
if (req.method === 'POST') {
const {query} = url.parse(req.url, true);
const {appId} = query;
if (typeof appId !== 'string') {
res.writeHead(400);
res.end();
return;
}
const targets = await queryInspectorTargets(getDevServerUrl(req));
const target = targets.find(_target => _target.description === appId);
if (!target) {
res.writeHead(404);
res.end('Unable to find Chrome DevTools inspector target');
logger?.warn(
'No compatible apps connected. JavaScript debugging can only be used with the Hermes engine.',
);
return;
}
try {
logger?.info('Launching JS debugger...');
debuggerInstances.get(appId)?.kill();
debuggerInstances.set(
appId,
await launchChromeDevTools(target.webSocketDebuggerUrl),
);
res.end();
return;
} catch (e) {
logger?.error(
'Error launching JS debugger: ' + e.message ?? 'Unknown error',
);
res.writeHead(500);
res.end();
return;
}
}
next();
};
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
export type Logger = $ReadOnly<{
error: (...message: Array<string>) => void,
info: (...message: Array<string>) => void,
warn: (...message: Array<string>) => void,
...
}>;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import type {IncomingMessage} from 'http';
import net from 'net';
import {TLSSocket} from 'tls';
/**
* Get the base URL to address the current development server.
*/
export default function getDevServerUrl(req: IncomingMessage): string {
const scheme =
req.socket instanceof TLSSocket && req.socket.encrypted === true
? 'https'
: 'http';
const {localAddress, localPort} = req.socket;
const address =
localAddress && net.isIPv6(localAddress)
? `[${localAddress}]`
: localAddress;
return `${scheme}:${address}:${localPort}`;
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import type {LaunchedChrome} from 'chrome-launcher';
import launchDebuggerAppWindow from './launchDebuggerAppWindow';
/**
* The Chrome DevTools frontend revision to use. This should be set to the
* latest version known to be compatible with Hermes.
*
* Revision should be the full identifier from:
* https://chromium.googlesource.com/chromium/src.git
*/
const DEVTOOLS_FRONTEND_REV = 'd9568d04d7dd79269c5a655d7ada69650c5a8336'; // Chrome 100.0.4896.75
/**
* Attempt to launch Chrome DevTools on the host machine for a given CDP target.
*/
export default async function launchChromeDevTools(
webSocketDebuggerUrl: string,
): Promise<LaunchedChrome> {
const urlBase = `https://chrome-devtools-frontend.appspot.com/serve_rev/@${DEVTOOLS_FRONTEND_REV}/devtools_app.html`;
const ws = webSocketDebuggerUrl.replace(/^ws:\/\//, '');
return launchDebuggerAppWindow(
`${urlBase}?panel=console&ws=${encodeURIComponent(ws)}`,
'open-debugger',
);
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import type {LaunchedChrome} from 'chrome-launcher';
import {promises as fs} from 'fs';
import path from 'path';
import osTempDir from 'temp-dir';
const ChromeLauncher = require('chrome-launcher');
/**
* Attempt to open a debugger frontend URL as a Google Chrome app window.
*/
export default async function launchDebuggerAppWindow(
url: string,
/**
* Used to construct the temp browser dir to preserve settings such as window
* position.
*/
intent: 'open-debugger',
): Promise<LaunchedChrome> {
const browserType = 'chrome';
const userDataDir = await createTempDir(
`react-native-${intent}-${browserType}`,
);
try {
return ChromeLauncher.launch({
chromeFlags: [
`--app=${url}`,
`--user-data-dir=${userDataDir}`,
'--window-size=1200,600',
],
});
} catch (e) {
throw new Error(
'Unable to find a browser on the host to open the debugger. Supported browsers: Google Chrome',
);
}
}
async function createTempDir(dirName: string): Promise<string> {
const tempDir = path.join(osTempDir, dirName);
await fs.mkdir(tempDir, {recursive: true});
return tempDir;
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
import fetch from 'node-fetch';
type ReactNativeCDPTarget = {
id: string,
description: string,
title: string,
type: string,
devtoolsFrontendUrl: string,
webSocketDebuggerUrl: string,
vm: string,
deviceName?: string,
};
/**
* Get the list of available debug targets from the React Native dev server.
*
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export default async function queryInspectorTargets(
devServerUrl: string,
): Promise<ReactNativeCDPTarget[]> {
const res = await fetch(`${devServerUrl}/json/list`);
const apps = (await res.json(): Array<ReactNativeCDPTarget>);
// Only use targets with better reloading support
return apps.filter(
app => app.title === 'React Native Experimental (Improved Chrome Reloads)',
);
}

View File

@ -3694,6 +3694,16 @@ chownr@^2.0.0:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chrome-launcher@^0.15.2:
version "0.15.2"
resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da"
integrity sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==
dependencies:
"@types/node" "*"
escape-string-regexp "^4.0.0"
is-wsl "^2.2.0"
lighthouse-logger "^1.0.0"
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@ -4032,7 +4042,7 @@ dayjs@^1.8.15:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.15.tgz#7121bc04e6a7f2621ed6db566be4a8aaf8c3913e"
integrity sha512-HYHCI1nohG52B45vCQg8Re3hNDZbMroWPkhz50yaX7Lu0ATyjGsTdoYZBpjED9ar6chqTx2dmSmM8A51mojnAg==
debug@2.6.9, debug@^2.2.0:
debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@ -5625,6 +5635,11 @@ is-directory@^0.3.1:
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
is-docker@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-extendable@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@ -5759,6 +5774,13 @@ is-wsl@^1.1.0:
resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -6552,6 +6574,14 @@ lie@~3.3.0:
dependencies:
immediate "~3.0.5"
lighthouse-logger@^1.0.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa"
integrity sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==
dependencies:
debug "^2.6.9"
marky "^1.2.2"
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@ -6709,6 +6739,11 @@ makeerror@1.0.12:
dependencies:
tmpl "1.0.5"
marky@^1.2.2:
version "1.2.5"
resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
memfs-or-file-map-to-github-branch@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/memfs-or-file-map-to-github-branch/-/memfs-or-file-map-to-github-branch-1.2.1.tgz#fdb9a85408262316a9bd5567409bf89be7d72f96"
@ -8729,6 +8764,11 @@ tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
temp-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"
integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==
temp@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2"