Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-use an existing browser tab with with the same URL when launching frontity dev #374

Merged
merged 16 commits into from Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rare-mayflies-clap.md
@@ -0,0 +1,5 @@
---
"@frontity/core": patch
---

Try to re-use an already open browser tab when running `frontity dev` on MacOS.
10 changes: 7 additions & 3 deletions packages/core/package.json
Expand Up @@ -23,9 +23,10 @@
"test:inspect": "node --inspect ../../node_modules/.bin/jest --watch --no-cache --runInBand",
"dev:inspect": "node --inspect-brk -r ts-node/register src/scripts/dev.ts",
"serve:inspect": "node --inspect-brk -r ts-node/register src/scripts/serve.ts",
"build": "../../node_modules/.bin/tsc --project ./tsconfig.build.json",
"build": "../../node_modules/.bin/tsc --project ./tsconfig.build.json && npm run copy-files",
"build:watch": "../../node_modules/.bin/tsc --project ./tsconfig.build.json --watch",
"prepublish": "npm run build"
"prepublish": "npm run build",
"copy-files": "copyfiles -u 1 \"src/**/*.applescript\" dist/src"
},
"dependencies": {
"@babel/core": "^7.3.4",
Expand All @@ -49,6 +50,7 @@
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"babel-polyfill": "^6.26.0",
"core-js": "^3.0.1",
"cross-spawn": "^7.0.2",
"deepmerge": "^3.2.0",
"express": "^4.16.4",
"file-loader": "^3.0.1",
Expand Down Expand Up @@ -80,6 +82,7 @@
"devDependencies": {
"@frontity/types": "^1.2.0",
"@types/babel-core": "^6.25.6",
"@types/cross-spawn": "^6.0.1",
"@types/express": "^4.16.1",
"@types/fs-extra": "^5.0.5",
"@types/htmlescape": "^1.1.1",
Expand All @@ -99,6 +102,7 @@
"@types/webpack-bundle-analyzer": "^2.13.1",
"@types/webpack-dev-middleware": "^3.7.0",
"@types/webpack-env": "^1.13.9",
"@types/webpack-hot-middleware": "^2.16.5"
"@types/webpack-hot-middleware": "^2.16.5",
"copyfiles": "^2.2.0"
}
}
4 changes: 2 additions & 2 deletions packages/core/src/scripts/utils/create-app.ts
@@ -1,7 +1,7 @@
import open from "open";
import { MultiCompiler } from "webpack";
import express from "express";
import createServer from "./create-server";
import { openBrowserTab } from "./open-browser";
import { Mode } from "../../../types";

// Create an express app ready to be used with webpack-dev-middleware.
Expand Down Expand Up @@ -55,7 +55,7 @@ export default async ({
});

// Open localhost on the local browser.
if (openBrowser) open(url);
if (openBrowser) openBrowserTab(url);

// Check if webpack has finished (both the client and server bundles).
const done = (compiler: MultiCompiler) => {
Expand Down
149 changes: 149 additions & 0 deletions packages/core/src/scripts/utils/open-browser.ts
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file: https://github.com/facebook/create-react-app/blob/a4fa63fcc1fb97fa50778b7c1a73a01da3a3e022/LICENSE
*/

import chalk from "chalk";
import { execSync } from "child_process";
import spawn from "cross-spawn";
import open from "open";

// https://github.com/sindresorhus/open#app
const OSX_CHROME = "google chrome";

const Actions = Object.freeze({
NONE: 0,
BROWSER: 1,
SCRIPT: 2,
});

function getBrowserEnv() {
// Attempt to honor this environment variable.
// It is specific to the operating system.
// See https://github.com/sindresorhus/open#app for documentation.
const value = process.env.BROWSER;
const args = process.env.BROWSER_ARGS
? process.env.BROWSER_ARGS.split(" ")
: [];
let action;
if (!value) {
// Default.
action = Actions.BROWSER;
} else if (value.toLowerCase().endsWith(".js")) {
action = Actions.SCRIPT;
} else if (value.toLowerCase() === "none") {
action = Actions.NONE;
} else {
action = Actions.BROWSER;
}
return { action, value, args };
}

function executeNodeScript(scriptPath: string, url: string) {
const extraArgs = process.argv.slice(2);
const child = spawn("node", [scriptPath, ...extraArgs, url], {
stdio: "inherit",
});
child.on("close", (code) => {
if (code !== 0) {
console.log();
console.log(
chalk.red(
"The script specified as BROWSER environment variable failed."
)
);
console.log(chalk.cyan(scriptPath) + " exited with code " + code + ".");
console.log();
return;
}
});
return true;
}

function startBrowserProcess(browser, url: string, args) {
// If we're on OS X, the user hasn't specifically
// requested a different browser, we can try opening
// Chrome with AppleScript. This lets us reuse an
// existing tab when possible instead of creating a new one.
const shouldTryOpenChromiumWithAppleScript =
process.platform === "darwin" &&
(typeof browser !== "string" || browser === OSX_CHROME);

if (shouldTryOpenChromiumWithAppleScript) {
// Will use the first open browser found from list
const supportedChromiumBrowsers = [
"Google Chrome Canary",
"Google Chrome",
"Microsoft Edge",
"Brave Browser",
"Vivaldi",
"Chromium",
];

for (const chromiumBrowser of supportedChromiumBrowsers) {
try {
// Try our best to reuse existing tab
// on OSX Chromium-based browser with AppleScript
execSync('ps cax | grep "' + chromiumBrowser + '"');
execSync(
'osascript openChrome.applescript "' +
encodeURI(url) +
'" "' +
chromiumBrowser +
'"',
{
cwd: __dirname,
stdio: "ignore",
}
);
return true;
} catch (err) {
// Ignore errors.
}
}
}

// Another special case: on OS X, check if BROWSER has been set to "open".
// In this case, instead of passing `open` to `opn` (which won't work),
// just ignore it (thus ensuring the intended behavior, i.e. opening the system browser):
// https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768
if (process.platform === "darwin" && browser === "open") {
browser = undefined;
}

// If there are arguments, they must be passed as array with the browser
if (typeof browser === "string" && args.length > 0) {
browser = [browser].concat(args);
}

// Fallback to open
// (It will always open new tab)
try {
const options = { app: browser, wait: false, url: true };
open(url, options).catch(() => {}); // Prevent `unhandledRejection` error.
return true;
} catch (err) {
return false;
}
}

/**
* Reads the BROWSER environment variable and decides what to do with it. Returns
* true if it opened a browser or ran a node.js script, otherwise false.
*/
export function openBrowserTab(url: string) {
const { action, value, args } = getBrowserEnv();
switch (action) {
case Actions.NONE:
// Special case: BROWSER="none" will prevent opening completely.
return false;
case Actions.SCRIPT:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luisherranz I don't exactly know as this was copied 1:1 from create-react-app.

By just looking at the code, it seems that if the value of the BROWSER environment variable is a .js script, it executes this script instead of launching a browser. I'm not sure if this is a workaround for some edge case, etc. so I would just keep this code as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't like to add code to the framework without understanding it so please check it out before we merge.

Also, if this covers some use cases, it needs to go to the documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is just an advanced feature, in case you want to run a js script that eventually launches a browser in order to do some initialization, etc. There are some more details here: https://create-react-app.dev/docs/advanced-configuration/

Maybe this could be useful for automated testing with cypress? 🤔

return executeNodeScript(value, url);
case Actions.BROWSER:
return startBrowserProcess(value, url, args);
default:
throw new Error("Not implemented.");
}
}
94 changes: 94 additions & 0 deletions packages/core/src/scripts/utils/openChrome.applescript
@@ -0,0 +1,94 @@
(*
Copyright (c) 2015-present, Facebook, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*)

property targetTab: null
property targetTabIndex: -1
property targetWindow: null
property theProgram: "Google Chrome"

on run argv
set theURL to item 1 of argv

-- Allow requested program to be optional,
-- default to Google Chrome
if (count of argv) > 1 then
set theProgram to item 2 of argv
end if

using terms from application "Google Chrome"
tell application theProgram

if (count every window) = 0 then
make new window
end if

-- 1: Looking for tab running debugger
-- then, Reload debugging tab if found
-- then return
set found to my lookupTabWithUrl(theURL)
if found then
set targetWindow's active tab index to targetTabIndex
tell targetTab to reload
tell targetWindow to activate
set index of targetWindow to 1
return
end if

-- 2: Looking for Empty tab
-- In case debugging tab was not found
-- We try to find an empty tab instead
set found to my lookupTabWithUrl("chrome://newtab/")
if found then
set targetWindow's active tab index to targetTabIndex
set URL of targetTab to theURL
tell targetWindow to activate
return
end if

-- 3: Create new tab
-- both debugging and empty tab were not found
-- make a new tab with url
tell window 1
activate
make new tab with properties {URL:theURL}
end tell
end tell
end using terms from
end run

-- Function:
-- Lookup tab with given url
-- if found, store tab, index, and window in properties
-- (properties were declared on top of file)
on lookupTabWithUrl(lookupUrl)
using terms from application "Google Chrome"
tell application theProgram
-- Find a tab with the given url
set found to false
set theTabIndex to -1
repeat with theWindow in every window
set theTabIndex to 0
repeat with theTab in every tab of theWindow
set theTabIndex to theTabIndex + 1
if (theTab's URL as string) contains lookupUrl then
-- assign tab, tab index, and window to properties
set targetTab to theTab
set targetTabIndex to theTabIndex
set targetWindow to theWindow
set found to true
exit repeat
end if
end repeat

if found then
exit repeat
end if
end repeat
end tell
end using terms from
return found
end lookupTabWithUrl