Skip to content

Commit

Permalink
[cli] fix react devtools inspector error (expo#29162)
Browse files Browse the repository at this point in the history
# Why

react devtools is broken when inspect a component

# How

the react-devtools we used and packed by jspm is outdated. let's use the
newer version. unfortunately, the newer react-devtools is bundled by
webpack 5 which does not have nodejs api polyfill. i have to build the
local bundled with esm support by myself. this pr also updates
`ReactDevToolsPageMiddleware` to support other static files and
`CorsMiddleware` to support same origin request (because `<script
type="module" src="...">` will send the Origin header)
  • Loading branch information
Kudo authored Jun 4, 2024
1 parent 7e94f1d commit 56ed3a0
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 44 deletions.
2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### 🐛 Bug fixes

- Fixed broken React DevTools since SDK 51. ([#29181](https://github.com/expo/expo/pull/29181) by [@kudo](https://github.com/kudo))

### 💡 Others

- Reduce export code paths. ([#29218](https://github.com/expo/expo/pull/29218) by [@EvanBacon](https://github.com/EvanBacon))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export function createCorsMiddleware(exp: ExpoConfig) {

return (req: ServerRequest, res: ServerResponse, next: (err?: Error) => void) => {
if (typeof req.headers.origin === 'string') {
const { hostname } = new URL(req.headers.origin);
if (!allowedHostnames.includes(hostname)) {
const { host, hostname } = new URL(req.headers.origin);
const isSameOrigin = host === req.headers.host;
if (!isSameOrigin && !allowedHostnames.includes(hostname)) {
next(
new Error(
`Unauthorized request from ${req.headers.origin}. ` +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile } from 'fs/promises';
import assert from 'assert';
import path from 'path';
import resolveFrom from 'resolve-from';
import send from 'send';

import { ExpoMiddleware } from './ExpoMiddleware';
import { ServerRequest, ServerResponse } from './server.types';
Expand All @@ -12,15 +13,25 @@ export class ReactDevToolsPageMiddleware extends ExpoMiddleware {
super(projectRoot, [ReactDevToolsEndpoint]);
}

override shouldHandleRequest(req: ServerRequest): boolean {
if (!req.url?.startsWith(ReactDevToolsEndpoint)) {
return false;
}
return true;
}

async handleRequestAsync(req: ServerRequest, res: ServerResponse): Promise<void> {
const templatePath =
assert(req.headers.host, 'Request headers must include host');
const { pathname } = new URL(req.url ?? '/', `http://${req.headers.host}`);
const requestPath = pathname.substring(ReactDevToolsEndpoint.length) || '/';

const entryPath =
// Production: This will resolve when installed in the project.
resolveFrom.silent(this.projectRoot, 'expo/static/react-devtools-page/index.html') ??
// Development: This will resolve when testing locally.
path.resolve(__dirname, '../../../../../static/react-devtools-page/index.html');
const content = (await readFile(templatePath)).toString('utf-8');

res.setHeader('Content-Type', 'text/html');
res.end(content);
const staticRoot = path.dirname(entryPath);
send(req, requestPath, { root: staticRoot }).pipe(res);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,43 @@ describe(createCorsMiddleware, () => {
middleware(asRequest({ url: 'http://localhost:8081/', headers: {} }), res, next);
expect(resHeaders['Access-Control-Allow-Origin']).toBeUndefined();
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should allow CORS from localhost', () => {
const origin = 'http://localhost:8082/';
middleware(asRequest({ url: 'http://localhost:8081/', headers: { origin } }), res, next);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should allow requests from origin same as host', () => {
const host = '192.168.1.1:8081';
const origin = 'http://192.168.1.1:8081/';
middleware(
asRequest({ url: 'http://192.168.1.1:8081/', headers: { host, origin } }),
res,
next
);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should allow CORS from devtools://devtools', () => {
const origin = 'devtools://devtools';
middleware(asRequest({ url: 'http://localhost:8081/', headers: { origin } }), res, next);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should allow CORS from https://chrome-devtools-frontend.appspot.com/', () => {
const origin = 'https://chrome-devtools-frontend.appspot.com/';
middleware(asRequest({ url: 'http://localhost:8081/', headers: { origin } }), res, next);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it(`should allow CORS from expo-router's origin`, () => {
Expand All @@ -56,6 +72,7 @@ describe(createCorsMiddleware, () => {
middleware(asRequest({ url: 'http://localhost:8081/', headers: { origin } }), res, next);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it(`should allow CORS from expo-router's headOrigin`, () => {
Expand All @@ -68,6 +85,7 @@ describe(createCorsMiddleware, () => {
);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(headOrigin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it(`should allow CORS from expo-router's origin to a full URL request`, () => {
Expand All @@ -81,6 +99,7 @@ describe(createCorsMiddleware, () => {
);
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should prevent metro reset the hardcoded CORS header', () => {
Expand All @@ -95,6 +114,7 @@ describe(createCorsMiddleware, () => {

expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});

it('should show explicit error from disallowed CORS', () => {
Expand Down Expand Up @@ -132,5 +152,6 @@ describe(createCorsMiddleware, () => {
expect(resHeaders['Access-Control-Allow-Origin']).toBe(origin);
expect(resHeaders['Access-Control-Allow-Methods']).toBeUndefined();
expect(next).toHaveBeenCalled();
expect(next.mock.calls[0][0]).not.toBeInstanceOf(Error);
});
});
43 changes: 6 additions & 37 deletions packages/@expo/cli/static/react-devtools-page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
width: 100%;
height: 100%;
}

</style>
</head>

Expand All @@ -45,45 +44,15 @@
</noscript>
<div id="hint">Connecting to ReactDevToolsProxy...</div>
<div id="root"></div>
<!--
JSPM Generator Import Map
Edit URL: https://generator.jspm.io/#U2NgYGBkDM0rySzJSU1hKEpNTC7RTUktK8nPzynWTc4vSnUw0TMy1zPSLy5JzEtJzMnPSwUAiUm0+zQA
-->
<script type="importmap">
{
"imports": {
"react-devtools-core/standalone": "https://ga.jspm.io/npm:react-devtools-core@4.27.2/standalone.js"
},
"scopes": {
"https://ga.jspm.io/": {
"buffer": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/buffer.js",
"child_process": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/child_process.js",
"crypto": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/crypto.js",
"events": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/events.js",
"fs": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/fs.js",
"http": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/http.js",
"https": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/https.js",
"net": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/net.js",
"path": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/path.js",
"process": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/process-production.js",
"stream": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/stream.js",
"tls": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/tls.js",
"url": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/url.js",
"util": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/util.js",
"vm": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/vm.js",
"zlib": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/zlib.js"
}
}
{
"imports": {
"react-devtools-core/standalone": "./react-devtools/standalone.js"
}
</script>

<!-- ES Module Shims: Import maps polyfill for modules browsers without import maps support (all except Chrome 89+) -->
<script async src="https://ga.jspm.io/npm:es-module-shims@1.5.1/dist/es-module-shims.js"
crossorigin="anonymous"></script>

}
</script>
<script type="module">
import { default as DevToolsUIWrapper } from "react-devtools-core/standalone";
const DevTools = DevToolsUIWrapper.default;
import DevTools from "react-devtools-core/standalone";

/**
* Private command to support DevTools frontend reload
Expand Down
3 changes: 3 additions & 0 deletions packages/@expo/cli/static/react-devtools-page/standalone.js

Large diffs are not rendered by default.

0 comments on commit 56ed3a0

Please sign in to comment.