diff --git a/.eslintignore b/.eslintignore index 2b7e221a11..c21ee38faa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,7 @@ node_modules +shells/browser/chrome/build +shells/browser/firefox/build shells/dev/build package-lock.json diff --git a/.flowconfig b/.flowconfig index 784a049997..35306bd54a 100644 --- a/.flowconfig +++ b/.flowconfig @@ -13,6 +13,9 @@ .*node_modules/webpack.* .*node_modules/fbjs/flow.* .*node_modules/web-ext.* +shells/browser/chrome/build/* +shells/browser/firefox/build/* +shells/dev/build/* [include] diff --git a/.prettierignore b/.prettierignore index 2b7e221a11..c21ee38faa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,7 @@ node_modules +shells/browser/chrome/build +shells/browser/firefox/build shells/dev/build package-lock.json diff --git a/package.json b/package.json index 8dc27444c4..043c75ee8c 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,17 @@ }, "scripts": { "build:extension": "yarn run build:extension:chrome && yarn run build:extension:firefox", - "build:extension:chrome": "node ./shells/chrome/build", - "build:extension:firefox": "node ./shells/firefox/build", + "build:extension:chrome": "node ./shells/browser/chrome/build", + "build:extension:firefox": "node ./shells/browser/firefox/build", "build:standalone": "cd packages/react-devtools-core && yarn run build", - "deploy": "cd ./shells/theme-preview && ./build.sh && gh-pages -d .", "linc": "lint-staged", "lint": "eslint '**/*.js'", "precommit": "lint-staged", "prettier": "prettier --write '**/*.{js,json,css}'", "start": "cd ./shells/dev && open ./index.html && webpack --config webpack.config.js --watch", "test": "jest", - "test:chrome": "node ./shells/chrome/test", - "test:firefox": "node ./shells/firefox/test", + "test:chrome": "node ./shells/browser/chrome/test", + "test:firefox": "node ./shells/browser/firefox/test", "test:standalone": "cd packages/react-devtools && yarn start", "typecheck": "flow check" }, diff --git a/shells/browser/chrome/README.md b/shells/browser/chrome/README.md new file mode 100644 index 0000000000..dd77d072c8 --- /dev/null +++ b/shells/browser/chrome/README.md @@ -0,0 +1,12 @@ +# The Chrome extension + +The source code for this extension has moved to `shells/webextension`. + +Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:chrome` from the root directory. + +## Testing in Chrome + +You can test a local build of the web extension like so: + + 1. Build the extension: `node build` + 1. Follow the on-screen instructions. diff --git a/shells/browser/chrome/build.js b/shells/browser/chrome/build.js new file mode 100644 index 0000000000..1311527d59 --- /dev/null +++ b/shells/browser/chrome/build.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const chalk = require('chalk'); +const { join } = require('path'); +const build = require('../shared/build'); + +const main = async () => { + await build( + 'chrome', + join(__dirname, 'manifest.json'), + join(__dirname, 'build') + ); + + console.log(chalk.green('\nThe Chrome extension has been built!')); + console.log(chalk.green('You can test this build by running:')); + console.log(chalk.gray('\n# From the react-devtools root directory:')); + console.log('yarn run test:chrome'); +}; + +main(); diff --git a/shells/browser/chrome/manifest.json b/shells/browser/chrome/manifest.json index 9d6543254f..c22e4b3849 100644 --- a/shells/browser/chrome/manifest.json +++ b/shells/browser/chrome/manifest.json @@ -1,6 +1,46 @@ { - "name": "React v16 DevTools", - "version": "0.1", - "description": "DevTools for React version 16.0+", - "manifest_version": 2 + "manifest_version": 2, + "name": "React Developer Tools", + "description": "Adds React debugging tools to the Chrome Developer Tools.", + "version": "4.0.0", + + "minimum_chrome_version": "49", + + "icons": { + "16": "icons/16-production.png", + "32": "icons/32-production.png", + "48": "icons/48-production.png", + "128": "icons/128-production.png" + }, + + "browser_action": { + "default_icon": { + "16": "icons/16-disabled.png", + "32": "icons/32-disabled.png", + "48": "icons/48-disabled.png", + "128": "icons/128-disabled.png" + }, + + "default_popup": "popups/disabled.html" + }, + + "devtools_page": "main.html", + + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], + + "background": { + "scripts": ["build/background.js"], + "persistent": false + }, + + "permissions": ["file:///*", "http://*/*", "https://*/*"], + + "content_scripts": [ + { + "matches": [""], + "js": ["build/inject.js"], + "run_at": "document_start" + } + ] } diff --git a/shells/browser/chrome/test.js b/shells/browser/chrome/test.js new file mode 100644 index 0000000000..9b9bbbf98f --- /dev/null +++ b/shells/browser/chrome/test.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +const chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no-extraneous-dependencies +const { resolve } = require('path'); + +const EXTENSION_PATH = resolve('shells/chrome/build/unpacked'); +const START_URL = 'https://facebook.github.io/react/'; + +chromeLaunch(START_URL, { + args: [`--load-extension=${EXTENSION_PATH}`], +}); diff --git a/shells/browser/firefox/README.md b/shells/browser/firefox/README.md new file mode 100644 index 0000000000..c4bbc8d2de --- /dev/null +++ b/shells/browser/firefox/README.md @@ -0,0 +1,12 @@ +# The Firefox extension + +The source code for this extension has moved to `shells/webextension`. + +Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:firefox` from the root directory. + +## Testing in Firefox + + 1. Build the extension: `node build` + 1. Follow the on-screen instructions. + +You can test upcoming releases of Firefox by downloading the Beta or Nightly build from the [Firefox releases](https://www.mozilla.org/en-US/firefox/channel/desktop/) page and then following the on-screen instructions after building. diff --git a/shells/browser/firefox/build.js b/shells/browser/firefox/build.js new file mode 100644 index 0000000000..993399e278 --- /dev/null +++ b/shells/browser/firefox/build.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +const chalk = require('chalk'); +const { join } = require('path'); +const build = require('../shared/build'); + +const main = async () => { + await build( + 'firefox', + join(__dirname, 'manifest.json'), + join(__dirname, 'build') + ); + + console.log(chalk.green('\nThe Firefox extension has been built!')); + console.log(chalk.green('You can test this build by running:')); + console.log(chalk.gray('\n# From the react-devtools root directory:')); + console.log('yarn run test:firefox'); + console.log( + chalk.gray('\n# You can also test against upcoming Firefox releases.') + ); + console.log( + chalk.gray( + '# First download a release from https://www.mozilla.org/en-US/firefox/channel/desktop/' + ) + ); + console.log( + chalk.gray( + '# And then tell web-ext which release to use (eg firefoxdeveloperedition, nightly, beta):' + ) + ); + console.log('WEB_EXT_FIREFOX=nightly yarn run test:firefox'); + console.log(chalk.gray('\n# You can test against older versions too:')); + console.log( + 'WEB_EXT_FIREFOX=/Applications/Firefox52.app/Contents/MacOS/firefox-bin yarn run test:firefox' + ); +}; + +main(); diff --git a/shells/browser/firefox/manifest.json b/shells/browser/firefox/manifest.json new file mode 100644 index 0000000000..e0015a59ff --- /dev/null +++ b/shells/browser/firefox/manifest.json @@ -0,0 +1,52 @@ +{ + "manifest_version": 2, + "name": "React Developer Tools", + "description": "Adds React debugging tools to the Firefox Developer Tools.", + "version": "4.0.0", + + "applications": { + "gecko": { + "id": "@react-devtools", + "strict_min_version": "54.0" + } + }, + + "icons": { + "16": "icons/16-production.png", + "32": "icons/32-production.png", + "48": "icons/48-production.png", + "128": "icons/128-production.png" + }, + + "browser_action": { + "default_icon": { + "16": "icons/16-disabled.png", + "32": "icons/32-disabled.png", + "48": "icons/48-disabled.png", + "128": "icons/128-disabled.png" + }, + + "default_popup": "popups/disabled.html", + "browser_style": true + }, + + "devtools_page": "main.html", + + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], + + "background": { + "scripts": ["build/background.js"], + "persistent": false + }, + + "permissions": ["file:///*", "http://*/*", "https://*/*"], + + "content_scripts": [ + { + "matches": [""], + "js": ["build/inject.js"], + "run_at": "document_start" + } + ] +} diff --git a/shells/browser/firefox/test.js b/shells/browser/firefox/test.js new file mode 100644 index 0000000000..850d87c0f7 --- /dev/null +++ b/shells/browser/firefox/test.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +const { exec } = require('child-process-promise'); +const { Finder } = require('firefox-profile'); +const { resolve } = require('path'); + +const EXTENSION_PATH = resolve('shells/firefox/build/unpacked'); +const START_URL = 'https://facebook.github.io/react/'; + +const main = async () => { + const finder = new Finder(); + + // Use default Firefox profile for testing purposes. + // This prevents users from having to re-login-to sites before testing. + const findPathPromise = new Promise((resolvePromise, rejectPromise) => { + finder.getPath('default', (error, profile) => { + if (error) { + rejectPromise(error); + } else { + resolvePromise(profile); + } + }); + }); + + const options = [ + `--source-dir=${EXTENSION_PATH}`, + `--start-url=${START_URL}`, + '--browser-console', + ]; + + try { + const path = await findPathPromise; + const trimmedPath = path.replace(' ', '\\ '); + options.push(`--firefox-profile=${trimmedPath}`); + } catch (err) { + console.warn('Could not find default profile, using temporary profile.'); + } + + try { + await exec(`web-ext run ${options.join(' ')}`); + } catch (err) { + console.error('`web-ext run` failed', err.stdout, err.stderr); + } +}; + +main(); diff --git a/shells/browser/shared/build.js b/shells/browser/shared/build.js new file mode 100644 index 0000000000..5f6747af54 --- /dev/null +++ b/shells/browser/shared/build.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +const AdmZip = require('adm-zip'); +const { execSync } = require('child_process'); +const { copy, ensureDir, move, remove } = require('fs-extra'); +const { join } = require('path'); + +// These files are copied along with Webpack-bundled files +// to produce the final web extension +const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html']; + +const preProcess = async (destinationPath, tempPath) => { + await remove(destinationPath); // Clean up from previously completed builds + await remove(tempPath); // Clean up from any previously failed builds + await ensureDir(tempPath); // Create temp dir for this new build +}; + +const build = async (tempPath, manifestPath) => { + const binPath = join(tempPath, 'bin'); + const zipPath = join(tempPath, 'zip'); + + const webpackPath = join( + __dirname, + '..', + '..', + '..', + 'node_modules', + '.bin', + 'webpack' + ); + execSync( + `${webpackPath} --config webpack.config.js --output-path ${binPath}`, + { + cwd: __dirname, + env: Object.assign({}, process.env, { NODE_ENV: 'production' }), + stdio: 'inherit', + } + ); + execSync( + `${webpackPath} --config webpack.backend.js --output-path ${binPath}`, + { + cwd: __dirname, + env: Object.assign({}, process.env, { NODE_ENV: 'production' }), + stdio: 'inherit', + } + ); + + // Make temp dir + await ensureDir(zipPath); + + // Copy unbuilt source files to zip dir to be packaged: + await copy(binPath, join(zipPath, 'build')); + await copy(manifestPath, join(zipPath, 'manifest.json')); + await Promise.all( + STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))) + ); + + // Pack the extension + const zip = new AdmZip(); + zip.addLocalFolder(zipPath); + zip.writeZip(join(tempPath, 'packed.zip')); +}; + +const postProcess = async (tempPath, destinationPath) => { + const unpackedSourcePath = join(tempPath, 'zip'); + const packedSourcePath = join(tempPath, 'packed.zip'); + const packedDestPath = join(destinationPath, 'packed.zip'); + const unpackedDestPath = join(destinationPath, 'unpacked'); + + await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination + await move(packedSourcePath, packedDestPath); // Copy built files to destination + await remove(tempPath); // Clean up temp directory and files +}; + +const main = async (buildId, manifestPath, destinationPath) => { + try { + const tempPath = join(__dirname, 'build', buildId); + await preProcess(destinationPath, tempPath); + await build(tempPath, manifestPath); + + const builtUnpackedPath = join(destinationPath, 'unpacked'); + await postProcess(tempPath, destinationPath); + + return builtUnpackedPath; + } catch (error) { + console.error(error); + process.exit(1); + } + + return null; +}; + +module.exports = main; diff --git a/shells/browser/shared/icons/128-deadcode.png b/shells/browser/shared/icons/128-deadcode.png new file mode 100644 index 0000000000..b6ecc88e13 Binary files /dev/null and b/shells/browser/shared/icons/128-deadcode.png differ diff --git a/shells/browser/shared/icons/128-development.png b/shells/browser/shared/icons/128-development.png new file mode 100644 index 0000000000..b6ecc88e13 Binary files /dev/null and b/shells/browser/shared/icons/128-development.png differ diff --git a/shells/browser/shared/icons/128-disabled.png b/shells/browser/shared/icons/128-disabled.png new file mode 100644 index 0000000000..67b1c9a31a Binary files /dev/null and b/shells/browser/shared/icons/128-disabled.png differ diff --git a/shells/browser/shared/icons/128-outdated.png b/shells/browser/shared/icons/128-outdated.png new file mode 100644 index 0000000000..05792b7628 Binary files /dev/null and b/shells/browser/shared/icons/128-outdated.png differ diff --git a/shells/browser/shared/icons/128-production.png b/shells/browser/shared/icons/128-production.png new file mode 100644 index 0000000000..b932794644 Binary files /dev/null and b/shells/browser/shared/icons/128-production.png differ diff --git a/shells/browser/shared/icons/128-unminified.png b/shells/browser/shared/icons/128-unminified.png new file mode 100644 index 0000000000..b6ecc88e13 Binary files /dev/null and b/shells/browser/shared/icons/128-unminified.png differ diff --git a/shells/browser/shared/icons/16-deadcode.png b/shells/browser/shared/icons/16-deadcode.png new file mode 100644 index 0000000000..33d99798e0 Binary files /dev/null and b/shells/browser/shared/icons/16-deadcode.png differ diff --git a/shells/browser/shared/icons/16-development.png b/shells/browser/shared/icons/16-development.png new file mode 100644 index 0000000000..33d99798e0 Binary files /dev/null and b/shells/browser/shared/icons/16-development.png differ diff --git a/shells/browser/shared/icons/16-disabled.png b/shells/browser/shared/icons/16-disabled.png new file mode 100644 index 0000000000..2f0317ea3f Binary files /dev/null and b/shells/browser/shared/icons/16-disabled.png differ diff --git a/shells/browser/shared/icons/16-outdated.png b/shells/browser/shared/icons/16-outdated.png new file mode 100644 index 0000000000..aa42bfe0b4 Binary files /dev/null and b/shells/browser/shared/icons/16-outdated.png differ diff --git a/shells/browser/shared/icons/16-production.png b/shells/browser/shared/icons/16-production.png new file mode 100644 index 0000000000..1c253dca5d Binary files /dev/null and b/shells/browser/shared/icons/16-production.png differ diff --git a/shells/browser/shared/icons/16-unminified.png b/shells/browser/shared/icons/16-unminified.png new file mode 100644 index 0000000000..33d99798e0 Binary files /dev/null and b/shells/browser/shared/icons/16-unminified.png differ diff --git a/shells/browser/shared/icons/32-deadcode.png b/shells/browser/shared/icons/32-deadcode.png new file mode 100644 index 0000000000..c4a6bda3e6 Binary files /dev/null and b/shells/browser/shared/icons/32-deadcode.png differ diff --git a/shells/browser/shared/icons/32-development.png b/shells/browser/shared/icons/32-development.png new file mode 100644 index 0000000000..c4a6bda3e6 Binary files /dev/null and b/shells/browser/shared/icons/32-development.png differ diff --git a/shells/browser/shared/icons/32-disabled.png b/shells/browser/shared/icons/32-disabled.png new file mode 100644 index 0000000000..7c75045323 Binary files /dev/null and b/shells/browser/shared/icons/32-disabled.png differ diff --git a/shells/browser/shared/icons/32-outdated.png b/shells/browser/shared/icons/32-outdated.png new file mode 100644 index 0000000000..6eae901bf0 Binary files /dev/null and b/shells/browser/shared/icons/32-outdated.png differ diff --git a/shells/browser/shared/icons/32-production.png b/shells/browser/shared/icons/32-production.png new file mode 100644 index 0000000000..9192719e5b Binary files /dev/null and b/shells/browser/shared/icons/32-production.png differ diff --git a/shells/browser/shared/icons/32-unminified.png b/shells/browser/shared/icons/32-unminified.png new file mode 100644 index 0000000000..c4a6bda3e6 Binary files /dev/null and b/shells/browser/shared/icons/32-unminified.png differ diff --git a/shells/browser/shared/icons/48-deadcode.png b/shells/browser/shared/icons/48-deadcode.png new file mode 100644 index 0000000000..f3224021a5 Binary files /dev/null and b/shells/browser/shared/icons/48-deadcode.png differ diff --git a/shells/browser/shared/icons/48-development.png b/shells/browser/shared/icons/48-development.png new file mode 100644 index 0000000000..f3224021a5 Binary files /dev/null and b/shells/browser/shared/icons/48-development.png differ diff --git a/shells/browser/shared/icons/48-disabled.png b/shells/browser/shared/icons/48-disabled.png new file mode 100644 index 0000000000..372b6e00e8 Binary files /dev/null and b/shells/browser/shared/icons/48-disabled.png differ diff --git a/shells/browser/shared/icons/48-outdated.png b/shells/browser/shared/icons/48-outdated.png new file mode 100644 index 0000000000..342fedea1e Binary files /dev/null and b/shells/browser/shared/icons/48-outdated.png differ diff --git a/shells/browser/shared/icons/48-production.png b/shells/browser/shared/icons/48-production.png new file mode 100644 index 0000000000..9aac93a622 Binary files /dev/null and b/shells/browser/shared/icons/48-production.png differ diff --git a/shells/browser/shared/icons/48-unminified.png b/shells/browser/shared/icons/48-unminified.png new file mode 100644 index 0000000000..f3224021a5 Binary files /dev/null and b/shells/browser/shared/icons/48-unminified.png differ diff --git a/shells/browser/shared/icons/deadcode.svg b/shells/browser/shared/icons/deadcode.svg new file mode 100644 index 0000000000..ccd6e66906 --- /dev/null +++ b/shells/browser/shared/icons/deadcode.svg @@ -0,0 +1 @@ +development780780 \ No newline at end of file diff --git a/shells/browser/shared/icons/development.svg b/shells/browser/shared/icons/development.svg new file mode 100644 index 0000000000..ccd6e66906 --- /dev/null +++ b/shells/browser/shared/icons/development.svg @@ -0,0 +1 @@ +development780780 \ No newline at end of file diff --git a/shells/browser/shared/icons/disabled.svg b/shells/browser/shared/icons/disabled.svg new file mode 100644 index 0000000000..73c2bb51cd --- /dev/null +++ b/shells/browser/shared/icons/disabled.svg @@ -0,0 +1 @@ +disabled \ No newline at end of file diff --git a/shells/browser/shared/icons/outdated.svg b/shells/browser/shared/icons/outdated.svg new file mode 100644 index 0000000000..03b83c1eb5 --- /dev/null +++ b/shells/browser/shared/icons/outdated.svg @@ -0,0 +1 @@ +outdated \ No newline at end of file diff --git a/shells/browser/shared/icons/production.svg b/shells/browser/shared/icons/production.svg new file mode 100644 index 0000000000..1e974f5131 --- /dev/null +++ b/shells/browser/shared/icons/production.svg @@ -0,0 +1 @@ +production \ No newline at end of file diff --git a/shells/browser/shared/main.html b/shells/browser/shared/main.html new file mode 100644 index 0000000000..f1c96d4a7a --- /dev/null +++ b/shells/browser/shared/main.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shells/browser/shared/panel.html b/shells/browser/shared/panel.html new file mode 100644 index 0000000000..60fd1bdf13 --- /dev/null +++ b/shells/browser/shared/panel.html @@ -0,0 +1,32 @@ + + + + + + + + +
Unable to find React on the page.
+ + + diff --git a/shells/browser/shared/popups/deadcode.html b/shells/browser/shared/popups/deadcode.html new file mode 100644 index 0000000000..5e74dc06c4 --- /dev/null +++ b/shells/browser/shared/popups/deadcode.html @@ -0,0 +1,32 @@ + + +

+ This page includes an extra development build of React. 🚧 +

+

+ The React build on this page includes both development and production versions because dead code elimination has not been applied correctly. +
+
+ This makes its size larger, and causes React to run slower. +
+
+ Make sure to set up dead code elimination before deployment. +

+
+

+ Open the developer tools, and the React tab will appear to the right. +

diff --git a/shells/browser/shared/popups/development.html b/shells/browser/shared/popups/development.html new file mode 100644 index 0000000000..9c2089cc2f --- /dev/null +++ b/shells/browser/shared/popups/development.html @@ -0,0 +1,28 @@ + + +

+ This page is using the development build of React. 🚧 +

+

+ Note that the development build is not suitable for production. +
+ Make sure to use the production build before deployment. +

+
+

+ Open the developer tools, and the React tab will appear to the right. +

diff --git a/shells/browser/shared/popups/disabled.html b/shells/browser/shared/popups/disabled.html new file mode 100644 index 0000000000..a89b178d49 --- /dev/null +++ b/shells/browser/shared/popups/disabled.html @@ -0,0 +1,21 @@ + + +

+ This page doesn’t appear to be using React. +
+ If this seems wrong, follow the troubleshooting instructions. +

diff --git a/shells/browser/shared/popups/outdated.html b/shells/browser/shared/popups/outdated.html new file mode 100644 index 0000000000..a6ec12bcaf --- /dev/null +++ b/shells/browser/shared/popups/outdated.html @@ -0,0 +1,29 @@ + + +

+ This page is using an outdated version of React. ⌛ +

+

+ We recommend updating React to ensure that you receive important bugfixes and performance improvements. +
+
+ You can find the upgrade instructions on the React blog. +

+
+

+ Open the developer tools, and the React tab will appear to the right. +

diff --git a/shells/browser/shared/popups/production.html b/shells/browser/shared/popups/production.html new file mode 100644 index 0000000000..1b65eb5b21 --- /dev/null +++ b/shells/browser/shared/popups/production.html @@ -0,0 +1,21 @@ + + +

+ This page is using the production build of React. ✅ +
+ Open the developer tools, and the React tab will appear to the right. +

diff --git a/shells/browser/shared/popups/shared.js b/shells/browser/shared/popups/shared.js new file mode 100644 index 0000000000..130f1f457e --- /dev/null +++ b/shells/browser/shared/popups/shared.js @@ -0,0 +1,22 @@ +/* globals chrome */ + +document.addEventListener('DOMContentLoaded', function() { + // Make links work + const links = document.getElementsByTagName('a'); + for (let i = 0; i < links.length; i++) { + (function() { + const ln = links[i]; + const location = ln.href; + ln.onclick = function() { + chrome.tabs.create({ active: true, url: location }); + }; + })(); + } + + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044 + document.body.style.opacity = 0; + document.body.style.transition = 'opacity ease-out .4s'; + requestAnimationFrame(function() { + document.body.style.opacity = 1; + }); +}); diff --git a/shells/browser/shared/popups/unminified.html b/shells/browser/shared/popups/unminified.html new file mode 100644 index 0000000000..553c9ac6ac --- /dev/null +++ b/shells/browser/shared/popups/unminified.html @@ -0,0 +1,31 @@ + + +

+ This page is using an unminified build of React. 🚧 +

+

+ The React build on this page appears to be unminified. +
+ This makes its size larger, and causes React to run slower. +
+
+ Make sure to set up minification before deployment. +

+
+

+ Open the developer tools, and the React tab will appear to the right. +

diff --git a/shells/browser/shared/src/GlobalHook.js b/shells/browser/shared/src/GlobalHook.js new file mode 100644 index 0000000000..9ccab3bb8a --- /dev/null +++ b/shells/browser/shared/src/GlobalHook.js @@ -0,0 +1,64 @@ +/* global chrome */ + +import nullthrows from 'nullthrows'; +import { installHook } from 'src/hook'; + +let lastDetectionResult; + +// We want to detect when a renderer attaches, and notify the "background +// page" (which is shared between tabs and can highlight the React icon). +// Currently we are in "content script" context, so we can't listen +// to the hook directly (it will be injected directly into the page). +// So instead, the hook will use postMessage() to pass message to us here. +// And when this happens, we'll send a message to the "background page". +window.addEventListener('message', function(evt) { + if ( + evt.source === window && + evt.data && + evt.data.source === 'react-devtools-detector' + ) { + lastDetectionResult = { + hasDetectedReact: true, + reactBuildType: evt.data.reactBuildType, + }; + chrome.runtime.sendMessage(lastDetectionResult); + } +}); + +// NOTE: Firefox WebExtensions content scripts are still alive and not re-injected +// while navigating the history to a document that has not been destroyed yet, +// replay the last detection result if the content script is active and the +// document has been hidden and shown again. +window.addEventListener('pageshow', function(evt) { + if (!lastDetectionResult || evt.target !== window.document) { + return; + } + chrome.runtime.sendMessage(lastDetectionResult); +}); + +const detectReact = ` +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function(evt) { + window.postMessage({ + source: 'react-devtools-detector', + reactBuildType: evt.reactBuildType, + }, '*'); +}); +`; +const saveNativeValues = ` +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set; +`; + +// Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the +// devtools are installed (and skip its suggestion to install the devtools). +const js = + ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact; + +// This script runs before the element is created, so we add the script +// to instead. +const script = document.createElement('script'); +script.textContent = js; +nullthrows(document.documentElement).appendChild(script); +nullthrows(script.parentNode).removeChild(script); diff --git a/shells/browser/shared/src/backend.js b/shells/browser/shared/src/backend.js new file mode 100644 index 0000000000..fa13e02527 --- /dev/null +++ b/shells/browser/shared/src/backend.js @@ -0,0 +1,67 @@ +// Do not use imports or top-level requires here! +// Running module factories is intentionally delayed until we know the hook exists. +// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039 + +/** @flow */ + +function welcome(event) { + if ( + event.source !== window || + event.data.source !== 'react-devtools-content-script' + ) { + return; + } + + window.removeEventListener('message', welcome); + + setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__); +} + +window.addEventListener('message', welcome); + +function setup(hook) { + const Agent = require('src/backend/agent').default; + const Bridge = require('src/bridge').default; + const { initBackend } = require('src/backend'); + + const listeners = []; + + const bridge = new Bridge({ + listen(fn) { + const listener = event => { + if ( + event.source !== window || + !event.data || + event.data.source !== 'react-devtools-content-script' || + !event.data.payload + ) { + return; + } + fn(event.data.payload); + }; + listeners.push(listener); + window.addEventListener('message', listener); + }, + send(data) { + window.postMessage( + { + source: 'react-devtools-bridge', + payload: data, + }, + '*' + ); + }, + }); + + const agent = new Agent(); + agent.addBridge(bridge); + agent.addListener('shutdown', () => { + hook.emit('shutdown'); + listeners.forEach(fn => { + window.removeEventListener('message', fn); + }); + listeners.splice(0); + }); + + initBackend(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent); +} diff --git a/shells/browser/shared/src/background.js b/shells/browser/shared/src/background.js new file mode 100644 index 0000000000..ca22f3950c --- /dev/null +++ b/shells/browser/shared/src/background.js @@ -0,0 +1,111 @@ +/* global chrome */ + +const ports = {}; + +const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0; + +chrome.runtime.onConnect.addListener(function(port) { + let tab = null; + let name = null; + if (isNumeric(port.name)) { + tab = port.name; + name = 'devtools'; + installContentScript(+port.name); + } else { + tab = port.sender.tab.id; + name = 'content-script'; + } + + if (!ports[tab]) { + ports[tab] = { + devtools: null, + 'content-script': null, + }; + } + ports[tab][name] = port; + + if (ports[tab].devtools && ports[tab]['content-script']) { + doublePipe(ports[tab].devtools, ports[tab]['content-script']); + } +}); + +function isNumeric(str: string): boolean { + return +str + '' === str; +} + +function installContentScript(tabId: number) { + chrome.tabs.executeScript( + tabId, + { file: '/build/contentScript.js' }, + function() {} + ); +} + +function doublePipe(one, two) { + one.onMessage.addListener(lOne); + function lOne(message) { + two.postMessage(message); + } + two.onMessage.addListener(lTwo); + function lTwo(message) { + one.postMessage(message); + } + function shutdown() { + one.onMessage.removeListener(lOne); + two.onMessage.removeListener(lTwo); + one.disconnect(); + two.disconnect(); + } + one.onDisconnect.addListener(shutdown); + two.onDisconnect.addListener(shutdown); +} + +function setIconAndPopup(reactBuildType, tabId) { + chrome.browserAction.setIcon({ + tabId: tabId, + path: { + '16': 'icons/16-' + reactBuildType + '.png', + '32': 'icons/32-' + reactBuildType + '.png', + '48': 'icons/48-' + reactBuildType + '.png', + '128': 'icons/128-' + reactBuildType + '.png', + }, + }); + chrome.browserAction.setPopup({ + tabId: tabId, + popup: 'popups/' + reactBuildType + '.html', + }); +} + +// Listen to URL changes on the active tab and reset the DevTools icon. +// This prevents non-disabled icons from sticking in Firefox. +// Don't listen to this event in Chrome though. +// It fires more frequently, often after onMessage() has been called. +if (IS_FIREFOX) { + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (tab.active && changeInfo.status === 'loading') { + setIconAndPopup('disabled', tabId); + } + }); +} + +chrome.runtime.onMessage.addListener((req, sender) => { + // This is sent from the hook content script. + // It tells us a renderer has attached. + if (req.hasDetectedReact && sender.tab) { + // We use browserAction instead of pageAction because this lets us + // display a custom default popup when React is *not* detected. + // It is specified in the manifest. + let reactBuildType = req.reactBuildType; + if (sender.url.indexOf('facebook.github.io/react') !== -1) { + // Cheat: We use the development version on the website because + // it is better for interactive examples. However we're going + // to get misguided bug reports if the extension highlights it + // as using the dev version. We're just going to special case + // our own documentation and cheat. It is acceptable to use dev + // version of React in React docs, but not in any other case. + reactBuildType = 'production'; + } + + setIconAndPopup(reactBuildType, sender.tab.id); + } +}); diff --git a/shells/browser/shared/src/checkForReact.js b/shells/browser/shared/src/checkForReact.js new file mode 100644 index 0000000000..b0334ff3ae --- /dev/null +++ b/shells/browser/shared/src/checkForReact.js @@ -0,0 +1,12 @@ +/* global chrome */ + +export default function checkForReact(done: (pageHasReact: boolean) => void) { + chrome.devtools.inspectedWindow.eval( + `!!( + (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && Object.keys(window.__REACT_DEVTOOLS_GLOBAL_HOOK__._renderers).length) || window.React || (window.require && (require('react') || require('React'))) + )`, + function(pageHasReact, err) { + done(pageHasReact); + } + ); +} diff --git a/shells/browser/shared/src/contentScript.js b/shells/browser/shared/src/contentScript.js new file mode 100644 index 0000000000..c72d17918f --- /dev/null +++ b/shells/browser/shared/src/contentScript.js @@ -0,0 +1,52 @@ +/* global chrome */ + +// proxy from main page to devtools (via the background page) +var port = chrome.runtime.connect({ + name: 'content-script', +}); + +port.onMessage.addListener(handleMessageFromDevtools); +port.onDisconnect.addListener(handleDisconnect); +window.addEventListener('message', handleMessageFromPage); + +window.postMessage( + { + source: 'react-devtools-content-script', + hello: true, + }, + '*' +); + +function handleMessageFromDevtools(message) { + window.postMessage( + { + source: 'react-devtools-content-script', + payload: message, + }, + '*' + ); +} + +function handleMessageFromPage(evt) { + if ( + evt.source === window && + evt.data && + evt.data.source === 'react-devtools-bridge' + ) { + port.postMessage(evt.data.payload); + } +} + +function handleDisconnect() { + window.removeEventListener('message', handleMessageFromPage); + window.postMessage( + { + source: 'react-devtools-content-script', + payload: { + type: 'event', + evt: 'shutdown', + }, + }, + '*' + ); +} diff --git a/shells/browser/shared/src/inject.js b/shells/browser/shared/src/inject.js new file mode 100644 index 0000000000..522b322786 --- /dev/null +++ b/shells/browser/shared/src/inject.js @@ -0,0 +1,21 @@ +/* global chrome */ + +export default function inject(scriptName: string, done: () => void) { + const src = ` + // the prototype stuff is in case document.createElement has been modified + (function () { + var script = document.constructor.prototype.createElement.call(document, 'script'); + script.src = "${scriptName}"; + script.charset = "utf-8"; + document.documentElement.appendChild(script); + script.parentNode.removeChild(script); + })() + `; + + chrome.devtools.inspectedWindow.eval(src, function(res, err) { + if (err) { + console.log(err); + } + done(); + }); +} diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js new file mode 100644 index 0000000000..ab511445cd --- /dev/null +++ b/shells/browser/shared/src/main.js @@ -0,0 +1,40 @@ +/* global chrome */ + +let panelCreated = false; + +function createPanelIfReactLoaded() { + if (panelCreated) { + return; + } + chrome.devtools.inspectedWindow.eval( + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && Object.keys(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers).length > 0', + function(pageHasReact, err) { + if (!pageHasReact || panelCreated) { + return; + } + + clearInterval(loadCheckInterval); + panelCreated = true; + chrome.devtools.panels.create('React', '', 'panel.html', function(panel) { + panel.onShown.addListener(function(window) { + // TODO: When the user switches to the panel, check for an Elements tab selection. + }); + panel.onHidden.addListener(function() { + // TODO: Stop highlighting and stuff. + }); + }); + } + ); +} + +chrome.devtools.network.onNavigated.addListener(function() { + createPanelIfReactLoaded(); +}); + +// Check to see if React has loaded once per second in case React is added +// after page load +const loadCheckInterval = setInterval(function() { + createPanelIfReactLoaded(); +}, 1000); + +createPanelIfReactLoaded(); diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js new file mode 100644 index 0000000000..e93f1c66f7 --- /dev/null +++ b/shells/browser/shared/src/panel.js @@ -0,0 +1,62 @@ +/* global chrome */ + +import { createElement } from 'react'; +import { render } from 'react-dom'; +import Bridge from 'src/bridge'; +import App from 'src/devtools/views/App'; +import inject from './inject'; + +const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0; + +let browserName; +let themeName; + +if (IS_CHROME) { + browserName = 'Chrome'; + + // chrome.devtools.panels added in Chrome 18. + // chrome.devtools.panels.themeName added in Chrome 54. + themeName = chrome.devtools.panels.themeName === 'dark' ? 'Dark' : 'Default'; +} else { + browserName = 'Firefox'; + + // chrome.devtools.panels.themeName added in Firefox 55. + // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName + if (chrome.devtools && chrome.devtools.panels) { + switch (chrome.devtools.panels.themeName) { + case 'dark': + themeName = 'Dark'; + break; + default: + themeName = 'Light'; + break; + } + } +} + +const node = ((document.getElementById('container'): any): HTMLElement); + +inject(chrome.runtime.getURL('build/backend.js'), () => { + let disconnected = false; + + const port = chrome.runtime.connect({ + name: '' + chrome.devtools.inspectedWindow.tabId, + }); + port.onDisconnect.addListener(() => { + disconnected = true; + }); + + const bridge = new Bridge({ + listen(fn) { + port.onMessage.addListener(message => fn(message)); + }, + send(data) { + if (disconnected) { + return; + } + port.postMessage(data); + }, + }); + + render(createElement(App, { bridge, browserName, themeName }), node); +}); diff --git a/shells/browser/webpack.backend.js b/shells/browser/shared/webpack.backend.js similarity index 61% rename from shells/browser/webpack.backend.js rename to shells/browser/shared/webpack.backend.js index 9c1aa96809..59e0b617bc 100644 --- a/shells/browser/webpack.backend.js +++ b/shells/browser/shared/webpack.backend.js @@ -1,7 +1,8 @@ const { readFileSync } = require('fs'); const { resolve } = require('path'); -const __DEV__ = process.env.NODE_ENV !== 'production'; +//const __DEV__ = process.env.NODE_ENV !== 'production'; +const __DEV__ = true; // TODO module.exports = { mode: __DEV__ ? 'development' : 'production', @@ -13,12 +14,19 @@ module.exports = { path: __dirname + '/build', filename: '[name].js', }, + resolve: { + alias: { + src: resolve(__dirname, '../../../src'), + }, + }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', - options: JSON.parse(readFileSync(resolve(__dirname, '../../.babelrc'))), + options: JSON.parse( + readFileSync(resolve(__dirname, '../../../.babelrc')) + ), }, ], }, diff --git a/shells/browser/webpack.config.js b/shells/browser/shared/webpack.config.js similarity index 80% rename from shells/browser/webpack.config.js rename to shells/browser/shared/webpack.config.js index 39c08ff056..933d7a79ce 100644 --- a/shells/browser/webpack.config.js +++ b/shells/browser/shared/webpack.config.js @@ -2,7 +2,8 @@ const { readFileSync } = require('fs'); const { resolve } = require('path'); const webpack = require('webpack'); -const __DEV__ = process.env.NODE_ENV !== 'production'; +//const __DEV__ = process.env.NODE_ENV !== 'production'; +const __DEV__ = true; // TODO module.exports = { mode: __DEV__ ? 'development' : 'production', @@ -18,6 +19,11 @@ module.exports = { path: __dirname + '/build', filename: '[name].js', }, + resolve: { + alias: { + src: resolve(__dirname, '../../../src'), + }, + }, plugins: __DEV__ ? [] : [ @@ -31,7 +37,9 @@ module.exports = { { test: /\.js$/, loader: 'babel-loader', - options: JSON.parse(readFileSync(resolve(__dirname, '../../.babelrc'))), + options: JSON.parse( + readFileSync(resolve(__dirname, '../../../.babelrc')) + ), }, { test: /\.css$/, diff --git a/shells/createConfig.js b/shells/createConfig.js deleted file mode 100644 index a5eea836b7..0000000000 --- a/shells/createConfig.js +++ /dev/null @@ -1,148 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const merge = require('webpack-merge'); -const { VueLoaderPlugin } = require('vue-loader'); -const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); - -module.exports = (config, target = { chrome: 52, firefox: 48 }) => { - const bubleOptions = { - target, - objectAssign: 'Object.assign', - transforms: { - forOf: false, - modules: false, - }, - }; - - const baseConfig = { - mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', - resolve: { - alias: { - src: path.resolve(__dirname, '../src'), - views: path.resolve(__dirname, '../src/devtools/views'), - components: path.resolve(__dirname, '../src/devtools/components'), - filters: path.resolve(__dirname, '../src/devtools/filters'), - }, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: 'buble-loader', - exclude: /node_modules|vue\/dist|vuex\/dist/, - options: bubleOptions, - }, - { - test: /\.vue$/, - loader: 'vue-loader', - options: { - compilerOptions: { - preserveWhitespace: false, - }, - transpileOptions: bubleOptions, - }, - }, - { - test: /\.css$/, - use: ['vue-style-loader', 'css-loader', 'postcss-loader'], - }, - { - test: /\.styl(us)?$/, - use: [ - 'vue-style-loader', - 'css-loader', - 'postcss-loader', - 'stylus-loader', - { - loader: 'style-resources-loader', - options: { - patterns: [ - path.resolve(__dirname, '../src/devtools/style/imports.styl'), - ], - }, - }, - ], - }, - { - test: /\.(png|woff2)$/, - loader: 'url-loader?limit=0', - }, - ], - }, - performance: { - hints: false, - }, - plugins: [ - new VueLoaderPlugin(), - ...(process.env.VUE_DEVTOOL_TEST ? [] : [new FriendlyErrorsPlugin()]), - new webpack.DefinePlugin({ - 'process.env.RELEASE_CHANNEL': JSON.stringify( - process.env.RELEASE_CHANNEL || 'stable' - ), - }), - ], - devServer: { - port: process.env.PORT, - }, - stats: { - colors: true, - }, - }; - - if (process.env.NODE_ENV === 'production') { - const UglifyPlugin = require('uglifyjs-webpack-plugin'); - baseConfig.plugins.push( - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': '"production"', - }) - ); - baseConfig.optimization = { - minimizer: [ - new UglifyPlugin({ - exclude: /backend/, - uglifyOptions: { - compress: { - // turn off flags with small gains to speed up minification - arrows: false, - collapse_vars: false, // 0.3kb - comparisons: false, - computed_props: false, - hoist_funs: false, - hoist_props: false, - hoist_vars: false, - inline: false, - loops: false, - negate_iife: false, - properties: false, - reduce_funcs: false, - reduce_vars: false, - switches: false, - toplevel: false, - typeofs: false, - - // a few flags with noticable gains/speed ratio - // numbers based on out of the box vendor bundle - booleans: true, // 0.7kb - if_return: true, // 0.4kb - sequences: true, // 0.7kb - unused: true, // 2.3kb - - // required features to drop conditional branches - conditionals: true, - dead_code: true, - evaluate: true, - }, - mangle: { - safari10: true, - }, - }, - sourceMap: false, - cache: true, - parallel: true, - }), - ], - }; - } - - return merge(baseConfig, config); -}; diff --git a/shells/dev/app/ElementTypes/index.js b/shells/dev/app/ElementTypes/index.js index 1a5ab821fb..ccfcb5fb33 100644 --- a/shells/dev/app/ElementTypes/index.js +++ b/shells/dev/app/ElementTypes/index.js @@ -15,7 +15,8 @@ import React, { } from 'react'; const Context = createContext('abc'); -Context.displayName = 'testContext'; +// $FlowFixMe Flow doesn't support this property yet. +Context.displayName = 'ExampleContext'; class ClassComponent extends Component { render() { diff --git a/shells/dev/src/devtools.js b/shells/dev/src/devtools.js index 948dbf565f..da162a517a 100644 --- a/shells/dev/src/devtools.js +++ b/shells/dev/src/devtools.js @@ -30,7 +30,11 @@ initDevTools({ cb(bridge); render( - createElement(App, { bridge }), + createElement(App, { + bridge, + browserName: 'Chrome', + themeName: 'light', + }), ((document.getElementById('devtools'): any): HTMLElement) ); }); diff --git a/src/backend/agent.js b/src/backend/agent.js index 25241716df..218743d2a1 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -15,7 +15,7 @@ import type { Bridge } from '../types'; import type { Element } from 'src/devtools/types'; const debug = (methodName, ...args) => { - //console.log(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args); + // debug(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args); }; const THROTTLE_BY_MS = 350; diff --git a/src/backend/renderer.js b/src/backend/renderer.js index bfc34a9eda..3b29dc1274 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -263,8 +263,8 @@ export function attach( const symbolOrNumber = typeof type === 'object' && type !== null ? type.$$typeof : type; - // $FlowFixMe facebook/flow/issues/2362 const switchValue = + // $FlowFixMe facebook/flow/issues/2362 typeof symbolOrNumber === 'symbol' ? symbolOrNumber.toString() : symbolOrNumber; diff --git a/src/devtools/store.js b/src/devtools/store.js index e3bcf4082e..14d75b0dc5 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -6,7 +6,7 @@ import type { Element } from './types'; import type { Bridge } from '../types'; const debug = (methodName, ...args) => { - //console.log(`%cAgent %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args); + // console.log(`%cAgent %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args); }; /** diff --git a/src/devtools/views/App.js b/src/devtools/views/App.js index 0615a60e34..24b5898fb0 100644 --- a/src/devtools/views/App.js +++ b/src/devtools/views/App.js @@ -8,11 +8,13 @@ import styles from './App.css'; import type { Bridge } from '../../types'; -type Props = {| +export type Props = {| bridge: Bridge, + browserName: string, + themeName: string, |}; -export default function App({ bridge }: Props) { +export default function App({ bridge, browserName, themeName }: Props) { const store = useMemo(() => new Store(bridge), []); return (